From 5d8172656b82cab0e9b66aafb60469c97cfcf317 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Mon, 11 Nov 2024 09:00:36 -0600 Subject: [PATCH] Pre-release handling --- Cmdline/Action/Available.cs | 10 +- Cmdline/Action/Import.cs | 2 +- Cmdline/Action/Install.cs | 7 +- Cmdline/Action/List.cs | 12 +- Cmdline/Action/Prompt.cs | 2 +- Cmdline/Action/Replace.cs | 10 +- Cmdline/Action/Repo.cs | 4 +- Cmdline/Action/Search.cs | 33 +-- Cmdline/Action/Show.cs | 3 +- Cmdline/Action/Stability.cs | 189 ++++++++++++++ Cmdline/Action/Update.cs | 15 +- Cmdline/Action/Upgrade.cs | 2 +- Cmdline/Main.cs | 2 + Cmdline/Options.cs | 3 + Cmdline/Properties/Resources.resx | 5 + ConsoleUI/DependencyScreen.cs | 17 +- ConsoleUI/GameInstanceEditScreen.cs | 53 ++-- ConsoleUI/GameInstanceListScreen.cs | 2 +- ConsoleUI/InstallScreen.cs | 16 +- ConsoleUI/ModInfoScreen.cs | 50 ++-- ConsoleUI/ModListScreen.cs | 114 +++++---- ConsoleUI/Properties/Resources.resx | 4 + ConsoleUI/ReleaseStatusComboButtons.cs | 52 ++++ ConsoleUI/Toolkit/ConsoleRadioButtons.cs | 163 ++++++++++++ ConsoleUI/Toolkit/ConsoleTextBox.cs | 38 ++- ConsoleUI/Toolkit/ConsoleTheme.cs | 29 ++- ConsoleUI/Toolkit/ScreenObject.cs | 10 +- .../CompatibleGameVersions.cs | 2 +- Core/Configuration/IConfiguration.cs | 3 + Core/Configuration/JsonConfiguration.cs | 11 + .../Configuration/StabilityToleranceConfig.cs | 84 +++++++ .../Win32RegistryConfiguration.cs | 6 + Core/Converters/JsonReleaseStatusConverter.cs | 27 ++ Core/Extensions/EnumerableExtensions.cs | 43 +--- Core/Extensions/I18nExtensions.cs | 10 +- Core/GameInstance.cs | 9 + Core/Meta.cs | 5 + Core/ModuleInstaller.cs | 21 +- Core/Properties/Resources.resx | 6 + Core/Registry/CompatibilitySorter.cs | 14 +- Core/Registry/IRegistryQuerier.cs | 57 +++-- Core/Registry/Registry.cs | 37 ++- Core/Registry/RegistryManager.cs | 10 +- Core/Relationships/RelationshipResolver.cs | 8 +- .../RelationshipResolverOptions.cs | 28 ++- Core/Relationships/ResolvedRelationship.cs | 28 ++- .../ResolvedRelationshipsTree.cs | 88 ++++--- Core/Repositories/AvailableModule.cs | 19 +- Core/Repositories/ReadProgressStream.cs | 6 - Core/Types/CkanModule.cs | 18 +- Core/Types/RelationshipDescriptor.cs | 15 +- Core/Types/ReleaseStatus.cs | 68 ++--- Core/Types/SpecVersionAnalyzer.cs | 6 +- Core/Versioning/GameVersion.cs | 7 +- Core/Versioning/GameVersionBound.cs | 12 +- Core/Versioning/ModuleVersion.cs | 28 +-- GUI/Controls/Changeset.cs | 2 +- GUI/Controls/ChooseRecommendedMods.cs | 6 +- GUI/Controls/InstallationHistory.cs | 18 +- GUI/Controls/LabeledProgressBar.cs | 35 ++- GUI/Controls/ManageMods.cs | 11 +- GUI/Controls/ModInfo.Designer.cs | 3 + GUI/Controls/ModInfo.cs | 16 +- GUI/Controls/ModInfoTabs/Contents.cs | 45 +++- GUI/Controls/ModInfoTabs/Metadata.cs | 8 +- GUI/Controls/ModInfoTabs/Relationships.cs | 120 +++++---- GUI/Controls/ModInfoTabs/Versions.Designer.cs | 234 +++++++++++------- GUI/Controls/ModInfoTabs/Versions.cs | 171 ++++++++++--- GUI/Controls/ModInfoTabs/Versions.resx | 13 +- GUI/Controls/Wait.cs | 12 +- .../GameCommandLineOptionsDialog.Designer.cs | 3 + GUI/Dialogs/InstallFiltersDialog.cs | 9 +- GUI/Dialogs/SettingsDialog.Designer.cs | 34 ++- GUI/Dialogs/SettingsDialog.cs | 37 +++ GUI/Dialogs/SettingsDialog.resx | 1 + GUI/Localization/de-DE/Versions.de-DE.resx | 23 +- GUI/Localization/fr-FR/Versions.fr-FR.resx | 23 +- GUI/Localization/it-IT/Versions.it-IT.resx | 23 +- GUI/Localization/ja-JP/Versions.ja-JP.resx | 23 +- GUI/Localization/ko-KR/Versions.ko-KR.resx | 23 +- GUI/Localization/nl-NL/Versions.nl-NL.resx | 23 +- GUI/Localization/pl-PL/Versions.pl-PL.resx | 23 +- GUI/Localization/pt-BR/Versions.pt-BR.resx | 23 +- GUI/Localization/ru-RU/Versions.ru-RU.resx | 23 +- GUI/Localization/tr-TR/Versions.tr-TR.resx | 23 +- GUI/Localization/zh-CN/Versions.zh-CN.resx | 23 +- GUI/Main/Main.cs | 29 ++- GUI/Main/MainChangeset.cs | 33 +-- GUI/Main/MainDownload.cs | 7 - GUI/Main/MainHistory.cs | 2 + GUI/Main/MainInstall.cs | 50 ++-- GUI/Main/MainRecommendations.cs | 2 +- GUI/Main/MainRepo.cs | 5 +- GUI/Main/MainTrayIcon.cs | 13 +- GUI/Model/GUIMod.cs | 19 +- GUI/Model/ModChange.cs | 2 +- GUI/Model/ModList.cs | 44 ++-- GUI/Model/ReleaseStatusItem.cs | 17 ++ GUI/Properties/Resources.resx | 3 + NetKAN.schema | 4 + Netkan/CmdLineOptions.cs | 4 +- Netkan/Model/Metadata.cs | 26 +- Netkan/Processors/Inflator.cs | 21 +- Netkan/Processors/QueueHandler.cs | 2 +- Netkan/Properties/AssemblyInfo.cs | 4 +- Netkan/Sources/Github/GithubApi.cs | 21 +- Netkan/Sources/Github/GithubConfig.cs | 13 + Netkan/Sources/Github/GithubRef.cs | 8 +- Netkan/Sources/Github/IGithubApi.cs | 4 +- Netkan/Sources/Spacedock/SDVersion.cs | 35 +-- Netkan/Transformers/GithubTransformer.cs | 36 ++- Netkan/Transformers/GitlabTransformer.cs | 4 - Netkan/Transformers/NetkanTransformer.cs | 2 +- Netkan/Transformers/SpacedockTransformer.cs | 42 ++-- Netkan/Transformers/StagingTransformer.cs | 4 +- .../StripNetkanMetadataTransformer.cs | 38 ++- .../VersionedOverrideTransformer.cs | 12 +- Spec.md | 4 + Tests/Core/Configuration/FakeConfiguration.cs | 10 +- Tests/Core/ModuleInstallerDirTest.cs | 2 +- Tests/Core/ModuleInstallerTests.cs | 34 +-- Tests/Core/Registry/CompatibilitySorter.cs | 5 +- Tests/Core/Registry/Registry.cs | 22 +- Tests/Core/Registry/RegistryLive.cs | 3 +- .../RelationshipResolverTests.cs | 38 +-- Tests/Core/Relationships/SanityChecker.cs | 32 +-- .../RepositoryDataManagerTests.cs | 4 +- Tests/Core/Types/ReleaseStatus.cs | 87 ++++--- Tests/GUI/Model/GUIMod.cs | 7 +- Tests/GUI/Model/ModList.cs | 57 +++-- Tests/NetKAN/Sources/Github/GithubApiTests.cs | 2 +- .../Transformers/GithubTransformerTests.cs | 8 +- 132 files changed, 2189 insertions(+), 1186 deletions(-) create mode 100644 Cmdline/Action/Stability.cs create mode 100644 ConsoleUI/ReleaseStatusComboButtons.cs create mode 100644 ConsoleUI/Toolkit/ConsoleRadioButtons.cs rename Core/{ => Configuration}/CompatibleGameVersions.cs (95%) create mode 100644 Core/Configuration/StabilityToleranceConfig.cs create mode 100644 Core/Converters/JsonReleaseStatusConverter.cs create mode 100644 GUI/Model/ReleaseStatusItem.cs create mode 100644 Netkan/Sources/Github/GithubConfig.cs diff --git a/Cmdline/Action/Available.cs b/Cmdline/Action/Available.cs index e29776cad1..40f0dedaf5 100644 --- a/Cmdline/Action/Available.cs +++ b/Cmdline/Action/Available.cs @@ -16,11 +16,13 @@ public Available(RepositoryDataManager repoData, IUser user) public int RunCommand(CKAN.GameInstance instance, object raw_options) { - AvailableOptions opts = (AvailableOptions)raw_options; - IRegistryQuerier registry = RegistryManager.Instance(instance, repoData).registry; + AvailableOptions opts = (AvailableOptions)raw_options; - IEnumerable compatible = registry - .CompatibleModules(instance.VersionCriteria()) + IEnumerable compatible = RegistryManager + .Instance(instance, repoData) + .registry + .CompatibleModules(instance.StabilityToleranceConfig, + instance.VersionCriteria()) .Where(m => !m.IsDLC) .OrderBy(m => m.identifier); diff --git a/Cmdline/Action/Import.cs b/Cmdline/Action/Import.cs index ca1dac0f4e..5819a6e4d1 100644 --- a/Cmdline/Action/Import.cs +++ b/Cmdline/Action/Import.cs @@ -60,7 +60,7 @@ public int RunCommand(CKAN.GameInstance instance, object options) { HashSet? possibleConfigOnlyDirs = null; installer.InstallList(toInstall, - new RelationshipResolverOptions(), + new RelationshipResolverOptions(instance.StabilityToleranceConfig), regMgr, ref possibleConfigOnlyDirs, opts?.NetUserAgent); diff --git a/Cmdline/Action/Install.cs b/Cmdline/Action/Install.cs index e3b7a6dc7b..2e070c5549 100644 --- a/Cmdline/Action/Install.cs +++ b/Cmdline/Action/Install.cs @@ -89,8 +89,9 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) (options?.allow_incompatible ?? false) ? null : crit) - ?? registry.LatestAvailable(arg, crit, - null, installed) + ?? registry.LatestAvailable(arg, + instance.StabilityToleranceConfig, + crit, null, installed) ?? registry.InstalledModule(arg)?.Module) .OfType() .ToList(); @@ -102,7 +103,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) } var installer = new ModuleInstaller(instance, manager.Cache, user, options?.NetUserAgent); - var install_ops = new RelationshipResolverOptions + var install_ops = new RelationshipResolverOptions(instance.StabilityToleranceConfig) { with_all_suggests = options?.with_all_suggests ?? false, with_suggests = options?.with_suggests ?? false, diff --git a/Cmdline/Action/List.cs b/Cmdline/Action/List.cs index 3100470abe..28035d7e14 100644 --- a/Cmdline/Action/List.cs +++ b/Cmdline/Action/List.cs @@ -87,7 +87,9 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) { // Check if upgrades are available, and show appropriately. log.DebugFormat("Check if upgrades are available for {0}", mod.Key); - var latest = registry.LatestAvailable(mod.Key, instance.VersionCriteria()); + var latest = registry.LatestAvailable(mod.Key, + instance.StabilityToleranceConfig, + instance.VersionCriteria()); var current = registry.GetInstalledVersion(mod.Key); var inst = registry.InstalledModule(mod.Key); @@ -103,7 +105,9 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) // Check if mod is replaceable else if (current.replaced_by != null) { - var replacement = registry.GetReplacement(mod.Key, instance.VersionCriteria()); + var replacement = registry.GetReplacement(mod.Key, + instance.StabilityToleranceConfig, + instance.VersionCriteria()); if (replacement != null) { // Replaceable! @@ -120,7 +124,9 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) // Check if mod is replaceable if (current?.replaced_by != null) { - var replacement = registry.GetReplacement(latest.identifier, instance.VersionCriteria()); + var replacement = registry.GetReplacement(latest.identifier, + instance.StabilityToleranceConfig, + instance.VersionCriteria()); if (replacement != null) { // Replaceable! diff --git a/Cmdline/Action/Prompt.cs b/Cmdline/Action/Prompt.cs index 36d083373d..effccbecda 100644 --- a/Cmdline/Action/Prompt.cs +++ b/Cmdline/Action/Prompt.cs @@ -185,7 +185,7 @@ private string[] GetAvailIdentifiers(string prefix) CKAN.GameInstance inst = MainClass.GetGameInstance(manager); return RegistryManager.Instance(inst, repoData) .registry - .CompatibleModules(inst.VersionCriteria()) + .CompatibleModules(inst.StabilityToleranceConfig, inst.VersionCriteria()) .Where(m => !m.IsDLC) .Select(m => m.identifier) .Where(ident => ident.StartsWith(prefix, diff --git a/Cmdline/Action/Replace.cs b/Cmdline/Action/Replace.cs index c1597b9687..b0c98b6cbb 100644 --- a/Cmdline/Action/Replace.cs +++ b/Cmdline/Action/Replace.cs @@ -36,7 +36,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) } // Prepare options. Can these all be done in the new() somehow? - var replace_ops = new RelationshipResolverOptions + var replace_ops = new RelationshipResolverOptions(instance.StabilityToleranceConfig) { with_all_suggests = options.with_all_suggests, with_suggests = options.with_suggests, @@ -68,7 +68,9 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) log.DebugFormat("Testing {0} {1} for possible replacement", mod.Key, mod.Value); // Check if replacement is available - var replacement = registry.GetReplacement(mod.Key, instance.VersionCriteria()); + var replacement = registry.GetReplacement(mod.Key, + instance.StabilityToleranceConfig, + instance.VersionCriteria()); if (replacement != null) { // Replaceable @@ -100,7 +102,9 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) try { // Check if replacement is available - var replacement = registry.GetReplacement(modToReplace.identifier, instance.VersionCriteria()); + var replacement = registry.GetReplacement(modToReplace.identifier, + instance.StabilityToleranceConfig, + instance.VersionCriteria()); if (replacement != null) { // Replaceable diff --git a/Cmdline/Action/Repo.cs b/Cmdline/Action/Repo.cs index 4de1295245..731e95a3f4 100644 --- a/Cmdline/Action/Repo.cs +++ b/Cmdline/Action/Repo.cs @@ -93,7 +93,7 @@ public class RepoAddOptions : InstanceSpecificOptions public class RepoPriorityOptions : InstanceSpecificOptions { [ValueOption(0)] public string? name { get; set; } - [ValueOption(1)] public int priority { get; set; } + [ValueOption(1)] public int priority { get; set; } } public class RepoDefaultOptions : InstanceSpecificOptions @@ -459,6 +459,6 @@ private void PrintUsage(string verb) private readonly RepositoryDataManager repoData; private IUser? user; - private static readonly ILog log = LogManager.GetLogger(typeof (Repo)); + private static readonly ILog log = LogManager.GetLogger(typeof(Repo)); } } diff --git a/Cmdline/Action/Search.cs b/Cmdline/Action/Search.cs index 1b8262e759..ebd032b7dc 100644 --- a/Cmdline/Action/Search.cs +++ b/Cmdline/Action/Search.cs @@ -17,7 +17,7 @@ public Search(RepositoryDataManager repoData, IUser user) this.user = user; } - public int RunCommand(CKAN.GameInstance ksp, object raw_options) + public int RunCommand(CKAN.GameInstance instance, object raw_options) { SearchOptions options = (SearchOptions)raw_options; @@ -32,11 +32,11 @@ public int RunCommand(CKAN.GameInstance ksp, object raw_options) return Exit.BADOPT; } - var matching_compatible = PerformSearch(ksp, options.search_term, options.author_term, false); + var matching_compatible = PerformSearch(instance, options.search_term, options.author_term, false); var matching_incompatible = new List(); if (options.all) { - matching_incompatible = PerformSearch(ksp, options.search_term, options.author_term, true); + matching_incompatible = PerformSearch(instance, options.search_term, options.author_term, true); } // Show how many matches we have. @@ -94,10 +94,10 @@ public int RunCommand(CKAN.GameInstance ksp, object raw_options) user.RaiseMessage(Properties.Resources.SearchIncompatibleModsHeader); foreach (CkanModule mod in matching_incompatible) { - CkanModule.GetMinMaxVersions(new List { mod } , out _, out _, out var minKsp, out var maxKsp); - var gv = GameVersionRange.VersionSpan(ksp.game, - minKsp ?? GameVersion.Any, - maxKsp ?? GameVersion.Any) + CkanModule.GetMinMaxVersions(new List { mod } , out _, out _, out var mininstance, out var maxinstance); + var gv = GameVersionRange.VersionSpan(instance.game, + mininstance ?? GameVersion.Any, + maxinstance ?? GameVersion.Any) .ToString(); user.RaiseMessage(Properties.Resources.SearchIncompatibleMod, @@ -125,15 +125,15 @@ public int RunCommand(CKAN.GameInstance ksp, object raw_options) } /// - /// Searches for the term in the list of compatible or incompatible modules for the ksp instance. + /// Searches for the term in the list of compatible or incompatible modules for the instance instance. /// Looks in name, identifier and description fields, and if given, restricts to authors matching the author term. /// /// List of matching modules. - /// The KSP instance to perform the search for. + /// The instance instance to perform the search for. /// The search term. Case insensitive. /// Name of author to find /// True to look for incompatible modules, false (default) to look for compatible - public List PerformSearch(CKAN.GameInstance ksp, + public List PerformSearch(CKAN.GameInstance instance, string? term, string? author = null, bool searchIncompatible = false) @@ -142,11 +142,12 @@ public List PerformSearch(CKAN.GameInstance ksp, term = string.IsNullOrWhiteSpace(term) ? string.Empty : CkanModule.nonAlphaNums.Replace(term, ""); author = string.IsNullOrWhiteSpace(author) ? string.Empty : CkanModule.nonAlphaNums.Replace(author, ""); - var registry = RegistryManager.Instance(ksp, repoData).registry; + var registry = RegistryManager.Instance(instance, repoData).registry; return searchIncompatible ? registry - .IncompatibleModules(ksp.VersionCriteria()) + .IncompatibleModules(instance.StabilityToleranceConfig, + instance.VersionCriteria()) // Look for a match in each string. .Where(module => (module.SearchableName.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 || module.SearchableIdentifier.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 @@ -155,7 +156,8 @@ public List PerformSearch(CKAN.GameInstance ksp, && module.SearchableAuthors.Any((auth) => auth.IndexOf(author, StringComparison.OrdinalIgnoreCase) > -1)) .ToList() : registry - .CompatibleModules(ksp.VersionCriteria()) + .CompatibleModules(instance.StabilityToleranceConfig, + instance.VersionCriteria()) // Look for a match in each string. .Where(module => (module.SearchableName.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 || module.SearchableIdentifier.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 @@ -190,9 +192,10 @@ private static string CaseInsensitiveExactMatch(List mods, string mo /// List of strings to convert, format 'identifier' or 'identifier=version' public static void AdjustModulesCase(CKAN.GameInstance instance, Registry registry, List modules) { + var stabilityTolerance = instance.StabilityToleranceConfig; // Get the list of all compatible and incompatible mods - List mods = registry.CompatibleModules(instance.VersionCriteria()).ToList(); - mods.AddRange(registry.IncompatibleModules(instance.VersionCriteria())); + var mods = registry.CompatibleModules(stabilityTolerance, instance.VersionCriteria()).ToList(); + mods.AddRange(registry.IncompatibleModules(stabilityTolerance, instance.VersionCriteria())); for (int i = 0; i < modules.Count; ++i) { Match match = CkanModule.idAndVersionMatcher.Match(modules[i]); diff --git a/Cmdline/Action/Show.cs b/Cmdline/Action/Show.cs index 5b67f57de4..4d0e6c0c9a 100644 --- a/Cmdline/Action/Show.cs +++ b/Cmdline/Action/Show.cs @@ -51,7 +51,8 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) // Module was not installed, look for an exact match in the available modules, // either by "name" (the user-friendly display name) or by identifier - var moduleToShow = registry.CompatibleModules(instance.VersionCriteria()) + var moduleToShow = registry.CompatibleModules(instance.StabilityToleranceConfig, + instance.VersionCriteria()) .SingleOrDefault( mod => mod.name == modName || mod.identifier == modName); diff --git a/Cmdline/Action/Stability.cs b/Cmdline/Action/Stability.cs new file mode 100644 index 0000000000..afba29f28c --- /dev/null +++ b/Cmdline/Action/Stability.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Autofac; +using CommandLine; +using CommandLine.Text; + +using CKAN.Extensions; + +namespace CKAN.CmdLine +{ + public class Stability : ISubCommand + { + public int RunSubCommand(GameInstanceManager? manager, + CommonOptions? opts, + SubCommandOptions options) + { + int exitCode = Exit.OK; + Parser.Default.ParseArgumentsStrict(options.options.ToArray(), + new StabilitySubOptions(), + (string option, object suboptions) => + { + // ParseArgumentsStrict calls us unconditionally, even with bad arguments + if (!string.IsNullOrEmpty(option) && suboptions != null) + { + CommonOptions options = (CommonOptions)suboptions; + options.Merge(opts); + var user = new ConsoleUser(options.Headless); + manager ??= new GameInstanceManager(user); + exitCode = options.Handle(manager, user); + if (exitCode == Exit.OK) + { + switch (option) + { + case "list": + exitCode = List(MainClass.GetGameInstance(manager), + user); + break; + + case "set": + exitCode = Set((StabilitySetOptions)suboptions, + MainClass.GetGameInstance(manager), + user); + break; + + default: + user.RaiseMessage(Properties.Resources.StabilityUnknownCommand, + option); + exitCode = Exit.BADOPT; + break; + } + } + } + }, () => { exitCode = MainClass.AfterHelp(); }); + return exitCode; + } + + private static int List(CKAN.GameInstance instance, IUser user) + { + var stabilityTolerance = instance.StabilityToleranceConfig; + user.RaiseMessage(Properties.Resources.StabilityOverallLabel, + stabilityTolerance.OverallStabilityTolerance); + var rows = stabilityTolerance.OverriddenModIdentifiers + .OrderBy(ident => ident) + .Select(ident => stabilityTolerance.ModStabilityTolerance(ident) + is ReleaseStatus relStat + ? Tuple.Create(ident, relStat.ToString()) + : null) + .OfType>() + .ToArray(); + if (rows.Length > 0) + { + var modHeader = Properties.Resources.StabilityListModHeader; + var stabilityHeader = Properties.Resources.StabilityListStabilityHeader; + var modWidth = Enumerable.Repeat(modHeader, 1) + .Concat(rows.Select(tuple => tuple.Item1)) + .Max(str => str.Length); + var stabilityWidth = Enumerable.Repeat(stabilityHeader, 1) + .Concat(rows.Select(tuple => tuple.Item2)) + .Max(str => str.Length); + user.RaiseMessage(""); + user.RaiseMessage("{0} {1}", modHeader.PadRight(modWidth), + stabilityHeader.PadRight(stabilityWidth)); + user.RaiseMessage("{0} {1}", new string('-', modWidth), + new string('-', stabilityWidth)); + foreach (var (ident, relStat) in rows) + { + user.RaiseMessage("{0} {1}", ident.PadRight(modWidth), + relStat.PadRight(stabilityWidth)); + } + } + return Exit.OK; + } + + private static int Set(StabilitySetOptions opts, CKAN.GameInstance instance, IUser user) + { + var stabilityTolerance = instance.StabilityToleranceConfig; + if (opts.Identifier == null) + { + if (opts.Stability is ReleaseStatus relStat) + { + stabilityTolerance.OverallStabilityTolerance = relStat; + } + else + { + user.RaiseError(Properties.Resources.ArgumentMissing); + PrintUsage(user, "set"); + return Exit.BADOPT; + } + } + else + { + var repoData = ServiceLocator.Container.Resolve(); + var registry = RegistryManager.Instance(instance, repoData).registry; + var idents = new List { opts.Identifier }; + Search.AdjustModulesCase(instance, registry, idents); + stabilityTolerance.SetModStabilityTolerance(idents[0], opts.Stability); + } + List(instance, user); + return Exit.OK; + } + + private static void PrintUsage(IUser user, string verb) + { + foreach (var h in StabilitySubOptions.GetHelp(verb)) + { + user.RaiseError("{0}", h); + } + } + } + + public class StabilitySubOptions: VerbCommandOptions + { + [VerbOption("list", HelpText = "Print stability preferences")] + public InstanceSpecificOptions? ListOptions { get; set; } + + [VerbOption("set", HelpText = "Change stability preferences")] + public StabilitySetOptions? SetOptions { get; set; } + + [HelpVerbOption] + public string GetUsage(string verb) + { + var ht = HelpText.AutoBuild(this, verb); + foreach (var h in GetHelp(verb)) + { + ht.AddPreOptionsLine(h); + } + return ht; + } + + public static IEnumerable GetHelp(string verb) + { + // Add a usage prefix line + yield return " "; + if (string.IsNullOrEmpty(verb)) + { + yield return $"ckan stability - {Properties.Resources.StabilityHelpSummary}"; + yield return $"{Properties.Resources.Usage}: ckan stability <{Properties.Resources.Command}> [{Properties.Resources.Options}]"; + } + else + { + yield return "stability " + verb + " - " + GetDescription(typeof(StabilitySubOptions), verb); + switch (verb) + { + // Commands with one argument + case "set": + yield return $"{Properties.Resources.Usage}: ckan stability {verb} [{Properties.Resources.Options}] release_status"; + break; + + // Commands with only --flag type options + case "list": + default: + yield return $"{Properties.Resources.Usage}: ckan stability {verb} [{Properties.Resources.Options}]"; + break; + } + } + } + } + + public class StabilitySetOptions : InstanceSpecificOptions + { + [Option("mod", HelpText = "Identifier of mod to override")] + public string? Identifier { get; set; } + + [ValueOption(0)] + public ReleaseStatus? Stability { get; set; } + } +} diff --git a/Cmdline/Action/Update.cs b/Cmdline/Action/Update.cs index ec1c4cdb52..1a6f732856 100644 --- a/Cmdline/Action/Update.cs +++ b/Cmdline/Action/Update.cs @@ -38,6 +38,8 @@ public int RunCommand(object raw_options) try { + var instance = MainClass.GetGameInstance(manager); + var stabilityTolerance = instance.StabilityToleranceConfig; if (options.repositoryURLs != null || options.game != null) { var game = options.game == null ? KnownGames.knownGames.First() @@ -59,13 +61,13 @@ public int RunCommand(object raw_options) if (options.list_changes) { var availablePrior = repoData.GetAllAvailableModules(repos) - .Select(am => am.Latest()) + .Select(am => am.Latest(stabilityTolerance)) .OfType() .ToList(); UpdateRepositories(game, repos, options.NetUserAgent, options.force); PrintChanges(availablePrior, repoData.GetAllAvailableModules(repos) - .Select(am => am.Latest()) + .Select(am => am.Latest(stabilityTolerance)) .OfType() .ToList()); } @@ -76,16 +78,15 @@ public int RunCommand(object raw_options) } else { - var instance = MainClass.GetGameInstance(manager); if (options.list_changes) { // Get a list of compatible modules prior to the update. var registry = RegistryManager.Instance(instance, repoData).registry; var crit = instance.VersionCriteria(); - var compatible_prior = registry.CompatibleModules(crit).ToList(); + var compatible_prior = registry.CompatibleModules(stabilityTolerance, crit).ToList(); UpdateRepositories(instance, options.NetUserAgent, options.force); PrintChanges(compatible_prior, - registry.CompatibleModules(crit).ToList()); + registry.CompatibleModules(stabilityTolerance, crit).ToList()); } else { @@ -186,7 +187,9 @@ private void UpdateRepositories(CKAN.GameInstance instance, string? userAgent, b if (result == RepositoryDataManager.UpdateResult.Updated) { user.RaiseMessage(Properties.Resources.UpdateSummary, - registry.CompatibleModules(instance.VersionCriteria()).Count()); + registry.CompatibleModules(instance.StabilityToleranceConfig, + instance.VersionCriteria()) + .Count()); } } diff --git a/Cmdline/Action/Upgrade.cs b/Cmdline/Action/Upgrade.cs index 8ac5be431e..c7a8734f0e 100644 --- a/Cmdline/Action/Upgrade.cs +++ b/Cmdline/Action/Upgrade.cs @@ -226,7 +226,7 @@ private void UpgradeModules(NetModuleCache cache, // The modules we'll have after upgrading as aggressively as possible var limiters = identsAndVersions.Select(req => CkanModule.FromIDandVersion(registry, req, crit) ?? Utilities.DefaultIfThrows( - () => registry.LatestAvailable(req, crit)) + () => registry.LatestAvailable(req, instance.StabilityToleranceConfig, crit)) ?? registry.GetInstalledVersion(req)) .Concat(heldIdents.Select(ident => registry.GetInstalledVersion(ident))) .OfType() diff --git a/Cmdline/Main.cs b/Cmdline/Main.cs index 6d33fe1c43..0c3e1894ae 100644 --- a/Cmdline/Main.cs +++ b/Cmdline/Main.cs @@ -115,6 +115,8 @@ public static int Execute(GameInstanceManager? manager, CommonOptions? opts, str case "filter": return (new Filter()).RunSubCommand(manager, opts, new SubCommandOptions(args)); + case "stability": + return (new Stability()).RunSubCommand(manager, opts, new SubCommandOptions(args)); } } } diff --git a/Cmdline/Options.cs b/Cmdline/Options.cs index 32a581769f..0f7708a8fe 100644 --- a/Cmdline/Options.cs +++ b/Cmdline/Options.cs @@ -123,6 +123,9 @@ internal class Actions : VerbCommandOptions [VerbOption("filter", HelpText = "View or edit installation filters")] public FilterSubOptions? Filter { get; set; } + [VerbOption("stability", HelpText = "View or edit stability settings")] + public StabilitySubOptions? Stability { get; set; } + [HelpVerbOption] public string GetUsage(string verb) { diff --git a/Cmdline/Properties/Resources.resx b/Cmdline/Properties/Resources.resx index b568f770c9..d15e4519e7 100644 --- a/Cmdline/Properties/Resources.resx +++ b/Cmdline/Properties/Resources.resx @@ -395,6 +395,11 @@ Proceeding with {0} in case it fixes it. Global filters not found: {0} Instance filters not found: {0} View or edit installation filters + Unknown command: stability {0} + View or edit stability settings + Overall stability tolerance: {0} + Mod + Override No game instance selected, identifier completion not available. Use the `instance default` command to choose an instance. diff --git a/ConsoleUI/DependencyScreen.cs b/ConsoleUI/DependencyScreen.cs index 10b89f0380..4db1fb8319 100644 --- a/ConsoleUI/DependencyScreen.cs +++ b/ConsoleUI/DependencyScreen.cs @@ -23,6 +23,7 @@ public class DependencyScreen : ConsoleScreen { /// /// The visual theme to use to draw the dialog /// Game instance manager containing instances + /// The game instance /// Registry of the current instance for finding mods /// HTTP useragent string to use /// Plan of mods to add and remove @@ -30,6 +31,7 @@ public class DependencyScreen : ConsoleScreen { /// True if debug options should be available, false otherwise public DependencyScreen(ConsoleTheme theme, GameInstanceManager mgr, + GameInstance instance, Registry reg, string? userAgent, ChangePlan cp, @@ -106,7 +108,7 @@ public DependencyScreen(ConsoleTheme theme, dependencyList.AddTip(Properties.Resources.Enter, Properties.Resources.Details); dependencyList.AddBinding(Keys.Enter, (object sender) => { if (dependencyList.Selection != null) { - LaunchSubScreen(new ModInfoScreen(theme, manager, reg, userAgent, plan, + LaunchSubScreen(new ModInfoScreen(theme, manager, instance, reg, userAgent, plan, dependencyList.Selection.module, null, debug)); @@ -186,9 +188,14 @@ out Dictionary> supporters private IEnumerable ReplacementModules(IEnumerable replaced_identifiers, GameVersionCriteria crit) - => replaced_identifiers.Select(replaced => registry.GetReplacement(replaced, crit)) - .OfType() - .Select(repl => repl.ReplaceWith); + => manager.CurrentInstance == null + ? Enumerable.Empty() + : replaced_identifiers.Select(replaced => registry.GetReplacement( + replaced, + manager.CurrentInstance.StabilityToleranceConfig, + crit)) + .OfType() + .Select(repl => repl.ReplaceWith); private string StatusSymbol(CkanModule mod) => accepted.Contains(mod) ? installing @@ -215,7 +222,7 @@ private bool HasConflicts(IEnumerable toAdd, plan.Install.Concat(toAdd).Distinct(), plan.Remove.Select(ident => registry.InstalledModule(ident)?.Module) .OfType(), - RelationshipResolverOptions.ConflictsOpts(), registry, + RelationshipResolverOptions.ConflictsOpts(manager.CurrentInstance.StabilityToleranceConfig), registry, manager.CurrentInstance.game, manager.CurrentInstance.VersionCriteria()); descriptions = resolver.ConflictDescriptions.ToList(); diff --git a/ConsoleUI/GameInstanceEditScreen.cs b/ConsoleUI/GameInstanceEditScreen.cs index 5180032819..17f940808d 100644 --- a/ConsoleUI/GameInstanceEditScreen.cs +++ b/ConsoleUI/GameInstanceEditScreen.cs @@ -18,19 +18,19 @@ public class GameInstanceEditScreen : GameInstanceScreen { /// The visual theme to use to draw the dialog /// Game instance manager containing the instances /// Repository data manager providing info from repos - /// Instance to edit + /// Instance to edit /// HTTP useragent string to use - public GameInstanceEditScreen(ConsoleTheme theme, + public GameInstanceEditScreen(ConsoleTheme theme, GameInstanceManager mgr, RepositoryDataManager repoData, - GameInstance k, + GameInstance inst, string? userAgent) - : base(theme, mgr, k.Name, Platform.FormatPath(k.GameDir())) + : base(theme, mgr, inst.Name, Platform.FormatPath(inst.GameDir())) { - ksp = k; + instance = inst; try { // If we can't parse the registry, just leave the repo list blank - regMgr = RegistryManager.Instance(ksp, repoData); + regMgr = RegistryManager.Instance(instance, repoData); registry = regMgr.registry; } catch { } @@ -48,7 +48,14 @@ public GameInstanceEditScreen(ConsoleTheme theme, } // Also edit copy of the compatible versions - compatEditList = new List(ksp.GetCompatibleVersions()); + compatEditList = new List(instance.GetCompatibleVersions()); + + stabilityToleranceButtons = new ReleaseStatusComboButtons( + 1, stabilityToleranceTop, + Properties.Resources.InstanceEditStabilityToleranceHeader, + null, + instance.StabilityToleranceConfig.OverallStabilityTolerance); + AddObject(stabilityToleranceButtons); // I'm not a huge fan of this layout, but I think it's better than just a label AddObject(new ConsoleDoubleFrame( @@ -83,7 +90,7 @@ public GameInstanceEditScreen(ConsoleTheme theme, AddObject(repoList); repoList.AddTip("A", Properties.Resources.Add); repoList.AddBinding(Keys.A, (object sender) => { - LaunchSubScreen(new RepoAddScreen(theme, ksp.game, repoEditList, userAgent)); + LaunchSubScreen(new RepoAddScreen(theme, instance.game, repoEditList, userAgent)); repoList.SetData(new List(repoEditList.Values)); return true; }); @@ -107,7 +114,7 @@ public GameInstanceEditScreen(ConsoleTheme theme, repoList.AddBinding(Keys.E, (object sender) => { if (repoList.Selection is Repository repo) { - LaunchSubScreen(new RepoEditScreen(theme, ksp.game, repoEditList, repo, userAgent)); + LaunchSubScreen(new RepoEditScreen(theme, instance.game, repoEditList, repo, userAgent)); repoList.SetData(new List(repoEditList.Values)); } return true; @@ -159,7 +166,7 @@ public GameInstanceEditScreen(ConsoleTheme theme, compatList.AddTip("A", Properties.Resources.Add); compatList.AddBinding(Keys.A, (object sender) => { - CompatibleVersionDialog vd = new CompatibleVersionDialog(theme, ksp.game); + CompatibleVersionDialog vd = new CompatibleVersionDialog(theme, instance.game); var newVersion = vd.Run(); DrawBackground(); if (newVersion != null && !compatEditList.Contains(newVersion)) { @@ -183,7 +190,7 @@ public GameInstanceEditScreen(ConsoleTheme theme, // Notify the user that the registry doesn't parse AddObject(new ConsoleLabel( 1, repoFrameTop, -1, - () => string.Format(Properties.Resources.InstanceEditRegistryParseError, ksp.Name) + () => string.Format(Properties.Resources.InstanceEditRegistryParseError, instance.Name) )); } @@ -210,8 +217,8 @@ protected override string CenterHeader() /// Similar to adding, except leaving the fields unchanged is allowed. /// protected override bool Valid() - => (name.Value == ksp.Name || nameValid()) - && (path.Value == ksp.GameDir() || pathValid()); + => (name.Value == instance.Name || nameValid()) + && (path.Value == instance.GameDir() || pathValid()); /// /// Save the changes. @@ -220,28 +227,33 @@ protected override bool Valid() /// protected override void Save() { + if (stabilityToleranceButtons?.Selection != null) + { + instance.StabilityToleranceConfig.OverallStabilityTolerance = + stabilityToleranceButtons.Selection.Value; + } if (repoEditList != null) { // Copy the temp list of repositories to the registry registry?.RepositoriesSet(repoEditList); regMgr?.Save(); } if (compatEditList != null) { - ksp.SetCompatibleVersions(compatEditList); + instance.SetCompatibleVersions(compatEditList); } - string oldName = ksp.Name; - if (path.Value != ksp.GameDir()) { + string oldName = instance.Name; + if (path.Value != instance.GameDir()) { // If the path is changed, then we have to remove the old instance // and replace it with a new one, whether or not the name is changed. manager.RemoveInstance(oldName); manager.AddInstance(path.Value, name.Value, new NullUser()); } else if (name.Value != oldName) { // If only the name changed, there's an API for that. - manager.RenameInstance(ksp.Name, name.Value); + manager.RenameInstance(instance.Name, name.Value); } } - private readonly GameInstance ksp; + private readonly GameInstance instance; private readonly RegistryManager? regMgr; private readonly Registry? registry; @@ -249,8 +261,11 @@ protected override void Save() private readonly ConsoleListBox? repoList; private readonly List? compatEditList; private readonly ConsoleListBox? compatList; + private readonly ReleaseStatusComboButtons? stabilityToleranceButtons; - private const int repoFrameTop = pathRow + 2; + private const int stabilityToleranceTop = pathRow + 2; + private const int stabilityToleranceHeight = 4; + private const int repoFrameTop = stabilityToleranceTop + stabilityToleranceHeight + 1; private const int repoListTop = repoFrameTop + 2; private const int repoFrameHeight = 9; private const int repoFrameBottom = repoFrameTop + repoFrameHeight - 1; diff --git a/ConsoleUI/GameInstanceListScreen.cs b/ConsoleUI/GameInstanceListScreen.cs index e90bfa4594..65f621d023 100644 --- a/ConsoleUI/GameInstanceListScreen.cs +++ b/ConsoleUI/GameInstanceListScreen.cs @@ -210,7 +210,7 @@ public static bool TryGetInstance(ConsoleTheme theme, var regMgr = RegistryManager.Instance(ksp, repoData); repoData.Prepopulate(regMgr.registry.Repositories.Values.ToList(), progress); - var compat = regMgr.registry.CompatibleModules(ksp.VersionCriteria()); + var compat = regMgr.registry.CompatibleModules(ksp.StabilityToleranceConfig, ksp.VersionCriteria()); } catch (RegistryInUseKraken k) { diff --git a/ConsoleUI/InstallScreen.cs b/ConsoleUI/InstallScreen.cs index e3595c642f..62af7d4fe1 100644 --- a/ConsoleUI/InstallScreen.cs +++ b/ConsoleUI/InstallScreen.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; +using CKAN.Configuration; using CKAN.ConsoleUI.Toolkit; namespace CKAN.ConsoleUI { @@ -60,12 +61,13 @@ public override void Run(Action? process = null) var regMgr = RegistryManager.Instance(manager.CurrentInstance, repoData); var registry = regMgr.registry; + var stabilityTolerance = manager.CurrentInstance.StabilityToleranceConfig; // GUI prompts user to choose recs/sugs, // CmdLine assumes recs and ignores sugs if (plan.Install.Count > 0) { // Track previously rejected optional dependencies and don't prompt for them again. - DependencyScreen ds = new DependencyScreen(theme, manager, registry, userAgent, plan, rejected, debug); + DependencyScreen ds = new DependencyScreen(theme, manager, manager.CurrentInstance, registry, userAgent, plan, rejected, debug); if (ds.HaveOptions()) { LaunchSubScreen(ds); } @@ -85,7 +87,7 @@ public override void Run(Action? process = null) if (plan.Install.Count > 0) { var iList = plan.Install .Select(m => Utilities.DefaultIfThrows(() => - registry.LatestAvailable(m.identifier, + registry.LatestAvailable(m.identifier, stabilityTolerance, manager.CurrentInstance.VersionCriteria(), null, registry.InstalledModules @@ -94,7 +96,7 @@ public override void Run(Action? process = null) plan.Install)) ?? m) .ToArray(); - inst.InstallList(iList, resolvOpts, regMgr, ref possibleConfigOnlyDirs, userAgent, dl); + inst.InstallList(iList, resolvOpts(stabilityTolerance), regMgr, ref possibleConfigOnlyDirs, userAgent, dl); plan.Install.Clear(); } if (plan.Upgrade.Count > 0) { @@ -109,7 +111,7 @@ public override void Run(Action? process = null) plan.Upgrade.Clear(); } if (plan.Replace.Count > 0) { - inst.Replace(AllReplacements(plan.Replace), resolvOpts, dl, ref possibleConfigOnlyDirs, regMgr, true); + inst.Replace(AllReplacements(plan.Replace), resolvOpts(stabilityTolerance), dl, ref possibleConfigOnlyDirs, regMgr, true); } trans.Complete(); @@ -214,7 +216,8 @@ private IEnumerable AllReplacements(IEnumerable ident foreach (string id in identifiers) { var repl = registry.GetReplacement( - id, manager.CurrentInstance.VersionCriteria()); + id, manager.CurrentInstance.StabilityToleranceConfig, + manager.CurrentInstance.VersionCriteria()); if (repl != null) { yield return repl; } @@ -222,7 +225,8 @@ private IEnumerable AllReplacements(IEnumerable ident } } - private static readonly RelationshipResolverOptions resolvOpts = new RelationshipResolverOptions() { + private static RelationshipResolverOptions resolvOpts(StabilityToleranceConfig stabTolCfg) + => new RelationshipResolverOptions(stabTolCfg) { with_all_suggests = false, with_suggests = false, with_recommends = false, diff --git a/ConsoleUI/ModInfoScreen.cs b/ConsoleUI/ModInfoScreen.cs index df8d9a35ff..5a277045db 100644 --- a/ConsoleUI/ModInfoScreen.cs +++ b/ConsoleUI/ModInfoScreen.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; +using CKAN.Configuration; using CKAN.Versioning; using CKAN.ConsoleUI.Toolkit; @@ -18,6 +19,7 @@ public class ModInfoScreen : ConsoleScreen { /// /// The visual theme to use to draw the dialog /// Game instance manager containing game instances + /// Game instance /// Registry of the current instance for finding mods /// HTTP useragent string to use /// Plan of other mods to be added or removed @@ -26,6 +28,7 @@ public class ModInfoScreen : ConsoleScreen { /// True if debug options should be available, false otherwise public ModInfoScreen(ConsoleTheme theme, GameInstanceManager mgr, + GameInstance instance, Registry registry, string? userAgent, ChangePlan cp, @@ -41,8 +44,7 @@ public ModInfoScreen(ConsoleTheme theme, this.registry = registry; this.userAgent = userAgent; this.upgradeable = upgradeable - ?? registry.CheckUpgradeable(manager.CurrentInstance, - new HashSet()) + ?? registry.CheckUpgradeable(instance, new HashSet()) [true]; int midL = (Console.WindowWidth / 2) - 1; @@ -89,7 +91,7 @@ public ModInfoScreen(ConsoleTheme theme, { AddObject(new ConsoleLabel( midL / 2, 5, (midL / 2) + 9, - () => "Install:", + () => Properties.Resources.ModInfoInstall, null, th => th.DimLabelFg )); @@ -104,7 +106,21 @@ public ModInfoScreen(ConsoleTheme theme, )); int depsBot = addDependencies(); - int versBot = addVersionDisplay(); + int versBot = addVersionDisplay(instance.StabilityToleranceConfig); + + if (!mod.IsDLC) + { + stabilityToleranceButtons = new ReleaseStatusComboButtons( + 1, depsBot + 1, + Properties.Resources.ModInfoStabilityToleranceHeader, + Properties.Resources.ModInfoStabilityToleranceNull, + instance.StabilityToleranceConfig.ModStabilityTolerance(mod.identifier)); + stabilityToleranceButtons.SelectionChanged += () => + instance.StabilityToleranceConfig.SetModStabilityTolerance( + mod.identifier, stabilityToleranceButtons.Selection); + AddObject(stabilityToleranceButtons); + depsBot += 5; + } AddObject(new ConsoleFrame( 1, Math.Max(depsBot, versBot) + 1, -1, -1, @@ -124,10 +140,9 @@ public ModInfoScreen(ConsoleTheme theme, tb.AddLine(mod.description); } AddObject(tb); - if (!ChangePlan.IsAnyAvailable(registry, mod.identifier)) { + if (!ChangePlan.IsAnyAvailable(registry, instance.StabilityToleranceConfig, mod.identifier)) { tb.AddLine(Properties.Resources.ModInfoUnavailableWarning); } - tb.AddScrollBindings(this, theme); AddTip(Properties.Resources.Esc, Properties.Resources.Back); AddBinding(Keys.Escape, (object sender) => false); @@ -342,7 +357,7 @@ rel is ModuleRelationshipDescriptor mrd // This can be null for manually installed mods => registry.InstalledModule(identifier)?.InstallTime; - private int addVersionDisplay() + private int addVersionDisplay(StabilityToleranceConfig stabilityTolerance) { int boxLeft = (Console.WindowWidth / 2) + 1, boxTop = 3; @@ -350,10 +365,10 @@ private int addVersionDisplay() boxH = 5; if (manager.CurrentInstance != null - && ChangePlan.IsAnyAvailable(registry, mod.identifier)) { + && ChangePlan.IsAnyAvailable(registry, stabilityTolerance, mod.identifier)) { var inst = registry.GetInstalledVersion(mod.identifier); - var latest = registry.LatestAvailable(mod.identifier, null); + var latest = registry.LatestAvailable(mod.identifier, stabilityTolerance, null); var others = registry.AvailableByIdentifier(mod.identifier) .Except(new[] { inst, latest }.OfType()) .OfType() @@ -368,7 +383,7 @@ private int addVersionDisplay() if (latestIsInstalled) { - if (registry.GetReplacement(mod.identifier, + if (registry.GetReplacement(mod.identifier, stabilityTolerance, manager.CurrentInstance.VersionCriteria()) is ModuleReplacement mr) { @@ -601,13 +616,14 @@ private void Download() { "forum.kerbalspaceprogram.com", "KSP Forums" } }; - private readonly GameInstanceManager manager; - private readonly IRegistryQuerier registry; - private readonly string? userAgent; - private readonly List upgradeable; - private readonly ChangePlan plan; - private readonly CkanModule mod; - private readonly bool debug; + private readonly GameInstanceManager manager; + private readonly IRegistryQuerier registry; + private readonly string? userAgent; + private readonly List upgradeable; + private readonly ChangePlan plan; + private readonly CkanModule mod; + private readonly ReleaseStatusComboButtons? stabilityToleranceButtons; + private readonly bool debug; } } diff --git a/ConsoleUI/ModListScreen.cs b/ConsoleUI/ModListScreen.cs index 0a8f544ac2..b426ef18a0 100644 --- a/ConsoleUI/ModListScreen.cs +++ b/ConsoleUI/ModListScreen.cs @@ -45,6 +45,10 @@ public ModListScreen(ConsoleTheme theme, this.repoData = repoData; this.userAgent = userAgent; + if (manager.CurrentInstance != null) { + manager.CurrentInstance.StabilityToleranceConfig.Changed += StabilityToleranceConfig_Changed; + } + moduleList = new ConsoleListBox( 1, 4, -1, -2, GetAllMods(), @@ -199,8 +203,8 @@ is int i and > 0 () => moduleList.Selection != null ); moduleList.AddBinding(Keys.Enter, (object sender) => { - if (moduleList.Selection != null) { - LaunchSubScreen(new ModInfoScreen(theme, manager, registry, userAgent, + if (moduleList.Selection != null && manager.CurrentInstance != null) { + LaunchSubScreen(new ModInfoScreen(theme, manager, manager.CurrentInstance, registry, userAgent, plan, moduleList.Selection, upgradeableGroups?[true], debug)); } return true; @@ -219,7 +223,9 @@ is int i and > 0 moduleList.AddTip("+", Properties.Resources.ModListReplaceTip, () => moduleList.Selection != null && manager.CurrentInstance != null - && registry.GetReplacement(moduleList.Selection.identifier, manager.CurrentInstance.VersionCriteria()) != null + && registry.GetReplacement(moduleList.Selection.identifier, + manager.CurrentInstance.StabilityToleranceConfig, + manager.CurrentInstance.VersionCriteria()) != null ); moduleList.AddBinding(Keys.Plus, (object sender) => { if (moduleList.Selection != null && !moduleList.Selection.IsDLC && manager.CurrentInstance != null) { @@ -228,7 +234,9 @@ is int i and > 0 } else if (registry.IsInstalled(moduleList.Selection.identifier, false) && (upgradeableGroups?[true].Any(upg => upg.identifier == moduleList.Selection.identifier) ?? false)) { plan.ToggleUpgrade(moduleList.Selection); - } else if (registry.GetReplacement(moduleList.Selection.identifier, manager.CurrentInstance.VersionCriteria()) != null) { + } else if (registry.GetReplacement(moduleList.Selection.identifier, + manager.CurrentInstance.StabilityToleranceConfig, + manager.CurrentInstance.VersionCriteria()) != null) { plan.ToggleReplace(moduleList.Selection.identifier); } } @@ -425,37 +433,42 @@ private bool UpgradeAll() private bool ViewSuggestions() { - ChangePlan reinstall = new ChangePlan(); - foreach (InstalledModule im in registry.InstalledModules) { - // Only check mods that are still available - try { - if (registry.LatestAvailable(im.identifier, manager.CurrentInstance?.VersionCriteria()) != null) { - reinstall.Install.Add(im.Module); + if (manager.CurrentInstance != null) + { + ChangePlan reinstall = new ChangePlan(); + foreach (InstalledModule im in registry.InstalledModules) { + // Only check mods that are still available + try { + if (registry.LatestAvailable(im.identifier, + manager.CurrentInstance.StabilityToleranceConfig, + manager.CurrentInstance.VersionCriteria()) != null) { + reinstall.Install.Add(im.Module); + } + } catch { + // The registry object badly needs an IsAvailable check } - } catch { - // The registry object badly needs an IsAvailable check } - } - try { - DependencyScreen ds = new DependencyScreen(theme, manager, registry, userAgent, reinstall, new HashSet(), debug); - if (ds.HaveOptions()) { - LaunchSubScreen(ds); - bool needRefresh = false; - // Copy the right ones into our real plan - foreach (CkanModule mod in reinstall.Install) { - if (!registry.IsInstalled(mod.identifier, false)) { - plan.Install.Add(mod); - needRefresh = true; + try { + DependencyScreen ds = new DependencyScreen(theme, manager, manager.CurrentInstance, registry, userAgent, reinstall, new HashSet(), debug); + if (ds.HaveOptions()) { + LaunchSubScreen(ds); + bool needRefresh = false; + // Copy the right ones into our real plan + foreach (CkanModule mod in reinstall.Install) { + if (!registry.IsInstalled(mod.identifier, false)) { + plan.Install.Add(mod); + needRefresh = true; + } } + if (needRefresh) { + RefreshList(); + } + } else { + RaiseError(Properties.Resources.ModListAuditNotFound); } - if (needRefresh) { - RefreshList(); - } - } else { - RaiseError(Properties.Resources.ModListAuditNotFound); + } catch (ModuleNotFoundKraken k) { + RaiseError("{0} {1}: {2}", k.module, k.version ?? "", k.Message); } - } catch (ModuleNotFoundKraken k) { - RaiseError("{0} {1}: {2}", k.module, k.version ?? "", k.Message); } return true; } @@ -483,7 +496,8 @@ private bool UpdateRegistry(bool showNewModsPrompt = true) LaunchSubScreen(ps, () => { if (manager.CurrentInstance != null) { - var availBefore = registry.CompatibleModules(manager.CurrentInstance.VersionCriteria()) + var availBefore = registry.CompatibleModules(manager.CurrentInstance.StabilityToleranceConfig, + manager.CurrentInstance.VersionCriteria()) .Select(l => l.identifier) .ToHashSet(); recent.Clear(); @@ -499,9 +513,8 @@ private bool UpdateRegistry(bool showNewModsPrompt = true) ps.RaiseError("{0}", ex.Message + ex.StackTrace); } // Update recent with mods that were updated in this pass - foreach (CkanModule mod in registry.CompatibleModules( - manager.CurrentInstance.VersionCriteria() - )) { + foreach (CkanModule mod in registry.CompatibleModules(manager.CurrentInstance.StabilityToleranceConfig, + manager.CurrentInstance.VersionCriteria())) { if (!availBefore.Contains(mod.identifier)) { recent.Add(mod.identifier); } @@ -551,6 +564,12 @@ private bool InstanceSettings() return true; } + private void StabilityToleranceConfig_Changed(string? identifier, + ReleaseStatus? relStat) + { + RefreshList(); + } + private bool SelectInstall() { if (manager.CurrentInstance != null) @@ -561,6 +580,8 @@ private bool SelectInstall() LaunchSubScreen(new GameInstanceListScreen(theme, manager, repoData, userAgent)); if (!prevInst.Equals(manager.CurrentInstance)) { // Game instance changed, reset everything + prevInst.StabilityToleranceConfig.Changed -= StabilityToleranceConfig_Changed; + manager.CurrentInstance.StabilityToleranceConfig.Changed += StabilityToleranceConfig_Changed; plan.Reset(); regMgr = RegistryManager.Instance(manager.CurrentInstance, repoData); registry = regMgr.registry; @@ -608,6 +629,7 @@ private List GetAllMods(bool force = false) { if (manager.CurrentInstance != null) { + var stabilityTolerance = manager.CurrentInstance.StabilityToleranceConfig; timeSinceUpdate = repoData.LastUpdate(registry.Repositories.Values); ScanForMods(); if (allMods == null || force) { @@ -616,9 +638,9 @@ private List GetAllMods(bool force = false) UpdateRegistry(false); } var crit = manager.CurrentInstance.VersionCriteria(); - allMods = new List(registry.CompatibleModules(crit)); + allMods = new List(registry.CompatibleModules(stabilityTolerance, crit)); foreach (InstalledModule im in registry.InstalledModules) { - var m = Utilities.DefaultIfThrows(() => registry.LatestAvailable(im.identifier, crit)); + var m = Utilities.DefaultIfThrows(() => registry.LatestAvailable(im.identifier, stabilityTolerance, crit)); if (m == null) { // Add unavailable installed mods to the list allMods.Add(im.Module); @@ -655,6 +677,7 @@ private bool InstallFromCkan() { if (manager.CurrentInstance != null) { + var stabilityTolerance = manager.CurrentInstance.StabilityToleranceConfig; var modules = InstallFromCkanDialog.ChooseCkanFiles(theme, manager.CurrentInstance); if (modules.Length > 0) { var crit = manager.CurrentInstance.VersionCriteria(); @@ -667,9 +690,9 @@ private bool InstallFromCkan() .Select(rel => // If there's a compatible match, return it // Metapackages aren't intending to prompt users to choose providing mods - rel.ExactMatch(regMgr.registry, crit, installed, modules) + rel.ExactMatch(regMgr.registry, stabilityTolerance, crit, installed, modules) // Otherwise look for incompatible - ?? rel.ExactMatch(regMgr.registry, null, installed, modules)) + ?? rel.ExactMatch(regMgr.registry, stabilityTolerance, null, installed, modules)) .OfType() ?? Enumerable.Empty()))); LaunchSubScreen(new InstallScreen(theme, manager, repoData, userAgent, cp, debug)); @@ -872,7 +895,8 @@ public InstallStatus GetModStatus(GameInstanceManager manager, string identifier, List upgradeable) { - if (registry.IsInstalled(identifier, false)) { + if (manager.CurrentInstance != null + && registry.IsInstalled(identifier, false)) { if (Remove.Contains(identifier)) { return InstallStatus.Removing; } else if (upgradeable.Any(m => m.identifier == identifier)) { @@ -885,10 +909,11 @@ public InstallStatus GetModStatus(GameInstanceManager manager, return InstallStatus.AutoDetected; } else if (Replace.Contains(identifier)) { return InstallStatus.Replacing; - } else if (manager.CurrentInstance != null - && registry.GetReplacement(identifier, manager.CurrentInstance.VersionCriteria()) != null) { + } else if (registry.GetReplacement(identifier, + manager.CurrentInstance.StabilityToleranceConfig, + manager.CurrentInstance.VersionCriteria()) != null) { return InstallStatus.Replaceable; - } else if (!IsAnyAvailable(registry, identifier)) { + } else if (!IsAnyAvailable(registry, manager.CurrentInstance.StabilityToleranceConfig, identifier)) { return InstallStatus.Unavailable; } else if (registry.InstalledModule(identifier)?.AutoInstalled ?? false) { return InstallStatus.AutoInstalled; @@ -911,14 +936,15 @@ public InstallStatus GetModStatus(GameInstanceManager manager, /// Check whether an identifier is anywhere in the registry. /// /// Reference to registry to query + /// Mod stability settings /// Mod name to Find /// /// True if there are any versions of this mod available, false otherwise. /// - public static bool IsAnyAvailable(IRegistryQuerier registry, string identifier) + public static bool IsAnyAvailable(IRegistryQuerier registry, StabilityToleranceConfig stabilityTolerance, string identifier) { try { - registry.LatestAvailable(identifier, null); + registry.LatestAvailable(identifier, stabilityTolerance, null); return true; } catch (ModuleNotFoundKraken) { return false; diff --git a/ConsoleUI/Properties/Resources.resx b/ConsoleUI/Properties/Resources.resx index 7944f2cabd..c947d1a0cb 100644 --- a/ConsoleUI/Properties/Resources.resx +++ b/ConsoleUI/Properties/Resources.resx @@ -201,6 +201,7 @@ Example: {0} Add Game Instance Edit Game Instance + Stability tolerance: Mod List Sources Additional Compatible Versions Index @@ -251,6 +252,9 @@ Edit authentication tokens now? By {0} Licence: Download: + Install: + Mod stability tolerance override: + Do not override Description NOTE: This mod is installed but no longer available. diff --git a/ConsoleUI/ReleaseStatusComboButtons.cs b/ConsoleUI/ReleaseStatusComboButtons.cs new file mode 100644 index 0000000000..3da383ba02 --- /dev/null +++ b/ConsoleUI/ReleaseStatusComboButtons.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; + +using CKAN.Extensions; +using CKAN.ConsoleUI.Toolkit; + +namespace CKAN.ConsoleUI { + + /// + /// Combo buttons for choosing release statuses + /// + public class ReleaseStatusComboButtons : ConsoleRadioButtons + { + /// X coordinate of left edge + /// Y coordinate of top edge + /// Label to show above the list + /// Allow null values if non-null and represent with this string + /// Initially selected value + public ReleaseStatusComboButtons(int l, int t, + string header, + string? nullValueString, + ReleaseStatus? value) + : base(l, t, + l + UIWidth + nonNullOptions.Max(rs => baseRenderer(rs)?.Length ?? 0) - 1, + t + nonNullOptions.Length + (nullValueString != null ? 1 : 0), + header, + (nullValueString != null ? nonNullOptions.Prepend(null) + : nonNullOptions) + .ToArray(), + value) + { + this.nullValueString = nullValueString; + } + + /// + protected override string Renderer(ReleaseStatus? rs) + => baseRenderer(rs) ?? nullValueString ?? ""; + + private static string? baseRenderer(ReleaseStatus? rs) + => rs.HasValue ? $"{rs.LocalizeName()} - {rs.LocalizeDescription()}" + : null; + + private readonly string? nullValueString; + + private static readonly ReleaseStatus?[] nonNullOptions = + Enum.GetValues(typeof(ReleaseStatus)) + .OfType() + .OrderBy(relStat => (int)relStat) + .OfType() + .ToArray(); + } +} diff --git a/ConsoleUI/Toolkit/ConsoleRadioButtons.cs b/ConsoleUI/Toolkit/ConsoleRadioButtons.cs new file mode 100644 index 0000000000..d02e86c0e9 --- /dev/null +++ b/ConsoleUI/Toolkit/ConsoleRadioButtons.cs @@ -0,0 +1,163 @@ +using System; +using System.Text.RegularExpressions; +using System.Collections.Generic; + +namespace CKAN.ConsoleUI.Toolkit { + + /// + /// A group of radio buttons that let the user choose one of several options + /// + /// Type of object represented by each option + public class ConsoleRadioButtons : ScreenObject { + + /// + /// Initialize a radio button group + /// + /// X coordinate of left edge + /// Y coordinate of top edge + /// X coordinate of right edge + /// Y coordinate of bottom edge + /// Label to show above the list + /// Values represented by the radio buttons + /// Initially selected value + public ConsoleRadioButtons(int l, int t, int r, int b, + string header, + IList dataList, + RowT value) + : base(l, t, r, b) + { + rows = dataList; + selectedRow = rows.IndexOf(value); + this.header = header; + } + + /// + /// Fired when the user picks a different radio button + /// + public event Action? SelectionChanged; + + /// + /// Currently selected row's object + /// + public RowT Selection => rows[selectedRow]; + + /// + /// Handle key bindings for the list box. + /// Mostly moving around wiht cursor keys. + /// + /// Key the user pressed + public override void OnKeyPress(ConsoleKeyInfo k) + { + switch (k.Key) { + case ConsoleKey.UpArrow: + if (selectedRow > 0) { + --selectedRow; + SelectionChanged?.Invoke(); + } + break; + case ConsoleKey.DownArrow: + if (selectedRow < rows.Count - 1) { + ++selectedRow; + SelectionChanged?.Invoke(); + } + break; + case ConsoleKey.Home: + selectedRow = 0; + SelectionChanged?.Invoke(); + break; + case ConsoleKey.End: + selectedRow = rows.Count - 1; + SelectionChanged?.Invoke(); + break; + case ConsoleKey.Tab: + Blur(!k.Modifiers.HasFlag(ConsoleModifiers.Shift)); + break; + default: + // Go backwards if k.Modifiers.HasFlag(ConsoleModifiers.Shift) + if (!char.IsControl(k.KeyChar) + && (k.Modifiers | ConsoleModifiers.Shift) == ConsoleModifiers.Shift) { + + bool forward = !k.Modifiers.HasFlag(ConsoleModifiers.Shift); + // Find first row after current, wrap + int startRow = forward + ? selectedRow + 1 + : selectedRow + rows.Count - 1; + for (int i = 0; i < rows.Count; ++i) { + int candidateRow = (forward + ? startRow + i + : startRow + rows.Count - i + ) % rows.Count; + if (nonAlphaNumPrefix.Replace(Renderer(rows[candidateRow]), "") + .StartsWith($"{k.KeyChar}", + StringComparison.CurrentCultureIgnoreCase)) { + selectedRow = candidateRow; + SelectionChanged?.Invoke(); + break; + } + } + } + break; + } + } + + /// + /// Move the screen cursor to the middle the active radio button + /// + public override void PlaceCursor() + { + Console.SetCursorPosition(GetLeft() + 2, GetTop() + selectedRow + 1); + } + + /// + public override void Draw(ConsoleTheme theme, bool focused) + { + int l = GetLeft(), r = GetRight(), + t = GetTop(), b = GetBottom(), + w = r - l + 1; + + // Prevent selection from running off the end of the list + if (selectedRow > rows.Count - 1) { + selectedRow = rows.Count - 1; + } + + // Ensure selection is not before the top of the list + if (selectedRow < 0) { + selectedRow = 0; + } + + Console.SetCursorPosition(l, t); + Console.BackgroundColor = theme.MainBg; + Console.ForegroundColor = theme.RadioButtonsHeaderFg; + Console.Write(FormatExactWidth(header, w)); + + Console.BackgroundColor = theme.RadioButtonsGroupBg; + Console.ForegroundColor = theme.RadioButtonsGroupFg; + for (int index = 0, y = t + 1; index < rows.Count && y <= b; ++index, ++y) { + Console.SetCursorPosition(l, y); + Console.Write(" ({0}) {1} ", + index == selectedRow ? Symbols.dot : " ", + FormatExactWidth(Renderer(rows[index]), w - UIWidth)); + } + } + + /// + /// The number of extra characters we draw per line in addition to the value strings + /// + protected const int UIWidth = 6; + + /// + /// Generate a display string for a given row + /// + /// The row to display + /// A string representing the given row + protected virtual string Renderer(RowT row) => row?.ToString() ?? ""; + + private readonly IList rows; + private int selectedRow; + private readonly string header; + + private static readonly Regex nonAlphaNumPrefix = + new Regex("^[^a-zA-Z0-9]*", + RegexOptions.Compiled); + } +} diff --git a/ConsoleUI/Toolkit/ConsoleTextBox.cs b/ConsoleUI/Toolkit/ConsoleTextBox.cs index 426b4f7bf9..e95cf71819 100644 --- a/ConsoleUI/Toolkit/ConsoleTextBox.cs +++ b/ConsoleUI/Toolkit/ConsoleTextBox.cs @@ -238,16 +238,36 @@ public void AddScrollBindings(ScreenContainer cont, ConsoleTheme theme, bool dra } } - /// - /// Tell the container we can't receive focus - /// - public override bool Focusable() { return false; } + /// + public override bool Focusable() => needScroll; + + /// + public override void PlaceCursor() + { + Console.SetCursorPosition(GetLeft(), GetTop()); + } + + /// + public override void OnKeyPress(ConsoleKeyInfo k) + { + switch (k.Key) { + case ConsoleKey.Home: ScrollToTop(); break; + case ConsoleKey.End: ScrollToBottom(); break; + case ConsoleKey.PageUp: ScrollUp(); break; + case ConsoleKey.PageDown: ScrollDown(); break; + case ConsoleKey.UpArrow: ScrollUp(1); break; + case ConsoleKey.DownArrow: ScrollDown(1); break; + case ConsoleKey.Tab: + Blur(!k.Modifiers.HasFlag(ConsoleModifiers.Shift)); + break; + } + } - private bool needScroll = false; - private int prevTextW; - private readonly bool scrollToBottom; - private int topLine; - private readonly TextAlign align; + private bool needScroll = false; + private int prevTextW; + private readonly bool scrollToBottom; + private int topLine; + private readonly TextAlign align; private readonly SynchronizedCollection lines = new SynchronizedCollection(); private readonly SynchronizedCollection displayLines = new SynchronizedCollection(); private readonly Func? getBgColor; diff --git a/ConsoleUI/Toolkit/ConsoleTheme.cs b/ConsoleUI/Toolkit/ConsoleTheme.cs index 986327eef9..6ea4d8302d 100644 --- a/ConsoleUI/Toolkit/ConsoleTheme.cs +++ b/ConsoleUI/Toolkit/ConsoleTheme.cs @@ -29,22 +29,22 @@ public class ConsoleTheme { /// Background color for exit screen /// public ConsoleColor? ExitOuterBg; - + /// /// Background color for info pane of exit screen /// public ConsoleColor ExitInnerBg; - + /// /// Foreground color for normal text on exit screen /// public ConsoleColor ExitNormalFg; - + /// /// Foreground color for highlighted text on exit screen /// public ConsoleColor ExitHighlightFg; - + /// /// Foreground color for links on exit screen /// @@ -136,6 +136,19 @@ public class ConsoleTheme { /// public ConsoleColor ListBoxSelectedFg; + /// + /// Text color for the label above a radio buttons group + /// + public ConsoleColor RadioButtonsHeaderFg; + /// + /// Background for radio buttons group + /// + public ConsoleColor RadioButtonsGroupBg; + /// + /// Foreground for radio buttons group + /// + public ConsoleColor RadioButtonsGroupFg; + /// /// Background for scroll bars /// @@ -246,7 +259,7 @@ public class ConsoleTheme { /// Foreground for important/abnormal box frames /// public ConsoleColor AlertFrameFg; - + /// /// Available themes /// @@ -282,6 +295,9 @@ public class ConsoleTheme { ListBoxUnselectedFg = ConsoleColor.Black, ListBoxSelectedBg = ConsoleColor.DarkGreen, ListBoxSelectedFg = ConsoleColor.White, + RadioButtonsHeaderFg = ConsoleColor.Gray, + RadioButtonsGroupBg = ConsoleColor.DarkCyan, + RadioButtonsGroupFg = ConsoleColor.Black, ScrollBarBg = ConsoleColor.DarkBlue, ScrollBarFg = ConsoleColor.DarkCyan, MenuBg = ConsoleColor.Gray, @@ -340,6 +356,9 @@ public class ConsoleTheme { ListBoxUnselectedFg = ConsoleColor.DarkGreen, ListBoxSelectedBg = ConsoleColor.Black, ListBoxSelectedFg = ConsoleColor.Green, + RadioButtonsHeaderFg = ConsoleColor.DarkGreen, + RadioButtonsGroupBg = ConsoleColor.Black, + RadioButtonsGroupFg = ConsoleColor.DarkGreen, ScrollBarBg = ConsoleColor.Black, ScrollBarFg = ConsoleColor.DarkGreen, MenuBg = ConsoleColor.DarkGreen, diff --git a/ConsoleUI/Toolkit/ScreenObject.cs b/ConsoleUI/Toolkit/ScreenObject.cs index fadd89fe24..a458f87c49 100644 --- a/ConsoleUI/Toolkit/ScreenObject.cs +++ b/ConsoleUI/Toolkit/ScreenObject.cs @@ -147,19 +147,19 @@ protected void DrawScrollbar(ConsoleTheme theme, int r, int t, int b, int dragRo /// /// X coordinate of left edge of dialog /// - protected int GetLeft() { return Formatting.ConvertCoord(left, Console.WindowWidth); } + protected int GetLeft() => Formatting.ConvertCoord(left, Console.WindowWidth); /// /// Y coordinate of top edge of dialog /// - protected int GetTop() { return Formatting.ConvertCoord(top, Console.WindowHeight); } + protected int GetTop() => Formatting.ConvertCoord(top, Console.WindowHeight); /// /// X coordinate of right edge of dialog /// - protected int GetRight() { return Formatting.ConvertCoord(right, Console.WindowWidth); } + protected int GetRight() => Formatting.ConvertCoord(right, Console.WindowWidth); /// /// Y coordinate of bottom edge of dialog /// - protected int GetBottom() { return Formatting.ConvertCoord(bottom, Console.WindowHeight); } + protected int GetBottom() => Formatting.ConvertCoord(bottom, Console.WindowHeight); /// /// Draw the UI element @@ -171,7 +171,7 @@ protected void DrawScrollbar(ConsoleTheme theme, int r, int t, int b, int dragRo /// /// Return whether the UI element can accept focus /// - public virtual bool Focusable() { return true; } + public virtual bool Focusable() => true; /// /// Place focus based on the UI element's positioning /// diff --git a/Core/CompatibleGameVersions.cs b/Core/Configuration/CompatibleGameVersions.cs similarity index 95% rename from Core/CompatibleGameVersions.cs rename to Core/Configuration/CompatibleGameVersions.cs index 2fd9c3e3f1..54310487ff 100644 --- a/Core/CompatibleGameVersions.cs +++ b/Core/Configuration/CompatibleGameVersions.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; -namespace CKAN +namespace CKAN.Configuration { [JsonConverter(typeof(CompatibleGameVersionsConverter))] public class CompatibleGameVersions diff --git a/Core/Configuration/IConfiguration.cs b/Core/Configuration/IConfiguration.cs index e99946899e..1a26b732b4 100644 --- a/Core/Configuration/IConfiguration.cs +++ b/Core/Configuration/IConfiguration.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -71,5 +72,7 @@ bool TryGetAuthToken(string host, /// true if user wants to use nightly builds from S3, false to use releases from GitHub /// bool? DevBuilds { get; set; } + + event PropertyChangedEventHandler? PropertyChanged; } } diff --git a/Core/Configuration/JsonConfiguration.cs b/Core/Configuration/JsonConfiguration.cs index 413c48f426..cd3e6de24a 100644 --- a/Core/Configuration/JsonConfiguration.cs +++ b/Core/Configuration/JsonConfiguration.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Collections.Generic; using System.IO; using System.Linq; @@ -6,6 +7,7 @@ using System.Runtime.Versioning; #endif using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Newtonsoft.Json; @@ -70,6 +72,8 @@ public JsonConfiguration(string? newConfig = null) LoadConfig(); } + public event PropertyChangedEventHandler? PropertyChanged; + // The standard location of the config file. Where this actually points is platform dependent, // but it's the same place as the downloads folder. The location can be overwritten with the // CKAN_CONFIG_FILE environment variable. @@ -216,6 +220,8 @@ public string[] GlobalInstallFilters { config.GlobalInstallFilters = value; SaveConfig(); + // Refresh the Contents tab + OnPropertyChanged(); } } @@ -242,6 +248,11 @@ public bool? DevBuilds } } + private void OnPropertyChanged([CallerMemberName] string? name = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + } + // // Save the JSON configuration file. // diff --git a/Core/Configuration/StabilityToleranceConfig.cs b/Core/Configuration/StabilityToleranceConfig.cs new file mode 100644 index 0000000000..29bae7e75c --- /dev/null +++ b/Core/Configuration/StabilityToleranceConfig.cs @@ -0,0 +1,84 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.ComponentModel; + +using Newtonsoft.Json; + +namespace CKAN.Configuration +{ + [JsonObject(MemberSerialization.OptIn)] + public class StabilityToleranceConfig + { + public StabilityToleranceConfig(string path) + { + this.path = path; + try + { + JsonConvert.PopulateObject(File.ReadAllText(this.path), this); + } + catch + { + // File doesn't exist yet, we can create it at save + } + } + + public bool Save() + { + try + { + File.WriteAllText(path, JsonConvert.SerializeObject(this, Formatting.Indented)); + return true; + } + catch + { + return false; + } + } + + public ReleaseStatus? ModStabilityTolerance(string identifier) + => modStabilityTolerance.TryGetValue(identifier, out ReleaseStatus relStat) + ? relStat + : null; + + public void SetModStabilityTolerance(string identifier, ReleaseStatus? relStat) + { + if (relStat is ReleaseStatus rs) + { + modStabilityTolerance[identifier] = rs; + } + else + { + modStabilityTolerance.Remove(identifier); + } + Changed?.Invoke(identifier, relStat); + Save(); + } + + public ReleaseStatus OverallStabilityTolerance + { + get => overallStabilityTolerance; + set + { + overallStabilityTolerance = value; + Save(); + Changed?.Invoke(null, value); + } + } + + public ICollection OverriddenModIdentifiers => modStabilityTolerance.Keys; + + public event Action? Changed; + + [JsonProperty("overall_stability_tolerance", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(ReleaseStatus.stable)] + private ReleaseStatus overallStabilityTolerance = ReleaseStatus.stable; + + [JsonProperty("mod_stability_tolerance", NullValueHandling = NullValueHandling.Ignore)] + private readonly SortedDictionary modStabilityTolerance = + new SortedDictionary(); + + [JsonIgnore] + private readonly string path; + } +} diff --git a/Core/Configuration/Win32RegistryConfiguration.cs b/Core/Configuration/Win32RegistryConfiguration.cs index 32b7cddd51..b499817831 100644 --- a/Core/Configuration/Win32RegistryConfiguration.cs +++ b/Core/Configuration/Win32RegistryConfiguration.cs @@ -3,6 +3,8 @@ using System.Linq; using System.IO; using System.Diagnostics.CodeAnalysis; +using System.ComponentModel; + #if NET5_0_OR_GREATER using System.Runtime.Versioning; #endif @@ -191,6 +193,10 @@ public void SetAuthToken(string host, string? token) /// public bool? DevBuilds { get; set; } + #pragma warning disable CS0067 + public event PropertyChangedEventHandler? PropertyChanged; + #pragma warning restore CS0067 + public static bool DoesRegistryConfigurationExist() { var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(CKAN_KEY_NO_PREFIX); diff --git a/Core/Converters/JsonReleaseStatusConverter.cs b/Core/Converters/JsonReleaseStatusConverter.cs new file mode 100644 index 0000000000..8a91291d0d --- /dev/null +++ b/Core/Converters/JsonReleaseStatusConverter.cs @@ -0,0 +1,27 @@ +using System; + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace CKAN +{ + public class JsonReleaseStatusConverter : StringEnumConverter + { + public override object? ReadJson(JsonReader reader, + Type objectType, + object? existingValue, + JsonSerializer serializer) + => reader.Value?.ToString() switch + { + "alpha" => ReleaseStatus.development, + "beta" => ReleaseStatus.testing, + null => ReleaseStatus.stable, + "" => throw new JsonException("Empty release_status string"), + _ => base.ReadJson(reader, objectType, + existingValue, serializer), + }; + + public override bool CanWrite => true; + public override bool CanConvert(Type object_type) => false; + } +} diff --git a/Core/Extensions/EnumerableExtensions.cs b/Core/Extensions/EnumerableExtensions.cs index 78f3ed8442..8408f871f4 100644 --- a/Core/Extensions/EnumerableExtensions.cs +++ b/Core/Extensions/EnumerableExtensions.cs @@ -11,9 +11,7 @@ namespace CKAN.Extensions public static class EnumerableExtensions { public static ICollection AsCollection(this IEnumerable source) - => source == null - ? throw new ArgumentNullException(nameof(source)) - : source is ICollection collection ? collection : source.ToArray(); + => source is ICollection collection ? collection : source.ToArray(); #if NET45 || NETSTANDARD2_0 @@ -23,14 +21,7 @@ public static ICollection AsCollection(this IEnumerable source) internal #endif static HashSet ToHashSet(this IEnumerable source) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - return new HashSet(source); - } + => new HashSet(source); #if NET45 public @@ -38,15 +29,8 @@ static HashSet ToHashSet(this IEnumerable source) internal #endif static HashSet ToHashSet(this IEnumerable source, - IEqualityComparer comparer) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - return new HashSet(source, comparer); - } + IEqualityComparer comparer) + => new HashSet(source, comparer); #endif @@ -99,21 +83,10 @@ public static ParallelQuery WithProgress(this ParallelQuery source, } public static IEnumerable Memoize(this IEnumerable source) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - else if (source is Memoized) - { - // Already memoized, don't wrap another layer - return source; - } - else - { - return new Memoized(source); - } - } + => source is Memoized + // Already memoized, don't wrap another layer + ? source + : new Memoized(source); public static void RemoveWhere(this Dictionary source, Func, bool> predicate) where K: class diff --git a/Core/Extensions/I18nExtensions.cs b/Core/Extensions/I18nExtensions.cs index 82b14a6a85..7cdb1e0100 100644 --- a/Core/Extensions/I18nExtensions.cs +++ b/Core/Extensions/I18nExtensions.cs @@ -8,7 +8,7 @@ namespace CKAN.Extensions public static class I18nExtensions { - public static string Localize(this Enum val) + public static string LocalizeDescription(this Enum val) => val.GetType() ?.GetMember(val.ToString()) ?.First() @@ -16,5 +16,13 @@ public static string Localize(this Enum val) ?.GetDescription() ?? ""; + public static string LocalizeName(this Enum val) + => val.GetType() + ?.GetMember(val.ToString()) + ?.First() + .GetCustomAttribute() + ?.GetName() + ?? ""; + } } diff --git a/Core/GameInstance.cs b/Core/GameInstance.cs index 7156621369..b00d10e3e7 100644 --- a/Core/GameInstance.cs +++ b/Core/GameInstance.cs @@ -11,6 +11,7 @@ using log4net; using Newtonsoft.Json; +using CKAN.Configuration; using CKAN.Games; using CKAN.Versioning; @@ -208,6 +209,14 @@ public string[] InstallFilters private string InstallFiltersFile => Path.Combine(CkanDir(), "install_filters.json"); + public StabilityToleranceConfig StabilityToleranceConfig + => stabilityToleranceConfig ??= new StabilityToleranceConfig(StabilityToleranceFile); + + private StabilityToleranceConfig? stabilityToleranceConfig; + + private string StabilityToleranceFile + => Path.Combine(CkanDir(), "stability_tolerance.json"); + #endregion #region KSP Directory Detection and Versioning diff --git a/Core/Meta.cs b/Core/Meta.cs index ef15043acd..6bd1dd2208 100644 --- a/Core/Meta.cs +++ b/Core/Meta.cs @@ -20,6 +20,11 @@ public static string GetProductName() public static readonly ModuleVersion ReleaseVersion = new ModuleVersion(GetVersion()); + public static bool IsNetKAN + => Assembly.GetExecutingAssembly() + .GetAssemblyAttribute() + .Title.Contains("NetKAN"); + public static string GetVersion(VersionFormat format = VersionFormat.Normal) { var version = Assembly diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 36e9887540..84520700aa 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -141,8 +141,7 @@ public void InstallList(ICollection modules, } var resolver = new RelationshipResolver(modules, null, options, registry_manager.registry, - instance.game, - instance.VersionCriteria()); + instance.game, instance.VersionCriteria()); var modsToInstall = resolver.ModList().ToList(); // Alert about attempts to install DLC before downloading or installing anything if (modsToInstall.Any(m => m.IsDLC)) @@ -850,8 +849,7 @@ public void UninstallList(IEnumerable mods, .Where(im => !revdep.Contains(im.identifier)) .Concat(installing?.Select(m => new InstalledModule(null, m, Array.Empty(), false)) ?? Array.Empty()) .ToList(), - instance.game, - instance.VersionCriteria()) + instance.game, instance.StabilityToleranceConfig, instance.VersionCriteria()) .Select(im => im.identifier)) .ToList(); @@ -1324,10 +1322,9 @@ public void Upgrade(ICollection modules, modules, modules.Select(m => registry.InstalledModule(m.identifier)?.Module) .OfType(), - RelationshipResolverOptions.DependsOnlyOpts(), + RelationshipResolverOptions.DependsOnlyOpts(instance.StabilityToleranceConfig), registry, - instance.game, - instance.VersionCriteria()); + instance.game, instance.VersionCriteria()); modules = resolver.ModList().ToArray(); var autoInstalled = modules.ToDictionary(m => m, resolver.IsAutoInstalled); @@ -1424,8 +1421,7 @@ public void Upgrade(ICollection modules, .Where(im => !removingIdents.Contains(im.identifier)) .Concat(modules.Select(m => new InstalledModule(null, m, Array.Empty(), false))) .ToList(), - instance.game, - instance.VersionCriteria()) + instance.game, instance.StabilityToleranceConfig, instance.VersionCriteria()) .ToList(); if (autoRemoving.Count > 0) { @@ -1584,7 +1580,7 @@ public static bool FindRecommendations(GameInstance var crit = instance.VersionCriteria(); var resolver = new RelationshipResolver(sourceModules.Where(m => !m.IsDLC), null, - RelationshipResolverOptions.KitchenSinkOpts(), + RelationshipResolverOptions.KitchenSinkOpts(instance.StabilityToleranceConfig), registry, instance.game, crit); var recommenders = resolver.Dependencies().ToHashSet(); @@ -1593,7 +1589,7 @@ public static bool FindRecommendations(GameInstance .Any(r => r is SelectionReason.Recommended { ProvidesIndex: 0 })) .ToHashSet(); var conflicting = new RelationshipResolver(toInstall.Concat(checkedRecs), null, - RelationshipResolverOptions.ConflictsOpts(), + RelationshipResolverOptions.ConflictsOpts(instance.StabilityToleranceConfig), registry, instance.game, crit) .ConflictList.Keys; // Don't check recommendations that conflict with installed or installing mods @@ -1621,7 +1617,7 @@ public static bool FindRecommendations(GameInstance .Select(m => m.identifier) .ToList()); - var opts = RelationshipResolverOptions.DependsOnlyOpts(); + var opts = RelationshipResolverOptions.DependsOnlyOpts(instance.StabilityToleranceConfig); supporters = resolver.Supporters(recommenders, recommenders.Concat(recommendations.Keys) .Concat(suggestions.Keys)) @@ -1768,6 +1764,7 @@ public static bool ImportFiles(HashSet files, } var installable = matched.Values.SelectMany(modules => modules) .Where(m => registry.IdentifierCompatible(m.identifier, + instance.StabilityToleranceConfig, instance.VersionCriteria())) .ToHashSet(); diff --git a/Core/Properties/Resources.resx b/Core/Properties/Resources.resx index 1f49f5bcdd..8edb2a43c1 100644 --- a/Core/Properties/Resources.resx +++ b/Core/Properties/Resources.resx @@ -341,4 +341,10 @@ Free up space on that device or change your settings to use another location. installed-{0} {0} (installed {1}) {0} (installed {1}, auto-installed) + Stable + Normal releases + Testing + Pre-releases for adventurous users + Development + Bleeding edge unstable diff --git a/Core/Registry/CompatibilitySorter.cs b/Core/Registry/CompatibilitySorter.cs index 935bb62ad1..4ddedf59b2 100644 --- a/Core/Registry/CompatibilitySorter.cs +++ b/Core/Registry/CompatibilitySorter.cs @@ -5,6 +5,7 @@ using log4net; +using CKAN.Configuration; using CKAN.Extensions; using CKAN.Versioning; @@ -24,13 +25,15 @@ public class CompatibilitySorter /// Dictionary mapping every identifier to the modules providing it /// Collection of found dlls /// Collection of installed DLCs - public CompatibilitySorter(GameVersionCriteria crit, + public CompatibilitySorter(StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria crit, IEnumerable> available, IDictionary> providers, IDictionary installed, ICollection dlls, IDictionary dlc) { + StabilityTolerance = stabilityTolerance; CompatibleVersions = crit; this.installed = installed; this.dlls = dlls; @@ -76,11 +79,16 @@ public CompatibilitySorter(GameVersionCriteria crit /// public readonly ConcurrentDictionary Compatible; + /// + /// The least stable category of modules to consider + /// + public readonly StabilityToleranceConfig StabilityTolerance; + public ICollection LatestCompatible { get { - latestCompatible ??= Compatible.Values.Select(avail => avail.Latest(CompatibleVersions)) + latestCompatible ??= Compatible.Values.Select(avail => avail.Latest(StabilityTolerance, CompatibleVersions)) .OfType() .ToList(); return latestCompatible; @@ -96,7 +104,7 @@ public ICollection LatestIncompatible { get { - latestIncompatible ??= Incompatible.Values.Select(avail => avail.Latest(null)) + latestIncompatible ??= Incompatible.Values.Select(avail => avail.Latest(StabilityTolerance)) .OfType() .ToList(); return latestIncompatible; diff --git a/Core/Registry/IRegistryQuerier.cs b/Core/Registry/IRegistryQuerier.cs index 9c568ea4a8..e38210cb46 100644 --- a/Core/Registry/IRegistryQuerier.cs +++ b/Core/Registry/IRegistryQuerier.cs @@ -29,7 +29,8 @@ public interface IRegistryQuerier /// Returns a simple array of the latest compatible module for each identifier for /// the specified game version. /// - IEnumerable CompatibleModules(GameVersionCriteria? ksp_version); + IEnumerable CompatibleModules(StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria? ksp_version); /// /// Get full JSON metadata string for a mod's available versions @@ -47,6 +48,7 @@ public interface IRegistryQuerier /// Throws if asked for a non-existent module. /// CkanModule? LatestAvailable(string identifier, + StabilityToleranceConfig stabilityTolerance, GameVersionCriteria? ksp_version, RelationshipDescriptor? relationship_descriptor = null, ICollection? installed = null, @@ -74,6 +76,7 @@ public interface IRegistryQuerier /// If no KSP version is provided, the latest module for *any* KSP version is given. /// List LatestAvailableWithProvides(string identifier, + StabilityToleranceConfig stabilityTolerance, GameVersionCriteria? ksp_version, RelationshipDescriptor? relationship_descriptor = null, ICollection? installed = null, @@ -109,7 +112,8 @@ IEnumerable FindReverseDependencies(List /// Returns a simple array of all incompatible modules for /// the specified version of KSP. /// - IEnumerable IncompatibleModules(GameVersionCriteria ksp_version); + IEnumerable IncompatibleModules(StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria ksp_version); /// /// Returns a dictionary of all modules installed, along with their @@ -140,7 +144,7 @@ IEnumerable FindReverseDependencies(List /// Identifier of mod /// Game versions /// true if any version is recursively compatible, false otherwise - bool IdentifierCompatible(string identifier, GameVersionCriteria crit); + bool IdentifierCompatible(string identifier, StabilityToleranceConfig stabilityTolerance, GameVersionCriteria crit); } /// @@ -176,6 +180,7 @@ public static bool IsAutodetected(this IRegistryQuerier querier, string identifi /// public static bool HasUpdate(this IRegistryQuerier querier, string identifier, + StabilityToleranceConfig stabilityTolerance, GameInstance? instance, HashSet filters, bool checkMissingFiles, @@ -192,7 +197,8 @@ public static bool HasUpdate(this IRegistryQuerier querier, // Check if it's available try { - latestMod = querier.LatestAvailable(identifier, instance?.VersionCriteria(), null, installed); + latestMod = querier.LatestAvailable(identifier, stabilityTolerance, + instance?.VersionCriteria(), null, installed); } catch { @@ -225,21 +231,20 @@ public static bool HasUpdate(this IRegistryQuerier querier, } public static Dictionary> CheckUpgradeable(this IRegistryQuerier querier, - GameInstance? instance, + GameInstance instance, HashSet heldIdents, HashSet? ignoreMissingIdents = null) { var filters = ServiceLocator.Container.Resolve() .GlobalInstallFilters - .Concat(instance?.InstallFilters - ?? Enumerable.Empty()) + .Concat(instance.InstallFilters) .ToHashSet(); // Get the absolute latest versions ignoring restrictions, // to break out of mutual version-depending deadlocks var unlimited = querier.Installed(false) .Keys .Select(ident => !heldIdents.Contains(ident) - && querier.HasUpdate(ident, instance, filters, + && querier.HasUpdate(ident, instance.StabilityToleranceConfig, instance, filters, !ignoreMissingIdents?.Contains(ident) ?? true, out CkanModule? latest) && latest is not null @@ -252,7 +257,7 @@ public static Dictionary> CheckUpgradeable(this IRegistry } public static Dictionary> CheckUpgradeable(this IRegistryQuerier querier, - GameInstance? instance, + GameInstance instance, HashSet heldIdents, List initial, HashSet? filters = null, @@ -260,8 +265,7 @@ public static Dictionary> CheckUpgradeable(this IRegistry { filters ??= ServiceLocator.Container.Resolve() .GlobalInstallFilters - .Concat(instance?.InstallFilters - ?? Enumerable.Empty()) + .Concat(instance.InstallFilters) .ToHashSet(); // Use those as the installed modules var upgradeable = new List(); @@ -269,7 +273,7 @@ public static Dictionary> CheckUpgradeable(this IRegistry foreach (var ident in initial.Select(module => module.identifier)) { if (!heldIdents.Contains(ident) - && querier.HasUpdate(ident, instance, filters, + && querier.HasUpdate(ident, instance.StabilityToleranceConfig, instance, filters, !ignoreMissingIdents?.Contains(ident) ?? true, out CkanModule? latest, initial) && latest is not null @@ -373,17 +377,19 @@ public static string CompatibleGameVersions(this CkanModule module, IGame game) /// Given a mod identifier, return a ModuleReplacement containing the relevant replacement /// if compatibility matches. /// - public static ModuleReplacement? GetReplacement(this IRegistryQuerier querier, - string identifier, - GameVersionCriteria version) + public static ModuleReplacement? GetReplacement(this IRegistryQuerier querier, + string identifier, + StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria version) // We only care about the installed version => querier.GetInstalledVersion(identifier) is CkanModule mod - ? Utilities.DefaultIfThrows(() => querier.GetReplacement(mod, version)) + ? Utilities.DefaultIfThrows(() => querier.GetReplacement(mod, stabilityTolerance, version)) : null; - public static ModuleReplacement? GetReplacement(this IRegistryQuerier querier, - CkanModule installedVersion, - GameVersionCriteria version) + public static ModuleReplacement? GetReplacement(this IRegistryQuerier querier, + CkanModule installedVersion, + StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria version) { // No replaced_by relationship if (installedVersion.replaced_by == null) @@ -405,7 +411,7 @@ public static string CompatibleGameVersions(this CkanModule module, IGame game) return new ModuleReplacement(installedVersion, replacement); } } - else if (querier.LatestAvailable(installedVersion.replaced_by.name, version, replacedBy) + else if (querier.LatestAvailable(installedVersion.replaced_by.name, stabilityTolerance, version, replacedBy) is CkanModule replacement && replacement.IsCompatible(version)) { return new ModuleReplacement(installedVersion, replacement); @@ -430,10 +436,11 @@ public static string CompatibleGameVersions(this CkanModule module, IGame game) /// Sequence of removable auto-installed modules, if any /// public static IEnumerable FindRemovableAutoInstalled( - this IRegistryQuerier querier, - List installedModules, - IGame game, - GameVersionCriteria crit) + this IRegistryQuerier querier, + List installedModules, + IGame game, + StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria crit) { log.DebugFormat("Finding removable autoInstalled for: {0}", string.Join(", ", installedModules.Select(im => im.identifier))); @@ -442,7 +449,7 @@ public static IEnumerable FindRemovableAutoInstalled( var autoInstIds = autoInstMods.Select(im => im.Module.identifier).ToHashSet(); // Need to get the full changeset for this to work as intended - RelationshipResolverOptions opts = RelationshipResolverOptions.DependsOnlyOpts(); + RelationshipResolverOptions opts = RelationshipResolverOptions.DependsOnlyOpts(stabilityTolerance); opts.without_toomanyprovides_kraken = true; opts.without_enforce_consistency = true; opts.proceed_with_inconsistencies = true; diff --git a/Core/Registry/Registry.cs b/Core/Registry/Registry.cs index bec1acdcf0..6983f3bd00 100644 --- a/Core/Registry/Registry.cs +++ b/Core/Registry/Registry.cs @@ -11,6 +11,7 @@ using Newtonsoft.Json; using log4net; +using CKAN.Configuration; using CKAN.Extensions; using CKAN.Versioning; @@ -575,15 +576,19 @@ public bool HasAnyAvailable() /// compatible and incompatible groups. /// /// Version criteria to determine compatibility - public CompatibilitySorter SetCompatibleVersion(GameVersionCriteria versCrit) + public CompatibilitySorter SetCompatibleVersion(StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria versCrit) { - if (!versCrit.Equals(sorter?.CompatibleVersions)) + if (sorter == null + || stabilityTolerance != sorter.StabilityTolerance + || !versCrit.Equals(sorter.CompatibleVersions)) { if (providers == null) { BuildProvidesIndex(); } sorter = new CompatibilitySorter( + stabilityTolerance, versCrit, repoDataMgr?.GetAllAvailDicts(Repositories.Values.OrderBy(r => r.priority) // Break ties alphanumerically @@ -598,20 +603,22 @@ public CompatibilitySorter SetCompatibleVersion(GameVersionCriteria versCrit) /// /// /// - public IEnumerable CompatibleModules(GameVersionCriteria? crit) + public IEnumerable CompatibleModules(StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria? crit) // Set up our compatibility partition - => crit != null ? SetCompatibleVersion(crit).LatestCompatible + => crit != null ? SetCompatibleVersion(stabilityTolerance, crit).LatestCompatible : repoDataMgr?.GetAllAvailableModules(Repositories.Values) - .Select(am => am.Latest()) + .Select(am => am.Latest(stabilityTolerance)) .OfType() ?? Enumerable.Empty(); /// /// /// - public IEnumerable IncompatibleModules(GameVersionCriteria crit) + public IEnumerable IncompatibleModules(StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria crit) // Set up our compatibility partition - => SetCompatibleVersion(crit).LatestIncompatible; + => SetCompatibleVersion(stabilityTolerance, crit).LatestIncompatible; /// /// Check whether any versions of this mod are installable (including dependencies) on the given game versions. @@ -620,9 +627,11 @@ public IEnumerable IncompatibleModules(GameVersionCriteria crit) /// Identifier of mod /// Game versions /// true if any version is recursively compatible, false otherwise - public bool IdentifierCompatible(string identifier, GameVersionCriteria crit) + public bool IdentifierCompatible(string identifier, + StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria crit) // Set up our compatibility partition - => SetCompatibleVersion(crit).Compatible.ContainsKey(identifier); + => SetCompatibleVersion(stabilityTolerance, crit).Compatible.ContainsKey(identifier); private AvailableModule[] getAvail(string identifier) { @@ -641,11 +650,12 @@ private AvailableModule[] getAvail(string identifier) /// /// public CkanModule? LatestAvailable(string identifier, + StabilityToleranceConfig stabilityTolerance, GameVersionCriteria? gameVersion, RelationshipDescriptor? relationshipDescriptor = null, ICollection? installed = null, ICollection? toInstall = null) - => getAvail(identifier)?.Select(am => am.Latest(gameVersion, relationshipDescriptor, + => getAvail(identifier)?.Select(am => am.Latest(stabilityTolerance, gameVersion, relationshipDescriptor, installed, toInstall)) .OfType() .OrderByDescending(m => m.version) @@ -779,6 +789,7 @@ is Dictionary> allProvs /// /// public List LatestAvailableWithProvides(string identifier, + StabilityToleranceConfig stabilityTolerance, GameVersionCriteria? gameVersion, RelationshipDescriptor? relationship = null, ICollection? installed = null, @@ -788,7 +799,7 @@ is Dictionary> allProvs && Repositories.Values.ToArray() is Repository[] repos && allProvs.TryGetValue(identifier, out HashSet? provs) // For each AvailableModule, we want the latest one matching our constraints - ? provs.Select(am => am.Latest(gameVersion, relationship, installed, toInstall)) + ? provs.Select(am => am.Latest(stabilityTolerance, gameVersion, relationship, installed, toInstall)) .OfType() .Where(m => m.ProvidesList.Contains(identifier)) // Put the most popular one on top @@ -1256,9 +1267,9 @@ public IEnumerable GetAllHosts() => repoDataMgr?.GetAllAvailableModules(Repositories.Values) // Pick all latest modules where download is not null // Merge all the URLs into one sequence - .SelectMany(availMod => (availMod?.Latest()?.download + .SelectMany(availMod => (availMod?.Latest(ReleaseStatus.development)?.download ?? Enumerable.Empty()) - .Append(availMod?.Latest()?.InternetArchiveDownload)) + .Append(availMod?.Latest(ReleaseStatus.development)?.InternetArchiveDownload)) .OfType() // Skip relative URLs because they don't have hosts .Where(dlUri => dlUri.IsAbsoluteUri) diff --git a/Core/Registry/RegistryManager.cs b/Core/Registry/RegistryManager.cs index e43c9d31fd..1471a0dc11 100644 --- a/Core/Registry/RegistryManager.cs +++ b/Core/Registry/RegistryManager.cs @@ -13,6 +13,7 @@ using log4net; using Newtonsoft.Json; +using CKAN.Configuration; using CKAN.Versioning; #if !NET8_0_OR_GREATER using CKAN.Extensions; @@ -502,12 +503,13 @@ public CkanModule GenerateModpack(bool recommends = false, bool with_versions = }; var mods = registry.InstalledModules - .Where(inst => !inst.Module.IsDLC && !inst.AutoInstalled && IsAvailable(inst)) + .Where(inst => !inst.Module.IsDLC && !inst.AutoInstalled + && IsAvailable(inst, gameInstance.StabilityToleranceConfig)) .Select(inst => inst.Module) .ToHashSet(); // Sort dependencies before dependers var resolver = new RelationshipResolver(mods, null, - RelationshipResolverOptions.ConflictsOpts(), + RelationshipResolverOptions.ConflictsOpts(gameInstance.StabilityToleranceConfig), registry, gameInstance.game, gameInstance.VersionCriteria()); var rels = resolver.ModList() .Intersect(mods) @@ -529,11 +531,11 @@ public CkanModule GenerateModpack(bool recommends = false, bool with_versions = return module; } - private bool IsAvailable(InstalledModule inst) + private bool IsAvailable(InstalledModule inst, StabilityToleranceConfig stabilityTolerance) { try { - var avail = registry.LatestAvailable(inst.identifier, null, null); + var avail = registry.LatestAvailable(inst.identifier, stabilityTolerance, null); return true; } catch diff --git a/Core/Relationships/RelationshipResolver.cs b/Core/Relationships/RelationshipResolver.cs index 20adfc2ff4..b865b97c26 100644 --- a/Core/Relationships/RelationshipResolver.cs +++ b/Core/Relationships/RelationshipResolver.cs @@ -4,6 +4,7 @@ using log4net; +using CKAN.Configuration; using CKAN.Games; using CKAN.Versioning; using CKAN.Extensions; @@ -55,7 +56,9 @@ public RelationshipResolver(IEnumerable modulesToInstall, .Except(modulesToInstall.Select(m => m.identifier)) .ToHashSet(); resolved = new ResolvedRelationshipsTree(toInst, registry, dlls, - installed_modules, versionCrit, + installed_modules, + options.stability_tolerance ?? new StabilityToleranceConfig(""), + versionCrit, options.OptionalHandling()); if (!options.proceed_with_inconsistencies) { @@ -557,7 +560,8 @@ private bool ValidRecSugReasons(HashSet dependencie public ParallelQuery>> Supporters( HashSet supported, IEnumerable toExclude) - => registry.CompatibleModules(versionCrit) + => registry.CompatibleModules(options.stability_tolerance ?? new StabilityToleranceConfig(""), + versionCrit) .Except(toExclude) .AsParallel() // Find installable modules with "supports" relationships diff --git a/Core/Relationships/RelationshipResolverOptions.cs b/Core/Relationships/RelationshipResolverOptions.cs index 1edc1aa523..f10d1c469f 100644 --- a/Core/Relationships/RelationshipResolverOptions.cs +++ b/Core/Relationships/RelationshipResolverOptions.cs @@ -1,3 +1,5 @@ +using CKAN.Configuration; + namespace CKAN { // TODO: It would be lovely to get rid of the `without` fields, @@ -5,17 +7,22 @@ namespace CKAN // cases in their heads. public class RelationshipResolverOptions { + public RelationshipResolverOptions(StabilityToleranceConfig stabTolCfg) + { + stability_tolerance = stabTolCfg; + } + /// /// Default options for relationship resolution. /// - public static RelationshipResolverOptions DefaultOpts() - => new RelationshipResolverOptions(); + public static RelationshipResolverOptions DefaultOpts(StabilityToleranceConfig stabTolCfg) + => new RelationshipResolverOptions(stabTolCfg); /// /// Options to install without recommendations. /// - public static RelationshipResolverOptions DependsOnlyOpts() - => new RelationshipResolverOptions() + public static RelationshipResolverOptions DependsOnlyOpts(StabilityToleranceConfig stabTolCfg) + => new RelationshipResolverOptions(stabTolCfg) { with_recommends = false, with_suggests = false, @@ -27,8 +34,8 @@ public static RelationshipResolverOptions DependsOnlyOpts() /// of anything in the changeset (except when suppress_recommendations==true), /// without throwing exceptions, so the calling code can decide what to do about conflicts /// - public static RelationshipResolverOptions KitchenSinkOpts() - => new RelationshipResolverOptions() + public static RelationshipResolverOptions KitchenSinkOpts(StabilityToleranceConfig stabTolCfg) + => new RelationshipResolverOptions(stabTolCfg) { with_recommends = true, with_suggests = true, @@ -38,8 +45,8 @@ public static RelationshipResolverOptions KitchenSinkOpts() get_recommenders = true, }; - public static RelationshipResolverOptions ConflictsOpts() - => new RelationshipResolverOptions() + public static RelationshipResolverOptions ConflictsOpts(StabilityToleranceConfig stabTolCfg) + => new RelationshipResolverOptions(stabTolCfg) { without_toomanyprovides_kraken = true, proceed_with_inconsistencies = true, @@ -101,6 +108,11 @@ public static RelationshipResolverOptions ConflictsOpts() /// public bool get_recommenders = false; + /// + /// The least stable category of mods to allow + /// + public StabilityToleranceConfig? stability_tolerance; + public RelationshipResolverOptions OptionsFor(RelationshipDescriptor descr) => descr.suppress_recommendations ? WithoutRecommendations() : this; diff --git a/Core/Relationships/ResolvedRelationship.cs b/Core/Relationships/ResolvedRelationship.cs index 9a58c43b62..357d8f1082 100644 --- a/Core/Relationships/ResolvedRelationship.cs +++ b/Core/Relationships/ResolvedRelationship.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Concurrent; +using CKAN.Configuration; using CKAN.Versioning; namespace CKAN @@ -135,22 +136,23 @@ public ResolvedByNew(CkanModule source, { } - public ResolvedByNew(CkanModule source, - RelationshipDescriptor relationship, - SelectionReason reason, - IEnumerable providers, - ICollection definitelyInstalling, - ICollection allInstalling, - IRegistryQuerier registry, - ICollection dlls, - ICollection installed, - GameVersionCriteria crit, - OptionalRelationships optRels, - RelationshipCache relationshipCache) + public ResolvedByNew(CkanModule source, + RelationshipDescriptor relationship, + SelectionReason reason, + IEnumerable providers, + ICollection definitelyInstalling, + ICollection allInstalling, + IRegistryQuerier registry, + ICollection dlls, + ICollection installed, + StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria crit, + OptionalRelationships optRels, + RelationshipCache relationshipCache) : this(source, relationship, reason, providers.ToDictionary(prov => prov, prov => ResolvedRelationshipsTree.ResolveModule( - prov, definitelyInstalling, allInstalling, registry, dlls, installed, crit, + prov, definitelyInstalling, allInstalling, registry, dlls, installed, stabilityTolerance, crit, relationship.suppress_recommendations ? optRels & ~OptionalRelationships.Recommendations & ~OptionalRelationships.Suggestions diff --git a/Core/Relationships/ResolvedRelationshipsTree.cs b/Core/Relationships/ResolvedRelationshipsTree.cs index 1db97f94fa..3504c660ec 100644 --- a/Core/Relationships/ResolvedRelationshipsTree.cs +++ b/Core/Relationships/ResolvedRelationshipsTree.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Concurrent; +using CKAN.Configuration; using CKAN.Versioning; using CKAN.Extensions; @@ -21,35 +22,37 @@ public enum OptionalRelationships public class ResolvedRelationshipsTree { - public ResolvedRelationshipsTree(ICollection modules, - IRegistryQuerier registry, - ICollection dlls, - ICollection installed, - GameVersionCriteria crit, - OptionalRelationships optRels) + public ResolvedRelationshipsTree(ICollection modules, + IRegistryQuerier registry, + ICollection dlls, + ICollection installed, + StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria crit, + OptionalRelationships optRels) { - resolved = ResolveManyCached(modules, registry, dlls, installed, crit, optRels, relationshipCache).ToArray(); + resolved = ResolveManyCached(modules, registry, dlls, installed, stabilityTolerance, crit, optRels, relationshipCache).ToArray(); } - public static IEnumerable ResolveModule(CkanModule module, - ICollection definitelyInstalling, - ICollection allInstalling, - IRegistryQuerier registry, - ICollection dlls, - ICollection installed, - GameVersionCriteria crit, - OptionalRelationships optRels, - RelationshipCache relationshipCache) + public static IEnumerable ResolveModule(CkanModule module, + ICollection definitelyInstalling, + ICollection allInstalling, + IRegistryQuerier registry, + ICollection dlls, + ICollection installed, + StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria crit, + OptionalRelationships optRels, + RelationshipCache relationshipCache) => ResolveRelationships(module, module.depends, new SelectionReason.Depends(module), - definitelyInstalling, allInstalling, registry, dlls, installed, crit, optRels, relationshipCache) + definitelyInstalling, allInstalling, registry, dlls, installed, stabilityTolerance, crit, optRels, relationshipCache) .Concat((optRels & OptionalRelationships.Recommendations) == 0 ? Enumerable.Empty() : ResolveRelationships(module, module.recommends, new SelectionReason.Recommended(module, 0), - definitelyInstalling, allInstalling, registry, dlls, installed, crit, optRels, relationshipCache)) + definitelyInstalling, allInstalling, registry, dlls, installed, stabilityTolerance, crit, optRels, relationshipCache)) .Concat((optRels & OptionalRelationships.Suggestions) == 0 ? Enumerable.Empty() : ResolveRelationships(module, module.suggests, new SelectionReason.Suggested(module), - definitelyInstalling, allInstalling, registry, dlls, installed, crit, optRels, relationshipCache)); + definitelyInstalling, allInstalling, registry, dlls, installed, stabilityTolerance, crit, optRels, relationshipCache)); public IEnumerable Unsatisfied() => resolved.SelectMany(UnsatisfiedFrom); @@ -102,14 +105,15 @@ public override string ToString() => string.Join(Environment.NewLine, resolved.Select(rr => rr.ToString())); - private static IEnumerable ResolveManyCached(ICollection modules, - IRegistryQuerier registry, - ICollection dlls, - ICollection installed, - GameVersionCriteria crit, - OptionalRelationships optRels, - RelationshipCache relationshipCache) - => modules.SelectMany(m => ResolveModule(m, modules, modules, registry, dlls, installed, crit, optRels, + private static IEnumerable ResolveManyCached(ICollection modules, + IRegistryQuerier registry, + ICollection dlls, + ICollection installed, + StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria crit, + OptionalRelationships optRels, + RelationshipCache relationshipCache) + => modules.SelectMany(m => ResolveModule(m, modules, modules, registry, dlls, installed, stabilityTolerance, crit, optRels, relationshipCache)); private static IEnumerable ResolveRelationships(CkanModule module, @@ -120,25 +124,27 @@ private static IEnumerable ResolveRelationships(CkanModule IRegistryQuerier registry, ICollection dlls, ICollection installed, + StabilityToleranceConfig stabilityTolerance, GameVersionCriteria crit, OptionalRelationships optRels, RelationshipCache relationshipCache) => relationships?.Select(dep => Resolve(module, dep, reason, definitelyInstalling, allInstalling, registry, dlls, installed, - crit, optRels, relationshipCache)) + stabilityTolerance, crit, optRels, relationshipCache)) ?? Enumerable.Empty(); - private static ResolvedRelationship Resolve(CkanModule source, - RelationshipDescriptor relationship, - SelectionReason reason, - ICollection definitelyInstalling, - ICollection allInstalling, - IRegistryQuerier registry, - ICollection dlls, - ICollection installed, - GameVersionCriteria crit, - OptionalRelationships optRels, - RelationshipCache relationshipCache) + private static ResolvedRelationship Resolve(CkanModule source, + RelationshipDescriptor relationship, + SelectionReason reason, + ICollection definitelyInstalling, + ICollection allInstalling, + IRegistryQuerier registry, + ICollection dlls, + ICollection installed, + StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria crit, + OptionalRelationships optRels, + RelationshipCache relationshipCache) => relationshipCache.TryGetValue(relationship, out ResolvedRelationship? cachedRel) ? cachedRel.WithSource(source, reason) @@ -157,11 +163,11 @@ private static ResolvedRelationship Resolve(CkanModule source, : relationshipCache.GetOrAdd( relationship, new ResolvedByNew(source, relationship, reason, - relationship.LatestAvailableWithProvides(registry, crit, + relationship.LatestAvailableWithProvides(registry, stabilityTolerance, crit, installed, definitelyInstalling), definitelyInstalling, allInstalling.Append(source).ToArray(), - registry, dlls, installed, crit, optRels, + registry, dlls, installed, stabilityTolerance, crit, optRels, relationshipCache)); private readonly ResolvedRelationship[] resolved; diff --git a/Core/Repositories/AvailableModule.cs b/Core/Repositories/AvailableModule.cs index 30523b9f65..c9e14ea082 100644 --- a/Core/Repositories/AvailableModule.cs +++ b/Core/Repositories/AvailableModule.cs @@ -10,6 +10,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; +using CKAN.Configuration; using CKAN.Versioning; using CKAN.Extensions; @@ -28,7 +29,7 @@ public class AvailableModule /// The module to keep track of [JsonConstructor] - public AvailableModule(string identifier) + private AvailableModule(string identifier) { this.identifier = identifier; } @@ -100,12 +101,24 @@ private void Add(CkanModule module) /// Modules that are already installed /// Modules that are planned to be installed /// - public CkanModule? Latest(GameVersionCriteria? ksp_version = null, + public CkanModule? Latest(StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria? ksp_version = null, + RelationshipDescriptor? relationship = null, + ICollection? installed = null, + ICollection? toInstall = null) + => Latest(stabilityTolerance.ModStabilityTolerance(identifier) + ?? stabilityTolerance.OverallStabilityTolerance, + ksp_version, relationship, installed, toInstall); + + public CkanModule? Latest(ReleaseStatus stabilityTolerance, + GameVersionCriteria? ksp_version = null, RelationshipDescriptor? relationship = null, ICollection? installed = null, ICollection? toInstall = null) { - IEnumerable modules = module_version.Values.Reverse(); + var modules = module_version.Values + .Where(m => m.release_status <= stabilityTolerance) + .Reverse(); if (relationship != null) { modules = modules.Where(relationship.WithinBounds); diff --git a/Core/Repositories/ReadProgressStream.cs b/Core/Repositories/ReadProgressStream.cs index 083d678842..5e12b8d776 100644 --- a/Core/Repositories/ReadProgressStream.cs +++ b/Core/Repositories/ReadProgressStream.cs @@ -40,12 +40,6 @@ public abstract class ContainerStream : Stream { protected ContainerStream(Stream stream) { - if (stream == null) - { - #pragma warning disable IDE0016 - throw new ArgumentNullException(nameof(stream)); - #pragma warning restore IDE0016 - } inner = stream; } diff --git a/Core/Types/CkanModule.cs b/Core/Types/CkanModule.cs index 81c8b12442..ccca9f4ba6 100644 --- a/Core/Types/CkanModule.cs +++ b/Core/Types/CkanModule.cs @@ -116,8 +116,11 @@ public class CkanModule : IEquatable [JsonConverter(typeof(JsonRelationshipConverter))] public List? recommends; - [JsonProperty("release_status", Order = 14, NullValueHandling = NullValueHandling.Ignore)] - public ReleaseStatus? release_status; + [JsonProperty("release_status", Order = 14, + NullValueHandling = NullValueHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(ReleaseStatus.stable)] + public ReleaseStatus? release_status = ReleaseStatus.stable; [JsonProperty("resources", Order = 15, NullValueHandling = NullValueHandling.Ignore)] public ResourcesDescriptor? resources; @@ -355,6 +358,15 @@ private void CheckHealth() string.Format(Properties.Resources.CkanModuleMissingRequired, identifier, "download")); } + if (release_status is not (ReleaseStatus.stable + or ReleaseStatus.development + or ReleaseStatus.testing)) + { + throw new BadMetadataKraken( + null, + string.Format(Properties.Resources.ReleaseStatusInvalid, + release_status)); + } } private static readonly ModuleVersion v1p28 = new ModuleVersion("v1.28"); @@ -668,7 +680,7 @@ bool IEquatable.Equals(CkanModule? other) /// Returns true if we support at least spec_version of the CKAN spec. /// internal static bool IsSpecSupported(ModuleVersion spec_version) - => Meta.ReleaseVersion.IsGreaterThan(spec_version); + => Meta.IsNetKAN || Meta.ReleaseVersion.IsGreaterThan(spec_version); /// /// Returns true if we support the CKAN spec used by this module. diff --git a/Core/Types/RelationshipDescriptor.cs b/Core/Types/RelationshipDescriptor.cs index 736f7e2f5b..3ff6da138b 100644 --- a/Core/Types/RelationshipDescriptor.cs +++ b/Core/Types/RelationshipDescriptor.cs @@ -5,6 +5,7 @@ using Newtonsoft.Json; +using CKAN.Configuration; using CKAN.Games; using CKAN.Versioning; using CKAN.Extensions; @@ -26,11 +27,13 @@ public abstract bool MatchesAny(ICollection modules, public abstract bool WithinBounds(CkanModule otherModule); public abstract List LatestAvailableWithProvides(IRegistryQuerier registry, + StabilityToleranceConfig stabilityTolerance, GameVersionCriteria? crit, ICollection? installed = null, ICollection? toInstall = null); public abstract CkanModule? ExactMatch(IRegistryQuerier registry, + StabilityToleranceConfig stabilityTolerance, GameVersionCriteria? crit, ICollection? installed = null, ICollection? toInstall = null); @@ -146,16 +149,20 @@ public override bool MatchesAny(ICollection modules, } public override List LatestAvailableWithProvides(IRegistryQuerier registry, + StabilityToleranceConfig stabilityTolerance, GameVersionCriteria? crit, ICollection? installed = null, ICollection? toInstall = null) - => registry.LatestAvailableWithProvides(name, crit, this, installed, toInstall); + => registry.LatestAvailableWithProvides(name, stabilityTolerance, + crit, this, installed, toInstall); public override CkanModule? ExactMatch(IRegistryQuerier registry, + StabilityToleranceConfig stabilityTolerance, GameVersionCriteria? crit, ICollection? installed = null, ICollection? toInstall = null) - => Utilities.DefaultIfThrows(() => registry.LatestAvailable(name, crit, this, installed, toInstall)); + => Utilities.DefaultIfThrows(() => registry.LatestAvailable(name, stabilityTolerance, + crit, this, installed, toInstall)); public override bool Equals(RelationshipDescriptor? other) => Equals(other as ModuleRelationshipDescriptor); @@ -250,16 +257,18 @@ public override bool MatchesAny(ICollection modules, } public override List LatestAvailableWithProvides(IRegistryQuerier registry, + StabilityToleranceConfig stabilityTolerance, GameVersionCriteria? crit, ICollection? installed = null, ICollection? toInstall = null) - => (any_of?.SelectMany(r => r.LatestAvailableWithProvides(registry, crit, installed, toInstall)) + => (any_of?.SelectMany(r => r.LatestAvailableWithProvides(registry, stabilityTolerance, crit, installed, toInstall)) .Distinct() ?? Enumerable.Empty()) .ToList(); // Exact match is not possible for any_of public override CkanModule? ExactMatch(IRegistryQuerier registry, + StabilityToleranceConfig stabilityTolerance, GameVersionCriteria? crit, ICollection? installed = null, ICollection? toInstall = null) diff --git a/Core/Types/ReleaseStatus.cs b/Core/Types/ReleaseStatus.cs index 37a3d0abcd..b492ba6e9f 100644 --- a/Core/Types/ReleaseStatus.cs +++ b/Core/Types/ReleaseStatus.cs @@ -1,61 +1,23 @@ -using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using Newtonsoft.Json; namespace CKAN { - /// - /// A release status, complying to the CKAN spec. - /// - [JsonConverter(typeof(JsonSimpleStringConverter))] - public class ReleaseStatus + [JsonConverter(typeof(JsonReleaseStatusConverter))] + public enum ReleaseStatus { - private static readonly HashSet valid_statuses = new HashSet { - "stable", "testing", "development" // Spec 1.0 statuses - }; - - private readonly string status; - - /// - /// Creates a ReleaseStatus object which compiles to the CKAN spec. - /// Throws a BadMetadataKraken if passed a non-compliant string. - /// - /// Status. - public ReleaseStatus(string? status) - { - switch (status) - { - // As per the spec, if the status is null, we assume stable. - case null: - status = "stable"; - break; - - // For compatibility with older metadata, we map 'alpha' and 'beta' - // to 'development' and 'testing'. - - case "alpha": - status = "development"; - break; - - case "beta": - status = "testing"; - break; - } - - if (!valid_statuses.Contains(status)) - { - throw new BadMetadataKraken( - null, - string.Format(Properties.Resources.ReleaseStatusInvalid, status) - ); - } - - this.status = status; - } - - public override string ToString() - { - return status; - } + [Display(Name = "ReleaseStatusStableName", + Description = "ReleaseStatusStableDescription", + ResourceType = typeof(Properties.Resources))] + stable = 0, + [Display(Name = "ReleaseStatusTestingName", + Description = "ReleaseStatusTestingDescription", + ResourceType = typeof(Properties.Resources))] + testing = 1, + [Display(Name = "ReleaseStatusDevelopmentName", + Description = "ReleaseStatusDevelopmentDescription", + ResourceType = typeof(Properties.Resources))] + development = 2, } } diff --git a/Core/Types/SpecVersionAnalyzer.cs b/Core/Types/SpecVersionAnalyzer.cs index 6e08cc1582..807615d7ed 100644 --- a/Core/Types/SpecVersionAnalyzer.cs +++ b/Core/Types/SpecVersionAnalyzer.cs @@ -14,7 +14,10 @@ public static ModuleVersion MinimumSpecVersion(CkanModule module) public static ModuleVersion MinimumSpecVersion(JObject json) // Add new stuff at the top, versions in this function should be in descending order - => json["download_hash"] is JObject hashes + => json.TryGetValue("release_status", out JToken? relStat) + && (string?)relStat is string and not "stable" ? v1p36 + + : json["download_hash"] is JObject hashes && (!hashes.ContainsKey("sha256") || !hashes.ContainsKey("sha1")) ? v1p35 : json["download"] is JArray ? v1p34 @@ -123,5 +126,6 @@ private static IEnumerable AllRelationships(JObject json) private static readonly ModuleVersion v1p31 = new ModuleVersion("v1.31"); private static readonly ModuleVersion v1p34 = new ModuleVersion("v1.34"); private static readonly ModuleVersion v1p35 = new ModuleVersion("v1.35"); + private static readonly ModuleVersion v1p36 = new ModuleVersion("v1.36"); } } diff --git a/Core/Versioning/GameVersion.cs b/Core/Versioning/GameVersion.cs index 70dda715f7..88e76ea860 100644 --- a/Core/Versioning/GameVersion.cs +++ b/Core/Versioning/GameVersion.cs @@ -323,13 +323,8 @@ public GameVersionRange ToVersionRange() /// A object that is equivalent to the version number specified in the /// parameter. /// - public static GameVersion Parse(string? input) + public static GameVersion Parse(string input) { - if (input is null) - { - throw new ArgumentNullException(nameof(input)); - } - if (TryParse(input, out GameVersion? result) && result is not null) { return result; diff --git a/Core/Versioning/GameVersionBound.cs b/Core/Versioning/GameVersionBound.cs index 8fbdab31cb..c019465228 100644 --- a/Core/Versioning/GameVersionBound.cs +++ b/Core/Versioning/GameVersionBound.cs @@ -111,23 +111,13 @@ public sealed partial class GameVersionBound /// /// The set of objects to compare. /// The lowest value in . - public static GameVersionBound Lowest(params GameVersionBound?[] versionBounds) + public static GameVersionBound Lowest(params GameVersionBound[] versionBounds) { - if (versionBounds == null) - { - throw new ArgumentNullException(nameof(versionBounds)); - } - if (!versionBounds.Any()) { throw new ArgumentException("Value cannot be empty.", nameof(versionBounds)); } - if (versionBounds.Contains(null)) - { - throw new ArgumentException("Value cannot contain null.", nameof(versionBounds)); - } - return versionBounds.OfType() .OrderBy(i => i == Unbounded) .ThenBy(i => i.Value) diff --git a/Core/Versioning/ModuleVersion.cs b/Core/Versioning/ModuleVersion.cs index 1e5ea0bbe3..218b37887e 100644 --- a/Core/Versioning/ModuleVersion.cs +++ b/Core/Versioning/ModuleVersion.cs @@ -591,19 +591,7 @@ public partial class ModuleVersion /// particular instance will be returned. /// public static ModuleVersion Max(ModuleVersion ver1, ModuleVersion ver2) - { - if (ver1 == null) - { - throw new ArgumentNullException(nameof(ver1)); - } - - if (ver2 == null) - { - throw new ArgumentNullException(nameof(ver2)); - } - - return ver1.IsGreaterThan(ver2) ? ver1 : ver2; - } + => ver1.IsGreaterThan(ver2) ? ver1 : ver2; /// /// Returns the smaller of two objects. @@ -616,19 +604,7 @@ public static ModuleVersion Max(ModuleVersion ver1, ModuleVersion ver2) /// particular instance will be returned. /// public static ModuleVersion Min(ModuleVersion ver1, ModuleVersion ver2) - { - if (ver1 == null) - { - throw new ArgumentNullException(nameof(ver1)); - } - - if (ver2 == null) - { - throw new ArgumentNullException(nameof(ver2)); - } - - return ver1.IsLessThan(ver2) ? ver1 : ver2; - } + => ver1.IsLessThan(ver2) ? ver1 : ver2; /// /// Converts the specified string to a new instance of the class. diff --git a/GUI/Controls/Changeset.cs b/GUI/Controls/Changeset.cs index a0d98489fa..9c862f933d 100644 --- a/GUI/Controls/Changeset.cs +++ b/GUI/Controls/Changeset.cs @@ -161,7 +161,7 @@ public ChangesetRow(ModChange change, public readonly string? Conflict = null; public string Mod => Change.NameAndStatus ?? ""; - public string ChangeType => Change.ChangeType.Localize(); + public string ChangeType => Change.ChangeType.LocalizeDescription(); public string Reasons { get; private set; } public Bitmap DeleteImage => Change.IsRemovable ? EmbeddedImages.textClear ?? EmptyBitmap : EmptyBitmap; diff --git a/GUI/Controls/ChooseRecommendedMods.cs b/GUI/Controls/ChooseRecommendedMods.cs index ea416d654b..e7519e9304 100644 --- a/GUI/Controls/ChooseRecommendedMods.cs +++ b/GUI/Controls/ChooseRecommendedMods.cs @@ -133,7 +133,8 @@ private void RecommendedModsListView_ItemChecked(object? sender, ItemCheckedEven private void MarkConflicts() { - if (registry != null && versionCrit != null && game != null) + if (registry != null && versionCrit != null && game != null + && Main.Instance?.CurrentInstance is GameInstance inst) { try { @@ -145,7 +146,8 @@ private void MarkConflicts() .Concat(toInstall) .Distinct(), toUninstall, - RelationshipResolverOptions.ConflictsOpts(), registry, game, versionCrit); + RelationshipResolverOptions.ConflictsOpts(inst.StabilityToleranceConfig), + registry, game, versionCrit); var conflicts = resolver.ConflictList; foreach (var item in RecommendedModsListView.Items.Cast() // Apparently ListView handes AddRange by: diff --git a/GUI/Controls/InstallationHistory.cs b/GUI/Controls/InstallationHistory.cs index e89c05b15a..1f82a2e656 100644 --- a/GUI/Controls/InstallationHistory.cs +++ b/GUI/Controls/InstallationHistory.cs @@ -185,21 +185,25 @@ private void HistoryListView_ItemSelectionChanged(object? sender, ListViewItemSe // Registry.LatestAvailable without exceptions private CkanModule? SaneLatestAvail(string identifier) { - try - { - return registry?.LatestAvailable(identifier, inst?.VersionCriteria()); - } - catch + if (inst != null) { + var stabilityTolerance = inst.StabilityToleranceConfig; try { - return registry?.LatestAvailable(identifier, null); + return registry?.LatestAvailable(identifier, stabilityTolerance, inst.VersionCriteria()); } catch { - return null; + try + { + return registry?.LatestAvailable(identifier, stabilityTolerance, null); + } + catch + { + } } } + return null; } private void ModsListView_ItemSelectionChanged(object? sender, ListViewItemSelectionChangedEventArgs? e) diff --git a/GUI/Controls/LabeledProgressBar.cs b/GUI/Controls/LabeledProgressBar.cs index 0d864aafc7..846dde54f1 100644 --- a/GUI/Controls/LabeledProgressBar.cs +++ b/GUI/Controls/LabeledProgressBar.cs @@ -19,7 +19,9 @@ public class LabeledProgressBar : ProgressBar public LabeledProgressBar() : base() { - SetStyle(ControlStyles.OptimizedDoubleBuffer, true); + SetStyle(ControlStyles.OptimizedDoubleBuffer + | ControlStyles.UserPaint, + true); Font = SystemFonts.DefaultFont; Text = ""; } @@ -31,12 +33,12 @@ public LabeledProgressBar() [EditorBrowsable(EditorBrowsableState.Always)] // If we use override instead of new, the nullability never matches (!) public new string Text { - get => text; - [MemberNotNull(nameof(text), nameof(textSize))] + get => base.Text; + [MemberNotNull(nameof(textSize))] set { - text = value; - textSize = TextRenderer.MeasureText(text, Font); + base.Text = value; + textSize = TextRenderer.MeasureText(Text, Font); } } @@ -45,18 +47,33 @@ public LabeledProgressBar() [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] [EditorBrowsable(EditorBrowsableState.Always)] // If we use override instead of new, the nullability never matches (!) - public new Font Font { get; set; } + public new Font Font + { + get => base.Font; + [MemberNotNull(nameof(textSize))] + set + { + base.Font = value; + textSize = TextRenderer.MeasureText(Text, Font); + } + } protected override void OnPaint(PaintEventArgs e) { - base.OnPaint(e); + ProgressBarRenderer.DrawHorizontalBar(e.Graphics, ClientRectangle); + ProgressBarRenderer.DrawHorizontalChunks(e.Graphics, + new Rectangle(ClientRectangle.X, + ClientRectangle.Y, + ClientRectangle.Width + * (Value - Minimum) + / (Maximum - Minimum), + ClientRectangle.Height)); TextRenderer.DrawText(e.Graphics, Text, Font, new Point((Width - textSize.Width) / 2, (Height - textSize.Height) / 2), SystemColors.ControlText); } - private string text; - private Size textSize; + private Size textSize; } } diff --git a/GUI/Controls/ManageMods.cs b/GUI/Controls/ManageMods.cs index 5af11bfc17..f83e6b9e32 100644 --- a/GUI/Controls/ManageMods.cs +++ b/GUI/Controls/ManageMods.cs @@ -967,7 +967,7 @@ private void guiModule_PropertyChanged(object? sender, PropertyChangedEventArgs? { switch (e?.PropertyName) { - case "SelectedMod": + case nameof(GUIMod.SelectedMod): Util.Invoke(this, () => { if (row.Cells[Installed.Index] is DataGridViewCheckBoxCell instCell) @@ -1002,13 +1002,13 @@ private void guiModule_PropertyChanged(object? sender, PropertyChangedEventArgs? }); break; - case "IsAutoInstalled": + case nameof(GUIMod.IsAutoInstalled): // Update the changeset UpdateChangeSetAndConflicts(currentInstance, RegistryManager.Instance(currentInstance, repoData).registry); break; - case "IsCached": + case nameof(GUIMod.IsCached): row.Visible = mainModList.IsVisible(gmod, currentInstance.Name, currentInstance.game, @@ -2009,10 +2009,11 @@ public void UpdateChangeSetAndConflicts(GameInstance inst, IRegistryQuerier regi gmod.SelectedMod = ch.targetMod; } } - var tuple = mainModList.ComputeFullChangeSetFromUserChangeSet(registry, user_change_set, inst.game, gameVersion); + var tuple = mainModList.ComputeFullChangeSetFromUserChangeSet(registry, user_change_set, inst.game, + inst.StabilityToleranceConfig, gameVersion); full_change_set = tuple.Item1.ToList(); new_conflicts = tuple.Item2.ToDictionary( - item => new GUIMod(item.Key, repoData, registry, gameVersion, null, + item => new GUIMod(item.Key, repoData, registry, inst.StabilityToleranceConfig, gameVersion, null, guiConfig?.HideEpochs ?? false, guiConfig?.HideV ?? false), item => item.Value); diff --git a/GUI/Controls/ModInfo.Designer.cs b/GUI/Controls/ModInfo.Designer.cs index 423855f4e9..06ee3c718c 100644 --- a/GUI/Controls/ModInfo.Designer.cs +++ b/GUI/Controls/ModInfo.Designer.cs @@ -44,6 +44,7 @@ private void InitializeComponent() this.Contents = new CKAN.GUI.Contents(); this.VersionsTabPage = new System.Windows.Forms.TabPage(); this.Versions = new CKAN.GUI.Versions(); + this.ModInfoTable.SuspendLayout(); this.SuspendLayout(); // // ModInfoTable @@ -214,6 +215,8 @@ private void InitializeComponent() this.Padding = new System.Windows.Forms.Padding(0); this.Size = new System.Drawing.Size(500, 500); resources.ApplyResources(this, "$this"); + this.ModInfoTable.ResumeLayout(false); + this.ModInfoTable.PerformLayout(); this.ResumeLayout(false); this.PerformLayout(); } diff --git a/GUI/Controls/ModInfo.cs b/GUI/Controls/ModInfo.cs index 0b0f931b58..25be5c85f5 100644 --- a/GUI/Controls/ModInfo.cs +++ b/GUI/Controls/ModInfo.cs @@ -9,7 +9,6 @@ using CKAN.Configuration; using CKAN.Versioning; -using CKAN.GUI.Attributes; namespace CKAN.GUI { @@ -40,19 +39,13 @@ public GUIMod? SelectedModule { ModInfoTabControl.Enabled = true; UpdateHeaderInfo(value, crit); - LoadTab(value); } + LoadTab(value); } } get => selectedModule; } - [ForbidGUICalls] - public void RefreshModContentsTree() - { - Contents.RefreshModContentsTree(); - } - public void SwitchTab(string name) { ModInfoTabControl.SelectedTab = ModInfoTabControl.TabPages[name]; @@ -77,12 +70,15 @@ protected override void OnResize(EventArgs e) private GUIMod? selectedModule; - private void LoadTab(GUIMod gm) + private void LoadTab(GUIMod? gm) { switch (ModInfoTabControl.SelectedTab?.Name) { case "MetadataTabPage": - Metadata.UpdateModInfo(gm); + if (gm != null) + { + Metadata.UpdateModInfo(gm); + } break; case "ContentTabPage": diff --git a/GUI/Controls/ModInfoTabs/Contents.cs b/GUI/Controls/ModInfoTabs/Contents.cs index 6b0a8df84b..6ec00406d8 100644 --- a/GUI/Controls/ModInfoTabs/Contents.cs +++ b/GUI/Controls/ModInfoTabs/Contents.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Linq; using System.Drawing; using System.Windows.Forms; @@ -23,6 +24,8 @@ public partial class Contents : UserControl public Contents() { InitializeComponent(); + var coreCfg = ServiceLocator.Container.Resolve(); + coreCfg.PropertyChanged += Configuration_PropertyChanged; } public GUIMod? SelectedModule @@ -31,22 +34,32 @@ public GUIMod? SelectedModule { if (value != selectedModule) { + if (selectedModule != null) + { + selectedModule.PropertyChanged -= SelectedMod_PropertyChanged; + } selectedModule = value; - Util.Invoke(ContentsPreviewTree, () => _UpdateModContentsTree(selectedModule?.InstalledMod, - selectedModule?.ToModule())); + if (selectedModule != null) + { + selectedModule.PropertyChanged += SelectedMod_PropertyChanged; + } + Util.Invoke(ContentsPreviewTree, + () => _UpdateModContentsTree(selectedModule?.InstalledMod, + selectedModule?.ToModule())); } } get => selectedModule; } [ForbidGUICalls] - public void RefreshModContentsTree() + private void RefreshModContentsTree() { if (currentModContentsInstalledModule != null || currentModContentsModule != null) { - Util.Invoke(ContentsPreviewTree, () => _UpdateModContentsTree(currentModContentsInstalledModule, - currentModContentsModule, true)); + Util.Invoke(ContentsPreviewTree, + () => _UpdateModContentsTree(currentModContentsInstalledModule, + currentModContentsModule, true)); } } @@ -108,11 +121,33 @@ private void ContentsOpenButton_Click(object? sender, EventArgs? e) } } + private void SelectedMod_PropertyChanged(object? sender, PropertyChangedEventArgs? e) + { + switch (e?.PropertyName) + { + case nameof(GUIMod.IsCached): + RefreshModContentsTree(); + break; + } + } + + private void Configuration_PropertyChanged(object? sender, PropertyChangedEventArgs? e) + { + switch (e?.PropertyName) + { + case nameof(IConfiguration.GlobalInstallFilters): + RefreshModContentsTree(); + break; + } + } + private void _UpdateModContentsTree(InstalledModule? instMod, CkanModule? module, bool force = false) { if (module == null) { + currentModContentsModule = null; + currentModContentsInstalledModule = null; NotCachedLabel.Text = ""; ContentsPreviewTree.Enabled = false; ContentsDownloadButton.Enabled = false; diff --git a/GUI/Controls/ModInfoTabs/Metadata.cs b/GUI/Controls/ModInfoTabs/Metadata.cs index dd8079fe22..72e54d7b70 100644 --- a/GUI/Controls/ModInfoTabs/Metadata.cs +++ b/GUI/Controls/ModInfoTabs/Metadata.cs @@ -28,6 +28,7 @@ public void UpdateModInfo(GUIMod gui_module) Util.Invoke(this, () => { + MetadataTable.SuspendLayout(); MetadataModuleVersionTextBox.Text = gui_module.LatestVersion.ToString(); MetadataModuleLicenseTextBox.Text = string.Join(", ", module.license); @@ -35,7 +36,7 @@ public void UpdateModInfo(GUIMod gui_module) MetadataIdentifierTextBox.Text = module.identifier; - if (module.release_status == null) + if (module.release_status is null or ReleaseStatus.stable) { ReleaseLabel.Visible = false; MetadataModuleReleaseStatusTextBox.Visible = false; @@ -46,7 +47,7 @@ public void UpdateModInfo(GUIMod gui_module) ReleaseLabel.Visible = true; MetadataModuleReleaseStatusTextBox.Visible = true; MetadataTable.LayoutSettings.RowStyles[3].Height = 30; - MetadataModuleReleaseStatusTextBox.Text = module.release_status.ToString(); + MetadataModuleReleaseStatusTextBox.Text = module.release_status.LocalizeName(); } var compatMod = gui_module.LatestCompatibleMod @@ -90,6 +91,7 @@ public void UpdateModInfo(GUIMod gui_module) AddResourceLink(Properties.Resources.ModInfoStoreLabel, res.store); AddResourceLink(Properties.Resources.ModInfoSteamStoreLabel, res.steamstore); } + MetadataTable.ResumeLayout(); }); } @@ -235,6 +237,7 @@ private void ResizeResourceRows() { if (staticRowCount > 0) { + MetadataTable.SuspendLayout(); var rWidth = RightColumnWidth; for (int row = staticRowCount; row < MetadataTable.RowStyles.Count; ++row) { @@ -247,6 +250,7 @@ private void ResizeResourceRows() LinkLabelStringHeight(link, rWidth)); } } + MetadataTable.ResumeLayout(); } } diff --git a/GUI/Controls/ModInfoTabs/Relationships.cs b/GUI/Controls/ModInfoTabs/Relationships.cs index 5a8d8e0ebf..75d074f279 100644 --- a/GUI/Controls/ModInfoTabs/Relationships.cs +++ b/GUI/Controls/ModInfoTabs/Relationships.cs @@ -11,6 +11,7 @@ using Autofac; +using CKAN.Configuration; using CKAN.Versioning; using CKAN.Extensions; using CKAN.GUI.Attributes; @@ -87,7 +88,7 @@ private void UpdateModDependencyGraph(CkanModule? module) } private GUIMod? selectedModule; - private static GameInstanceManager? manager => Main.Instance?.Manager; + private static GameInstanceManager? Manager => Main.Instance?.Manager; private readonly RepositoryDataManager repoData; private void DependsGraphTree_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e) @@ -132,20 +133,20 @@ private void ReverseRelationshipsCheckbox_CheckedChanged(object? sender, EventAr private void _UpdateModDependencyGraph(CkanModule module) { - if (manager?.CurrentInstance != null) + if (Manager?.CurrentInstance != null) { DependsGraphTree.BeginUpdate(); DependsGraphTree.BackColor = SystemColors.Window; DependsGraphTree.LineColor = SystemColors.WindowText; DependsGraphTree.Nodes.Clear(); - IRegistryQuerier registry = RegistryManager.Instance(manager.CurrentInstance, repoData).registry; + IRegistryQuerier registry = RegistryManager.Instance(Manager.CurrentInstance, repoData).registry; TreeNode root = new TreeNode($"{module.name} {module.version}", 0, 0) { Name = module.identifier, Tag = module }; DependsGraphTree.Nodes.Add(root); - AddChildren(registry, root); + AddChildren(registry, Manager.CurrentInstance.StabilityToleranceConfig, root); root.Expand(); // Expand virtual depends nodes foreach (var node in root.Nodes.OfType() @@ -160,9 +161,9 @@ private void _UpdateModDependencyGraph(CkanModule module) private void BeforeExpand(object? sender, TreeViewCancelEventArgs? args) { - if (manager?.CurrentInstance != null && args?.Node is TreeNode node) + if (Manager?.CurrentInstance != null && args?.Node is TreeNode node) { - IRegistryQuerier registry = RegistryManager.Instance(manager.CurrentInstance, repoData).registry; + IRegistryQuerier registry = RegistryManager.Instance(Manager.CurrentInstance, repoData).registry; const int modsPerUpdate = 10; // Load in groups to reduce flickering @@ -175,7 +176,8 @@ private void BeforeExpand(object? sender, TreeViewCancelEventArgs? args) int nodesLeft = node.Nodes.Count - start; Task.Factory.StartNew(() => ExpandOnePage( - registry, node, threadStart, + registry, Manager.CurrentInstance.StabilityToleranceConfig, + node, threadStart, // If next page is small (last), add it to this one, // so the final page will be slower rather than faster nodesLeft >= 2 * modsPerUpdate ? modsPerUpdate : nodesLeft)); @@ -184,7 +186,11 @@ private void BeforeExpand(object? sender, TreeViewCancelEventArgs? args) } [ForbidGUICalls] - private void ExpandOnePage(IRegistryQuerier registry, TreeNode parent, int start, int length) + private void ExpandOnePage(IRegistryQuerier registry, + StabilityToleranceConfig stabilityTolerance, + TreeNode parent, + int start, + int length) { // Should already have children, since the user is expanding it var nodesAndChildren = parent.Nodes.Cast() @@ -196,7 +202,7 @@ private void ExpandOnePage(IRegistryQuerier registry, TreeNode parent, int start && child.TreeView != null) .Select(child => new KeyValuePair( child, - GetChildren(registry, child).ToArray())) + GetChildren(registry, stabilityTolerance, child).ToArray())) .ToArray(); // If user switched to another mod, stop loading if (parent.TreeView != null) @@ -231,22 +237,26 @@ private void ExpandOnePage(IRegistryQuerier registry, TreeNode parent, int start RelationshipType.Conflicts }; - private void AddChildren(IRegistryQuerier registry, TreeNode node) + private void AddChildren(IRegistryQuerier registry, + StabilityToleranceConfig stabilityTolerance, + TreeNode node) { - var nodes = GetChildren(registry, node).ToArray(); + var nodes = GetChildren(registry, stabilityTolerance, node).ToArray(); Util.Invoke(this, () => node.Nodes.AddRange(nodes)); } // Load one layer of grandchildren on demand - private IEnumerable GetChildren(IRegistryQuerier registry, TreeNode node) + private IEnumerable GetChildren(IRegistryQuerier registry, + StabilityToleranceConfig stabilityTolerance, + TreeNode node) // Skip children of nodes from circular dependencies // Tag is null for non-indexed nodes => !ImMyOwnGrandpa(node) && node.Tag is CkanModule module - && manager?.CurrentInstance?.VersionCriteria() is GameVersionCriteria crit + && Manager?.CurrentInstance?.VersionCriteria() is GameVersionCriteria crit ? ReverseRelationshipsCheckbox.CheckState == CheckState.Unchecked - ? ForwardRelationships(registry, module, crit) - : ReverseRelationships(registry, module, crit) + ? ForwardRelationships(registry, module, stabilityTolerance, crit) + : ReverseRelationships(registry, module, stabilityTolerance, crit) : Enumerable.Empty(); private static IEnumerable GetModRelationships(CkanModule module, RelationshipType which) @@ -272,28 +282,32 @@ private static IEnumerable GetModRelationships(CkanModul return Enumerable.Empty(); } - private IEnumerable ForwardRelationships(IRegistryQuerier registry, CkanModule module, GameVersionCriteria crit) - => (module.provides?.Select(providedNode) + private IEnumerable ForwardRelationships(IRegistryQuerier registry, + CkanModule module, + StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria crit) + => (module.provides?.Select(ProvidedNode) ?? Enumerable.Empty()) .Concat(kindsOfRelationships.SelectMany(relationship => GetModRelationships(module, relationship).Select(dependency => // Look for compatible mods - findDependencyShallow(registry, dependency, relationship, crit) + FindDependencyShallow(registry, dependency, relationship, stabilityTolerance, crit) // Then incompatible mods - ?? findDependencyShallow(registry, dependency, relationship, null) + ?? FindDependencyShallow(registry, dependency, relationship, stabilityTolerance, null) // Then give up and note the name without a module - ?? nonindexedNode(dependency, relationship)))); + ?? NonindexedNode(dependency, relationship)))); - private TreeNode? findDependencyShallow(IRegistryQuerier registry, - RelationshipDescriptor relDescr, - RelationshipType relationship, - GameVersionCriteria? crit) + private TreeNode? FindDependencyShallow(IRegistryQuerier registry, + RelationshipDescriptor relDescr, + RelationshipType relationship, + StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria? crit) { var childNodes = relDescr.LatestAvailableWithProvides( - registry, crit, + registry, stabilityTolerance, crit, // Ignore conflicts with installed mods new List()) - .Select(dep => indexedNode(registry, dep, relationship, relDescr, crit)) + .Select(dep => IndexedNode(registry, dep, relationship, relDescr, stabilityTolerance, crit)) .ToList(); // Check if this dependency is installed @@ -305,11 +319,11 @@ private IEnumerable ForwardRelationships(IRegistryQuerier registry, Ck { if (matched == null) { - childNodes.Add(nonModuleNode(relDescr, null, relationship)); + childNodes.Add(NonModuleNode(relDescr, null, relationship)); } else { - var newNode = indexedNode(registry, matched, relationship, relDescr, crit); + var newNode = IndexedNode(registry, matched, relationship, relDescr, stabilityTolerance, crit); if (childNodes.FindIndex(nd => (nd.Tag as CkanModule)?.identifier == matched.identifier) is int index && index != -1) { @@ -344,25 +358,28 @@ private IEnumerable ForwardRelationships(IRegistryQuerier registry, Ck } } - private IEnumerable ReverseRelationships(IRegistryQuerier registry, CkanModule module, GameVersionCriteria crit) + private IEnumerable ReverseRelationships(IRegistryQuerier registry, + CkanModule module, + StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria crit) { - var compat = registry.CompatibleModules(crit).ToArray(); - var incompat = registry.IncompatibleModules(crit).ToArray(); + var compat = registry.CompatibleModules(stabilityTolerance, crit).ToArray(); + var incompat = registry.IncompatibleModules(stabilityTolerance, crit).ToArray(); var toFind = new CkanModule[] { module }; return kindsOfRelationships.SelectMany(relationship => compat.SelectMany(otherMod => GetModRelationships(otherMod, relationship) .Where(r => r.MatchesAny(toFind, null, null)) - .Select(r => indexedNode(registry, otherMod, relationship, r, crit))) + .Select(r => IndexedNode(registry, otherMod, relationship, r, stabilityTolerance, crit))) .Concat(incompat.SelectMany(otherMod => GetModRelationships(otherMod, relationship) .Where(r => r.MatchesAny(toFind, null, null)) - .Select(r => indexedNode(registry, otherMod, relationship, r, crit))))); + .Select(r => IndexedNode(registry, otherMod, relationship, r, stabilityTolerance, crit))))); } private static TreeNode providesNode(string identifier, - RelationshipType relationship, - TreeNode[] children) + RelationshipType relationship, + TreeNode[] children) { int icon = (int)relationship + 1; var node = new TreeNode(string.Format(Properties.Resources.ModInfoVirtual, @@ -370,28 +387,29 @@ private static TreeNode providesNode(string identifier, icon, icon, children) { Name = identifier, - ToolTipText = relationship.Localize(), + ToolTipText = relationship.LocalizeDescription(), ForeColor = SystemColors.GrayText, }; return node; } - private TreeNode indexedNode(IRegistryQuerier registry, - CkanModule module, - RelationshipType relationship, - RelationshipDescriptor relDescr, - GameVersionCriteria? crit) + private TreeNode IndexedNode(IRegistryQuerier registry, + CkanModule module, + RelationshipType relationship, + RelationshipDescriptor relDescr, + StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria? crit) { int icon = (int)relationship + 1; bool missingDLC = module.IsDLC && !registry.InstalledDlc.ContainsKey(module.identifier); - bool compatible = crit != null && registry.IdentifierCompatible(module.identifier, crit); - string suffix = compatible || manager?.CurrentInstance == null + bool compatible = crit != null && registry.IdentifierCompatible(module.identifier, stabilityTolerance, crit); + string suffix = compatible || Manager?.CurrentInstance == null ? "" - : $" ({registry.CompatibleGameVersions(manager.CurrentInstance.game, module.identifier)})"; + : $" ({registry.CompatibleGameVersions(Manager.CurrentInstance.game, module.identifier)})"; return new TreeNode($"{module.name} {module.version}{suffix}", icon, icon) { Name = module.identifier, - ToolTipText = $"{relationship.Localize()} {relDescr}", + ToolTipText = $"{relationship.LocalizeDescription()} {relDescr}", Tag = module, ForeColor = (compatible && !missingDLC) ? SystemColors.WindowText @@ -403,7 +421,7 @@ private TreeNode indexedNode(IRegistryQuerier registry, }; } - private TreeNode nonModuleNode(RelationshipDescriptor relDescr, + private TreeNode NonModuleNode(RelationshipDescriptor relDescr, ModuleVersion? version, RelationshipType relationship) { @@ -411,13 +429,13 @@ private TreeNode nonModuleNode(RelationshipDescriptor relDescr, return new TreeNode($"{relDescr} {version}", icon, icon) { Name = relDescr.ToString(), - ToolTipText = relationship.Localize(), + ToolTipText = relationship.LocalizeDescription(), NodeFont = new Font(DependsGraphTree.Font, FontStyle.Bold), }; } - private static TreeNode nonindexedNode(RelationshipDescriptor relDescr, + private static TreeNode NonindexedNode(RelationshipDescriptor relDescr, RelationshipType relationship) { // Completely nonexistent dependency, e.g. "AJE" @@ -427,16 +445,16 @@ private static TreeNode nonindexedNode(RelationshipDescriptor relDescr, icon, icon) { Name = relDescr.ToString(), - ToolTipText = relationship.Localize(), + ToolTipText = relationship.LocalizeDescription(), ForeColor = Color.Red }; } - private TreeNode providedNode(string identifier) + private TreeNode ProvidedNode(string identifier) => new TreeNode(identifier, 1, 1) { Name = identifier, - ToolTipText = $"{RelationshipType.Provides.Localize()} {identifier}", + ToolTipText = $"{RelationshipType.Provides.LocalizeDescription()} {identifier}", }; } diff --git a/GUI/Controls/ModInfoTabs/Versions.Designer.cs b/GUI/Controls/ModInfoTabs/Versions.Designer.cs index b6943c0a0b..c5666ee987 100644 --- a/GUI/Controls/ModInfoTabs/Versions.Designer.cs +++ b/GUI/Controls/ModInfoTabs/Versions.Designer.cs @@ -30,33 +30,36 @@ private void InitializeComponent() { this.components = new System.ComponentModel.Container(); System.ComponentModel.ComponentResourceManager resources = new SingleAssemblyComponentResourceManager(typeof(Versions)); - this.label1 = new System.Windows.Forms.Label(); + this.OverallSummaryLabel = new System.Windows.Forms.Label(); this.VersionsListView = new ThemedListView(); this.ModVersion = new System.Windows.Forms.ColumnHeader(); this.CompatibleGameVersion = new System.Windows.Forms.ColumnHeader(); this.ReleaseDate = new System.Windows.Forms.ColumnHeader(); - this.label2 = new System.Windows.Forms.Label(); - this.label3 = new System.Windows.Forms.Label(); - this.label4 = new System.Windows.Forms.Label(); - this.label5 = new System.Windows.Forms.Label(); - this.label6 = new System.Windows.Forms.Label(); - this.label7 = new System.Windows.Forms.Label(); + this.LabelTable = new System.Windows.Forms.TableLayoutPanel(); + this.LatestCompatibleLabel = new System.Windows.Forms.Label(); + this.CompatibleLabel = new System.Windows.Forms.Label(); + this.InstalledLabel = new System.Windows.Forms.Label(); + this.PrereleaseLabel = new System.Windows.Forms.Label(); + this.StabilityToleranceLabel = new System.Windows.Forms.Label(); + this.StabilityToleranceComboBox = new System.Windows.Forms.ComboBox(); + this.LabelTable.SuspendLayout(); this.SuspendLayout(); // - // label1 + // OverallSummaryLabel // - this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(0, 0); - this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(183, 13); - this.label1.TabIndex = 0; - resources.ApplyResources(this.label1, "label1"); + this.OverallSummaryLabel.AutoSize = true; + this.OverallSummaryLabel.Dock = System.Windows.Forms.DockStyle.Top; + this.OverallSummaryLabel.Location = new System.Drawing.Point(0, 0); + this.OverallSummaryLabel.Margin = new System.Windows.Forms.Padding(0, 0, 0, 6); + this.OverallSummaryLabel.Padding = new System.Windows.Forms.Padding(0, 0, 0, 6); + this.OverallSummaryLabel.Name = "OverallSummaryLabel"; + this.OverallSummaryLabel.Size = new System.Drawing.Size(183, 13); + this.OverallSummaryLabel.TabIndex = 0; + resources.ApplyResources(this.OverallSummaryLabel, "OverallSummaryLabel"); // // VersionsListView // - this.VersionsListView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + this.VersionsListView.Dock = System.Windows.Forms.DockStyle.Fill; this.VersionsListView.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; this.VersionsListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { this.ModVersion, @@ -64,13 +67,15 @@ private void InitializeComponent() this.ReleaseDate}); this.VersionsListView.CheckBoxes = true; this.VersionsListView.FullRowSelect = true; - this.VersionsListView.Location = new System.Drawing.Point(6, 76); + this.VersionsListView.Location = new System.Drawing.Point(6, 95); this.VersionsListView.Name = "VersionsListView"; - this.VersionsListView.Size = new System.Drawing.Size(488, 416); + this.VersionsListView.Size = new System.Drawing.Size(488, 397); this.VersionsListView.TabIndex = 1; + this.VersionsListView.ShowItemToolTips = true; this.VersionsListView.UseCompatibleStateImageBehavior = false; this.VersionsListView.View = System.Windows.Forms.View.Details; this.VersionsListView.ItemCheck += new System.Windows.Forms.ItemCheckEventHandler(this.VersionsListView_ItemCheck); + this.VersionsListView.ItemSelectionChanged += new System.Windows.Forms.ListViewItemSelectionChangedEventHandler(this.VersionsListView_ItemSelectionChanged); // // ModVersion // @@ -87,93 +92,142 @@ private void InitializeComponent() this.ReleaseDate.Width = 140; resources.ApplyResources(this.ReleaseDate, "ReleaseDate"); // - // label2 - // - this.label2.AutoSize = true; - this.label2.BackColor = System.Drawing.Color.Green; - this.label2.ForeColor = System.Drawing.Color.White; - this.label2.Location = new System.Drawing.Point(4, 17); - this.label2.Name = "label2"; - this.label2.Size = new System.Drawing.Size(36, 13); - this.label2.TabIndex = 2; - resources.ApplyResources(this.label2, "label2"); - // - // label3 - // - this.label3.AutoSize = true; - this.label3.BackColor = System.Drawing.Color.LightGreen; - this.label3.Location = new System.Drawing.Point(4, 36); - this.label3.Name = "label3"; - this.label3.Size = new System.Drawing.Size(60, 13); - this.label3.TabIndex = 3; - resources.ApplyResources(this.label3, "label3"); - // - // label4 - // - this.label4.AutoSize = true; - this.label4.Font = new System.Drawing.Font("Microsoft Sans Serif", 7.2F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(238))); - this.label4.Location = new System.Drawing.Point(0, 56); - this.label4.Name = "label4"; - this.label4.Size = new System.Drawing.Size(32, 13); - this.label4.TabIndex = 4; - resources.ApplyResources(this.label4, "label4"); - // - // label5 - // - this.label5.AutoSize = true; - this.label5.Location = new System.Drawing.Point(65, 17); - this.label5.Name = "label5"; - this.label5.Size = new System.Drawing.Size(229, 13); - this.label5.TabIndex = 5; - resources.ApplyResources(this.label5, "label5"); - // - // label6 - // - this.label6.AutoSize = true; - this.label6.Location = new System.Drawing.Point(65, 36); - this.label6.Name = "label6"; - this.label6.Size = new System.Drawing.Size(180, 13); - this.label6.TabIndex = 6; - resources.ApplyResources(this.label6, "label6"); - // - // label7 - // - this.label7.AutoSize = true; - this.label7.Location = new System.Drawing.Point(65, 56); - this.label7.Name = "label7"; - this.label7.Size = new System.Drawing.Size(131, 13); - this.label7.TabIndex = 7; - resources.ApplyResources(this.label7, "label7"); + // LabelTable + // + this.LabelTable.AutoSize = true; + this.LabelTable.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.LabelTable.ColumnCount = 1; + this.LabelTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.LabelTable.Controls.Add(this.PrereleaseLabel, 0, 0); + this.LabelTable.Controls.Add(this.InstalledLabel, 0, 1); + this.LabelTable.Controls.Add(this.LatestCompatibleLabel, 0, 2); + this.LabelTable.Controls.Add(this.CompatibleLabel, 0, 3); + this.LabelTable.Controls.Add(this.StabilityToleranceLabel, 0, 4); + this.LabelTable.Controls.Add(this.StabilityToleranceComboBox, 0, 5); + this.LabelTable.Dock = System.Windows.Forms.DockStyle.Bottom; + this.LabelTable.Location = new System.Drawing.Point(0, 0); + this.LabelTable.Name = "LabelTable"; + this.LabelTable.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0); + this.LabelTable.RowCount = 6; + this.LabelTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.AutoSize)); + this.LabelTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.AutoSize)); + this.LabelTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.AutoSize)); + this.LabelTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.AutoSize)); + this.LabelTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.AutoSize)); + this.LabelTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.AutoSize)); + this.LabelTable.Size = new System.Drawing.Size(346, 255); + this.LabelTable.TabIndex = 0; + // + // LatestCompatibleLabel + // + this.LatestCompatibleLabel.AutoSize = true; + this.LatestCompatibleLabel.BackColor = System.Drawing.Color.Green; + this.LatestCompatibleLabel.ForeColor = System.Drawing.Color.White; + this.LatestCompatibleLabel.Location = new System.Drawing.Point(0, 17); + this.LatestCompatibleLabel.Margin = new System.Windows.Forms.Padding(0, 2, 0, 2); + this.LatestCompatibleLabel.Padding = new System.Windows.Forms.Padding(6, 1, 6, 1); + this.LatestCompatibleLabel.Name = "LatestCompatibleLabel"; + this.LatestCompatibleLabel.Size = new System.Drawing.Size(229, 13); + this.LatestCompatibleLabel.TabIndex = 5; + resources.ApplyResources(this.LatestCompatibleLabel, "LatestCompatibleLabel"); + // + // CompatibleLabel + // + this.CompatibleLabel.AutoSize = true; + this.CompatibleLabel.BackColor = System.Drawing.Color.LightGreen; + this.CompatibleLabel.ForeColor = System.Drawing.SystemColors.WindowText; + this.CompatibleLabel.Location = new System.Drawing.Point(0, 36); + this.CompatibleLabel.Margin = new System.Windows.Forms.Padding(0, 2, 0, 2); + this.CompatibleLabel.Padding = new System.Windows.Forms.Padding(6, 1, 6, 1); + this.CompatibleLabel.Name = "CompatibleLabel"; + this.CompatibleLabel.Size = new System.Drawing.Size(180, 13); + this.CompatibleLabel.TabIndex = 6; + resources.ApplyResources(this.CompatibleLabel, "CompatibleLabel"); + // + // InstalledLabel + // + this.InstalledLabel.AutoSize = true; + this.InstalledLabel.Font = new System.Drawing.Font(System.Drawing.SystemFonts.DefaultFont.Name, 8F, System.Drawing.FontStyle.Bold); + this.InstalledLabel.BackColor = System.Drawing.SystemColors.Window; + this.InstalledLabel.ForeColor = System.Drawing.SystemColors.WindowText; + this.InstalledLabel.Location = new System.Drawing.Point(0, 55); + this.InstalledLabel.Margin = new System.Windows.Forms.Padding(0, 2, 0, 2); + this.InstalledLabel.Padding = new System.Windows.Forms.Padding(6, 1, 6, 1); + this.InstalledLabel.Name = "InstalledLabel"; + this.InstalledLabel.Size = new System.Drawing.Size(131, 13); + this.InstalledLabel.TabIndex = 7; + this.InstalledLabel.Visible = false; + resources.ApplyResources(this.InstalledLabel, "InstalledLabel"); + // + // PrereleaseLabel + // + this.PrereleaseLabel.AutoSize = true; + this.PrereleaseLabel.Font = new System.Drawing.Font(System.Drawing.SystemFonts.DefaultFont.Name, 8F, System.Drawing.FontStyle.Italic); + this.PrereleaseLabel.BackColor = System.Drawing.Color.Gold; + this.PrereleaseLabel.ForeColor = System.Drawing.SystemColors.WindowText; + this.PrereleaseLabel.Location = new System.Drawing.Point(0, 74); + this.PrereleaseLabel.Margin = new System.Windows.Forms.Padding(0, 2, 0, 2); + this.PrereleaseLabel.Padding = new System.Windows.Forms.Padding(6, 1, 6, 1); + this.PrereleaseLabel.Name = "PrereleaseLabel"; + this.PrereleaseLabel.Size = new System.Drawing.Size(131, 13); + this.PrereleaseLabel.TabIndex = 7; + this.PrereleaseLabel.Visible = false; + resources.ApplyResources(this.PrereleaseLabel, "PrereleaseLabel"); + // + // StabilityToleranceLabel + // + this.StabilityToleranceLabel.AutoSize = true; + this.StabilityToleranceLabel.Location = new System.Drawing.Point(0, 146); + this.StabilityToleranceLabel.Name = "StabilityToleranceLabel"; + this.StabilityToleranceLabel.Margin = new System.Windows.Forms.Padding(0, 2, 0, 0); + this.StabilityToleranceLabel.Padding = new System.Windows.Forms.Padding(0, 1, 0, 1); + this.StabilityToleranceLabel.Size = new System.Drawing.Size(220, 17); + this.StabilityToleranceLabel.TabStop = false; + resources.ApplyResources(this.StabilityToleranceLabel, "StabilityToleranceLabel"); + // + // StabilityToleranceComboBox + // + this.StabilityToleranceComboBox.AutoSize = false; + this.StabilityToleranceComboBox.Location = new System.Drawing.Point(0, 146); + this.StabilityToleranceComboBox.Margin = new System.Windows.Forms.Padding(0, 1, 0, 1); + this.StabilityToleranceComboBox.Padding = new System.Windows.Forms.Padding(0); + this.StabilityToleranceComboBox.Name = "StabilityToleranceComboBox"; + this.StabilityToleranceComboBox.Size = new System.Drawing.Size(220, 17); + this.StabilityToleranceComboBox.TabIndex = 8; + this.StabilityToleranceComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.StabilityToleranceComboBox.SelectionChangeCommitted += new System.EventHandler(this.StabilityToleranceComboBox_SelectionChanged); + this.StabilityToleranceComboBox.MouseWheel += new System.Windows.Forms.MouseEventHandler(this.StabilityToleranceComboBox_MouseWheel); // // Versions // - this.Controls.Add(this.label7); - this.Controls.Add(this.label6); - this.Controls.Add(this.label5); - this.Controls.Add(this.label4); - this.Controls.Add(this.label3); - this.Controls.Add(this.label2); + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.Controls.Add(this.VersionsListView); - this.Controls.Add(this.label1); + this.Controls.Add(this.OverallSummaryLabel); + this.Controls.Add(this.LabelTable); this.Name = "Versions"; + this.Padding = new System.Windows.Forms.Padding(6); this.Size = new System.Drawing.Size(500, 500); resources.ApplyResources(this, "$this"); + this.LabelTable.ResumeLayout(false); + this.LabelTable.PerformLayout(); this.ResumeLayout(false); this.PerformLayout(); } #endregion - private System.Windows.Forms.Label label1; + private System.Windows.Forms.Label OverallSummaryLabel; private System.Windows.Forms.ListView VersionsListView; private System.Windows.Forms.ColumnHeader ModVersion; private System.Windows.Forms.ColumnHeader CompatibleGameVersion; private System.Windows.Forms.ColumnHeader ReleaseDate; - private System.Windows.Forms.Label label2; - private System.Windows.Forms.Label label3; - private System.Windows.Forms.Label label4; - private System.Windows.Forms.Label label5; - private System.Windows.Forms.Label label6; - private System.Windows.Forms.Label label7; + private System.Windows.Forms.TableLayoutPanel LabelTable; + private System.Windows.Forms.Label LatestCompatibleLabel; + private System.Windows.Forms.Label CompatibleLabel; + private System.Windows.Forms.Label InstalledLabel; + private System.Windows.Forms.Label PrereleaseLabel; + private System.Windows.Forms.Label StabilityToleranceLabel; + private System.Windows.Forms.ComboBox StabilityToleranceComboBox; } } diff --git a/GUI/Controls/ModInfoTabs/Versions.cs b/GUI/Controls/ModInfoTabs/Versions.cs index d58ba4799b..f3adfa12d1 100644 --- a/GUI/Controls/ModInfoTabs/Versions.cs +++ b/GUI/Controls/ModInfoTabs/Versions.cs @@ -14,6 +14,7 @@ using Autofac; using CKAN.Games; +using CKAN.Extensions; using CKAN.Versioning; using CKAN.GUI.Attributes; @@ -39,7 +40,7 @@ public void ForceRedraw() VersionsListView.EndUpdate(); } - public GUIMod SelectedModule + public GUIMod? SelectedModule { set { @@ -103,10 +104,21 @@ private void VersionsListView_ItemCheck(object sender, ItemCheckEventArgs e) } } + private void VersionsListView_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs? e) + { + if (e is {Item: {Selected: true}}) + { + e.Item.Selected = false; + e.Item.Focused = false; + } + } + [ForbidGUICalls] private static bool installable(CkanModule module, - IRegistryQuerier registry) + IRegistryQuerier registry, + ReleaseStatus stabilityTolerance) => currentInstance != null + && module.release_status <= stabilityTolerance && installable(module, registry, currentInstance.game, currentInstance.VersionCriteria()); @@ -117,9 +129,10 @@ private static bool installable(CkanModule module, IGame game, GameVersionCriteria crit) => module.IsCompatible(crit) - && ModuleInstaller.CanInstall(new List() { module }, - RelationshipResolverOptions.DependsOnlyOpts(), - registry, game, crit); + && currentInstance != null + && ModuleInstaller.CanInstall(new List() { module }, + RelationshipResolverOptions.DependsOnlyOpts(currentInstance.StabilityToleranceConfig), + registry, game, crit); private bool allowInstall(CkanModule module) { @@ -127,13 +140,21 @@ private bool allowInstall(CkanModule module) { return false; } + var stabilityTolerance = currentInstance.StabilityToleranceConfig.ModStabilityTolerance(module.identifier) + ?? currentInstance.StabilityToleranceConfig.OverallStabilityTolerance; IRegistryQuerier registry = RegistryManager.Instance(currentInstance, repoData).registry; - return installable(module, registry) + return installable(module, registry, stabilityTolerance) || (Main.Instance?.YesNoDialog( - string.Format(Properties.Resources.AllModVersionsInstallPrompt, - module.ToString(), - currentInstance.VersionCriteria().ToSummaryString(currentInstance.game)), + module.release_status > stabilityTolerance + ? string.Format(Properties.Resources.AllModVersionsPrereleasePrompt, + module, + module.release_status.LocalizeName(), + stabilityTolerance.LocalizeName()) + : string.Format(Properties.Resources.AllModVersionsInstallPrompt, + module, + currentInstance.VersionCriteria() + .ToSummaryString(currentInstance.game)), Properties.Resources.AllModVersionsInstallYes, Properties.Resources.AllModVersionsInstallNo) ?? false); } @@ -142,7 +163,7 @@ private void visibleGuiModule_PropertyChanged(object? sender, PropertyChangedEve { switch (e?.PropertyName) { - case "SelectedMod": + case nameof(GUIMod.SelectedMod): UpdateSelection(); break; } @@ -198,6 +219,8 @@ private ListViewItem[] getItems(GUIMod gmod, List versions) { var registry = RegistryManager.Instance(currentInstance, repoData).registry; var installedVersion = registry.InstalledVersion(gmod.Identifier); + var stabilityTolerance = currentInstance.StabilityToleranceConfig.ModStabilityTolerance(gmod.Identifier) + ?? currentInstance.StabilityToleranceConfig.OverallStabilityTolerance; var items = versions.OrderByDescending(module => module.version) .Select(module => @@ -215,11 +238,23 @@ private ListViewItem[] getItems(GUIMod gmod, List versions) module.release_date?.ToString("g") ?? "" }) { - Tag = module + Tag = module, }; if (installedVersion != null && installedVersion.IsEqualTo(module.version)) { - toRet.Font = new Font(toRet.Font, FontStyle.Bold); + toRet.Font = new Font(toRet.Font, + module.release_status <= stabilityTolerance + ? InstalledLabel.Font.Style + : InstalledLabel.Font.Style + | PrereleaseLabel.Font.Style); + } + else if (module.release_status > stabilityTolerance) + { + toRet.Font = new Font(toRet.Font, PrereleaseLabel.Font.Style); + } + if (module.release_status > stabilityTolerance) + { + toRet.BackColor = PrereleaseLabel.BackColor; } if (module.Equals(gmod.SelectedMod)) { @@ -239,7 +274,9 @@ private void checkInstallable(ListViewItem[] items) if (currentInstance != null && manager?.Cache != null && user != null && cancelTokenSrc != null && visibleGuiModule != null) { - var registry = RegistryManager.Instance(currentInstance, repoData).registry; + var stabilityTolerance = currentInstance.StabilityToleranceConfig.ModStabilityTolerance(visibleGuiModule.Identifier) + ?? currentInstance.StabilityToleranceConfig.OverallStabilityTolerance; + var registry = RegistryManager.Instance(currentInstance, repoData).registry; ListViewItem? latestCompatible = null; // Load balance the items so they're processed roughly in-order instead of blocks Partitioner.Create(items, true) @@ -254,7 +291,7 @@ private void checkInstallable(ListViewItem[] items) && (item.Tag as CkanModule) != visibleGuiModule.SelectedMod) // Slow step to be performed across multiple cores .Where(item => item.Tag is CkanModule m - && installable(m, registry)) + && installable(m, registry, stabilityTolerance)) // Jump back to GUI thread for the updates for each compatible item .ForAll(item => Util.Invoke(this, () => { @@ -264,17 +301,17 @@ private void checkInstallable(ListViewItem[] items) if (latestCompatible != null) { // Revert color of previous best guess - latestCompatible.BackColor = Color.LightGreen; - latestCompatible.ForeColor = SystemColors.ControlText; + latestCompatible.BackColor = CompatibleLabel.BackColor; + latestCompatible.ForeColor = CompatibleLabel.ForeColor; } latestCompatible = item; - item.BackColor = Color.Green; - item.ForeColor = Color.White; + item.BackColor = LatestCompatibleLabel.BackColor; + item.ForeColor = LatestCompatibleLabel.ForeColor; VersionsListView.EndUpdate(); } else { - item.BackColor = Color.LightGreen; + item.BackColor = CompatibleLabel.BackColor; } })); Util.Invoke(this, () => UseWaitCursor = false); @@ -283,28 +320,84 @@ private void checkInstallable(ListViewItem[] items) private void Refresh(GUIMod gmod) { - // checkInstallable needs this to stop background threads on switch to another mod - cancelTokenSrc = new CancellationTokenSource(); - var startingModule = gmod; - var items = getItems(gmod, getVersions(gmod)); - Util.AsyncInvoke(this, () => + if (currentInstance != null) { - VersionsListView.BeginUpdate(); - VersionsListView.Items.Clear(); - // Make sure user hasn't switched to another mod while we were loading - if (startingModule.Equals(visibleGuiModule)) + // checkInstallable needs this to stop background threads on switch to another mod + cancelTokenSrc = new CancellationTokenSource(); + var startingModule = gmod; + var versions = getVersions(gmod); + var items = getItems(gmod, versions); + var stabilityTolerance = currentInstance.StabilityToleranceConfig.ModStabilityTolerance(gmod.Identifier) + ?? currentInstance.StabilityToleranceConfig.OverallStabilityTolerance; + Util.AsyncInvoke(this, () => { - // Only show checkboxes for non-DLC modules - VersionsListView.CheckBoxes = !gmod.ToModule().IsDLC; - ignoreItemCheck = true; - VersionsListView.Items.AddRange(items); - ignoreItemCheck = false; - VersionsListView.EndUpdate(); - // Check installability in the background because it's slow - UseWaitCursor = true; - Task.Factory.StartNew(() => checkInstallable(items)); - } - }); + UpdateStabilityToleranceComboBox(gmod); + LabelTable.SuspendLayout(); + InstalledLabel.Visible = gmod.IsInstalled; + PrereleaseLabel.Visible = versions.Any(m => m.release_status > stabilityTolerance); + LabelTable.ResumeLayout(); + VersionsListView.BeginUpdate(); + VersionsListView.Items.Clear(); + // Make sure user hasn't switched to another mod while we were loading + if (startingModule.Equals(visibleGuiModule)) + { + // Only show checkboxes for non-DLC modules + VersionsListView.CheckBoxes = !gmod.ToModule().IsDLC; + ignoreItemCheck = true; + VersionsListView.Items.AddRange(items); + VersionsListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent); + VersionsListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize); + ignoreItemCheck = false; + VersionsListView.EndUpdate(); + // Check installability in the background because it's slow + UseWaitCursor = true; + Task.Factory.StartNew(() => checkInstallable(items)); + } + }); + } + } + + private void UpdateStabilityToleranceComboBox(GUIMod gmod) + { + if (currentInstance != null) + { + StabilityToleranceComboBox.Items.Clear(); + StabilityToleranceComboBox.Items.AddRange( + Enumerable.Repeat((ReleaseStatus?)null, 1) + .Concat(Enum.GetValues(typeof(ReleaseStatus)) + .OfType() + .OrderBy(relStat => (int)relStat) + .OfType()) + .Select(relStat => new ReleaseStatusItem(relStat)) + .ToArray()); + StabilityToleranceComboBox.SelectedIndex = + StabilityToleranceComboBox.Items + .OfType() + .Select(item => item.Value) + .ToList() + .IndexOf(currentInstance.StabilityToleranceConfig + .ModStabilityTolerance(gmod.Identifier)); + } + } + + private void StabilityToleranceComboBox_MouseWheel(object sender, MouseEventArgs e) + { + // Don't change values on scroll + if (e is HandledMouseEventArgs me) + { + me.Handled = true; + } + } + + private void StabilityToleranceComboBox_SelectionChanged(object? sender, EventArgs? e) + { + if (currentInstance != null && visibleGuiModule != null + && StabilityToleranceComboBox.SelectedItem is ReleaseStatusItem item) + { + var ident = visibleGuiModule.Identifier; + currentInstance.StabilityToleranceConfig.SetModStabilityTolerance( + ident, item.Value); + } } } } diff --git a/GUI/Controls/ModInfoTabs/Versions.resx b/GUI/Controls/ModInfoTabs/Versions.resx index 778c3f804d..6e6f8e9028 100644 --- a/GUI/Controls/ModInfoTabs/Versions.resx +++ b/GUI/Controls/ModInfoTabs/Versions.resx @@ -117,14 +117,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - All available versions of selected mod + All available versions of selected mod: Mod version Game versions Release date - Green - Light green - Bold - - latest compatible version (likely to be installed) - - version is compatible with your game - - currently installed version + Latest compatible version (likely to be installed) + Version is compatible with your game + Currently installed version + Less stable than stability tolerance setting + Mod stability override: diff --git a/GUI/Controls/Wait.cs b/GUI/Controls/Wait.cs index f59f8fe042..8c88f24313 100644 --- a/GUI/Controls/Wait.cs +++ b/GUI/Controls/Wait.cs @@ -226,16 +226,18 @@ private void RunWorkerCompleted(object? sender, RunWorkerCompletedEventArgs? e) private void ClearProgressBars() { - ProgressBarTable.Controls.Clear(); - ProgressBarTable.RowStyles.Clear(); - progressLabels.Clear(); - progressBars.Clear(); + VerticalSplitter.SplitterDistance = emptyHeight; foreach (var rc in rateCounters.Values) { rc.Stop(); } rateCounters.Clear(); - VerticalSplitter.SplitterDistance = emptyHeight; + ProgressBarTable.SuspendLayout(); + ProgressBarTable.Controls.Clear(); + ProgressBarTable.RowStyles.Clear(); + ProgressBarTable.ResumeLayout(); + progressLabels.Clear(); + progressBars.Clear(); } [ForbidGUICalls] diff --git a/GUI/Dialogs/GameCommandLineOptionsDialog.Designer.cs b/GUI/Dialogs/GameCommandLineOptionsDialog.Designer.cs index 05da3c7440..5a569bbf7c 100644 --- a/GUI/Dialogs/GameCommandLineOptionsDialog.Designer.cs +++ b/GUI/Dialogs/GameCommandLineOptionsDialog.Designer.cs @@ -37,6 +37,7 @@ private void InitializeComponent() this.AddButton = new System.Windows.Forms.Button(); this.AcceptChangesButton = new System.Windows.Forms.Button(); this.CancelChangesButton = new System.Windows.Forms.Button(); + this.BottomButtonPanel.SuspendLayout(); this.SuspendLayout(); // // CmdLineGrid @@ -163,6 +164,8 @@ private void InitializeComponent() this.Padding = new System.Windows.Forms.Padding(8, 8, 8, 0); this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; resources.ApplyResources(this, "$this"); + this.BottomButtonPanel.ResumeLayout(false); + this.BottomButtonPanel.PerformLayout(); this.ResumeLayout(false); this.PerformLayout(); diff --git a/GUI/Dialogs/InstallFiltersDialog.cs b/GUI/Dialogs/InstallFiltersDialog.cs index 6c312d1636..699658300a 100644 --- a/GUI/Dialogs/InstallFiltersDialog.cs +++ b/GUI/Dialogs/InstallFiltersDialog.cs @@ -50,12 +50,15 @@ private void InstallFiltersDialog_Load(object? sender, EventArgs? e) private void InstallFiltersDialog_Closing(object? sender, CancelEventArgs? e) { - var newGlobal = GlobalFiltersTextBox.Text.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); + var newGlobal = GlobalFiltersTextBox.Text.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); var newInstance = InstanceFiltersTextBox.Text.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); Changed = !globalConfig.GlobalInstallFilters.SequenceEqual(newGlobal) || !instance.InstallFilters.SequenceEqual(newInstance); - globalConfig.GlobalInstallFilters = newGlobal; - instance.InstallFilters = newInstance; + if (Changed) + { + globalConfig.GlobalInstallFilters = newGlobal; + instance.InstallFilters = newInstance; + } } private void AddMiniAVCButton_Click(object? sender, EventArgs? e) diff --git a/GUI/Dialogs/SettingsDialog.Designer.cs b/GUI/Dialogs/SettingsDialog.Designer.cs index 756e5089aa..1b64827919 100644 --- a/GUI/Dialogs/SettingsDialog.Designer.cs +++ b/GUI/Dialogs/SettingsDialog.Designer.cs @@ -85,6 +85,8 @@ private void InitializeComponent() this.HideEpochsCheckbox = new System.Windows.Forms.CheckBox(); this.HideVCheckbox = new System.Windows.Forms.CheckBox(); this.AutoSortUpdateCheckBox = new System.Windows.Forms.CheckBox(); + this.StabilityToleranceLabel = new System.Windows.Forms.Label(); + this.StabilityToleranceComboBox = new System.Windows.Forms.ComboBox(); this.RepositoryGroupBox.SuspendLayout(); this.AuthTokensGroupBox.SuspendLayout(); this.CacheGroupBox.SuspendLayout(); @@ -546,7 +548,7 @@ private void InitializeComponent() this.BehaviourGroupBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.BehaviourGroupBox.Location = new System.Drawing.Point(12, 310); this.BehaviourGroupBox.Name = "BehaviourGroupBox"; - this.BehaviourGroupBox.Size = new System.Drawing.Size(254, 150); + this.BehaviourGroupBox.Size = new System.Drawing.Size(254, 173); this.BehaviourGroupBox.TabIndex = 32; this.BehaviourGroupBox.TabStop = false; resources.ApplyResources(this.BehaviourGroupBox, "BehaviourGroupBox"); @@ -618,15 +620,17 @@ private void InitializeComponent() // this.MoreSettingsGroupBox.Controls.Add(this.LanguageSelectionLabel); this.MoreSettingsGroupBox.Controls.Add(this.LanguageSelectionComboBox); - this.MoreSettingsGroupBox.Controls.Add(this.AutoSortUpdateCheckBox); this.MoreSettingsGroupBox.Controls.Add(this.RefreshOnStartupCheckbox); this.MoreSettingsGroupBox.Controls.Add(this.HideEpochsCheckbox); this.MoreSettingsGroupBox.Controls.Add(this.HideVCheckbox); + this.MoreSettingsGroupBox.Controls.Add(this.AutoSortUpdateCheckBox); + this.MoreSettingsGroupBox.Controls.Add(this.StabilityToleranceLabel); + this.MoreSettingsGroupBox.Controls.Add(this.StabilityToleranceComboBox); this.MoreSettingsGroupBox.ForeColor = System.Drawing.SystemColors.ControlText; this.MoreSettingsGroupBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.MoreSettingsGroupBox.Location = new System.Drawing.Point(280, 310); this.MoreSettingsGroupBox.Name = "MoreSettingsGroupBox"; - this.MoreSettingsGroupBox.Size = new System.Drawing.Size(476, 150); + this.MoreSettingsGroupBox.Size = new System.Drawing.Size(476, 173); this.MoreSettingsGroupBox.TabIndex = 39; this.MoreSettingsGroupBox.TabStop = false; resources.ApplyResources(this.MoreSettingsGroupBox, "MoreSettingsGroupBox"); @@ -697,11 +701,31 @@ private void InitializeComponent() this.AutoSortUpdateCheckBox.CheckedChanged += new System.EventHandler(this.AutoSortUpdateCheckBox_CheckedChanged); resources.ApplyResources(this.AutoSortUpdateCheckBox, "AutoSortUpdateCheckBox"); // + // StabilityToleranceLabel + // + this.StabilityToleranceLabel.AutoSize = true; + this.StabilityToleranceLabel.Location = new System.Drawing.Point(12, 146); + this.StabilityToleranceLabel.Name = "StabilityToleranceLabel"; + this.StabilityToleranceLabel.Size = new System.Drawing.Size(220, 17); + this.StabilityToleranceLabel.TabStop = false; + resources.ApplyResources(this.StabilityToleranceLabel, "StabilityToleranceLabel"); + // + // StabilityToleranceComboBox + // + this.StabilityToleranceComboBox.AutoSize = true; + this.StabilityToleranceComboBox.Location = new System.Drawing.Point(244, 146); + this.StabilityToleranceComboBox.Name = "StabilityToleranceComboBox"; + this.StabilityToleranceComboBox.Size = new System.Drawing.Size(220, 17); + this.StabilityToleranceComboBox.TabIndex = 45; + this.StabilityToleranceComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.StabilityToleranceComboBox.SelectionChangeCommitted += new System.EventHandler(this.StabilityToleranceComboBox_SelectionChanged); + this.StabilityToleranceComboBox.MouseWheel += new System.Windows.Forms.MouseEventHandler(this.StabilityToleranceComboBox_MouseWheel); + // // SettingsDialog // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(768, 470); + this.ClientSize = new System.Drawing.Size(768, 493); this.Controls.Add(this.RepositoryGroupBox); this.Controls.Add(this.AuthTokensGroupBox); this.Controls.Add(this.CacheGroupBox); @@ -788,5 +812,7 @@ private void InitializeComponent() private System.Windows.Forms.CheckBox HideEpochsCheckbox; private System.Windows.Forms.CheckBox HideVCheckbox; private System.Windows.Forms.CheckBox AutoSortUpdateCheckBox; + private System.Windows.Forms.Label StabilityToleranceLabel; + private System.Windows.Forms.ComboBox StabilityToleranceComboBox; } } diff --git a/GUI/Dialogs/SettingsDialog.cs b/GUI/Dialogs/SettingsDialog.cs index b5f3edf5e2..9f15923a9d 100644 --- a/GUI/Dialogs/SettingsDialog.cs +++ b/GUI/Dialogs/SettingsDialog.cs @@ -76,6 +76,7 @@ public void UpdateDialog() UpdateRefreshRate(); UpdateCacheInfo(coreConfig.DownloadCacheDir ?? JsonConfiguration.DefaultDownloadCacheDir); + UpdateStabilityToleranceComboBox(); } protected override void OnFormClosing(FormClosingEventArgs e) @@ -777,6 +778,42 @@ private void AutoSortUpdateCheckBox_CheckedChanged(object? sender, EventArgs? e) guiConfig.AutoSortByUpdate = AutoSortUpdateCheckBox.Checked; } + private void UpdateStabilityToleranceComboBox() + { + StabilityToleranceComboBox.Items.Clear(); + StabilityToleranceComboBox.Items.AddRange(Enum.GetValues(typeof(ReleaseStatus)) + .OfType() + .OrderBy(relStat => (int)relStat) + .Select(relStat => new ReleaseStatusItem(relStat)) + .ToArray()); + if (manager?.CurrentInstance != null) + { + StabilityToleranceComboBox.SelectedIndex = (int)manager.CurrentInstance.StabilityToleranceConfig.OverallStabilityTolerance; + } + } + + public bool StabilityToleranceChanged { get; private set; } = false; + + private void StabilityToleranceComboBox_MouseWheel(object sender, MouseEventArgs e) + { + // Don't change values on scroll + if (e is HandledMouseEventArgs me) + { + me.Handled = true; + } + } + + private void StabilityToleranceComboBox_SelectionChanged(object? sender, EventArgs? e) + { + if (manager?.CurrentInstance != null + && StabilityToleranceComboBox.SelectedItem is ReleaseStatusItem item + && item.Value is ReleaseStatus relStat) + { + manager.CurrentInstance.StabilityToleranceConfig.OverallStabilityTolerance = relStat; + StabilityToleranceChanged = true; + } + } + #endregion #region Tray icon diff --git a/GUI/Dialogs/SettingsDialog.resx b/GUI/Dialogs/SettingsDialog.resx index 8061ee3fce..26ada39a32 100644 --- a/GUI/Dialogs/SettingsDialog.resx +++ b/GUI/Dialogs/SettingsDialog.resx @@ -162,6 +162,7 @@ Hide epoch numbers in mod list (requires restart) Hide "v" in mod list (requires restart) Automatically sort by "Update"-column when clicking "Add available updates" + Stability tolerance: Name URL Host diff --git a/GUI/Localization/de-DE/Versions.de-DE.resx b/GUI/Localization/de-DE/Versions.de-DE.resx index 392b001551..de9a075b39 100644 --- a/GUI/Localization/de-DE/Versions.de-DE.resx +++ b/GUI/Localization/de-DE/Versions.de-DE.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + Alle verfügbaren Versionen der ausgewählten Mod @@ -129,22 +129,13 @@ Veröffentlichungsdatum - - Grün + + neueste kompatible Version (wird vermutlich installiert) - - Hellgrün + + kompatible Version - - Fett - - - - neueste kompatible Version (wird vermutlich installiert) - - - - kompatible Version - - - - momentan installierte Version + + momentan installierte Version diff --git a/GUI/Localization/fr-FR/Versions.fr-FR.resx b/GUI/Localization/fr-FR/Versions.fr-FR.resx index cdd5994009..5ec74cb4d5 100644 --- a/GUI/Localization/fr-FR/Versions.fr-FR.resx +++ b/GUI/Localization/fr-FR/Versions.fr-FR.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + Toutes les versions disponibles du mod sélectionné @@ -129,22 +129,13 @@ Date de publication - - Vert + + version compatible la plus récente (préférence) - - Vert clair + + versions compatibles avec votre jeu - - Gras - - - - version compatible la plus récente (préférence) - - - - versions compatibles avec votre jeu - - - - version actuellement installée + + version actuellement installée diff --git a/GUI/Localization/it-IT/Versions.it-IT.resx b/GUI/Localization/it-IT/Versions.it-IT.resx index 40f63a7952..44294afcc3 100644 --- a/GUI/Localization/it-IT/Versions.it-IT.resx +++ b/GUI/Localization/it-IT/Versions.it-IT.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + Tutte le versioni disponibili della mod selezionata @@ -129,22 +129,13 @@ Data di rilascio - - Verde + + ultima versione compatibile (che probabilmente sarà installata) - - Verde chiaro + + la versione è compatibile con il gioco - - Grassetto - - - - ultima versione compatibile (che probabilmente sarà installata) - - - - la versione è compatibile con il gioco - - - - versione attualmente installata + + versione attualmente installata diff --git a/GUI/Localization/ja-JP/Versions.ja-JP.resx b/GUI/Localization/ja-JP/Versions.ja-JP.resx index e8313cecff..5b65859b94 100644 --- a/GUI/Localization/ja-JP/Versions.ja-JP.resx +++ b/GUI/Localization/ja-JP/Versions.ja-JP.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + このModの全てのバージョン @@ -129,22 +129,13 @@ リリース日時 - - + + 最新の対応するバージョン (推奨) - - 黄緑 + + 現在インストールされているゲームに対応するバージョン - - 太字 - - - - 最新の対応するバージョン (推奨) - - - - 現在インストールされているゲームに対応するバージョン - - - - 現在インストールされているバージョン + + 現在インストールされているバージョン diff --git a/GUI/Localization/ko-KR/Versions.ko-KR.resx b/GUI/Localization/ko-KR/Versions.ko-KR.resx index 7e75ada1ce..988c14bd85 100644 --- a/GUI/Localization/ko-KR/Versions.ko-KR.resx +++ b/GUI/Localization/ko-KR/Versions.ko-KR.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + 선택된 모드의 등록된 버전들 @@ -129,22 +129,13 @@ 출시일 - - 녹색 + + 호환되는 마지막 버전이에요(설치될 수 있음). - - 연두색 + + 이 버전은 게임과 호환돼요. - - 볼드체 - - - - 호환되는 마지막 버전이에요(설치될 수 있음). - - - - 이 버전은 게임과 호환돼요. - - - - 지금 설치한 버전이에요. + + 지금 설치한 버전이에요. diff --git a/GUI/Localization/nl-NL/Versions.nl-NL.resx b/GUI/Localization/nl-NL/Versions.nl-NL.resx index b2ec12087c..2ef99bc3be 100644 --- a/GUI/Localization/nl-NL/Versions.nl-NL.resx +++ b/GUI/Localization/nl-NL/Versions.nl-NL.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + Alle beschikbare versies van de geselecteerde mod @@ -129,22 +129,13 @@ Release datum - - Groen + + nieuwste compatibele versie (waarschijnlijk geïnstalleerd) - - Lichtgroen + + versie is compatibel met je spel - - Dikgedrukt - - - - nieuwste compatibele versie (waarschijnlijk geïnstalleerd) - - - - versie is compatibel met je spel - - - - momenteel geïnstalleerde versie + + momenteel geïnstalleerde versie diff --git a/GUI/Localization/pl-PL/Versions.pl-PL.resx b/GUI/Localization/pl-PL/Versions.pl-PL.resx index e63b51228e..07711d444c 100644 --- a/GUI/Localization/pl-PL/Versions.pl-PL.resx +++ b/GUI/Localization/pl-PL/Versions.pl-PL.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + Wszystkie dostępne wersje wybranej modyfikacji @@ -129,22 +129,13 @@ Data wydania - - Zielony + + najnowsza kompatybilna wersja (prawdopodobnie zostanie zainstalowana) - - Jasnozielony + + wersja jest kompatybilna z Twoją grą - - Pogrubiony - - - - najnowsza kompatybilna wersja (prawdopodobnie zostanie zainstalowana) - - - - wersja jest kompatybilna z Twoją grą - - - - aktualnie zainstalowana wersja + + aktualnie zainstalowana wersja diff --git a/GUI/Localization/pt-BR/Versions.pt-BR.resx b/GUI/Localization/pt-BR/Versions.pt-BR.resx index 885b56f18b..ebb2fa1916 100644 --- a/GUI/Localization/pt-BR/Versions.pt-BR.resx +++ b/GUI/Localization/pt-BR/Versions.pt-BR.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + Todas as versões disponíveis do mod selecionado @@ -129,22 +129,13 @@ Data do lançamento - - Verde + + última versão compativel (a que possívelmente será instalada) - - Verde claro + + versão que é compatível com o seu jogo - - Negrito - - - - última versão compativel (a que possívelmente será instalada) - - - - versão que é compatível com o seu jogo - - - - versão instalada atualmente + + versão instalada atualmente diff --git a/GUI/Localization/ru-RU/Versions.ru-RU.resx b/GUI/Localization/ru-RU/Versions.ru-RU.resx index 5c0de9a61c..ef3fdf0329 100644 --- a/GUI/Localization/ru-RU/Versions.ru-RU.resx +++ b/GUI/Localization/ru-RU/Versions.ru-RU.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + Все доступные версии выбранной модификации @@ -129,22 +129,13 @@ Дата выпуска - - Зелёный + + последняя совместимая версия (будет установлена) - - Св.-зел. + + совместима с установленной игрой - - Жирный - - - — последняя совместимая версия (будет установлена) - - - — совместима с установленной игрой - - - — текущая установленная версия + + текущая установленная версия diff --git a/GUI/Localization/tr-TR/Versions.tr-TR.resx b/GUI/Localization/tr-TR/Versions.tr-TR.resx index 014f2c2dd9..59a83b2aa0 100644 --- a/GUI/Localization/tr-TR/Versions.tr-TR.resx +++ b/GUI/Localization/tr-TR/Versions.tr-TR.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + Seçili modun mevcut tüm sürümleri @@ -129,22 +129,13 @@ Yayın tarihi - - Yeşil + + en son uyumlu sürüm (muhtemelen kurulacak) - - Açık yeşil + + oyununuzla uyumlu sürüm - - Kalın - - - - en son uyumlu sürüm (muhtemelen kurulacak) - - - - oyununuzla uyumlu sürüm - - - - şuanda yüklü sürüm + + şuanda yüklü sürüm diff --git a/GUI/Localization/zh-CN/Versions.zh-CN.resx b/GUI/Localization/zh-CN/Versions.zh-CN.resx index f97d673d6b..1c3e8e0917 100644 --- a/GUI/Localization/zh-CN/Versions.zh-CN.resx +++ b/GUI/Localization/zh-CN/Versions.zh-CN.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + 选中Mod的所有可用版本 @@ -129,22 +129,13 @@ 发布日期 - - 绿色 + + 最新的兼容版本(将会被安装) - - 浅绿色 + + 兼容您的游戏的版本 - - 粗体 - - - - 最新的兼容版本(将会被安装) - - - - 兼容您的游戏的版本 - - - - 当前安装版本 + + 当前安装版本 diff --git a/GUI/Main/Main.cs b/GUI/Main/Main.cs index fbfb829e0a..4412bc008c 100644 --- a/GUI/Main/Main.cs +++ b/GUI/Main/Main.cs @@ -357,6 +357,10 @@ private void manageGameInstancesMenuItem_Click(object? sender, EventArgs? e) { ManageMods.ModGrid.ClearSelection(); CurrentInstanceUpdated(); + if (old_instance != null) + { + old_instance.StabilityToleranceConfig.Changed -= StabilityToleranceConfig_Changed; + } done = true; } catch (RegistryInUseKraken kraken) @@ -381,6 +385,15 @@ private void manageGameInstancesMenuItem_Click(object? sender, EventArgs? e) } } + private void StabilityToleranceConfig_Changed(string? identifier, ReleaseStatus? relStat) + { + // null represents the overall setting, for which we'll refresh when the settings dialog closes + if (identifier != null) + { + RefreshModList(false); + } + } + private void UpdateStatusBar() { if (CurrentInstance != null) @@ -407,6 +420,7 @@ private void CurrentInstanceUpdated() { return; } + CurrentInstance.StabilityToleranceConfig.Changed += StabilityToleranceConfig_Changed; // This will throw RegistryInUseKraken if locked by another process var regMgr = RegistryManager.Instance(CurrentInstance, repoData); log.Debug("Current instance updated, scanning"); @@ -719,7 +733,8 @@ private void CKANSettingsToolStripMenuItem_Click(object? sender, EventArgs? e) { UpdateRepo(refreshWithoutChanges: true); } - else if (dialog.RepositoryRemoved || dialog.RepositoryMoved) + else if (dialog.RepositoryRemoved || dialog.RepositoryMoved + || dialog.StabilityToleranceChanged) { RefreshModList(false); } @@ -758,7 +773,6 @@ private void installFiltersToolStripMenuItem_Click(object? sender, EventArgs? e) { // The Update checkbox might appear or disappear if missing files were or are filtered out RefreshModList(false); - ModInfo.RefreshModContentsTree(); } } } @@ -785,6 +799,7 @@ private void InstallFromCkanFiles(string[] files) } // We'll need to make some registry changes to do this. var registry_manager = RegistryManager.Instance(CurrentInstance, repoData); + var stabilityTolerance = CurrentInstance.StabilityToleranceConfig; var crit = CurrentInstance.VersionCriteria(); var installed = registry_manager.registry.InstalledModules.Select(inst => inst.Module).ToList(); @@ -804,9 +819,9 @@ private void InstallFromCkanFiles(string[] files) .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) + rel.ExactMatch(registry_manager.registry, stabilityTolerance, crit, installed, toInstall) // Otherwise look for incompatible - ?? rel.ExactMatch(registry_manager.registry, null, installed, toInstall)) + ?? rel.ExactMatch(registry_manager.registry, stabilityTolerance, null, installed, toInstall)) .OfType()); } toInstall.Add(module); @@ -866,7 +881,7 @@ private void InstallFromCkanFiles(string[] files) } // Get all recursively incompatible module identifiers (quickly) - var allIncompat = registry_manager.registry.IncompatibleModules(crit) + var allIncompat = registry_manager.registry.IncompatibleModules(stabilityTolerance, crit) .Select(mod => mod.identifier) .ToHashSet(); // Get incompatible mods we're installing @@ -945,8 +960,8 @@ private GUIMod? ActiveModInfo { splitContainer1.Panel2Collapsed = false; } - ModInfo.SelectedModule = value; } + ModInfo.SelectedModule = value; } } @@ -958,6 +973,7 @@ private void ShowSelectionModInfo(CkanModule? module) module, repoData, RegistryManager.Instance(CurrentInstance, repoData).registry, + CurrentInstance.StabilityToleranceConfig, CurrentInstance.VersionCriteria(), null, configuration.HideEpochs, @@ -1144,6 +1160,7 @@ private void RefreshModList(bool allowAutoUpdate, Dictionary? oldM tabController.RenameTab("WaitTabPage", Properties.Resources.MainModListWaitTitle); ShowWaitDialog(); DisableMainWindow(); + ActiveModInfo = null; Wait.StartWaiting( ManageMods.Update, (sender, e) => diff --git a/GUI/Main/MainChangeset.cs b/GUI/Main/MainChangeset.cs index cfe322c87a..6b9aaf52ac 100644 --- a/GUI/Main/MainChangeset.cs +++ b/GUI/Main/MainChangeset.cs @@ -41,22 +41,25 @@ private void Changeset_OnCancelChanges(bool reset) private void Changeset_OnConfirmChanges(List changeset) { - DisableMainWindow(); - try - { - Wait.StartWaiting(InstallMods, PostInstallMods, true, - new InstallArgument( - // Only pass along user requested mods, so auto-installed can be determined - changeset.Where(ch => ch.Reasons.Any(r => r is SelectionReason.UserRequested) - // Include all removes and upgrades - || ch.ChangeType != GUIModChangeType.Install) - .ToList(), - RelationshipResolverOptions.DependsOnlyOpts())); - } - catch (InvalidOperationException) + if (CurrentInstance != null) { - // Thrown if it's already busy, can happen if the user double-clicks the button. Ignore it. - // More thread-safe than checking installWorker.IsBusy beforehand. + DisableMainWindow(); + try + { + Wait.StartWaiting(InstallMods, PostInstallMods, true, + new InstallArgument( + // Only pass along user requested mods, so auto-installed can be determined + changeset.Where(ch => ch.Reasons.Any(r => r is SelectionReason.UserRequested) + // Include all removes and upgrades + || ch.ChangeType != GUIModChangeType.Install) + .ToList(), + RelationshipResolverOptions.DependsOnlyOpts(CurrentInstance.StabilityToleranceConfig))); + } + catch (InvalidOperationException) + { + // Thrown if it's already busy, can happen if the user double-clicks the button. Ignore it. + // More thread-safe than checking installWorker.IsBusy beforehand. + } } } } diff --git a/GUI/Main/MainDownload.cs b/GUI/Main/MainDownload.cs index f1e2bdc5f0..f4ad2a31f3 100644 --- a/GUI/Main/MainDownload.cs +++ b/GUI/Main/MainDownload.cs @@ -124,19 +124,12 @@ private void OnCacheChanged(NetModuleCache? prev) Manager.Cache.ModPurged += OnModStoredOrPurged; } UpdateCachedByDownloads(null); - ModInfo.RefreshModContentsTree(); } [ForbidGUICalls] private void OnModStoredOrPurged(CkanModule? module) { UpdateCachedByDownloads(module); - - if (module == null - || ModInfo.SelectedModule?.Identifier == module.identifier) - { - ModInfo.RefreshModContentsTree(); - } } } } diff --git a/GUI/Main/MainHistory.cs b/GUI/Main/MainHistory.cs index 155de0fa4b..0738455bc2 100644 --- a/GUI/Main/MainHistory.cs +++ b/GUI/Main/MainHistory.cs @@ -27,6 +27,7 @@ private void InstallationHistory_Install(CkanModule[] modules) modules.Select(mod => new ModChange(mod, GUIModChangeType.Install)) .ToHashSet(), CurrentInstance.game, + CurrentInstance.StabilityToleranceConfig, CurrentInstance.VersionCriteria()); UpdateChangesDialog(tuple.Item1.ToList(), tuple.Item2); tabController.ShowTab("ChangesetTabPage", 1); @@ -48,6 +49,7 @@ private void InstallationHistory_OnSelectedModuleChanged(CkanModule m) ? null : new GUIMod(m, repoData, RegistryManager.Instance(CurrentInstance, repoData).registry, + CurrentInstance.StabilityToleranceConfig, CurrentInstance.VersionCriteria(), null, configuration?.HideEpochs ?? false, diff --git a/GUI/Main/MainInstall.cs b/GUI/Main/MainInstall.cs index 9f0d7cc796..982c16d5de 100644 --- a/GUI/Main/MainInstall.cs +++ b/GUI/Main/MainInstall.cs @@ -45,35 +45,38 @@ public partial class Main /// Module to install public void InstallModuleDriver(IRegistryQuerier registry, IEnumerable modules) { - try + if (CurrentInstance != null) { - DisableMainWindow(); - var userChangeSet = new List(); - foreach (var module in modules) + try { - var installed = registry.InstalledModule(module.identifier); - if (installed != null) + DisableMainWindow(); + var userChangeSet = new List(); + foreach (var module in modules) { - // Already installed, remove it first - userChangeSet.Add(new ModChange(installed.Module, GUIModChangeType.Remove)); + var 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)); + } + if (userChangeSet.Count > 0) + { + // Resolve the provides relationships in the dependencies + Wait.StartWaiting(InstallMods, PostInstallMods, true, + new InstallArgument(userChangeSet, + RelationshipResolverOptions.DependsOnlyOpts(CurrentInstance.StabilityToleranceConfig))); } - // Install the selected mod - userChangeSet.Add(new ModChange(module, GUIModChangeType.Install)); } - if (userChangeSet.Count > 0) + catch { - // Resolve the provides relationships in the dependencies - Wait.StartWaiting(InstallMods, PostInstallMods, true, - new InstallArgument(userChangeSet, - RelationshipResolverOptions.DependsOnlyOpts())); + // If we failed, do the clean-up normally done by PostInstallMods. + HideWaitDialog(); + EnableMainWindow(); } } - catch - { - // If we failed, do the clean-up normally done by PostInstallMods. - HideWaitDialog(); - EnableMainWindow(); - } } [ForbidGUICalls] @@ -93,6 +96,7 @@ private void InstallMods(object? sender, DoWorkEventArgs? e) var registry_manager = RegistryManager.Instance(CurrentInstance, repoData); var registry = registry_manager.registry; + var stabilityTolerance = CurrentInstance.StabilityToleranceConfig; var installer = new ModuleInstaller(CurrentInstance, Manager.Cache, currentUser, userAgent, cancelTokenSrc.Token); // Avoid accumulating multiple event handlers @@ -135,7 +139,7 @@ private void InstallMods(object? sender, DoWorkEventArgs? e) toInstall.Add(change.Mod); break; case GUIModChangeType.Replace: - var repl = registry.GetReplacement(change.Mod, CurrentInstance.VersionCriteria()); + var repl = registry.GetReplacement(change.Mod, stabilityTolerance, CurrentInstance.VersionCriteria()); if (repl != null) { toUninstall.Add(repl.ToReplace); @@ -288,7 +292,7 @@ private void InstallMods(object? sender, DoWorkEventArgs? e) .ToArray(), registry.InstalledDlls, registry.InstalledDlc, // Consider virtual dependencies satisfied so user can make a new choice if they skip - rel => rel.LatestAvailableWithProvides(registry, crit).Count > 1) + rel => rel.LatestAvailableWithProvides(registry, stabilityTolerance, crit).Count > 1) .ToHashSet(); toInstall.RemoveAll(m => dependers.Contains(m.identifier)); } diff --git a/GUI/Main/MainRecommendations.cs b/GUI/Main/MainRecommendations.cs index d4de5db07c..687a803074 100644 --- a/GUI/Main/MainRecommendations.cs +++ b/GUI/Main/MainRecommendations.cs @@ -68,7 +68,7 @@ private void AuditRecommendations(Registry registry, GameVersionCriteria version new InstallArgument( result.Select(mod => new ModChange(mod, GUIModChangeType.Install)) .ToList(), - RelationshipResolverOptions.DependsOnlyOpts())); + RelationshipResolverOptions.DependsOnlyOpts(CurrentInstance.StabilityToleranceConfig))); } } else diff --git a/GUI/Main/MainRepo.cs b/GUI/Main/MainRepo.cs index a9908d827c..3e601b6705 100644 --- a/GUI/Main/MainRepo.cs +++ b/GUI/Main/MainRepo.cs @@ -66,6 +66,7 @@ private void UpdateRepo(object? sender, DoWorkEventArgs? e) // Note the current mods' compatibility for the NewlyCompatible filter var registry = regMgr.registry; + var stabilityTolerance = CurrentInstance.StabilityToleranceConfig; var cancelTokenSrc = new CancellationTokenSource(); Wait.OnCancel += cancelTokenSrc.Cancel; @@ -78,9 +79,9 @@ private void UpdateRepo(object? sender, DoWorkEventArgs? e) new ProgressImmediate(p => currentUser.RaiseProgress(Properties.Resources.LoadingCachedRepoData, p))); var versionCriteria = CurrentInstance.VersionCriteria(); - var oldModules = registry.CompatibleModules(versionCriteria) + var oldModules = registry.CompatibleModules(stabilityTolerance, versionCriteria) .ToDictionary(m => m.identifier, m => false); - registry.IncompatibleModules(versionCriteria) + registry.IncompatibleModules(stabilityTolerance, versionCriteria) .Where(m => !oldModules.ContainsKey(m.identifier)) .ToList() .ForEach(m => oldModules.Add(m.identifier, true)); diff --git a/GUI/Main/MainTrayIcon.cs b/GUI/Main/MainTrayIcon.cs index 4a1cde6bf9..1961b51af8 100644 --- a/GUI/Main/MainTrayIcon.cs +++ b/GUI/Main/MainTrayIcon.cs @@ -163,11 +163,14 @@ private void minimizeNotifyIcon_BalloonTipClicked(object? sender, EventArgs? e) // Check all the upgrade checkboxes ManageMods.MarkAllUpdates(); - // Install - Wait.StartWaiting(InstallMods, PostInstallMods, true, - new InstallArgument(ManageMods.ComputeUserChangeSet() - .ToList(), - RelationshipResolverOptions.DependsOnlyOpts())); + if (CurrentInstance != null) + { + // Install + Wait.StartWaiting(InstallMods, PostInstallMods, true, + new InstallArgument(ManageMods.ComputeUserChangeSet() + .ToList(), + RelationshipResolverOptions.DependsOnlyOpts(CurrentInstance.StabilityToleranceConfig))); + } } #endregion } diff --git a/GUI/Model/GUIMod.cs b/GUI/Model/GUIMod.cs index f1fc95465a..9a58910374 100644 --- a/GUI/Model/GUIMod.cs +++ b/GUI/Model/GUIMod.cs @@ -8,6 +8,7 @@ using System.Runtime.Versioning; #endif +using CKAN.Configuration; using CKAN.Versioning; namespace CKAN.GUI @@ -131,11 +132,12 @@ public string Version public GUIMod(InstalledModule instMod, RepositoryDataManager repoDataMgr, IRegistryQuerier registry, + StabilityToleranceConfig stabilityTolerance, GameVersionCriteria current_game_version, bool? incompatible, bool hideEpochs, bool hideV) - : this(instMod.Module, repoDataMgr, registry, current_game_version, incompatible, hideEpochs, hideV) + : this(instMod.Module, repoDataMgr, registry, stabilityTolerance, current_game_version, incompatible, hideEpochs, hideV) { IsInstalled = true; InstalledMod = instMod; @@ -159,10 +161,11 @@ public GUIMod(InstalledModule instMod, /// CKAN registry object for current game instance /// Current game version /// If true, mark this module as incompatible - public GUIMod(CkanModule mod, - RepositoryDataManager repoDataMgr, - IRegistryQuerier registry, - GameVersionCriteria current_game_version, + public GUIMod(CkanModule mod, + RepositoryDataManager repoDataMgr, + IRegistryQuerier registry, + StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria current_game_version, bool? incompatible, bool hideEpochs, bool hideV) @@ -180,7 +183,7 @@ public GUIMod(CkanModule mod, { try { - LatestCompatibleMod = registry.LatestAvailable(Identifier, current_game_version); + LatestCompatibleMod = registry.LatestAvailable(Identifier, stabilityTolerance, current_game_version); latest_version = LatestCompatibleMod?.version; } catch (ModuleNotFoundKraken) @@ -199,7 +202,7 @@ public GUIMod(CkanModule mod, try { - LatestAvailableMod = registry.LatestAvailable(Identifier, null); + LatestAvailableMod = registry.LatestAvailable(Identifier, stabilityTolerance, null); } catch { } @@ -242,7 +245,7 @@ public GUIMod(CkanModule mod, .OfType() .ToArray()); - HasReplacement = registry.GetReplacement(mod, current_game_version) != null; + HasReplacement = registry.GetReplacement(mod, stabilityTolerance, current_game_version) != null; DownloadSize = mod.download_size == 0 ? Properties.Resources.GUIModNSlashA : CkanModule.FmtSize(mod.download_size); InstallSize = mod.install_size == 0 ? Properties.Resources.GUIModNSlashA : CkanModule.FmtSize(mod.install_size); diff --git a/GUI/Model/ModChange.cs b/GUI/Model/ModChange.cs index 4129908244..e0e18ccc49 100644 --- a/GUI/Model/ModChange.cs +++ b/GUI/Model/ModChange.cs @@ -112,7 +112,7 @@ public override int GetHashCode() : (((maxEnumVal + 1) * Mod.GetHashCode()) + (int)ChangeType); public override string ToString() - => $"{ChangeType.Localize()} {Mod} ({Description})"; + => $"{ChangeType.LocalizeDescription()} {Mod} ({Description})"; public virtual string? NameAndStatus => Main.Instance?.Manager?.Cache?.DescribeAvailability(Mod); diff --git a/GUI/Model/ModList.cs b/GUI/Model/ModList.cs index 052e5b8bc9..85ea02003c 100644 --- a/GUI/Model/ModList.cs +++ b/GUI/Model/ModList.cs @@ -9,8 +9,10 @@ using System.Runtime.Versioning; #endif +using Autofac; using log4net; +using CKAN.Configuration; using CKAN.Versioning; #if !NET8_0_OR_GREATER using CKAN.Extensions; @@ -110,13 +112,14 @@ public static SavedSearch FilterToSavedSearch(GUIModFilter filter, Values = new List() { new ModSearch(filter, tag, label).Combined ?? "" }, }; - private static readonly RelationshipResolverOptions conflictOptions = new RelationshipResolverOptions() - { - without_toomanyprovides_kraken = true, - proceed_with_inconsistencies = true, - without_enforce_consistency = true, - with_recommends = false - }; + private static RelationshipResolverOptions conflictOptions(StabilityToleranceConfig stabilityTolerance) + => new RelationshipResolverOptions(stabilityTolerance) + { + without_toomanyprovides_kraken = true, + proceed_with_inconsistencies = true, + without_enforce_consistency = true, + with_recommends = false + }; /// /// Returns a changeset and conflicts based on the selections of the user. @@ -125,17 +128,18 @@ public static SavedSearch FilterToSavedSearch(GUIModFilter filter, /// /// The version of the current game instance public Tuple, Dictionary, List> ComputeFullChangeSetFromUserChangeSet( - IRegistryQuerier registry, - HashSet changeSet, - IGame game, - GameVersionCriteria version) + IRegistryQuerier registry, + HashSet changeSet, + IGame game, + StabilityToleranceConfig stabilityTolerance, + GameVersionCriteria version) { var modules_to_install = new List(); var modules_to_remove = new HashSet(); var extraInstalls = new HashSet(); changeSet.UnionWith(changeSet.Where(ch => ch.ChangeType == GUIModChangeType.Replace) - .Select(ch => registry.GetReplacement(ch.Mod, version)) + .Select(ch => registry.GetReplacement(ch.Mod, stabilityTolerance, version)) .OfType() .GroupBy(repl => repl.ReplaceWith) .Select(grp => new ModChange(grp.Key, GUIModChangeType.Install, @@ -160,7 +164,7 @@ public Tuple, Dictionary, List, Dictionary, List, Dictionary, List, Dictionary, List>( @@ -507,7 +511,7 @@ public HashSet ComputeUserChangeSet(IRegistryQuerier registry, return (registry == null ? modChanges : modChanges.Union( - registry.FindRemovableAutoInstalled(registry.InstalledModules.ToList(), instance.game, crit) + registry.FindRemovableAutoInstalled(registry.InstalledModules.ToList(), instance.game, instance.StabilityToleranceConfig, crit) .Select(im => new ModChange( im.Module, GUIModChangeType.Remove, new SelectionReason.NoLongerUsed())))) @@ -610,6 +614,7 @@ private static IEnumerable GetGUIMods(IRegistryQuerier registry, .SelectMany(kvp => kvp.Value .Select(mod => registry.IsAutodetected(mod.identifier) ? new GUIMod(mod, repoData, registry, + inst.StabilityToleranceConfig, versionCriteria, null, hideEpochs, hideV) { @@ -619,6 +624,7 @@ private static IEnumerable GetGUIMods(IRegistryQuerier registry, is InstalledModule found ? new GUIMod(found, repoData, registry, + inst.StabilityToleranceConfig, versionCriteria, null, hideEpochs, hideV) { @@ -626,18 +632,20 @@ is InstalledModule found } : null)) .OfType() - .Concat(registry.CompatibleModules(versionCriteria) + .Concat(registry.CompatibleModules(inst.StabilityToleranceConfig, versionCriteria) .Where(m => !installedIdents.Contains(m.identifier)) .AsParallel() .Where(m => !m.IsDLC) .Select(m => new GUIMod(m, repoData, registry, + inst.StabilityToleranceConfig, versionCriteria, null, hideEpochs, hideV))) - .Concat(registry.IncompatibleModules(versionCriteria) + .Concat(registry.IncompatibleModules(inst.StabilityToleranceConfig, versionCriteria) .Where(m => !installedIdents.Contains(m.identifier)) .AsParallel() .Where(m => !m.IsDLC) .Select(m => new GUIMod(m, repoData, registry, + inst.StabilityToleranceConfig, versionCriteria, true, hideEpochs, hideV))); diff --git a/GUI/Model/ReleaseStatusItem.cs b/GUI/Model/ReleaseStatusItem.cs new file mode 100644 index 0000000000..86dd4b1a5d --- /dev/null +++ b/GUI/Model/ReleaseStatusItem.cs @@ -0,0 +1,17 @@ +using CKAN.Extensions; + +namespace CKAN.GUI +{ + public class ReleaseStatusItem + { + public ReleaseStatusItem(ReleaseStatus? value) + { + Value = value; + } + + public readonly ReleaseStatus? Value; + public override string ToString() + => Value == null ? "" + : $"{Value.LocalizeName()} - {Value.LocalizeDescription()}"; + } +} diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx index ba5741ecc1..a2802e6bbd 100644 --- a/GUI/Properties/Resources.resx +++ b/GUI/Properties/Resources.resx @@ -199,6 +199,9 @@ Which build would you like to use? 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. Force Cancel + {0} has a release status of '{1}', which is less stable than your stability tolerance setting of '{2}'. + +Are you sure you want to install it? {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? diff --git a/NetKAN.schema b/NetKAN.schema index 9ce37cc648..0aee2d2a26 100644 --- a/NetKAN.schema +++ b/NetKAN.schema @@ -218,6 +218,10 @@ "use_source_archive": { "description": "If true, the release's source ZIP will be used instead of an asset", "type": "boolean" + }, + "prereleases": { + "description": "Skip prereleases if false, skip regular releases if true, use both if absent", + "type": "boolean" } } }, diff --git a/Netkan/CmdLineOptions.cs b/Netkan/CmdLineOptions.cs index 4d0641659c..dadb50926b 100644 --- a/Netkan/CmdLineOptions.cs +++ b/Netkan/CmdLineOptions.cs @@ -37,8 +37,8 @@ internal class CmdLineOptions [Option("skip-releases", DefaultValue = "0", HelpText = "Number of releases to skip / index of release to inflate.")] public string? SkipReleases { get; set; } - [Option("prerelease", HelpText = "Index GitHub prereleases")] - public bool PreRelease { get; set; } + [Option("prerelease", DefaultValue = null, HelpText = "true to get only prereleases from GitHub, false to skip them, omit to get both")] + public bool? PreRelease { get; set; } [Option("overwrite-cache", HelpText = "Overwrite cached files")] public bool OverwriteCache { get; set; } diff --git a/Netkan/Model/Metadata.cs b/Netkan/Model/Metadata.cs index b36028902b..29c9329c93 100644 --- a/Netkan/Model/Metadata.cs +++ b/Netkan/Model/Metadata.cs @@ -46,13 +46,8 @@ public string[] Hosts .Select(u => new Uri(u).Host) .ToArray(); - public Metadata(JObject? json) + public Metadata(JObject json) { - if (json == null) - { - throw new ArgumentNullException(nameof(json)); - } - _json = json; if (json.TryGetValue(KrefPropertyName, out JToken? krefToken)) @@ -130,7 +125,7 @@ is string s } } - public Metadata(YamlMappingNode yaml) : this(yaml?.ToJObject()) + public Metadata(YamlMappingNode yaml) : this(yaml.ToJObject()) { } @@ -143,6 +138,21 @@ public static Metadata Merge(Metadata[] modules) new Metadata(MergeJson(modules.Select(m => m._json) .ToArray()))); + public Metadata MergeFrom(JObject[] jsons) + { + var mergeSettings = new JsonMergeSettings() + { + MergeArrayHandling = MergeArrayHandling.Replace, + MergeNullValueHandling = MergeNullValueHandling.Merge, + }; + var main = Json(); + foreach (var other in jsons) + { + main.Merge(other, mergeSettings); + } + return new Metadata(main); + } + private static JObject MergeJson(JObject[] jsons) { var mergeSettings = new JsonMergeSettings() @@ -209,6 +219,8 @@ public Uri? FallbackDownload ? new Uri($"https://archive.org/download/{Identifier}-{verStr}/{sha1[..8]}-{Identifier}-{verStr}.zip") : null; + public bool Prerelease => _json["release_status"]?.ToString() is "testing" or "development"; + public JObject Json() => (JObject)_json.DeepClone(); } } diff --git a/Netkan/Processors/Inflator.cs b/Netkan/Processors/Inflator.cs index f8a5edb993..8b083abbc4 100644 --- a/Netkan/Processors/Inflator.cs +++ b/Netkan/Processors/Inflator.cs @@ -21,7 +21,7 @@ public Inflator(string? cacheDir, string? githubToken, string? gitlabToken, string? userAgent, - bool prerelease, + bool? prerelease, IGame game) { log.Debug("Initializing inflator"); @@ -36,7 +36,9 @@ public Inflator(string? cacheDir, githubToken, gitlabToken, userAgent, prerelease, game, netkanValidator); } - internal IEnumerable Inflate(string filename, Metadata[] netkans, TransformOptions? opts) + internal IEnumerable Inflate(string filename, + Metadata[] netkans, + TransformOptions opts) { log.DebugFormat("Inflating {0}", filename); try @@ -44,6 +46,16 @@ internal IEnumerable Inflate(string filename, Metadata[] netkans, Tran // Tell the downloader that we're starting a new request http.ClearRequestedURLs(); + if (netkans.Length > 1) + { + // Mix properties between sections if they don't start with x_netkan + var stripped = netkans.Select(nk => nk.Json()) + .Select(StripNetkanMetadataTransformer.Strip) + .ToArray(); + netkans = netkans.Select(nk => nk.MergeFrom(stripped)) + .ToArray(); + } + foreach (var netkan in netkans) { netkanValidator.ValidateNetkan(netkan, filename); @@ -55,10 +67,11 @@ internal IEnumerable Inflate(string filename, Metadata[] netkans, Tran .Select(grp => Metadata.Merge(grp.ToArray())) .SelectMany(merged => specVersionTransformer.Transform(merged, opts)) .SelectMany(withSpecVersion => sortTransformer.Transform(withSpecVersion, opts)) + .OrderBy(m => !m.Prerelease) .ToList(); log.Debug("Finished transformation"); - if (ckans.Count > (opts?.Releases ?? 1)) + if (ckans.Count(m => !m.Prerelease) > (opts?.Releases ?? 1)) { throw new Kraken(string.Format("Generated {0} modules but only {1} requested: {2}", ckans.Count, @@ -66,6 +79,8 @@ internal IEnumerable Inflate(string filename, Metadata[] netkans, Tran string.Join("; ", ckans.Select(DescribeHosting)))); } + ckans = ckans.Take(opts?.Releases ?? 1).ToList(); + foreach (Metadata ckan in ckans) { ckanValidator.ValidateCkan(ckan, netkans.First()); diff --git a/Netkan/Processors/QueueHandler.cs b/Netkan/Processors/QueueHandler.cs index 3c14c41af7..4e272d57d6 100644 --- a/Netkan/Processors/QueueHandler.cs +++ b/Netkan/Processors/QueueHandler.cs @@ -31,7 +31,7 @@ public QueueHandler(string inputQueueName, string? githubToken, string? gitlabToken, string? userAgent, - bool prerelease, + bool? prerelease, IGame game) { this.outputDir = outputDir; diff --git a/Netkan/Properties/AssemblyInfo.cs b/Netkan/Properties/AssemblyInfo.cs index bcb8f34c50..8fa9c31fd2 100644 --- a/Netkan/Properties/AssemblyInfo.cs +++ b/Netkan/Properties/AssemblyInfo.cs @@ -1,8 +1,8 @@ using System.Reflection; using System.Runtime.CompilerServices; -[assembly: AssemblyTitle ("CKAN-NetKAN")] -[assembly: AssemblyDescription ("CKAN NetKAN Client")] +[assembly: AssemblyTitle("CKAN-NetKAN")] +[assembly: AssemblyDescription("CKAN NetKAN Client")] [assembly: InternalsVisibleTo("CKAN.Tests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/Netkan/Sources/Github/GithubApi.cs b/Netkan/Sources/Github/GithubApi.cs index 1be237733f..03aa842294 100644 --- a/Netkan/Sources/Github/GithubApi.cs +++ b/Netkan/Sources/Github/GithubApi.cs @@ -54,10 +54,14 @@ public GithubApi(IHttpService http, string? oauthToken = null) ? JsonConvert.DeserializeObject(s) : null; - public GithubRelease? GetLatestRelease(GithubRef reference) - => GetAllReleases(reference).FirstOrDefault(); + public GithubRelease? GetLatestRelease(GithubRef reference, bool? usePrerelease) + => GetAllReleases(reference, usePrerelease).FirstOrDefault(); - public IEnumerable GetAllReleases(GithubRef reference) + private static bool ReleaseTypeMatches(bool? UsePrerelease, bool isPreRelease) + => !UsePrerelease.HasValue + || UsePrerelease.Value == isPreRelease; + + public IEnumerable GetAllReleases(GithubRef reference, bool? usePrerelease) { const int perPage = 10; for (int page = 1; true; ++page) @@ -76,13 +80,10 @@ public IEnumerable GetAllReleases(GithubRef reference) } var ghReleases = jsonReleases .Select(rel => new GithubRelease(reference, rel)) - .Where(ghRel => - // Finding the most recent *stable* release means filtering - // out on pre-releases. - ghRel.PreRelease == reference.UsePrerelease - // Skip releases without assets - && ghRel.Assets != null - && ghRel.Assets.Count != 0) + .Where(ghRel => ReleaseTypeMatches(usePrerelease, ghRel.PreRelease) + // Skip releases without assets + && ghRel.Assets != null + && ghRel.Assets.Count != 0) // Insurance against GitHub returning them in the wrong order .OrderByDescending(ghRel => ghRel.PublishedAt) .ToList(); diff --git a/Netkan/Sources/Github/GithubConfig.cs b/Netkan/Sources/Github/GithubConfig.cs new file mode 100644 index 0000000000..dd40b2c549 --- /dev/null +++ b/Netkan/Sources/Github/GithubConfig.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace CKAN.NetKAN.Sources.Github +{ + internal sealed class GitHubConfig + { + [JsonProperty("use_source_archive")] + public bool UseSourceArchive { get; set; } = false; + + [JsonProperty("prereleases")] + public bool? Prereleases { get; set; } = null; + } +} diff --git a/Netkan/Sources/Github/GithubRef.cs b/Netkan/Sources/Github/GithubRef.cs index 5b804cb75d..17d71309c6 100644 --- a/Netkan/Sources/Github/GithubRef.cs +++ b/Netkan/Sources/Github/GithubRef.cs @@ -16,12 +16,11 @@ internal sealed class GithubRef : RemoteRef public Regex? Filter { get; private set; } public Regex? VersionFromAsset { get; private set; } public bool UseSourceArchive { get; private set; } - public bool UsePrerelease { get; private set; } - public GithubRef(string remoteRefToken, bool useSourceArchive, bool usePrerelease) - : this(new RemoteRef(remoteRefToken), useSourceArchive, usePrerelease) { } + public GithubRef(string remoteRefToken, bool useSourceArchive) + : this(new RemoteRef(remoteRefToken), useSourceArchive) { } - public GithubRef(RemoteRef remoteRef, bool useSourceArchive, bool usePrerelease) + public GithubRef(RemoteRef remoteRef, bool useSourceArchive) : base(remoteRef) { if (remoteRef.Id != null @@ -41,7 +40,6 @@ public GithubRef(RemoteRef remoteRef, bool useSourceArchive, bool usePrerelease) : null; UseSourceArchive = useSourceArchive; - UsePrerelease = usePrerelease; } else { diff --git a/Netkan/Sources/Github/IGithubApi.cs b/Netkan/Sources/Github/IGithubApi.cs index aa23084e7c..c5a0a51ed6 100644 --- a/Netkan/Sources/Github/IGithubApi.cs +++ b/Netkan/Sources/Github/IGithubApi.cs @@ -6,8 +6,8 @@ namespace CKAN.NetKAN.Sources.Github internal interface IGithubApi { GithubRepo? GetRepo(GithubRef reference); - GithubRelease? GetLatestRelease(GithubRef reference); - IEnumerable GetAllReleases(GithubRef reference); + GithubRelease? GetLatestRelease(GithubRef reference, bool? usePrerelease); + IEnumerable GetAllReleases(GithubRef reference, bool? usePrerelease); List getOrgMembers(GithubUser organization); string? DownloadText(Uri url); } diff --git a/Netkan/Sources/Spacedock/SDVersion.cs b/Netkan/Sources/Spacedock/SDVersion.cs index 75810b06d1..eed32c7ad4 100644 --- a/Netkan/Sources/Spacedock/SDVersion.cs +++ b/Netkan/Sources/Spacedock/SDVersion.cs @@ -32,33 +32,22 @@ public class SDVersion /// internal class JsonConvertGameVersion : JsonConverter { - public override object? ReadJson( - JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer - ) - { - if (reader.Value == null) - { - return null; - } - - var raw_version = reader.Value.ToString(); - - return GameVersion.Parse(ExpandVersionIfNeeded(raw_version)); - } + public override object? ReadJson(JsonReader reader, + Type objectType, + object? existingValue, + JsonSerializer serializer) + => reader.Value?.ToString() is string s + ? GameVersion.Parse(ExpandVersionIfNeeded(s)) + : null; /// /// Actually expand the KSP version. It's way easier to test this than the override. :) /// - public static string? ExpandVersionIfNeeded(string? version) - { - if (Regex.IsMatch(version ?? "", @"^\d+\.\d+$")) - { - // Two part string, add our .0 - return version + ".0"; - } - - return version; - } + public static string ExpandVersionIfNeeded(string version) + => Regex.IsMatch(version, @"^\d+\.\d+$") + // Two part string, add our .0 + ? version + ".0" + : version; public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { diff --git a/Netkan/Transformers/GithubTransformer.cs b/Netkan/Transformers/GithubTransformer.cs index edde9565e2..995a0197b4 100644 --- a/Netkan/Transformers/GithubTransformer.cs +++ b/Netkan/Transformers/GithubTransformer.cs @@ -18,17 +18,12 @@ internal sealed class GithubTransformer : ITransformer private static readonly ILog Log = LogManager.GetLogger(typeof(GithubTransformer)); private readonly IGithubApi _api; - private readonly bool _matchPreleases; + private readonly bool? _matchPreleases; public string Name => "github"; - public GithubTransformer(IGithubApi api, bool matchPreleases) + public GithubTransformer(IGithubApi api, bool? matchPreleases) { - if (api == null) - { - throw new ArgumentNullException(nameof(api)); - } - _api = api; _matchPreleases = matchPreleases; } @@ -45,20 +40,12 @@ public IEnumerable Transform(Metadata metadata, TransformOptions? opts Log.InfoFormat("Executing GitHub transformation with {0}", metadata.Kref); Log.DebugFormat("Input metadata:{0}{1}", Environment.NewLine, json); - var useSourceAchive = false; - - var githubMetadata = (JObject?)json["x_netkan_github"]; - if (githubMetadata != null) - { - var githubUseSourceArchive = (bool?)githubMetadata["use_source_archive"]; - - if (githubUseSourceArchive != null) - { - useSourceAchive = githubUseSourceArchive.Value; - } - } + var conf = (json.TryGetValue("x_netkan_github", out JToken? jtok) + && jtok is JObject jobj + ? jobj.ToObject() : null) + ?? new GitHubConfig(); - var ghRef = new GithubRef(metadata.Kref, useSourceAchive, _matchPreleases); + var ghRef = new GithubRef(metadata.Kref, conf.UseSourceArchive); // Get the GitHub repository var ghRepo = _api.GetRepo(ghRef); @@ -70,7 +57,7 @@ public IEnumerable Transform(Metadata metadata, TransformOptions? opts { Log.Warn("Repo is archived, consider freezing"); } - var releases = _api.GetAllReleases(ghRef); + var releases = _api.GetAllReleases(ghRef, _matchPreleases ?? conf.Prereleases); if (opts.SkipReleases.HasValue) { releases = releases.Skip(opts.SkipReleases.Value); @@ -210,6 +197,11 @@ private Metadata TransformOne(Metadata metadata, json.SafeAdd("name", repoName); } + if (ghRelease.PreRelease) + { + json.SafeAdd("release_status", "testing"); + } + json.SafeMerge( "x_netkan_version_pieces", JObject.FromObject(new Dictionary{ {"tag", ghRelease.Tag.ToString()} })); @@ -247,7 +239,7 @@ public static void SetRepoResources(GithubRepo repo, JObject resources) => repo.TraverseNodes(r => r.ParentRepo == null ? null : _api.GetRepo(new GithubRef($"#/ckan/github/{r.ParentRepo.FullName}", - false, _matchPreleases))) + false))) .Reverse() .SelectMany(r => r.Owner?.Type switch { diff --git a/Netkan/Transformers/GitlabTransformer.cs b/Netkan/Transformers/GitlabTransformer.cs index f0af0c1927..84eff994f4 100644 --- a/Netkan/Transformers/GitlabTransformer.cs +++ b/Netkan/Transformers/GitlabTransformer.cs @@ -22,10 +22,6 @@ internal sealed class GitlabTransformer : ITransformer /// Object to use for accessing the GitLab API public GitlabTransformer(IGitlabApi api) { - if (api == null) - { - throw new ArgumentNullException(nameof(api)); - } this.api = api; } diff --git a/Netkan/Transformers/NetkanTransformer.cs b/Netkan/Transformers/NetkanTransformer.cs index e103fe2f2d..2e367497d6 100644 --- a/Netkan/Transformers/NetkanTransformer.cs +++ b/Netkan/Transformers/NetkanTransformer.cs @@ -30,7 +30,7 @@ public NetkanTransformer(IHttpService http, string? githubToken, string? gitlabToken, string? userAgent, - bool prerelease, + bool? prerelease, IGame game, IValidator validator) { diff --git a/Netkan/Transformers/SpacedockTransformer.cs b/Netkan/Transformers/SpacedockTransformer.cs index 986ecba0f6..46503f2eb1 100644 --- a/Netkan/Transformers/SpacedockTransformer.cs +++ b/Netkan/Transformers/SpacedockTransformer.cs @@ -87,7 +87,8 @@ private Metadata TransformOne(Metadata metadata, JObject json, SpacedockMod sdMo json.SafeAdd("name", sdMod.name); json.SafeAdd("abstract", sdMod.short_description); - json.SafeAdd("version", latestVersion.friendly_version?.ToString()); + var ver = latestVersion.friendly_version?.ToString(); + json.SafeAdd("version", ver); json.Remove("$kref"); json.SafeAdd("download", latestVersion.download_path?.OriginalString); json.SafeAdd(Metadata.UpdatedPropertyName, latestVersion.created); @@ -97,25 +98,26 @@ private Metadata TransformOne(Metadata metadata, JObject json, SpacedockMod sdMo // SD provides users with the following default selection of licenses. Let's convert them to CKAN // compatible license strings if possible. // - // "MIT" - OK - // "BSD" - Specific version is indeterminate + // "MIT" - OK + // "BSD" - Specific version is indeterminate // "GPLv2" - Becomes "GPL-2.0" // "GPLv3" - Becomes "GPL-3.0" - // "LGPL" - Specific version is indeterminate - - var sdLicense = sdMod.license?.Trim().Replace(' ', '-'); - - switch (sdLicense) + // "LGPL" - Specific version is indeterminate + json.SafeAdd("license", + sdMod.license?.Trim().Replace(' ', '-') switch + { + "GPLv2" => "GPL-2.0", + "GPLv3" => "GPL-3.0", + "Other" => "unknown", + "ARR" or "All Rights Reserved" + or "All rights reserved" => "restricted", + var sdLicense => sdLicense, + }); + + if (ver?.ToLower() is string lowerV + && preReleaseSubstrings.Any(substr => lowerV.Contains(substr))) { - case "GPLv2": - json.SafeAdd("license", "GPL-2.0"); - break; - case "GPLv3": - json.SafeAdd("license", "GPL-3.0"); - break; - default: - json.SafeAdd("license", sdLicense); - break; + json.SafeAdd("release_status", "testing"); } // Make sure resources exist. @@ -146,7 +148,7 @@ private Metadata TransformOne(Metadata metadata, JObject json, SpacedockMod sdMo string.Format("#/ckan/github/{0}/{1}", match.Groups["owner"].Value, match.Groups["repo"].Value), - false, false)) + false)) is GithubRepo repoInfo) { GithubTransformer.SetRepoResources(repoInfo, resourcesJson); @@ -195,5 +197,9 @@ private static void TryAddResourceURL(string identifier, JObject? resources, str new Regex("^/(?[^/]+)/(?[^/]+)", RegexOptions.Compiled); + private static readonly string[] preReleaseSubstrings = new string[] + { + "pre", "alpha", "beta", + }; } } diff --git a/Netkan/Transformers/StagingTransformer.cs b/Netkan/Transformers/StagingTransformer.cs index f87591f620..a989033c7f 100644 --- a/Netkan/Transformers/StagingTransformer.cs +++ b/Netkan/Transformers/StagingTransformer.cs @@ -37,8 +37,8 @@ private bool VersionsNeedManualReview(Metadata metadata, out string reason) JObject json = metadata.Json(); var minStr = json["ksp_version_min"] ?? json["ksp_version"]; var maxStr = json["ksp_version_max"] ?? json["ksp_version"]; - var minVer = minStr == null ? GameVersion.Any : GameVersion.Parse((string?)minStr); - var maxVer = maxStr == null ? GameVersion.Any : GameVersion.Parse((string?)maxStr); + var minVer = (string?)minStr is string s1 ? GameVersion.Parse(s1) : GameVersion.Any; + var maxVer = (string?)maxStr is string s2 ? GameVersion.Parse(s2) : GameVersion.Any; if (currentRelease != null && currentRelease.IntersectWith(new GameVersionRange(minVer, maxVer)) == null) { reason = $"Hard-coded game versions not compatible with current release: {GameVersionRange.VersionSpan(game, minVer, maxVer)}\r\nPlease check that they match the forum thread."; diff --git a/Netkan/Transformers/StripNetkanMetadataTransformer.cs b/Netkan/Transformers/StripNetkanMetadataTransformer.cs index 04c8542db4..d76d7be35b 100644 --- a/Netkan/Transformers/StripNetkanMetadataTransformer.cs +++ b/Netkan/Transformers/StripNetkanMetadataTransformer.cs @@ -30,47 +30,39 @@ public IEnumerable Transform(Metadata metadata, TransformOptions? opts yield return new Metadata(json); } - private static void Strip(JObject metadata) + private static bool IsNetkanProperty(string propertyName) + => propertyName.StartsWith("x_netkan") || propertyName is "$kref" or "$vref"; + + public static JObject Strip(JObject json) { var propertiesToRemove = new List(); - - foreach (var property in metadata.Properties()) + foreach (var property in json.Properties()) { - if (property.Name.StartsWith("x_netkan")) + if (IsNetkanProperty(property.Name)) { propertiesToRemove.Add(property.Name); } else { - switch (property.Value.Type) + switch (property.Value) { - case JTokenType.Object: - Strip((JObject)property.Value); + case JObject jobj: + Strip(jobj); break; - case JTokenType.Array: - foreach (var element in ((JArray)property.Value).Where(i => i.Type == JTokenType.Object)) + case JArray jarr: + foreach (var element in jarr.OfType()) { - Strip((JObject)element); + Strip(element); } break; } } } - - foreach (var property in propertiesToRemove) - { - metadata.Remove(property); - } - - if (metadata["$kref"] != null) - { - metadata.Remove("$kref"); - } - - if (metadata["$vref"] != null) + foreach (var propertyName in propertiesToRemove) { - metadata.Remove("$vref"); + json.Remove(propertyName); } + return json; } } } diff --git a/Netkan/Transformers/VersionedOverrideTransformer.cs b/Netkan/Transformers/VersionedOverrideTransformer.cs index e52c8e5201..92c679ee3c 100644 --- a/Netkan/Transformers/VersionedOverrideTransformer.cs +++ b/Netkan/Transformers/VersionedOverrideTransformer.cs @@ -160,14 +160,14 @@ private void ProcessOverrideStanza(JObject overrideStanza, JObject metadata) { ModuleService.ApplyVersions( metadata, - overrides.ContainsKey("ksp_version") - ? GameVersion.Parse((string?)overrides["ksp_version"]) + (string?)overrides["ksp_version"] is string v1 + ? GameVersion.Parse(v1) : null, - overrides.ContainsKey("ksp_version_min") - ? GameVersion.Parse((string?)overrides["ksp_version_min"]) + (string?)overrides["ksp_version_min"] is string v2 + ? GameVersion.Parse(v2) : null, - overrides.ContainsKey("ksp_version_max") - ? GameVersion.Parse((string?)overrides["ksp_version_max"]) + (string?)overrides["ksp_version_max"] is string v3 + ? GameVersion.Parse(v3) : null ); foreach (var p in gameVersionProperties) diff --git a/Spec.md b/Spec.md index 1337469e83..976effe247 100644 --- a/Spec.md +++ b/Spec.md @@ -790,6 +790,7 @@ When used, the following fields will be auto-filled if not already present: - `resources.x_screenshot` - `ksp_version` - `release_date` +- `release_status` ###### `#/ckan/curse/:cid` @@ -833,6 +834,7 @@ When used, the following fields will be auto-filled if not already present: - `resources.repository` - `resources.bugtracker` - `release_date` +- `release_status` Optionally, one of `asset_match` with `:filter_regexp` *or* `version_from_asset` with `:version_regexp` *may* be provided: @@ -852,6 +854,8 @@ An `x_netkan_github` field may be provided to customize how the metadata is fetc - `use_source_archive` (type: `boolean`) (default: `false`)
Specifies that the source ZIP of the repository itself will be used instead of any assets in the release. +- `prereleases` (type: `boolean`) (default: `null`)
+ Skip prereleases if `false`, skip regular releases if `true`, use both if absent. ###### `#/ckan/gitlab/:user/:repo` diff --git a/Tests/Core/Configuration/FakeConfiguration.cs b/Tests/Core/Configuration/FakeConfiguration.cs index 145bc28bd3..5aff9b76c1 100644 --- a/Tests/Core/Configuration/FakeConfiguration.cs +++ b/Tests/Core/Configuration/FakeConfiguration.cs @@ -1,8 +1,10 @@ using System; +using System.ComponentModel; using System.Collections.Generic; using System.IO; using System.Linq; +using CKAN; using CKAN.Configuration; using CKAN.Games.KerbalSpaceProgram.GameVersionProviders; @@ -12,7 +14,7 @@ namespace Tests.Core.Configuration { public class FakeConfiguration : IConfiguration, IDisposable { - public FakeConfiguration(CKAN.GameInstance instance, string autostart) + public FakeConfiguration(GameInstance instance, string autostart) : this(new List> { new Tuple("test", instance.GameDir(), "KSP") @@ -93,7 +95,7 @@ public string? AutoStartInstance /// /// Returns /// - public void SetRegistryToInstances(SortedList instances) + public void SetRegistryToInstances(SortedList instances) { Instances = instances.Select(kvpair => new Tuple(kvpair.Key, kvpair.Value.GameDir(), "KSP")).ToList(); @@ -153,6 +155,10 @@ public string? Language public bool? DevBuilds { get; set; } + #pragma warning disable CS0067 + public event PropertyChangedEventHandler? PropertyChanged; + #pragma warning restore CS0067 + public void Dispose() { if (DownloadCacheDir != null) diff --git a/Tests/Core/ModuleInstallerDirTest.cs b/Tests/Core/ModuleInstallerDirTest.cs index ac772cf992..cd2054afb9 100644 --- a/Tests/Core/ModuleInstallerDirTest.cs +++ b/Tests/Core/ModuleInstallerDirTest.cs @@ -62,7 +62,7 @@ public void SetUp() HashSet? possibleConfigOnlyDirs = null; _installer.InstallList( new List() { _testModule! }, - new RelationshipResolverOptions(), + new RelationshipResolverOptions(_instance.KSP.StabilityToleranceConfig), _registryManager, ref possibleConfigOnlyDirs); } diff --git a/Tests/Core/ModuleInstallerTests.cs b/Tests/Core/ModuleInstallerTests.cs index 3c4760244e..4d208110e0 100644 --- a/Tests/Core/ModuleInstallerTests.cs +++ b/Tests/Core/ModuleInstallerTests.cs @@ -415,7 +415,7 @@ public void CanInstallMod() registry.RepositoriesClear(); registry.RepositoriesAdd(repo.repo); - Assert.AreEqual(1, registry.CompatibleModules(ksp.KSP.VersionCriteria()).Count()); + Assert.AreEqual(1, registry.CompatibleModules(ksp.KSP.StabilityToleranceConfig, ksp.KSP.VersionCriteria()).Count()); // Attempt to install it. var modules = new List { TestData.DogeCoinFlag_101_module() }; @@ -423,7 +423,7 @@ public void CanInstallMod() HashSet? possibleConfigOnlyDirs = null; new ModuleInstaller(ksp.KSP, manager.Cache!, nullUser) .InstallList(modules, - new RelationshipResolverOptions(), + new RelationshipResolverOptions(ksp.KSP.StabilityToleranceConfig), RegistryManager.Instance(manager.CurrentInstance, repoData.Manager), ref possibleConfigOnlyDirs); @@ -461,7 +461,7 @@ public void CanUninstallMod() HashSet? possibleConfigOnlyDirs = null; new ModuleInstaller(manager.CurrentInstance, manager.Cache!, nullUser) - .InstallList(modules, new RelationshipResolverOptions(), + .InstallList(modules, new RelationshipResolverOptions(ksp.KSP.StabilityToleranceConfig), RegistryManager.Instance(manager.CurrentInstance, repoData.Manager), ref possibleConfigOnlyDirs); @@ -510,7 +510,7 @@ public void UninstallEmptyDirs() HashSet? possibleConfigOnlyDirs = null; new ModuleInstaller(manager.CurrentInstance, manager.Cache!, nullUser) .InstallList(modules, - new RelationshipResolverOptions(), + new RelationshipResolverOptions(ksp.KSP.StabilityToleranceConfig), RegistryManager.Instance(manager.CurrentInstance, repoData.Manager), ref possibleConfigOnlyDirs); @@ -525,7 +525,7 @@ public void UninstallEmptyDirs() new ModuleInstaller(manager.CurrentInstance, manager.Cache!, nullUser) .InstallList(modules, - new RelationshipResolverOptions(), + new RelationshipResolverOptions(ksp.KSP.StabilityToleranceConfig), RegistryManager.Instance(manager.CurrentInstance, repoData.Manager), ref possibleConfigOnlyDirs); @@ -719,7 +719,7 @@ public void ModuleManagerInstancesAreDecoupled() HashSet? possibleConfigOnlyDirs = null; new ModuleInstaller(ksp.KSP, manager.Cache!, nullUser) .InstallList(modules, - new RelationshipResolverOptions(), + new RelationshipResolverOptions(ksp.KSP.StabilityToleranceConfig), RegistryManager.Instance(manager.CurrentInstance, repoData.Manager), ref possibleConfigOnlyDirs); @@ -878,7 +878,7 @@ public void FindRecommendations_WithDLCRecommendationsUnsatisfied_DLCRecommended // Act var result = ModuleInstaller.FindRecommendations(inst.KSP, - installIdents.Select(ident => registry.LatestAvailable(ident, crit)) + installIdents.Select(ident => registry.LatestAvailable(ident, ksp.KSP.StabilityToleranceConfig, crit)) .OfType() .ToHashSet(), new List(), @@ -890,7 +890,7 @@ public void FindRecommendations_WithDLCRecommendationsUnsatisfied_DLCRecommended // Assert Assert.IsTrue(result, "Should return something"); CollectionAssert.IsNotEmpty(recommendations, "Should return recommendations"); - CollectionAssert.AreEquivalent(dlcIdents.Select(ident => registry.LatestAvailable(ident, crit)), + CollectionAssert.AreEquivalent(dlcIdents.Select(ident => registry.LatestAvailable(ident, ksp.KSP.StabilityToleranceConfig, crit)), recommendations.Keys, "The DLC should be recommended"); } @@ -929,7 +929,7 @@ public void FindRecommendations_WithDLCRecommendationsSatisfied_DLCNotRecommende // Act var result = ModuleInstaller.FindRecommendations(inst.KSP, - installIdents.Select(ident => registry.LatestAvailable(ident, crit)) + installIdents.Select(ident => registry.LatestAvailable(ident, ksp.KSP.StabilityToleranceConfig, crit)) .OfType() .ToHashSet(), new List(), @@ -940,7 +940,7 @@ public void FindRecommendations_WithDLCRecommendationsSatisfied_DLCNotRecommende // Assert Assert.IsFalse(result, "Should return nothing"); - foreach (var mod in dlcIdents.Select(ident => registry.LatestAvailable(ident, crit))) + foreach (var mod in dlcIdents.Select(ident => registry.LatestAvailable(ident, ksp.KSP.StabilityToleranceConfig, crit))) { CollectionAssert.DoesNotContain(recommendations, mod, "DLC should not be recommended"); @@ -982,7 +982,7 @@ public void InstallList_RealZipSlip_Throws() HashSet? possibleConfigOnlyDirs = null; new ModuleInstaller(ksp.KSP, manager.Cache!, nullUser) .InstallList(modules, - new RelationshipResolverOptions(), + new RelationshipResolverOptions(ksp.KSP.StabilityToleranceConfig), RegistryManager.Instance(manager.CurrentInstance, repoData.Manager), ref possibleConfigOnlyDirs); }, @@ -1021,7 +1021,7 @@ public void InstallList_RealZipBomb_DoesNotThrow() HashSet? possibleConfigOnlyDirs = null; new ModuleInstaller(ksp.KSP, manager.Cache!, nullUser) .InstallList(modules, - new RelationshipResolverOptions(), + new RelationshipResolverOptions(ksp.KSP.StabilityToleranceConfig), RegistryManager.Instance(manager.CurrentInstance, repoData.Manager), ref possibleConfigOnlyDirs); } @@ -1082,10 +1082,10 @@ public void Replace_WithCompatibleModule_Succeeds() // Act registry.RegisterModule(replaced, new List(), inst.KSP, false); manager.Cache?.Store(replaced, TestData.DogeCoinFlagZip(), new Progress(bytes => {})); - var replacement = querier.GetReplacement(replaced.identifier, + var replacement = querier.GetReplacement(replaced.identifier, ksp.KSP.StabilityToleranceConfig, new GameVersionCriteria(new GameVersion(1, 12)))!; installer.Replace(Enumerable.Repeat(replacement, 1), - new RelationshipResolverOptions(), + new RelationshipResolverOptions(ksp.KSP.StabilityToleranceConfig), downloader, ref possibleConfigOnlyDirs, regMgr, false); @@ -1150,7 +1150,7 @@ public void Replace_WithIncompatibleModule_Fails() // Act registry.RegisterModule(replaced, new List(), inst.KSP, false); manager.Cache?.Store(replaced, TestData.DogeCoinFlagZip(), new Progress(bytes => {})); - var replacement = querier.GetReplacement(replaced.identifier, + var replacement = querier.GetReplacement(replaced.identifier, ksp.KSP.StabilityToleranceConfig, new GameVersionCriteria(new GameVersion(1, 11))); // Assert @@ -1326,7 +1326,7 @@ public void Upgrade_WithAutoInst_RemovesAutoRemovable(string[] regularMods, // Act installer.Upgrade(upgradeIdentifiers.Select(ident => - registry.LatestAvailable(ident, inst.KSP.VersionCriteria())) + registry.LatestAvailable(ident, ksp.KSP.StabilityToleranceConfig, inst.KSP.VersionCriteria())) .OfType() .ToArray(), downloader, ref possibleConfigOnlyDirs, regMgr, false); @@ -1387,7 +1387,7 @@ private void installTestPlugin(string unmanaged, string moduleJson, string zipPa HashSet? possibleConfigOnlyDirs = null; new ModuleInstaller(inst.KSP, manager.Cache!, nullUser) .InstallList(modules, - new RelationshipResolverOptions(), + new RelationshipResolverOptions(ksp.KSP.StabilityToleranceConfig), regMgr, ref possibleConfigOnlyDirs); } diff --git a/Tests/Core/Registry/CompatibilitySorter.cs b/Tests/Core/Registry/CompatibilitySorter.cs index fc2f00fa63..93ef561227 100644 --- a/Tests/Core/Registry/CompatibilitySorter.cs +++ b/Tests/Core/Registry/CompatibilitySorter.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using CKAN; +using CKAN.Configuration; using CKAN.Versioning; #if NET45 using CKAN.Extensions; @@ -86,11 +87,11 @@ public void Constructor_OverlappingModules_HigherPriorityOverrides(string[] modu .GetAvailableModules(Enumerable.Repeat(repo1.repo, 1), identifier) .First() - .Latest(); + .Latest(ReleaseStatus.stable); // Act var sorter = new CompatibilitySorter( - versCrit, + new StabilityToleranceConfig(""), versCrit, repoData.Manager.GetAllAvailDicts(repos), providers, installed, dlls, dlcs); diff --git a/Tests/Core/Registry/Registry.cs b/Tests/Core/Registry/Registry.cs index a590d88616..14e3781b46 100644 --- a/Tests/Core/Registry/Registry.cs +++ b/Tests/Core/Registry/Registry.cs @@ -8,6 +8,7 @@ using Tests.Data; using CKAN; +using CKAN.Configuration; using CKAN.Versioning; namespace Tests.Core.Registry @@ -16,6 +17,7 @@ namespace Tests.Core.Registry public class RegistryTests { private string? repoDataDir; + private readonly StabilityToleranceConfig stabilityTolerance = new StabilityToleranceConfig(""); private static readonly GameVersionCriteria v0_24_2 = new GameVersionCriteria(GameVersion.Parse("0.24.2")); private static readonly GameVersionCriteria v0_25_0 = new GameVersionCriteria(GameVersion.Parse("0.25.0")); @@ -55,15 +57,15 @@ public void LatestAvailable() var module = registry.GetModuleByVersion(identifier, "0.14"); // Make sure it's there for 0.24.2 - Assert.AreEqual(module?.ToString(), registry.LatestAvailable(identifier, v0_24_2)?.ToString()); + Assert.AreEqual(module?.ToString(), registry.LatestAvailable(identifier, stabilityTolerance, v0_24_2)?.ToString()); // But not for 0.25.0 - Assert.IsNull(registry.LatestAvailable(identifier, v0_25_0)); + Assert.IsNull(registry.LatestAvailable(identifier, stabilityTolerance, v0_25_0)); // And that we fail if we ask for something we don't know. Assert.Throws(delegate { - registry.LatestAvailable("ToTheMun", v0_24_2); + registry.LatestAvailable("ToTheMun", stabilityTolerance, v0_24_2); }); } } @@ -89,7 +91,7 @@ public void CompatibleModules_NoDLCInstalled_ExcludesModulesDependingOnMH() var DLCDepender = registry.GetModuleByVersion("DLC-Depender", "1.0.0"); // Act - var avail = registry.CompatibleModules(v0_24_2).OfType().ToList(); + var avail = registry.CompatibleModules(stabilityTolerance, v0_24_2).OfType().ToList(); // Assert Assert.IsFalse(avail.Contains(DLCDepender)); @@ -121,7 +123,7 @@ public void CompatibleModules_MHInstalled_IncludesModulesDependingOnMH() var DLCDepender = registry.GetModuleByVersion("DLC-Depender", "1.0.0"); // Act - var avail = registry.CompatibleModules(v0_24_2).OfType().ToList(); + var avail = registry.CompatibleModules(stabilityTolerance, v0_24_2).OfType().ToList(); // Assert Assert.IsTrue(avail.Contains(DLCDepender)); @@ -154,7 +156,7 @@ public void CompatibleModules_MH110Installed_IncludesModulesDependingOnMH110() var DLCDepender = registry.GetModuleByVersion("DLC-Depender", "1.0.0"); // Act - var avail = registry.CompatibleModules(v0_24_2).OfType().ToList(); + var avail = registry.CompatibleModules(stabilityTolerance, v0_24_2).OfType().ToList(); // Assert Assert.IsTrue(avail.Contains(DLCDepender)); @@ -187,7 +189,7 @@ public void CompatibleModules_MH100Installed_ExcludesModulesDependingOnMH110() var DLCDepender = registry.GetModuleByVersion("DLC-Depender", "1.0.0"); // Act - var avail = registry.CompatibleModules(v0_24_2).OfType().ToList(); + var avail = registry.CompatibleModules(stabilityTolerance, v0_24_2).OfType().ToList(); // Assert Assert.IsFalse(avail.Contains(DLCDepender)); @@ -232,7 +234,7 @@ public void CompatibleModules_PastAndFutureCompatibility_ReturnsCurrentOnly() // Act GameVersionCriteria v173 = new GameVersionCriteria(GameVersion.Parse("1.7.3")); - var compat = registry.CompatibleModules(v173).OfType().ToList(); + var compat = registry.CompatibleModules(stabilityTolerance, v173).OfType().ToList(); // Assert Assert.IsFalse(compat.Contains(modFor161)); @@ -271,7 +273,7 @@ public void HasUpdate_WithUpgradeableManuallyInstalledMod_ReturnsTrue() }); // Act - bool has = registry.HasUpdate(mod.identifier, gameInst, new HashSet(), false, out _); + bool has = registry.HasUpdate(mod.identifier, stabilityTolerance, gameInst, new HashSet(), false, out _); // Assert Assert.IsTrue(has, "Can't upgrade manually installed DLL"); @@ -327,7 +329,7 @@ public void HasUpdate_OtherModDependsOnCurrent_ReturnsFalse() GameVersionCriteria crit = new GameVersionCriteria(olderDepMod?.ksp_version); // Act - bool has = registry.HasUpdate(olderDepMod?.identifier!, gameInst, new HashSet(), false, + bool has = registry.HasUpdate(olderDepMod?.identifier!, stabilityTolerance, gameInst, new HashSet(), false, out _, registry.InstalledModules .Select(im => im.Module) diff --git a/Tests/Core/Registry/RegistryLive.cs b/Tests/Core/Registry/RegistryLive.cs index 6626a1746f..04369d9a9e 100644 --- a/Tests/Core/Registry/RegistryLive.cs +++ b/Tests/Core/Registry/RegistryLive.cs @@ -4,6 +4,7 @@ using Tests.Data; using CKAN; +using CKAN.Configuration; using CKAN.Versioning; namespace Tests.Core.Registry @@ -32,7 +33,7 @@ public void LatestAvailable() registry.RepositoriesAdd(repo); var module = - registry.LatestAvailable("AGExt", new GameVersionCriteria(temp_ksp.KSP.Version())); + registry.LatestAvailable("AGExt", new StabilityToleranceConfig(""), new GameVersionCriteria(temp_ksp.KSP.Version())); Assert.AreEqual("AGExt", module?.identifier); Assert.AreEqual("1.24a", module?.version.ToString()); diff --git a/Tests/Core/Relationships/RelationshipResolverTests.cs b/Tests/Core/Relationships/RelationshipResolverTests.cs index b645e2bc94..9cdd5fce50 100644 --- a/Tests/Core/Relationships/RelationshipResolverTests.cs +++ b/Tests/Core/Relationships/RelationshipResolverTests.cs @@ -9,6 +9,7 @@ using Tests.Data; using CKAN; +using CKAN.Configuration; using CKAN.Games; using CKAN.Games.KerbalSpaceProgram; using CKAN.Versioning; @@ -24,11 +25,12 @@ public class RelationshipResolverTests private RandomModuleGenerator? generator; private static readonly IGame game = new KerbalSpaceProgram(); private static readonly GameVersionCriteria crit = new GameVersionCriteria(null); + private readonly StabilityToleranceConfig stabilityTolerance = new StabilityToleranceConfig(""); [SetUp] public void Setup() { - options = RelationshipResolverOptions.DefaultOpts(); + options = RelationshipResolverOptions.DefaultOpts(stabilityTolerance); generator = new RandomModuleGenerator(new Random(0451)); //Sanity checker means even incorrect RelationshipResolver logic was passing options.without_enforce_consistency = true; @@ -38,7 +40,7 @@ public void Setup() public void Constructor_WithoutModules_AlwaysReturns() { var registry = CKAN.Registry.Empty(); - options = RelationshipResolverOptions.DefaultOpts(); + options = RelationshipResolverOptions.DefaultOpts(stabilityTolerance); Assert.DoesNotThrow(() => new RelationshipResolver(new List(), null, options, registry, game, crit)); } @@ -1061,7 +1063,7 @@ public void AutodetectedCanSatisfyRelationships() CkanModule mod = generator!.GeneratorRandomModule(depends: depends); new RelationshipResolver( - new CkanModule[] { mod }, null, RelationshipResolverOptions.DefaultOpts(), + new CkanModule[] { mod }, null, RelationshipResolverOptions.DefaultOpts(stabilityTolerance), registry, game, new GameVersionCriteria(GameVersion.Parse("1.0.0"))); } } @@ -1106,10 +1108,10 @@ public void Constructor_UpgradeVersionSpecificConflictedAutoDetected_DoesNotThro Assert.DoesNotThrow(() => { var rr = new RelationshipResolver( - installIdents.Select(ident => registry.LatestAvailable(ident, crit)) + installIdents.Select(ident => registry.LatestAvailable(ident, stabilityTolerance, crit)) .OfType(), null, - RelationshipResolverOptions.DependsOnlyOpts(), + RelationshipResolverOptions.DependsOnlyOpts(stabilityTolerance), registry, inst.KSP.game, crit); }); } @@ -1243,14 +1245,14 @@ public void Recommendations_WithDLCRecommendationUnsatisfied_ContainsDLC(string[ // Act var rr = new RelationshipResolver( - installIdents.Select(ident => registry.LatestAvailable(ident, crit)) + installIdents.Select(ident => registry.LatestAvailable(ident, stabilityTolerance, crit)) .OfType(), null, - RelationshipResolverOptions.KitchenSinkOpts(), + RelationshipResolverOptions.KitchenSinkOpts(stabilityTolerance), registry, game, crit); var deps = rr.Dependencies().ToHashSet(); var recs = rr.Recommendations(deps).ToArray(); - var dlcs = dlcIdents.Select(ident => registry.LatestAvailable(ident, crit)).ToArray(); + var dlcs = dlcIdents.Select(ident => registry.LatestAvailable(ident, stabilityTolerance, crit)).ToArray(); // Assert CollectionAssert.IsSubsetOf(dlcs, rr.ModList(false)); @@ -1290,16 +1292,16 @@ public void Recommendations_WithDLCRecommendationSatisfied_OmitsDLC(string[] ava // Act var rr = new RelationshipResolver( - installIdents.Select(ident => registry.LatestAvailable(ident, crit)) + installIdents.Select(ident => registry.LatestAvailable(ident, stabilityTolerance, crit)) .OfType(), null, - RelationshipResolverOptions.KitchenSinkOpts(), + RelationshipResolverOptions.KitchenSinkOpts(stabilityTolerance), registry, game, crit); var deps = rr.Dependencies().ToHashSet(); var recs = rr.Recommendations(deps).ToArray(); // Assert - foreach (var mod in dlcIdents.Select(ident => registry.LatestAvailable(ident, crit))) + foreach (var mod in dlcIdents.Select(ident => registry.LatestAvailable(ident, stabilityTolerance, crit))) { CollectionAssert.DoesNotContain(rr.ModList(false), mod); CollectionAssert.DoesNotContain(recs, mod); @@ -1337,10 +1339,10 @@ public void Constructor_WithDLCDependsUnsatisfied_Throws(string[] availableModul Assert.Throws(() => { var rr = new RelationshipResolver( - installIdents.Select(ident => registry.LatestAvailable(ident, crit)) + installIdents.Select(ident => registry.LatestAvailable(ident, stabilityTolerance, crit)) .OfType(), null, - RelationshipResolverOptions.DependsOnlyOpts(), + RelationshipResolverOptions.DependsOnlyOpts(stabilityTolerance), registry, game, crit); }); } @@ -1380,10 +1382,10 @@ public void Constructor_WithDLCDependsSatisfied_DoesNotThrow(string[] availableM Assert.DoesNotThrow(() => { var rr = new RelationshipResolver( - installIdents.Select(ident => registry.LatestAvailable(ident, crit)) + installIdents.Select(ident => registry.LatestAvailable(ident, stabilityTolerance, crit)) .OfType(), null, - RelationshipResolverOptions.DependsOnlyOpts(), + RelationshipResolverOptions.DependsOnlyOpts(stabilityTolerance), registry, game, crit); }); } @@ -1451,7 +1453,7 @@ public void Constructor_ModpackWithIncompatibleDepends_Throws(string[] available { var rr = new RelationshipResolver( Enumerable.Repeat(modpack, 1), null, - RelationshipResolverOptions.DependsOnlyOpts(), + RelationshipResolverOptions.DependsOnlyOpts(stabilityTolerance), registry, game, crit); }); Assert.AreEqual(exceptionMessage, exc?.Message); @@ -1556,10 +1558,10 @@ public void Constructor_WithPlayModes_DoesNotThrow(string[] availableModules, Assert.DoesNotThrow(() => { var rr = new RelationshipResolver( - installIdents.Select(ident => registry.LatestAvailable(ident, crit)) + installIdents.Select(ident => registry.LatestAvailable(ident, stabilityTolerance, crit)) .OfType(), null, - RelationshipResolverOptions.DependsOnlyOpts(), + RelationshipResolverOptions.DependsOnlyOpts(stabilityTolerance), registry, game, crit); var idents = rr.ModList().Select(m => m.identifier).ToArray(); foreach (var goodSubstring in goodSubstrings) diff --git a/Tests/Core/Relationships/SanityChecker.cs b/Tests/Core/Relationships/SanityChecker.cs index 14a2a18602..dd379bf038 100644 --- a/Tests/Core/Relationships/SanityChecker.cs +++ b/Tests/Core/Relationships/SanityChecker.cs @@ -6,6 +6,7 @@ using Tests.Data; using CKAN; +using CKAN.Configuration; using CKAN.Versioning; // We're exercising FindReverseDependencies in here, because: @@ -21,6 +22,7 @@ public class SanityChecker private CKAN.Registry? registry; private DisposableKSP? ksp; private TemporaryRepositoryData? repoData; + private readonly StabilityToleranceConfig stabilityTolerance = new StabilityToleranceConfig(""); private readonly string[] dlls = Array.Empty(); private readonly IDictionary dlc = new Dictionary(); @@ -64,7 +66,7 @@ public void Empty() public void DogeCoin() { // Test with a module that depends and conflicts with nothing. - var mods = new List { registry?.LatestAvailable("DogeCoinFlag", null) }.OfType(); + var mods = new List { registry?.LatestAvailable("DogeCoinFlag", stabilityTolerance, null) }.OfType(); Assert.IsTrue(CKAN.SanityChecker.IsConsistent(mods, dlls, dlc), "DogeCoinFlag"); } @@ -72,14 +74,14 @@ public void DogeCoin() [Test] public void CustomBiomes() { - var mods = Enumerable.Repeat(registry?.LatestAvailable("CustomBiomes", null), 1).OfType().ToList(); + var mods = Enumerable.Repeat(registry?.LatestAvailable("CustomBiomes", stabilityTolerance, null), 1).OfType().ToList(); Assert.IsFalse(CKAN.SanityChecker.IsConsistent(mods, dlls, dlc), "CustomBiomes without data"); - mods.Add(registry?.LatestAvailable("CustomBiomesKerbal", null)!); + mods.Add(registry?.LatestAvailable("CustomBiomesKerbal", stabilityTolerance, null)!); Assert.IsTrue(CKAN.SanityChecker.IsConsistent(mods, dlls, dlc), "CustomBiomes with stock data"); - mods.Add(registry?.LatestAvailable("CustomBiomesRSS", null)!); + mods.Add(registry?.LatestAvailable("CustomBiomesRSS", stabilityTolerance, null)!); Assert.IsFalse(CKAN.SanityChecker.IsConsistent(mods, dlls, dlc), "CustomBiomes with conflicting data"); } @@ -93,17 +95,17 @@ public void CustomBiomesWithDlls() // This would actually be a terrible thing for users to have, but it tests the // relationship we want. - mods.Add(registry?.LatestAvailable("CustomBiomesKerbal", null)!); + mods.Add(registry?.LatestAvailable("CustomBiomesKerbal", stabilityTolerance, null)!); Assert.IsTrue(CKAN.SanityChecker.IsConsistent(mods, dlls, dlc), "CustomBiomes DLL, with config added"); - mods.Add(registry?.LatestAvailable("CustomBiomesRSS", null)!); + mods.Add(registry?.LatestAvailable("CustomBiomesRSS", stabilityTolerance, null)!); Assert.IsFalse(CKAN.SanityChecker.IsConsistent(mods, dlls, dlc), "CustomBiomes with conflicting data"); } [Test] public void ConflictWithDll() { - var mods = new List { registry?.LatestAvailable("SRL", null)! }; + var mods = new List { registry?.LatestAvailable("SRL", stabilityTolerance, null)! }; var dlls = new Dictionary { { "QuickRevert", "" } }.Keys; Assert.IsTrue(CKAN.SanityChecker.IsConsistent(mods, this.dlls, dlc), "SRL can be installed by itself"); @@ -119,17 +121,17 @@ public void FindUnsatisfiedDepends() Assert.IsEmpty(CKAN.SanityChecker.FindUnsatisfiedDepends(mods, dlls, dlc), "Empty list"); - mods.Add(registry?.LatestAvailable("DogeCoinFlag", null)!); + mods.Add(registry?.LatestAvailable("DogeCoinFlag", stabilityTolerance, null)!); Assert.IsEmpty(CKAN.SanityChecker.FindUnsatisfiedDepends(mods, dlls, dlc), "DogeCoinFlag"); - mods.Add(registry?.LatestAvailable("CustomBiomes", null)!); + mods.Add(registry?.LatestAvailable("CustomBiomes", stabilityTolerance, null)!); Assert.Contains( "CustomBiomesData", CKAN.SanityChecker.FindUnsatisfiedDepends(mods, dlls, dlc).Select(kvp => kvp.Item2.ToString()).ToList(), "Missing CustomBiomesData" ); - mods.Add(registry?.LatestAvailable("CustomBiomesKerbal", null)!); + mods.Add(registry?.LatestAvailable("CustomBiomesKerbal", stabilityTolerance, null)!); Assert.IsEmpty(CKAN.SanityChecker.FindUnsatisfiedDepends(mods, dlls, dlc), "CBD+CBK"); mods.RemoveAll(x => x.identifier == "CustomBiomes"); @@ -147,14 +149,14 @@ public void ReverseDepends() { var mods = new CkanModule?[] { - registry?.LatestAvailable("CustomBiomes", null), - registry?.LatestAvailable("CustomBiomesKerbal", null), - registry?.LatestAvailable("DogeCoinFlag", null) + registry?.LatestAvailable("CustomBiomes", stabilityTolerance, null), + registry?.LatestAvailable("CustomBiomesKerbal", stabilityTolerance, null), + registry?.LatestAvailable("DogeCoinFlag", stabilityTolerance, null) }.OfType().ToHashSet(); // Make sure some of our expectations regarding dependencies are correct. - Assert.Contains("CustomBiomes", registry?.LatestAvailable("CustomBiomesKerbal", null)?.depends?.Select(x => x.ToString()).ToList()); - Assert.Contains("CustomBiomesData", registry?.LatestAvailable("CustomBiomes", null)?.depends?.Select(x => x.ToString()).ToList()); + Assert.Contains("CustomBiomes", registry?.LatestAvailable("CustomBiomesKerbal", stabilityTolerance, null)?.depends?.Select(x => x.ToString()).ToList()); + Assert.Contains("CustomBiomesData", registry?.LatestAvailable("CustomBiomes", stabilityTolerance, null)?.depends?.Select(x => x.ToString()).ToList()); // Removing DCF should only remove itself. var to_remove = new List {"DogeCoinFlag"}; diff --git a/Tests/Core/Repositories/RepositoryDataManagerTests.cs b/Tests/Core/Repositories/RepositoryDataManagerTests.cs index 1e9a2988ac..bab9ce5c84 100644 --- a/Tests/Core/Repositories/RepositoryDataManagerTests.cs +++ b/Tests/Core/Repositories/RepositoryDataManagerTests.cs @@ -25,7 +25,7 @@ public void UpdateRegistryTarGz() // Act var versions = repoData.Manager.GetAvailableModules(Enumerable.Repeat(testRepo, 1), "FerramAerospaceResearch") - .Select(am => am.Latest(crit)?.version.ToString()) + .Select(am => am.Latest(ReleaseStatus.stable, crit)?.version.ToString()) .ToArray(); // Assert @@ -47,7 +47,7 @@ public void UpdateRegistryZip() // Act var versions = repoData.Manager.GetAvailableModules(Enumerable.Repeat(testRepo, 1), "FerramAerospaceResearch") - .Select(am => am.Latest(crit)?.version.ToString()) + .Select(am => am.Latest(ReleaseStatus.stable, crit)?.version.ToString()) .ToArray(); // Assert diff --git a/Tests/Core/Types/ReleaseStatus.cs b/Tests/Core/Types/ReleaseStatus.cs index d54ca53ee8..03a2babef2 100644 --- a/Tests/Core/Types/ReleaseStatus.cs +++ b/Tests/Core/Types/ReleaseStatus.cs @@ -1,4 +1,5 @@ using CKAN; + using NUnit.Framework; namespace Tests.Core.Types @@ -6,46 +7,74 @@ namespace Tests.Core.Types [TestFixture] public class ReleaseStatus { - - // These get used by our tests, but we have to disable 'used only once' (0414) - // to stop the compiler from giving us warnings. - #pragma warning disable 0414, IDE0052 - - private static readonly string[] GoodStatuses = { - "stable", "testing", "development" - }; - - private static readonly string[] BadStatuses = { - "cheese", "some thing I wrote last night" , "", - "yo dawg I heard you like tests", - "42" - }; - - #pragma warning restore 0414, IDE0052 - - [Test][TestCaseSource("GoodStatuses")] - public void ReleaseGood(string status) + [TestCase("stable"), + TestCase("testing"), + TestCase("development"), + TestCase("alpha", "development"), + TestCase("beta", "testing")] + public void Deserialize_GoodString_DoesNotThrow(string status, + string? aliasValue = null) { - var release = new CKAN.ReleaseStatus(status); - Assert.IsInstanceOf(release); - Assert.AreEqual(status, release.ToString()); + Assert.DoesNotThrow(() => + { + var module = CkanModule.FromJson( + Relationships.RelationshipResolverTests.MergeWithDefaults( + $@"{{ + ""identifier"": ""aMod"", + ""release_status"": ""{status}"" + }}")); + Assert.AreEqual(aliasValue ?? status, + module.release_status.ToString()); + }); } - [Test][TestCaseSource("BadStatuses")] - public void ReleaseBad(string status) + [TestCase("cheese"), + TestCase("some thing I wrote last night"), + TestCase(""), + TestCase("yo dawg I heard you like tests"), + TestCase("42")] + public void Deserialize_BadString_Throws(string status) { Assert.Throws(delegate { - new CKAN.ReleaseStatus(status); + var module = CkanModule.FromJson( + Relationships.RelationshipResolverTests.MergeWithDefaults( + $@"{{ + ""identifier"": ""aMod"", + ""release_status"": ""{status}"" + }}")); }); } [Test] - public void Null() + public void Deserialize_Absent_DoesNotThrow() { - // According to the spec, no release status means "stable". - var release = new CKAN.ReleaseStatus(null); - Assert.AreEqual("stable", release.ToString()); + // According to the spec, no release status means "stable" + Assert.DoesNotThrow(() => + { + var module = CkanModule.FromJson( + Relationships.RelationshipResolverTests.MergeWithDefaults( + $@"{{ + ""identifier"": ""aMod"" + }}")); + Assert.AreEqual("stable", module.release_status.ToString()); + }); + } + + [Test] + public void Deserialize_NullOrEmpty_DoesNotThrow() + { + // According to the spec, no release status means "stable" + Assert.DoesNotThrow(() => + { + var module = CkanModule.FromJson( + Relationships.RelationshipResolverTests.MergeWithDefaults( + $@"{{ + ""identifier"": ""aMod"", + ""release_status"": null + }}")); + Assert.AreEqual("stable", module.release_status.ToString()); + }); } } } diff --git a/Tests/GUI/Model/GUIMod.cs b/Tests/GUI/Model/GUIMod.cs index 55032737f3..0e2c48c1cf 100644 --- a/Tests/GUI/Model/GUIMod.cs +++ b/Tests/GUI/Model/GUIMod.cs @@ -22,6 +22,7 @@ namespace Tests.GUI [TestFixture] public class GUIModTests { + [Test] public void NewGuiModsAreNotSelectedForUpgrade() { @@ -38,7 +39,7 @@ public void NewGuiModsAreNotSelectedForUpgrade() var registry = new Registry(repoData.Manager, repo.repo); var ckan_mod = registry.GetModuleByVersion("kOS", "0.14"); - var mod = new GUIMod(ckan_mod!, repoData.Manager, registry, manager.CurrentInstance.VersionCriteria(), + var mod = new GUIMod(ckan_mod!, repoData.Manager, registry, manager.CurrentInstance.StabilityToleranceConfig, manager.CurrentInstance.VersionCriteria(), null, false, false); Assert.True(mod.SelectedMod == mod.InstalledMod?.Module); } @@ -67,7 +68,7 @@ public void HasUpdate_UpdateAvailable_ReturnsTrue() var upgradeableGroups = registry.CheckUpgradeable(tidy.KSP, new HashSet()); - var mod = new GUIMod(old_version, repoData.Manager, registry, tidy.KSP.VersionCriteria(), + var mod = new GUIMod(old_version, repoData.Manager, registry, tidy.KSP.StabilityToleranceConfig, tidy.KSP.VersionCriteria(), null, false, false) { HasUpdate = upgradeableGroups[true].Any(m => m.identifier == old_version.identifier), @@ -108,7 +109,7 @@ public void GameCompatibility_OutOfOrderGameVersions_TrueMaxVersion() var prevVersion = registry.GetModuleByVersion("OutOfOrderMod", "1.1.0"); // Act - GUIMod m = new GUIMod(mainVersion!, repoData.Manager, registry, tidy.KSP.VersionCriteria(), + GUIMod m = new GUIMod(mainVersion!, repoData.Manager, registry, tidy.KSP.StabilityToleranceConfig, tidy.KSP.VersionCriteria(), null, false, false); // Assert diff --git a/Tests/GUI/Model/ModList.cs b/Tests/GUI/Model/ModList.cs index 8553f7c4fd..58aaeb61ee 100644 --- a/Tests/GUI/Model/ModList.cs +++ b/Tests/GUI/Model/ModList.cs @@ -15,7 +15,6 @@ using CKAN; using CKAN.Versioning; using CKAN.GUI; -using CKAN.Games.KerbalSpaceProgram; namespace Tests.GUI { @@ -31,9 +30,10 @@ public class ModListTests public void ComputeFullChangeSetFromUserChangeSet_WithEmptyList_HasEmptyChangeSet() { var item = new ModList(); - var game = new KerbalSpaceProgram(); - var inst = new GameInstance(game, "/", "dummy", new NullUser()); - Assert.That(item.ComputeUserChangeSet(Registry.Empty(), crit, inst, null, null), Is.Empty); + using (var tidy = new DisposableKSP()) + { + Assert.That(item.ComputeUserChangeSet(Registry.Empty(), crit, tidy.KSP, null, null), Is.Empty); + } } [Test] @@ -55,7 +55,7 @@ public void IsVisible_WithAllAndNoNameFilter_ReturnsTrueForCompatible() var item = new ModList(); Assert.That(item.IsVisible( - new GUIMod(ckan_mod!, repoData.Manager, registry, manager.CurrentInstance.VersionCriteria(), + new GUIMod(ckan_mod!, repoData.Manager, registry, tidy.KSP.StabilityToleranceConfig, manager.CurrentInstance.VersionCriteria(), null, false, false), manager.CurrentInstance.Name, manager.CurrentInstance.game, @@ -93,9 +93,9 @@ public void ConstructModList_NumberOfRows_IsEqualToNumberOfMods() var mod_list = main_mod_list.ConstructModList( new List { - new GUIMod(TestData.FireSpitterModule(), repoData.Manager, registry, manager.CurrentInstance.VersionCriteria(), + new GUIMod(TestData.FireSpitterModule(), repoData.Manager, registry, tidy.KSP.StabilityToleranceConfig, manager.CurrentInstance.VersionCriteria(), null, false, false), - new GUIMod(TestData.kOS_014_module(), repoData.Manager, registry, manager.CurrentInstance.VersionCriteria(), + new GUIMod(TestData.kOS_014_module(), repoData.Manager, registry, tidy.KSP.StabilityToleranceConfig, manager.CurrentInstance.VersionCriteria(), null, false, false) }, manager.CurrentInstance.Name, @@ -149,7 +149,10 @@ public void InstallAndSortByCompat_WithAnyCompat_NoCrash() using (var repoData = new TemporaryRepositoryData(user, repo.repo)) using (var instance = new DisposableKSP()) using (var config = new FakeConfiguration(instance.KSP, instance.KSP.Name)) - using (var manager = new GameInstanceManager(user, config)) + using (var manager = new GameInstanceManager(user, config) + { + CurrentInstance = instance.KSP + }) { var registryManager = RegistryManager.Instance(instance.KSP, repoData.Manager); var registry = registryManager.registry; @@ -172,7 +175,7 @@ public void InstallAndSortByCompat_WithAnyCompat_NoCrash() HashSet? possibleConfigOnlyDirs = null; installer.InstallList( new List { anyVersionModule }, - new RelationshipResolverOptions(), + new RelationshipResolverOptions(instance.KSP.StabilityToleranceConfig), registryManager, ref possibleConfigOnlyDirs, null, @@ -195,7 +198,7 @@ public void InstallAndSortByCompat_WithAnyCompat_NoCrash() Assert.IsNotNull(modList); var modules = repoData.Manager.GetAllAvailableModules(Enumerable.Repeat(repo.repo, 1)) - .Select(mod => new GUIMod(mod.Latest()!, repoData.Manager, registry, instance.KSP.VersionCriteria(), + .Select(mod => new GUIMod(mod.Latest(instance.KSP.StabilityToleranceConfig)!, repoData.Manager, registry, instance.KSP.StabilityToleranceConfig, instance.KSP.VersionCriteria(), null, false, false)) .ToList(); @@ -213,24 +216,24 @@ public void InstallAndSortByCompat_WithAnyCompat_NoCrash() Assert.IsTrue(otherModule.SelectedMod == otherModule.LatestAvailableMod); Assert.IsFalse(otherModule.IsInstalled); - var game = new KerbalSpaceProgram(); - var inst = new GameInstance(game, "/", "dummy", new NullUser()); - - Assert.DoesNotThrow(() => + using (var inst2 = new DisposableKSP()) { - // Install the "other" module - installer.InstallList( - modList.ComputeUserChangeSet(Registry.Empty(), crit, inst, null, null).Select(change => change.Mod).ToList(), - new RelationshipResolverOptions(), - registryManager, - ref possibleConfigOnlyDirs, - null, - downloader); - - // Now we need to sort - // Make sure refreshing the GUI state does not throw a NullReferenceException - listGui.Refresh(); - }); + Assert.DoesNotThrow(() => + { + // Install the "other" module + installer.InstallList( + modList.ComputeUserChangeSet(Registry.Empty(), crit, inst2.KSP, null, null).Select(change => change.Mod).ToList(), + new RelationshipResolverOptions(inst2.KSP.StabilityToleranceConfig), + registryManager, + ref possibleConfigOnlyDirs, + null, + downloader); + + // Now we need to sort + // Make sure refreshing the GUI state does not throw a NullReferenceException + listGui.Refresh(); + }); + } } } } diff --git a/Tests/NetKAN/Sources/Github/GithubApiTests.cs b/Tests/NetKAN/Sources/Github/GithubApiTests.cs index 57d033bbaa..542d4870e3 100644 --- a/Tests/NetKAN/Sources/Github/GithubApiTests.cs +++ b/Tests/NetKAN/Sources/Github/GithubApiTests.cs @@ -50,7 +50,7 @@ public void GetsLatestReleaseCorrectly() var sut = new GithubApi(new CachingHttpService(_cache!)); // Act - var githubRelease = sut.GetLatestRelease(new GithubRef("#/ckan/github/KSP-CKAN/Test", false, false)); + var githubRelease = sut.GetLatestRelease(new GithubRef("#/ckan/github/KSP-CKAN/Test", false), false); // Assert Assert.IsNotNull(githubRelease?.Author); diff --git a/Tests/NetKAN/Transformers/GithubTransformerTests.cs b/Tests/NetKAN/Transformers/GithubTransformerTests.cs index 225b16622e..ac47501e03 100644 --- a/Tests/NetKAN/Transformers/GithubTransformerTests.cs +++ b/Tests/NetKAN/Transformers/GithubTransformerTests.cs @@ -28,7 +28,7 @@ public void setupApiMockup() HtmlUrl = "https://github.com/ExampleAccount/ExampleProject" }); - mApi.Setup(i => i.GetLatestRelease(It.IsAny())) + mApi.Setup(i => i.GetLatestRelease(It.IsAny(), false)) .Returns(new GithubRelease( "ExampleProject", new ModuleVersion("1.0"), @@ -42,7 +42,7 @@ public void setupApiMockup() } )); - mApi.Setup(i => i.GetAllReleases(It.IsAny())) + mApi.Setup(i => i.GetAllReleases(It.IsAny(), false)) .Returns(new GithubRelease[] { new GithubRelease( "ExampleProject", @@ -137,7 +137,7 @@ public void Transform_DownloadURLWithEncodedCharacter_DontDoubleEncode() HtmlUrl = "https://github.com/jrodrigv/DestructionEffects" }); - mApi.Setup(i => i.GetLatestRelease(It.IsAny())) + mApi.Setup(i => i.GetLatestRelease(It.IsAny(), false)) .Returns(new GithubRelease( "DestructionEffects", new ModuleVersion("v1.8,0"), @@ -151,7 +151,7 @@ public void Transform_DownloadURLWithEncodedCharacter_DontDoubleEncode() } )); - mApi.Setup(i => i.GetAllReleases(It.IsAny())) + mApi.Setup(i => i.GetAllReleases(It.IsAny(), false)) .Returns(new GithubRelease[] { new GithubRelease( "DestructionEffects", new ModuleVersion("v1.8,0"),