diff --git a/CHANGELOG.md b/CHANGELOG.md index 543ecdf67c..c9e9a1fba0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. - [Multiple] Show recommendations of full changeset with opt-out (#3892 by: HebaruSan; reviewed: techman83) - [Multiple] Dutch translation and icon duplication guardrails (#3897 by: HebaruSan; reviewed: techman83) - [GUI] Shorten toolbar button labels (#3903 by: HebaruSan; reviewed: techman83) +- [Multiple] Refactor repository and available module handling (#3904 by: HebaruSan; reviewed: techman83) ### Bugfixes diff --git a/Cmdline/Action/Available.cs b/Cmdline/Action/Available.cs index 4b8becd8ff..b973615a01 100644 --- a/Cmdline/Action/Available.cs +++ b/Cmdline/Action/Available.cs @@ -5,18 +5,17 @@ namespace CKAN.CmdLine { public class Available : ICommand { - public IUser user { get; set; } - - public Available(IUser user) + public Available(RepositoryDataManager repoData, IUser user) { - this.user = user; + this.repoData = repoData; + this.user = user; } public int RunCommand(CKAN.GameInstance instance, object raw_options) { - AvailableOptions opts = (AvailableOptions)raw_options; - IRegistryQuerier registry = RegistryManager.Instance(instance).registry; - + AvailableOptions opts = (AvailableOptions)raw_options; + IRegistryQuerier registry = RegistryManager.Instance(instance, repoData).registry; + var compatible = registry .CompatibleModules(instance.VersionCriteria()) .Where(m => !m.IsDLC); @@ -42,5 +41,8 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) return Exit.OK; } + + private IUser user; + private RepositoryDataManager repoData; } } diff --git a/Cmdline/Action/Compat.cs b/Cmdline/Action/Compat.cs index 0818730f67..7080f006f2 100644 --- a/Cmdline/Action/Compat.cs +++ b/Cmdline/Action/Compat.cs @@ -1,8 +1,10 @@ using System.Linq; -using CKAN.Versioning; + using CommandLine; using CommandLine.Text; +using CKAN.Versioning; + namespace CKAN.CmdLine { public class CompatOptions : VerbCommandOptions diff --git a/Cmdline/Action/Filter.cs b/Cmdline/Action/Filter.cs index aca59458e9..1d922b19bd 100644 --- a/Cmdline/Action/Filter.cs +++ b/Cmdline/Action/Filter.cs @@ -214,8 +214,8 @@ private int RemoveFilters(FilterRemoveOptions opts, string verb) return Exit.OK; } - private GameInstanceManager manager { get; set; } - private IUser user { get; set; } + private GameInstanceManager manager; + private IUser user; private static readonly ILog log = LogManager.GetLogger(typeof(Filter)); } diff --git a/Cmdline/Action/GameInstance.cs b/Cmdline/Action/GameInstance.cs index e953831cd4..c26bfdd8df 100644 --- a/Cmdline/Action/GameInstance.cs +++ b/Cmdline/Action/GameInstance.cs @@ -2,11 +2,14 @@ using System.Collections.Generic; using System.IO; using System.Linq; + using CommandLine; using CommandLine.Text; using log4net; + using CKAN.Versioning; using CKAN.Games; +using CKAN.Games.KerbalSpaceProgram.DLC; namespace CKAN.CmdLine { @@ -566,7 +569,7 @@ int badArgument() { if (GameVersion.TryParse(options.makingHistoryVersion, out GameVersion ver)) { - dlcs.Add(new DLC.MakingHistoryDlcDetector(), ver); + dlcs.Add(new MakingHistoryDlcDetector(), ver); } else { @@ -578,7 +581,7 @@ int badArgument() { if (GameVersion.TryParse(options.breakingGroundVersion, out GameVersion ver)) { - dlcs.Add(new DLC.BreakingGroundDlcDetector(), ver); + dlcs.Add(new BreakingGroundDlcDetector(), ver); } else { diff --git a/Cmdline/Action/ICommand.cs b/Cmdline/Action/ICommand.cs index 85ba382baa..2852e89cdf 100644 --- a/Cmdline/Action/ICommand.cs +++ b/Cmdline/Action/ICommand.cs @@ -2,6 +2,6 @@ { public interface ICommand { - int RunCommand(CKAN.GameInstance ksp, object options); + int RunCommand(CKAN.GameInstance instance, object options); } } diff --git a/Cmdline/Action/Import.cs b/Cmdline/Action/Import.cs index 7934290e98..fc51d74546 100644 --- a/Cmdline/Action/Import.cs +++ b/Cmdline/Action/Import.cs @@ -1,26 +1,26 @@ using System; using System.IO; using System.Collections.Generic; + using log4net; namespace CKAN.CmdLine { - /// /// Handler for "ckan import" command. /// Imports manually downloaded ZIP files into the cache. /// public class Import : ICommand { - /// /// Initialize the command /// /// IUser object for user interaction - public Import(GameInstanceManager mgr, IUser user) + public Import(GameInstanceManager mgr, RepositoryDataManager repoData, IUser user) { - manager = mgr; - this.user = user; + manager = mgr; + this.repoData = repoData; + this.user = user; } /// @@ -31,7 +31,7 @@ public Import(GameInstanceManager mgr, IUser user) /// /// Process exit code /// - public int RunCommand(CKAN.GameInstance ksp, object options) + public int RunCommand(CKAN.GameInstance instance, object options) { try { @@ -45,19 +45,18 @@ public int RunCommand(CKAN.GameInstance ksp, object options) else { log.InfoFormat("Importing {0} files", toImport.Count); - List toInstall = new List(); - RegistryManager regMgr = RegistryManager.Instance(ksp); - ModuleInstaller inst = new ModuleInstaller(ksp, manager.Cache, user); - inst.ImportFiles(toImport, user, mod => toInstall.Add(mod.identifier), regMgr.registry, !opts.Headless); + var toInstall = new List(); + var installer = new ModuleInstaller(instance, manager.Cache, user); + var regMgr = RegistryManager.Instance(instance, repoData); + installer.ImportFiles(toImport, user, mod => toInstall.Add(mod.identifier), regMgr.registry, !opts.Headless); HashSet possibleConfigOnlyDirs = null; if (toInstall.Count > 0) { - inst.InstallList( + installer.InstallList( toInstall, new RelationshipResolverOptions(), regMgr, - ref possibleConfigOnlyDirs - ); + ref possibleConfigOnlyDirs); } return Exit.OK; } @@ -104,9 +103,11 @@ private void AddFile(HashSet files, string filename) } } - private readonly GameInstanceManager manager; - private readonly IUser user; - private static readonly ILog log = LogManager.GetLogger(typeof(Import)); + private readonly GameInstanceManager manager; + private readonly RepositoryDataManager repoData; + private readonly IUser user; + + private static readonly ILog log = LogManager.GetLogger(typeof(Import)); } } diff --git a/Cmdline/Action/Install.cs b/Cmdline/Action/Install.cs index f30b825ec3..c62af424fb 100644 --- a/Cmdline/Action/Install.cs +++ b/Cmdline/Action/Install.cs @@ -1,27 +1,24 @@ -using System; +using System; using System.IO; using System.Collections.Generic; using System.Linq; + using log4net; namespace CKAN.CmdLine { public class Install : ICommand { - private static readonly ILog log = LogManager.GetLogger(typeof(Install)); - - public IUser user { get; set; } - private GameInstanceManager manager; - /// /// Initialize the install command object /// /// GameInstanceManager containing our instances /// IUser object for interaction - public Install(GameInstanceManager mgr, IUser user) + public Install(GameInstanceManager mgr, RepositoryDataManager repoData, IUser user) { - manager = mgr; - this.user = user; + manager = mgr; + this.repoData = repoData; + this.user = user; } /// @@ -101,7 +98,9 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) } else { - Search.AdjustModulesCase(instance, options.modules); + Search.AdjustModulesCase(instance, + RegistryManager.Instance(instance, repoData).registry, + options.modules); } if (options.modules.Count == 0) @@ -127,7 +126,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) install_ops.without_enforce_consistency = true; } - RegistryManager regMgr = RegistryManager.Instance(instance); + var regMgr = RegistryManager.Instance(instance, repoData); List modules = options.modules; for (bool done = false; !done; ) @@ -268,5 +267,11 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) return Exit.OK; } + + private GameInstanceManager manager; + private RepositoryDataManager repoData; + private IUser user; + + private static readonly ILog log = LogManager.GetLogger(typeof(Install)); } } diff --git a/Cmdline/Action/List.cs b/Cmdline/Action/List.cs index 7bf9deaaa6..3a16482914 100644 --- a/Cmdline/Action/List.cs +++ b/Cmdline/Action/List.cs @@ -1,28 +1,28 @@ -using System; +using System; using System.Collections.Generic; + +using log4net; + using CKAN.Exporters; using CKAN.Types; using CKAN.Versioning; -using log4net; namespace CKAN.CmdLine { public class List : ICommand { - private static readonly ILog log = LogManager.GetLogger(typeof(List)); - - public IUser user { get; set; } - - public List(IUser user) + public List(RepositoryDataManager repoData, IUser user) { - this.user = user; + this.repoData = repoData; + this.user = user; } public int RunCommand(CKAN.GameInstance instance, object raw_options) { ListOptions options = (ListOptions) raw_options; - IRegistryQuerier registry = RegistryManager.Instance(instance).registry; + var regMgr = RegistryManager.Instance(instance, repoData); + var registry = regMgr.registry; ExportFileType? exportFileType = null; @@ -163,5 +163,10 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) default: return null; } } + + private RepositoryDataManager repoData; + private IUser user; + + private static readonly ILog log = LogManager.GetLogger(typeof(List)); } } diff --git a/Cmdline/Action/Mark.cs b/Cmdline/Action/Mark.cs index 19e73f524c..68d3968949 100644 --- a/Cmdline/Action/Mark.cs +++ b/Cmdline/Action/Mark.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; + using CommandLine; using CommandLine.Text; using log4net; @@ -14,7 +15,10 @@ public class Mark : ISubCommand /// /// Initialize the subcommand /// - public Mark() { } + public Mark(RepositoryDataManager repoData) + { + this.repoData = repoData; + } /// /// Run the subcommand @@ -77,10 +81,10 @@ private int MarkAuto(MarkAutoOptions opts, bool value, string verb, string descr return exitCode; } - var ksp = MainClass.GetGameInstance(manager); - var regMgr = RegistryManager.Instance(ksp); + var instance = MainClass.GetGameInstance(manager); + var regMgr = RegistryManager.Instance(instance, repoData); bool needSave = false; - Search.AdjustModulesCase(ksp, opts.modules); + Search.AdjustModulesCase(instance, regMgr.registry, opts.modules); foreach (string id in opts.modules) { InstalledModule im = regMgr.registry.InstalledModule(id); @@ -115,8 +119,9 @@ private int MarkAuto(MarkAutoOptions opts, bool value, string verb, string descr return Exit.OK; } - private IUser user { get; set; } - private GameInstanceManager manager { get; set; } + private GameInstanceManager manager; + private RepositoryDataManager repoData; + private IUser user; private static readonly ILog log = LogManager.GetLogger(typeof(Mark)); } diff --git a/Cmdline/Action/Prompt.cs b/Cmdline/Action/Prompt.cs index d3f287cb6f..3b6502dc32 100644 --- a/Cmdline/Action/Prompt.cs +++ b/Cmdline/Action/Prompt.cs @@ -13,9 +13,10 @@ namespace CKAN.CmdLine public class Prompt { - public Prompt(GameInstanceManager mgr) + public Prompt(GameInstanceManager mgr, RepositoryDataManager repoData) { manager = mgr; + this.repoData = repoData; } public int RunCommand(object raw_options) @@ -184,7 +185,7 @@ private static bool WantsAvailIdentifiers(TypeInfo ti) private string[] GetAvailIdentifiers(string prefix) { CKAN.GameInstance inst = MainClass.GetGameInstance(manager); - return RegistryManager.Instance(inst) + return RegistryManager.Instance(inst, repoData) .registry .CompatibleModules(inst.VersionCriteria()) .Where(m => !m.IsDLC) @@ -201,7 +202,7 @@ private static bool WantsInstIdentifiers(TypeInfo ti) private string[] GetInstIdentifiers(string prefix) { CKAN.GameInstance inst = MainClass.GetGameInstance(manager); - var registry = RegistryManager.Instance(inst).registry; + var registry = RegistryManager.Instance(inst, repoData).registry; return registry.Installed(false, false) .Select(kvp => kvp.Key) .Where(ident => ident.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase) @@ -219,8 +220,9 @@ private string[] GetGameInstances(string prefix) .Where(ident => ident.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) .ToArray(); - private readonly GameInstanceManager manager; - private const string exitCommand = "exit"; + private readonly GameInstanceManager manager; + private readonly RepositoryDataManager repoData; + private const string exitCommand = "exit"; } } diff --git a/Cmdline/Action/Remove.cs b/Cmdline/Action/Remove.cs index c8770b2aa8..64b377b87e 100644 --- a/Cmdline/Action/Remove.cs +++ b/Cmdline/Action/Remove.cs @@ -2,26 +2,23 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; + using log4net; namespace CKAN.CmdLine { public class Remove : ICommand { - private static readonly ILog log = LogManager.GetLogger(typeof(Remove)); - - public IUser user { get; set; } - private GameInstanceManager manager; - /// /// Initialize the remove command object /// /// GameInstanceManager containing our instances /// IUser object for interaction - public Remove(GameInstanceManager mgr, IUser user) + public Remove(GameInstanceManager mgr, RepositoryDataManager repoData, IUser user) { - manager = mgr; - this.user = user; + manager = mgr; + this.repoData = repoData; + this.user = user; } /// @@ -35,7 +32,7 @@ public Remove(GameInstanceManager mgr, IUser user) public int RunCommand(CKAN.GameInstance instance, object raw_options) { RemoveOptions options = (RemoveOptions) raw_options; - RegistryManager regMgr = RegistryManager.Instance(instance); + RegistryManager regMgr = RegistryManager.Instance(instance, repoData); // Use one (or more!) regex to select the modules to remove if (options.regex) @@ -78,7 +75,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) { HashSet possibleConfigOnlyDirs = null; var installer = new ModuleInstaller(instance, manager.Cache, user); - Search.AdjustModulesCase(instance, options.modules); + Search.AdjustModulesCase(instance, regMgr.registry, options.modules); installer.UninstallList(options.modules, ref possibleConfigOnlyDirs, regMgr); user.RaiseMessage(""); } @@ -114,5 +111,11 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) return Exit.OK; } + + private GameInstanceManager manager; + private RepositoryDataManager repoData; + private IUser user; + + private static readonly ILog log = LogManager.GetLogger(typeof(Remove)); } } diff --git a/Cmdline/Action/Repair.cs b/Cmdline/Action/Repair.cs index d91ad9ef93..658d5eb364 100644 --- a/Cmdline/Action/Repair.cs +++ b/Cmdline/Action/Repair.cs @@ -1,4 +1,4 @@ -using CommandLine; +using CommandLine; using CommandLine.Text; namespace CKAN.CmdLine @@ -37,7 +37,10 @@ public string GetUsage(string verb) public class Repair : ISubCommand { - public Repair() { } + public Repair(RepositoryDataManager repoData) + { + this.repoData = repoData; + } public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCommandOptions unparsed) { @@ -62,7 +65,8 @@ public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCom switch (option) { case "registry": - exitCode = Registry(MainClass.GetGameInstance(manager)); + exitCode = Registry(RegistryManager.Instance + (MainClass.GetGameInstance(manager), repoData)); break; default: @@ -76,15 +80,15 @@ public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCom } private IUser User { get; set; } + private RepositoryDataManager repoData; /// /// Try to repair our registry. /// - private int Registry(CKAN.GameInstance ksp) + private int Registry(RegistryManager regMgr) { - RegistryManager manager = RegistryManager.Instance(ksp); - manager.registry.Repair(); - manager.Save(); + regMgr.registry.Repair(); + regMgr.Save(); User.RaiseMessage(Properties.Resources.Repaired); return Exit.OK; } diff --git a/Cmdline/Action/Replace.cs b/Cmdline/Action/Replace.cs index 060744d48b..c58f226ab0 100644 --- a/Cmdline/Action/Replace.cs +++ b/Cmdline/Action/Replace.cs @@ -1,6 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; + using log4net; using CKAN.Versioning; @@ -8,18 +9,13 @@ namespace CKAN.CmdLine { public class Replace : ICommand { - private static readonly ILog log = LogManager.GetLogger(typeof(Replace)); - - public IUser User { get; set; } - - public Replace(CKAN.GameInstanceManager mgr, IUser user) + public Replace(CKAN.GameInstanceManager mgr, RepositoryDataManager repoData, IUser user) { - manager = mgr; - User = user; + manager = mgr; + this.repoData = repoData; + this.user = user; } - private GameInstanceManager manager; - public int RunCommand(CKAN.GameInstance instance, object raw_options) { ReplaceOptions options = (ReplaceOptions) raw_options; @@ -32,8 +28,8 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) if (options.modules.Count == 0 && ! options.replace_all) { // What? No mods specified? - User.RaiseMessage("{0}: ckan replace Mod [Mod2, ...]", Properties.Resources.Usage); - User.RaiseMessage(" or ckan replace --all"); + user.RaiseMessage("{0}: ckan replace Mod [Mod2, ...]", Properties.Resources.Usage); + user.RaiseMessage(" or ckan replace --all"); return Exit.BADOPT; } @@ -46,7 +42,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) allow_incompatible = options.allow_incompatible }; - var regMgr = RegistryManager.Instance(instance); + var regMgr = RegistryManager.Instance(instance, repoData); var registry = regMgr.registry; var to_replace = new List(); @@ -131,27 +127,27 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) } catch (ModuleNotFoundKraken kraken) { - User.RaiseMessage(Properties.Resources.ReplaceModuleNotFound, kraken.module); + user.RaiseMessage(Properties.Resources.ReplaceModuleNotFound, kraken.module); } } } if (to_replace.Count() != 0) { - User.RaiseMessage(""); - User.RaiseMessage(Properties.Resources.Replacing); - User.RaiseMessage(""); + user.RaiseMessage(""); + user.RaiseMessage(Properties.Resources.Replacing); + user.RaiseMessage(""); foreach (ModuleReplacement r in to_replace) { - User.RaiseMessage(Properties.Resources.ReplaceFound, + user.RaiseMessage(Properties.Resources.ReplaceFound, r.ReplaceWith.identifier, r.ReplaceWith.version, r.ToReplace.identifier, r.ToReplace.version); } - bool ok = User.RaiseYesNoDialog(Properties.Resources.ReplaceContinuePrompt); + bool ok = user.RaiseYesNoDialog(Properties.Resources.ReplaceContinuePrompt); if (!ok) { - User.RaiseMessage(Properties.Resources.ReplaceCancelled); + user.RaiseMessage(Properties.Resources.ReplaceCancelled); return Exit.ERROR; } @@ -159,22 +155,28 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) try { HashSet possibleConfigOnlyDirs = null; - new ModuleInstaller(instance, manager.Cache, User).Replace(to_replace, replace_ops, new NetAsyncModulesDownloader(User, manager.Cache), ref possibleConfigOnlyDirs, regMgr); - User.RaiseMessage(""); + new ModuleInstaller(instance, manager.Cache, user).Replace(to_replace, replace_ops, new NetAsyncModulesDownloader(user, manager.Cache), ref possibleConfigOnlyDirs, regMgr); + user.RaiseMessage(""); } catch (DependencyNotSatisfiedKraken ex) { - User.RaiseMessage(Properties.Resources.ReplaceDependencyNotSatisfied, + user.RaiseMessage(Properties.Resources.ReplaceDependencyNotSatisfied, ex.parent, ex.module, ex.version, instance.game.ShortName); } } else { - User.RaiseMessage(Properties.Resources.ReplaceNotFound); + user.RaiseMessage(Properties.Resources.ReplaceNotFound); return Exit.OK; } return Exit.OK; } + + private GameInstanceManager manager; + private RepositoryDataManager repoData; + private IUser user; + + private static readonly ILog log = LogManager.GetLogger(typeof(Replace)); } } diff --git a/Cmdline/Action/Repo.cs b/Cmdline/Action/Repo.cs index 530000b3d6..06ab6ccd62 100644 --- a/Cmdline/Action/Repo.cs +++ b/Cmdline/Action/Repo.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -101,7 +101,10 @@ public class RepoForgetOptions : InstanceSpecificOptions public class Repo : ISubCommand { - public Repo() { } + public Repo(RepositoryDataManager repoData) + { + this.repoData = repoData; + } // This is required by ISubCommand public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCommandOptions unparsed) @@ -217,7 +220,7 @@ private int AvailableRepositories() private int ListRepositories() { - var repositories = RegistryManager.Instance(MainClass.GetGameInstance(Manager)).registry.Repositories; + var repositories = RegistryManager.Instance(MainClass.GetGameInstance(Manager), repoData).registry.Repositories; string priorityHeader = Properties.Resources.RepoListPriorityHeader; string nameHeader = Properties.Resources.RepoListNameHeader; @@ -255,7 +258,7 @@ private int ListRepositories() private int AddRepository(RepoAddOptions options) { - RegistryManager manager = RegistryManager.Instance(MainClass.GetGameInstance(Manager)); + RegistryManager manager = RegistryManager.Instance(MainClass.GetGameInstance(Manager), repoData); if (options.name == null) { @@ -308,8 +311,8 @@ private int AddRepository(RepoAddOptions options) return Exit.BADOPT; } - repositories.Add(options.name, - new Repository(options.name, options.uri, manager.registry.Repositories.Count)); + manager.registry.RepositoriesAdd(new Repository(options.name, options.uri, + manager.registry.Repositories.Count)); User.RaiseMessage(Properties.Resources.RepoAdded, options.name, options.uri); manager.Save(); @@ -324,7 +327,7 @@ private int SetRepositoryPriority(RepoPriorityOptions options) User.RaiseMessage("priority - {0}", Properties.Resources.ArgumentMissing); return Exit.BADOPT; } - var manager = RegistryManager.Instance(MainClass.GetGameInstance(Manager)); + var manager = RegistryManager.Instance(MainClass.GetGameInstance(Manager), repoData); if (options.priority < 0 || options.priority > manager.registry.Repositories.Count) { User.RaiseMessage(Properties.Resources.RepoPriorityInvalid, @@ -375,7 +378,7 @@ private int ForgetRepository(RepoForgetOptions options) return Exit.BADOPT; } - RegistryManager manager = RegistryManager.Instance(MainClass.GetGameInstance(Manager)); + RegistryManager manager = RegistryManager.Instance(MainClass.GetGameInstance(Manager), repoData); log.DebugFormat("About to forget repository '{0}'", options.name); var repos = manager.registry.Repositories; @@ -392,7 +395,7 @@ private int ForgetRepository(RepoForgetOptions options) User.RaiseMessage(Properties.Resources.RepoForgetRemoving, name); } - repos.Remove(name); + manager.registry.RepositoriesRemove(name); var remaining = repos.Values.OrderBy(r => r.priority).ToArray(); for (int i = 0; i < remaining.Length; ++i) { @@ -410,16 +413,16 @@ private int DefaultRepository(RepoDefaultOptions options) var uri = options.uri ?? inst.game.DefaultRepositoryURL.ToString(); log.DebugFormat("About to add repository '{0}' - '{1}'", Repository.default_ckan_repo_name, uri); - RegistryManager manager = RegistryManager.Instance(inst); + RegistryManager manager = RegistryManager.Instance(inst, repoData); var repositories = manager.registry.Repositories; if (repositories.ContainsKey(Repository.default_ckan_repo_name)) { - repositories.Remove(Repository.default_ckan_repo_name); + manager.registry.RepositoriesRemove(Repository.default_ckan_repo_name); } - repositories.Add(Repository.default_ckan_repo_name, new Repository( - Repository.default_ckan_repo_name, uri, repositories.Count)); + manager.registry.RepositoriesAdd( + new Repository(Repository.default_ckan_repo_name, uri, repositories.Count)); User.RaiseMessage(Properties.Resources.RepoSet, Repository.default_ckan_repo_name, uri); manager.Save(); @@ -427,8 +430,9 @@ private int DefaultRepository(RepoDefaultOptions options) return Exit.OK; } - private GameInstanceManager Manager { get; set; } - private IUser User { get; set; } + private GameInstanceManager Manager; + private RepositoryDataManager repoData; + private IUser User; private static readonly ILog log = LogManager.GetLogger(typeof (Repo)); } diff --git a/Cmdline/Action/Search.cs b/Cmdline/Action/Search.cs index 508481669c..68a549d2b0 100644 --- a/Cmdline/Action/Search.cs +++ b/Cmdline/Action/Search.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -8,11 +8,10 @@ namespace CKAN.CmdLine { public class Search : ICommand { - public IUser user { get; set; } - - public Search(IUser user) + public Search(RepositoryDataManager repoData, IUser user) { - this.user = user; + this.repoData = repoData; + this.user = user; } public int RunCommand(CKAN.GameInstance ksp, object raw_options) @@ -87,7 +86,7 @@ public int RunCommand(CKAN.GameInstance ksp, object raw_options) user.RaiseMessage(Properties.Resources.SearchIncompatibleModsHeader); foreach (CkanModule mod in matching_incompatible) { - Registry.GetMinMaxVersions(new List { mod } , out _, out _, out var minKsp, out var maxKsp); + CkanModule.GetMinMaxVersions(new List { mod } , out _, out _, out var minKsp, out var maxKsp); string GameVersion = Versioning.GameVersionRange.VersionSpan(ksp.game, minKsp, maxKsp).ToString(); user.RaiseMessage(Properties.Resources.SearchIncompatibleMod, @@ -129,7 +128,7 @@ public List PerformSearch(CKAN.GameInstance ksp, string term, string 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).registry; + var registry = RegistryManager.Instance(ksp, repoData).registry; if (!searchIncompatible) { @@ -178,14 +177,13 @@ private static string CaseInsensitiveExactMatch(List mods, string mo /// /// Convert case insensitive mod names from the user to case sensitive identifiers /// - /// Game instance forgetting the mods + /// Game instance forgetting the mods /// List of strings to convert, format 'identifier' or 'identifier=version' - public static void AdjustModulesCase(CKAN.GameInstance ksp, List modules) + public static void AdjustModulesCase(CKAN.GameInstance instance, Registry registry, List modules) { - IRegistryQuerier registry = RegistryManager.Instance(ksp).registry; // Get the list of all compatible and incompatible mods - List mods = registry.CompatibleModules(ksp.VersionCriteria()).ToList(); - mods.AddRange(registry.IncompatibleModules(ksp.VersionCriteria())); + List mods = registry.CompatibleModules(instance.VersionCriteria()).ToList(); + mods.AddRange(registry.IncompatibleModules(instance.VersionCriteria())); for (int i = 0; i < modules.Count; ++i) { Match match = CkanModule.idAndVersionMatcher.Match(modules[i]); @@ -203,5 +201,7 @@ public static void AdjustModulesCase(CKAN.GameInstance ksp, List modules } } + private RepositoryDataManager repoData; + private IUser user; } } diff --git a/Cmdline/Action/Show.cs b/Cmdline/Action/Show.cs index 7a6d220f5b..dfa0817be7 100644 --- a/Cmdline/Action/Show.cs +++ b/Cmdline/Action/Show.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -9,9 +9,10 @@ namespace CKAN.CmdLine { public class Show : ICommand { - public Show(IUser user) + public Show(RepositoryDataManager repoData, IUser user) { - this.user = user; + this.repoData = repoData; + this.user = user; } public int RunCommand(CKAN.GameInstance instance, object raw_options) @@ -26,7 +27,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) int combined_exit_code = Exit.OK; // Check installed modules for an exact match. - var registry = RegistryManager.Instance(instance).registry; + var registry = RegistryManager.Instance(instance, repoData).registry; foreach (string modName in options.modules) { var installedModuleToShow = registry.InstalledModule(modName); @@ -62,7 +63,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) string.Join(", ", instance.VersionCriteria().Versions.Select(v => v.ToString()))); user.RaiseMessage(Properties.Resources.ShowLookingForClose); - Search search = new Search(user); + Search search = new Search(repoData, user); var matches = search.PerformSearch(instance, modName); // Display the results of the search. @@ -321,7 +322,7 @@ private void ShowVersionTable(CKAN.GameInstance inst, List modules) var gameVersions = modules.Select(m => { GameVersion minKsp = null, maxKsp = null; - Registry.GetMinMaxVersions(new List() { m }, out _, out _, out minKsp, out maxKsp); + CkanModule.GetMinMaxVersions(new List() { m }, out _, out _, out minKsp, out maxKsp); return GameVersionRange.VersionSpan(inst.game, minKsp, maxKsp); }).ToList(); string[] headers = new string[] { @@ -363,5 +364,6 @@ private static string RelationshipToPrintableString(RelationshipDescriptor dep) } private IUser user { get; set; } + private RepositoryDataManager repoData; } } diff --git a/Cmdline/Action/Update.cs b/Cmdline/Action/Update.cs index 408933a4c1..992042a505 100644 --- a/Cmdline/Action/Update.cs +++ b/Cmdline/Action/Update.cs @@ -5,18 +5,16 @@ namespace CKAN.CmdLine { public class Update : ICommand { - public IUser user { get; set; } - private GameInstanceManager manager; - /// /// Initialize the update command object /// /// GameInstanceManager containing our instances /// IUser object for interaction - public Update(GameInstanceManager mgr, IUser user) + public Update(GameInstanceManager mgr, RepositoryDataManager repoData, IUser user) { - manager = mgr; - this.user = user; + manager = mgr; + this.repoData = repoData; + this.user = user; } /// @@ -36,7 +34,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) if (options.list_changes) { // Get a list of compatible modules prior to the update. - var registry = RegistryManager.Instance(instance).registry; + var registry = RegistryManager.Instance(instance, repoData).registry; compatible_prior = registry.CompatibleModules(instance.VersionCriteria()).ToList(); } @@ -53,7 +51,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) if (options.list_changes) { - var registry = RegistryManager.Instance(instance).registry; + var registry = RegistryManager.Instance(instance, repoData).registry; PrintChanges(compatible_prior, registry.CompatibleModules(instance.VersionCriteria()).ToList()); } @@ -133,13 +131,16 @@ private void PrintModules(string message, IEnumerable modules) /// Repository to update. If null all repositories are used. private void UpdateRepository(CKAN.GameInstance instance) { - RegistryManager registry_manager = RegistryManager.Instance(instance); - - var downloader = new NetAsyncDownloader(user); + RegistryManager registry_manager = RegistryManager.Instance(instance, repoData); - CKAN.Repo.UpdateAllRepositories(registry_manager, instance, downloader, manager.Cache, user); + var result = repoData.Update(registry_manager.registry.Repositories.Values.ToArray(), + instance.game, false, new NetAsyncDownloader(user), user); user.RaiseMessage(Properties.Resources.UpdateSummary, registry_manager.registry.CompatibleModules(instance.VersionCriteria()).Count()); } + + private GameInstanceManager manager; + private RepositoryDataManager repoData; + private IUser user; } } diff --git a/Cmdline/Action/Upgrade.cs b/Cmdline/Action/Upgrade.cs index 9cfaf50750..919bc3a73f 100644 --- a/Cmdline/Action/Upgrade.cs +++ b/Cmdline/Action/Upgrade.cs @@ -11,20 +11,16 @@ namespace CKAN.CmdLine { public class Upgrade : ICommand { - private static readonly ILog log = LogManager.GetLogger(typeof(Upgrade)); - - public IUser User { get; set; } - private GameInstanceManager manager; - /// /// Initialize the upgrade command object /// /// GameInstanceManager containing our instances /// IUser object for interaction - public Upgrade(GameInstanceManager mgr, IUser user) + public Upgrade(GameInstanceManager mgr, RepositoryDataManager repoData, IUser user) { - manager = mgr; - User = user; + manager = mgr; + this.repoData = repoData; + this.user = user; } /// @@ -47,39 +43,39 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) if (options.modules.Count == 0 && !options.upgrade_all) { // What? No files specified? - User.RaiseMessage("{0}: ckan upgrade Mod [Mod2, ...]", Properties.Resources.Usage); - User.RaiseMessage(" or ckan upgrade --all"); + user.RaiseMessage("{0}: ckan upgrade Mod [Mod2, ...]", Properties.Resources.Usage); + user.RaiseMessage(" or ckan upgrade --all"); if (AutoUpdate.CanUpdate) { - User.RaiseMessage(" or ckan upgrade ckan"); + user.RaiseMessage(" or ckan upgrade ckan"); } return Exit.BADOPT; } if (!options.upgrade_all && options.modules[0] == "ckan" && AutoUpdate.CanUpdate) { - User.RaiseMessage(Properties.Resources.UpgradeQueryingCKAN); + user.RaiseMessage(Properties.Resources.UpgradeQueryingCKAN); AutoUpdate.Instance.FetchLatestReleaseInfo(); var latestVersion = AutoUpdate.Instance.latestUpdate.Version; var currentVersion = new ModuleVersion(Meta.GetVersion(VersionFormat.Short)); if (latestVersion.IsGreaterThan(currentVersion)) { - User.RaiseMessage(Properties.Resources.UpgradeNewCKANAvailable, latestVersion); + user.RaiseMessage(Properties.Resources.UpgradeNewCKANAvailable, latestVersion); var releaseNotes = AutoUpdate.Instance.latestUpdate.ReleaseNotes; - User.RaiseMessage(releaseNotes); - User.RaiseMessage(""); - User.RaiseMessage(""); + user.RaiseMessage(releaseNotes); + user.RaiseMessage(""); + user.RaiseMessage(""); - if (User.RaiseYesNoDialog(Properties.Resources.UpgradeProceed)) + if (user.RaiseYesNoDialog(Properties.Resources.UpgradeProceed)) { - User.RaiseMessage(Properties.Resources.UpgradePleaseWait); + user.RaiseMessage(Properties.Resources.UpgradePleaseWait); AutoUpdate.Instance.StartUpdateProcess(false); } } else { - User.RaiseMessage(Properties.Resources.UpgradeAlreadyHaveLatest); + user.RaiseMessage(Properties.Resources.UpgradeAlreadyHaveLatest); } return Exit.OK; @@ -87,7 +83,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) try { - var regMgr = RegistryManager.Instance(instance); + var regMgr = RegistryManager.Instance(instance, repoData); var registry = regMgr.registry; if (options.upgrade_all) { @@ -122,40 +118,40 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) mod.Key); } } - UpgradeModules(manager, User, instance, true, to_upgrade); + UpgradeModules(manager, user, instance, true, to_upgrade); } else { - Search.AdjustModulesCase(instance, options.modules); - UpgradeModules(manager, User, instance, options.modules); + Search.AdjustModulesCase(instance, registry, options.modules); + UpgradeModules(manager, user, instance, options.modules); } - User.RaiseMessage(""); + user.RaiseMessage(""); } catch (CancelledActionKraken k) { - User.RaiseMessage(Properties.Resources.UpgradeAborted, k.Message); + user.RaiseMessage(Properties.Resources.UpgradeAborted, k.Message); return Exit.ERROR; } catch (ModuleNotFoundKraken kraken) { - User.RaiseMessage(Properties.Resources.UpgradeNotFound, kraken.module); + user.RaiseMessage(Properties.Resources.UpgradeNotFound, kraken.module); return Exit.ERROR; } catch (InconsistentKraken kraken) { - User.RaiseMessage(kraken.ToString()); + user.RaiseMessage(kraken.ToString()); return Exit.ERROR; } catch (ModuleIsDLCKraken kraken) { - User.RaiseMessage(Properties.Resources.UpgradeDLC, kraken.module.name); + user.RaiseMessage(Properties.Resources.UpgradeDLC, kraken.module.name); var res = kraken?.module?.resources; var storePagesMsg = new Uri[] { res?.store, res?.steamstore } .Where(u => u != null) .Aggregate("", (a, b) => $"{a}\r\n- {b}"); if (!string.IsNullOrEmpty(storePagesMsg)) { - User.RaiseMessage(Properties.Resources.UpgradeDLCStorePage, storePagesMsg); + user.RaiseMessage(Properties.Resources.UpgradeDLCStorePage, storePagesMsg); } return Exit.ERROR; } @@ -170,9 +166,9 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) /// IUser object for output /// Game instance to use /// List of modules to upgrade - public static void UpgradeModules(GameInstanceManager manager, IUser user, CKAN.GameInstance instance, bool ConfirmPrompt, List modules) + public void UpgradeModules(GameInstanceManager manager, IUser user, CKAN.GameInstance instance, bool ConfirmPrompt, List modules) { - UpgradeModules(manager, user, instance, + UpgradeModules(manager, user, instance, repoData, (ModuleInstaller installer, NetAsyncModulesDownloader downloader, RegistryManager regMgr, ref HashSet possibleConfigOnlyDirs) => installer.Upgrade(modules, downloader, ref possibleConfigOnlyDirs, regMgr, true, true, ConfirmPrompt), @@ -187,9 +183,9 @@ public static void UpgradeModules(GameInstanceManager manager, IUser user, CKAN. /// IUser object for output /// Game instance to use /// List of identifier[=version] to upgrade - public static void UpgradeModules(GameInstanceManager manager, IUser user, CKAN.GameInstance instance, List identsAndVersions) + public void UpgradeModules(GameInstanceManager manager, IUser user, CKAN.GameInstance instance, List identsAndVersions) { - UpgradeModules(manager, user, instance, + UpgradeModules(manager, user, instance, repoData, (ModuleInstaller installer, NetAsyncModulesDownloader downloader, RegistryManager regMgr, ref HashSet possibleConfigOnlyDirs) => installer.Upgrade(identsAndVersions, downloader, ref possibleConfigOnlyDirs, regMgr, true), @@ -211,15 +207,16 @@ public static void UpgradeModules(GameInstanceManager manager, IUser user, CKAN. /// Game instance to use /// Function to call to try to perform the actual upgrade, may throw TooManyModsProvideKraken /// Function to call when the user has requested a new module added to the change set in response to TooManyModsProvideKraken - private static void UpgradeModules( + private void UpgradeModules( GameInstanceManager manager, IUser user, CKAN.GameInstance instance, + RepositoryDataManager repoData, AttemptUpgradeAction attemptUpgradeCallback, Action addUserChoiceCallback) { using (TransactionScope transact = CkanTransaction.CreateTransactionScope()) { var installer = new ModuleInstaller(instance, manager.Cache, user); var downloader = new NetAsyncModulesDownloader(user, manager.Cache); - var regMgr = RegistryManager.Instance(instance); + var regMgr = RegistryManager.Instance(instance, repoData); HashSet possibleConfigOnlyDirs = null; bool done = false; while (!done) @@ -248,5 +245,10 @@ private static void UpgradeModules( } } + private IUser user; + private GameInstanceManager manager; + private RepositoryDataManager repoData; + + private static readonly ILog log = LogManager.GetLogger(typeof(Upgrade)); } } diff --git a/Cmdline/Main.cs b/Cmdline/Main.cs index 25237e381a..61f7f9500d 100644 --- a/Cmdline/Main.cs +++ b/Cmdline/Main.cs @@ -1,4 +1,4 @@ -// Reference CKAN client +// Reference CKAN client // Paul '@pjf' Fenwick // // License: CC-BY 4.0, LGPL, or MIT (your choice) @@ -12,6 +12,7 @@ using System.Runtime.InteropServices; using System.Text.RegularExpressions; +using Autofac; using log4net; using log4net.Core; @@ -79,6 +80,7 @@ public static int Main(string[] args) public static int Execute(GameInstanceManager manager, CommonOptions opts, string[] args) { + var repoData = ServiceLocator.Container.Resolve(); // We shouldn't instantiate Options if it's a subcommand. // It breaks command-specific help, for starters. try @@ -86,7 +88,7 @@ public static int Execute(GameInstanceManager manager, CommonOptions opts, strin switch (args[0]) { case "repair": - return (new Repair()).RunSubCommand(manager, opts, new SubCommandOptions(args)); + return (new Repair(repoData)).RunSubCommand(manager, opts, new SubCommandOptions(args)); case "instance": return (new GameInstance()).RunSubCommand(manager, opts, new SubCommandOptions(args)); @@ -95,7 +97,7 @@ public static int Execute(GameInstanceManager manager, CommonOptions opts, strin return (new Compat()).RunSubCommand(manager, opts, new SubCommandOptions(args)); case "repo": - return (new Repo()).RunSubCommand(manager, opts, new SubCommandOptions(args)); + return (new Repo(repoData)).RunSubCommand(manager, opts, new SubCommandOptions(args)); case "authtoken": return (new AuthToken()).RunSubCommand(manager, opts, new SubCommandOptions(args)); @@ -104,7 +106,7 @@ public static int Execute(GameInstanceManager manager, CommonOptions opts, strin return (new Cache()).RunSubCommand(manager, opts, new SubCommandOptions(args)); case "mark": - return (new Mark()).RunSubCommand(manager, opts, new SubCommandOptions(args)); + return (new Mark(repoData)).RunSubCommand(manager, opts, new SubCommandOptions(args)); case "filter": return (new Filter()).RunSubCommand(manager, opts, new SubCommandOptions(args)); @@ -120,6 +122,7 @@ public static int Execute(GameInstanceManager manager, CommonOptions opts, strin log.Info("CKAN exiting."); } + // Why do we print "CKAN exiting" twice??? Options cmdline; try { @@ -186,6 +189,7 @@ public static CKAN.GameInstance GetGameInstance(GameInstanceManager manager) /// The exit status that should be returned to the system. private static int RunSimpleAction(Options cmdline, CommonOptions options, string[] args, IUser user, GameInstanceManager manager) { + var repoData = ServiceLocator.Container.Resolve(); try { switch (cmdline.action) @@ -197,48 +201,48 @@ private static int RunSimpleAction(Options cmdline, CommonOptions options, strin return ConsoleUi(manager, (ConsoleUIOptions)options, args); case "prompt": - return new Prompt(manager).RunCommand(cmdline.options); + return new Prompt(manager, repoData).RunCommand(cmdline.options); case "version": return Version(user); case "update": - return (new Update(manager, user)).RunCommand(GetGameInstance(manager), cmdline.options); + return (new Update(manager, repoData, user)).RunCommand(GetGameInstance(manager), cmdline.options); case "available": - return (new Available(user)).RunCommand(GetGameInstance(manager), cmdline.options); + return (new Available(repoData, user)).RunCommand(GetGameInstance(manager), cmdline.options); case "add": case "install": Scan(GetGameInstance(manager), user, cmdline.action); - return (new Install(manager, user)).RunCommand(GetGameInstance(manager), cmdline.options); + return (new Install(manager, repoData, user)).RunCommand(GetGameInstance(manager), cmdline.options); case "scan": return Scan(GetGameInstance(manager), user); case "list": - return (new List(user)).RunCommand(GetGameInstance(manager), cmdline.options); + return (new List(repoData, user)).RunCommand(GetGameInstance(manager), cmdline.options); case "show": - return (new Show(user)).RunCommand(GetGameInstance(manager), cmdline.options); + return (new Show(repoData, user)).RunCommand(GetGameInstance(manager), cmdline.options); case "replace": Scan(GetGameInstance(manager), user, cmdline.action); - return (new Replace(manager, user)).RunCommand(GetGameInstance(manager), (ReplaceOptions)cmdline.options); + return (new Replace(manager, repoData, user)).RunCommand(GetGameInstance(manager), (ReplaceOptions)cmdline.options); case "upgrade": Scan(GetGameInstance(manager), user, cmdline.action); - return (new Upgrade(manager, user)).RunCommand(GetGameInstance(manager), cmdline.options); + return (new Upgrade(manager, repoData, user)).RunCommand(GetGameInstance(manager), cmdline.options); case "search": - return (new Search(user)).RunCommand(GetGameInstance(manager), options); + return (new Search(repoData, user)).RunCommand(GetGameInstance(manager), options); case "uninstall": case "remove": - return (new Remove(manager, user)).RunCommand(GetGameInstance(manager), cmdline.options); + return (new Remove(manager, repoData, user)).RunCommand(GetGameInstance(manager), cmdline.options); case "import": - return (new Import(manager, user)).RunCommand(GetGameInstance(manager), options); + return (new Import(manager, repoData, user)).RunCommand(GetGameInstance(manager), options); case "clean": return Clean(manager.Cache); @@ -262,20 +266,7 @@ private static int RunSimpleAction(Options cmdline, CommonOptions options, strin } internal static CkanModule LoadCkanFromFile(CKAN.GameInstance current_instance, string ckan_file) - { - CkanModule module = CkanModule.FromFile(ckan_file); - - // We'll need to make some registry changes to do this. - RegistryManager registry_manager = RegistryManager.Instance(current_instance); - - // Remove this version of the module in the registry, if it exists. - registry_manager.registry.RemoveAvailable(module); - - // Sneakily add our version in... - registry_manager.registry.AddAvailable(module); - - return module; - } + => CkanModule.FromFile(ckan_file); private static int printMissingInstanceError(IUser user) { @@ -323,7 +314,8 @@ private static int Scan(CKAN.GameInstance inst, IUser user, string next_command { try { - inst.Scan(); + var repoData = ServiceLocator.Container.Resolve(); + RegistryManager.Instance(inst, repoData).ScanUnmanagedFiles(); return Exit.OK; } catch (InconsistentKraken kraken) diff --git a/ConsoleUI/CompatibleVersionDialog.cs b/ConsoleUI/CompatibleVersionDialog.cs index dbcd9fc760..83acb2d04c 100644 --- a/ConsoleUI/CompatibleVersionDialog.cs +++ b/ConsoleUI/CompatibleVersionDialog.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using System.ComponentModel; + +using Autofac; + using CKAN.Versioning; using CKAN.Games; -using CKAN.GameVersionProviders; using CKAN.ConsoleUI.Toolkit; -using Autofac; namespace CKAN.ConsoleUI { diff --git a/ConsoleUI/ConsoleCKAN.cs b/ConsoleUI/ConsoleCKAN.cs index 779c1c857f..e2dcec7faf 100644 --- a/ConsoleUI/ConsoleCKAN.cs +++ b/ConsoleUI/ConsoleCKAN.cs @@ -1,5 +1,8 @@ using System; using System.Linq; + +using Autofac; + using CKAN.ConsoleUI.Toolkit; namespace CKAN.ConsoleUI { @@ -18,6 +21,7 @@ public ConsoleCKAN(GameInstanceManager mgr, string themeName, bool debug) { if (ConsoleTheme.Themes.TryGetValue(themeName ?? "default", out ConsoleTheme theme)) { + var repoData = ServiceLocator.Container.Resolve(); // GameInstanceManager only uses its IUser object to construct game instance objects, // which only use it to inform the user about the creation of the CKAN/ folder. // These aren't really intended to be displayed, so the manager @@ -26,7 +30,7 @@ public ConsoleCKAN(GameInstanceManager mgr, string themeName, bool debug) // The splash screen returns true when it's safe to run the rest of the app. // This can be blocked by a lock file, for example. - if (new SplashScreen(manager).Run(theme)) { + if (new SplashScreen(manager, repoData).Run(theme)) { if (manager.CurrentInstance == null) { if (manager.Instances.Count == 0) { @@ -36,11 +40,14 @@ public ConsoleCKAN(GameInstanceManager mgr, string themeName, bool debug) manager.GetPreferredInstance(); } else { // Multiple instances, no default, pick one - new GameInstanceListScreen(manager).Run(theme); + new GameInstanceListScreen(manager, repoData).Run(theme); } } if (manager.CurrentInstance != null) { - new ModListScreen(manager, debug, theme).Run(theme); + new ModListScreen(manager, repoData, + RegistryManager.Instance(manager.CurrentInstance, repoData), + manager.CurrentInstance.game, + debug, theme).Run(theme); } new ExitScreen().Run(theme); diff --git a/ConsoleUI/DependencyScreen.cs b/ConsoleUI/DependencyScreen.cs index 5fd0992ec0..d9aa9a9b65 100644 --- a/ConsoleUI/DependencyScreen.cs +++ b/ConsoleUI/DependencyScreen.cs @@ -16,15 +16,16 @@ public class DependencyScreen : ConsoleScreen { /// Initialize the screen /// /// Game instance manager containing instances + /// Registry of the current instance for finding mods /// Plan of mods to add and remove /// Mods that the user saw and did not select, in this pass or a previous pass /// True if debug options should be available, false otherwise - public DependencyScreen(GameInstanceManager mgr, ChangePlan cp, HashSet rej, bool dbg) : base() + public DependencyScreen(GameInstanceManager mgr, Registry registry, ChangePlan cp, HashSet rej, bool dbg) : base() { debug = dbg; manager = mgr; plan = cp; - registry = RegistryManager.Instance(manager.CurrentInstance).registry; + this.registry = registry; installer = new ModuleInstaller(manager.CurrentInstance, manager.Cache, this); rejected = rej; @@ -89,10 +90,9 @@ public DependencyScreen(GameInstanceManager mgr, ChangePlan cp, HashSet dependencyList.AddBinding(Keys.Enter, (object sender, ConsoleTheme theme) => { if (dependencyList.Selection != null) { LaunchSubScreen(theme, new ModInfoScreen( - manager, plan, + manager, registry, plan, dependencyList.Selection.module, - debug - )); + debug)); } return true; }); diff --git a/ConsoleUI/DownloadImportDialog.cs b/ConsoleUI/DownloadImportDialog.cs index e9e9cde8ec..3bc4e7c5f2 100644 --- a/ConsoleUI/DownloadImportDialog.cs +++ b/ConsoleUI/DownloadImportDialog.cs @@ -16,9 +16,10 @@ public static class DownloadImportDialog { /// /// The visual theme to use to draw the dialog /// Game instance to import into + /// Repository data manager providing info from repos /// Cache object to import into /// Change plan object for marking things to be installed - public static void ImportDownloads(ConsoleTheme theme, GameInstance gameInst, NetModuleCache cache, ChangePlan cp) + public static void ImportDownloads(ConsoleTheme theme, GameInstance gameInst, RepositoryDataManager repoData, NetModuleCache cache, ChangePlan cp) { ConsoleFileMultiSelectDialog cfmsd = new ConsoleFileMultiSelectDialog( Properties.Resources.ImportSelectTitle, @@ -34,7 +35,7 @@ public static void ImportDownloads(ConsoleTheme theme, GameInstance gameInst, Ne Properties.Resources.ImportProgressMessage); ModuleInstaller inst = new ModuleInstaller(gameInst, cache, ps); ps.Run(theme, (ConsoleTheme th) => inst.ImportFiles(files, ps, - (CkanModule mod) => cp.Install.Add(mod), RegistryManager.Instance(gameInst).registry)); + (CkanModule mod) => cp.Install.Add(mod), RegistryManager.Instance(gameInst, repoData).registry)); // Don't let the installer re-use old screen references inst.User = null; } diff --git a/ConsoleUI/GameInstanceEditScreen.cs b/ConsoleUI/GameInstanceEditScreen.cs index 360be8920f..a72fd7b2e9 100644 --- a/ConsoleUI/GameInstanceEditScreen.cs +++ b/ConsoleUI/GameInstanceEditScreen.cs @@ -16,14 +16,16 @@ public class GameInstanceEditScreen : GameInstanceScreen { /// Initialize the Screen /// /// Game instance manager containing the instances + /// Repository data manager providing info from repos /// Instance to edit - public GameInstanceEditScreen(GameInstanceManager mgr, GameInstance k) + public GameInstanceEditScreen(GameInstanceManager mgr, RepositoryDataManager repoData, GameInstance k) : base(mgr, k.Name, k.GameDir()) { ksp = k; try { // If we can't parse the registry, just leave the repo list blank - registry = RegistryManager.Instance(ksp).registry; + regMgr = RegistryManager.Instance(ksp, repoData); + registry = regMgr.registry; } catch { } // Show the repositories if we can @@ -209,8 +211,8 @@ protected override void Save() { if (repoEditList != null) { // Copy the temp list of repositories to the registry - registry.Repositories = repoEditList; - RegistryManager.Instance(ksp).Save(); + registry.RepositoriesSet(repoEditList); + regMgr.Save(); } if (compatEditList != null) { ksp.SetCompatibleVersions(compatEditList); @@ -228,8 +230,9 @@ protected override void Save() } } - private GameInstance ksp; - private Registry registry; + private GameInstance ksp; + private RegistryManager regMgr; + private Registry registry; private SortedDictionary repoEditList; private ConsoleListBox repoList; diff --git a/ConsoleUI/GameInstanceListScreen.cs b/ConsoleUI/GameInstanceListScreen.cs index 9baa672dba..dc2d8c3427 100644 --- a/ConsoleUI/GameInstanceListScreen.cs +++ b/ConsoleUI/GameInstanceListScreen.cs @@ -16,10 +16,12 @@ public class GameInstanceListScreen : ConsoleScreen { /// Initialize the screen. /// /// Game instance manager object for getting hte Instances + /// Repository data manager providing info from repos /// If true, this is the first screen after the splash, so Ctrl+Q exits, else Esc exits - public GameInstanceListScreen(GameInstanceManager mgr, bool first = false) + public GameInstanceListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, bool first = false) { manager = mgr; + this.repoData = repoData; AddObject(new ConsoleLabel( 1, 2, -1, @@ -73,7 +75,7 @@ public GameInstanceListScreen(GameInstanceManager mgr, bool first = false) new List() ); - if (TryGetInstance(theme, instanceList.Selection, (ConsoleTheme th) => { d.Run(th, (ConsoleTheme thm) => {}); })) { + if (TryGetInstance(theme, instanceList.Selection, repoData, (ConsoleTheme th) => { d.Run(th, (ConsoleTheme thm) => {}); })) { try { manager.SetCurrentInstance(instanceList.Selection.Name); } catch (Exception ex) { @@ -106,10 +108,10 @@ public GameInstanceListScreen(GameInstanceManager mgr, bool first = false) string.Format(Properties.Resources.InstanceListLoadingInstance, instanceList.Selection.Name), new List() ); - TryGetInstance(theme, instanceList.Selection, (ConsoleTheme th) => { d.Run(theme, (ConsoleTheme thm) => {}); }); + TryGetInstance(theme, instanceList.Selection, repoData, (ConsoleTheme th) => { d.Run(theme, (ConsoleTheme thm) => {}); }); // Still launch the screen even if the load fails, // because you need to be able to fix the name/path. - LaunchSubScreen(theme, new GameInstanceEditScreen(manager, instanceList.Selection)); + LaunchSubScreen(theme, new GameInstanceEditScreen(manager, repoData, instanceList.Selection)); return true; }); @@ -166,11 +168,12 @@ protected override string MenuTip() /// /// The visual theme to use to draw the dialog /// Game instance + /// Repository data manager providing info from repos /// Function that shows a loading message /// /// True if successfully loaded, false if it's locked or the registry was corrupted, etc. /// - public static bool TryGetInstance(ConsoleTheme theme, GameInstance ksp, Action render) + public static bool TryGetInstance(ConsoleTheme theme, GameInstance ksp, RepositoryDataManager repoData, Action render) { bool retry; do { @@ -180,7 +183,7 @@ public static bool TryGetInstance(ConsoleTheme theme, GameInstance ksp, Action instanceList; private static readonly string defaultMark = Symbols.checkmark; diff --git a/ConsoleUI/InstallFilterAddDialog.cs b/ConsoleUI/InstallFilterAddDialog.cs index 94d5c500f6..b4ffadb2a2 100644 --- a/ConsoleUI/InstallFilterAddDialog.cs +++ b/ConsoleUI/InstallFilterAddDialog.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using System.ComponentModel; + +using Autofac; + using CKAN.Versioning; using CKAN.Games; -using CKAN.GameVersionProviders; using CKAN.ConsoleUI.Toolkit; -using Autofac; namespace CKAN.ConsoleUI { diff --git a/ConsoleUI/InstallScreen.cs b/ConsoleUI/InstallScreen.cs index 308e6116fd..e5ad54a2ae 100644 --- a/ConsoleUI/InstallScreen.cs +++ b/ConsoleUI/InstallScreen.cs @@ -15,9 +15,10 @@ public class InstallScreen : ProgressScreen { /// Initialize the Screen /// /// Game instance manager containing instances + /// Repository data manager providing info from repos /// Plan of mods to install or remove /// True if debug options should be available, false otherwise - public InstallScreen(GameInstanceManager mgr, ChangePlan cp, bool dbg) + public InstallScreen(GameInstanceManager mgr, RepositoryDataManager repoData, ChangePlan cp, bool dbg) : base( Properties.Resources.InstallTitle, Properties.Resources.InstallMessage @@ -26,6 +27,7 @@ public InstallScreen(GameInstanceManager mgr, ChangePlan cp, bool dbg) debug = dbg; manager = mgr; plan = cp; + this.repoData = repoData; } /// @@ -45,11 +47,13 @@ public override void Run(ConsoleTheme theme, Action process = null // Reset this so we stop unless an exception sets it to true retry = false; + RegistryManager regMgr = RegistryManager.Instance(manager.CurrentInstance, repoData); + // 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(manager, plan, rejected, debug); + DependencyScreen ds = new DependencyScreen(manager, regMgr.registry, plan, rejected, debug); if (ds.HaveOptions()) { LaunchSubScreen(theme, ds); } @@ -59,7 +63,6 @@ public override void Run(ConsoleTheme theme, Action process = null HashSet possibleConfigOnlyDirs = null; - RegistryManager regMgr = RegistryManager.Instance(manager.CurrentInstance); ModuleInstaller inst = new ModuleInstaller(manager.CurrentInstance, manager.Cache, this); inst.onReportModInstalled = OnModInstalled; if (plan.Remove.Count > 0) { @@ -152,7 +155,7 @@ private void OnModInstalled(CkanModule mod) private IEnumerable AllReplacements(IEnumerable identifiers) { - IRegistryQuerier registry = RegistryManager.Instance(manager.CurrentInstance).registry; + IRegistryQuerier registry = RegistryManager.Instance(manager.CurrentInstance, repoData).registry; foreach (string id in identifiers) { ModuleReplacement repl = registry.GetReplacement( @@ -172,9 +175,10 @@ private IEnumerable AllReplacements(IEnumerable ident without_enforce_consistency = false }; - private GameInstanceManager manager; - private ChangePlan plan; - private bool debug; + private GameInstanceManager manager; + private RepositoryDataManager repoData; + private ChangePlan plan; + private bool debug; } } diff --git a/ConsoleUI/ModInfoScreen.cs b/ConsoleUI/ModInfoScreen.cs index d4ff7ddfbc..84227cb944 100644 --- a/ConsoleUI/ModInfoScreen.cs +++ b/ConsoleUI/ModInfoScreen.cs @@ -19,16 +19,17 @@ public class ModInfoScreen : ConsoleScreen { /// Initialize the Screen /// /// Game instance manager containing game instances + /// Registry of the current instance for finding mods /// Plan of other mods to be added or removed /// The module to display /// True if debug options should be available, false otherwise - public ModInfoScreen(GameInstanceManager mgr, ChangePlan cp, CkanModule m, bool dbg) + public ModInfoScreen(GameInstanceManager mgr, Registry registry, ChangePlan cp, CkanModule m, bool dbg) { debug = dbg; mod = m; manager = mgr; plan = cp; - registry = RegistryManager.Instance(manager.CurrentInstance).registry; + this.registry = registry; int midL = Console.WindowWidth / 2 - 1; @@ -485,7 +486,7 @@ private void addVersionBox(int l, int t, int r, int b, Func title, Func< ModuleVersion minMod = null, maxMod = null; GameVersion minKsp = null, maxKsp = null; - Registry.GetMinMaxVersions(releases, out minMod, out maxMod, out minKsp, out maxKsp); + CkanModule.GetMinMaxVersions(releases, out minMod, out maxMod, out minKsp, out maxKsp); AddObject(new ConsoleLabel( l + 2, t + 1, r - 2, () => minMod == maxMod diff --git a/ConsoleUI/ModListScreen.cs b/ConsoleUI/ModListScreen.cs index de73c1e96a..1585a0f3f4 100644 --- a/ConsoleUI/ModListScreen.cs +++ b/ConsoleUI/ModListScreen.cs @@ -7,6 +7,8 @@ using Autofac; using CKAN.ConsoleUI.Toolkit; +using CKAN.Extensions; +using CKAN.Games; namespace CKAN.ConsoleUI { @@ -19,13 +21,18 @@ public class ModListScreen : ConsoleScreen { /// Initialize the screen /// /// Game instance manager object containing the current instance + /// Repository data manager providing info from repos + /// Registry manager for the current instance + /// The game of the current instance, used for getting known versions /// True if debug options should be available, false otherwise /// The theme to use for the registry update flow, if needed - public ModListScreen(GameInstanceManager mgr, bool dbg, ConsoleTheme regTheme) + public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, RegistryManager regMgr, IGame game, bool dbg, ConsoleTheme regTheme) { debug = dbg; manager = mgr; - registry = RegistryManager.Instance(manager.CurrentInstance).registry; + this.regMgr = regMgr; + this.registry = regMgr.registry; + this.repoData = repoData; moduleList = new ConsoleListBox( 1, 4, -1, -2, @@ -47,8 +54,8 @@ public ModListScreen(GameInstanceManager mgr, bool dbg, ConsoleTheme regTheme) }, new ConsoleListBoxColumn() { Header = Properties.Resources.ModListMaxGameVersionHeader, Width = 20, - Renderer = m => registry.LatestCompatibleKSP(m.identifier)?.ToString() ?? "", - Comparer = (a, b) => registry.LatestCompatibleKSP(a.identifier).CompareTo(registry.LatestCompatibleKSP(b.identifier)) + Renderer = m => registry.LatestCompatibleGameVersion(game.KnownVersions, m.identifier)?.ToString() ?? "", + Comparer = (a, b) => registry.LatestCompatibleGameVersion(game.KnownVersions, a.identifier).CompareTo(registry.LatestCompatibleGameVersion(game.KnownVersions, b.identifier)) } }, 1, 0, ListSortDirection.Descending, @@ -163,7 +170,7 @@ public ModListScreen(GameInstanceManager mgr, bool dbg, ConsoleTheme regTheme) ); moduleList.AddBinding(Keys.Enter, (object sender, ConsoleTheme theme) => { if (moduleList.Selection != null) { - LaunchSubScreen(theme, new ModInfoScreen(manager, plan, moduleList.Selection, debug)); + LaunchSubScreen(theme, new ModInfoScreen(manager, registry, plan, moduleList.Selection, debug)); } return true; }); @@ -222,7 +229,7 @@ public ModListScreen(GameInstanceManager mgr, bool dbg, ConsoleTheme regTheme) InstalledModule im = registry.InstalledModule(moduleList.Selection.identifier); if (im != null && !moduleList.Selection.IsDLC) { im.AutoInstalled = !im.AutoInstalled; - RegistryManager.Instance(manager.CurrentInstance).Save(false); + regMgr.Save(false); } return true; }); @@ -333,7 +340,7 @@ protected override string CenterHeader() private bool ImportDownloads(ConsoleTheme theme) { - DownloadImportDialog.ImportDownloads(theme, manager.CurrentInstance, manager.Cache, plan); + DownloadImportDialog.ImportDownloads(theme, manager.CurrentInstance, repoData, manager.Cache, plan); RefreshList(theme); return true; } @@ -387,7 +394,7 @@ private bool ViewSuggestions(ConsoleTheme theme) } } try { - DependencyScreen ds = new DependencyScreen(manager, reinstall, new HashSet(), debug); + DependencyScreen ds = new DependencyScreen(manager, registry, reinstall, new HashSet(), debug); if (ds.HaveOptions()) { LaunchSubScreen(theme, ds); bool needRefresh = false; @@ -437,15 +444,11 @@ private bool UpdateRegistry(ConsoleTheme theme, bool showNewModsPrompt = true) ); recent.Clear(); try { - var downloader = new NetAsyncDownloader(ps); - - Repo.UpdateAllRepositories( - RegistryManager.Instance(manager.CurrentInstance), - manager.CurrentInstance, - downloader, - manager.Cache, - ps - ); + repoData.Update(registry.Repositories.Values.ToArray(), + manager.CurrentInstance.game, + false, + new NetAsyncDownloader(ps), + ps); } catch (Exception ex) { // There can be errors while you re-install mods with changed metadata ps.RaiseError(ex.Message + ex.StackTrace); @@ -477,7 +480,7 @@ private string newModPrompt(int howMany) private bool ScanForMods() { try { - manager.CurrentInstance.Scan(); + regMgr.ScanUnmanagedFiles(); } catch (InconsistentKraken ex) { // Warn about inconsistent state RaiseError(Properties.Resources.ModListScanBad, ex.InconsistenciesPretty); @@ -489,8 +492,8 @@ private bool InstanceSettings(ConsoleTheme theme) { var prevRepos = new SortedDictionary(registry.Repositories); var prevVerCrit = manager.CurrentInstance.VersionCriteria(); - LaunchSubScreen(theme, new GameInstanceEditScreen(manager, manager.CurrentInstance)); - if (!SortedDictionaryEquals(registry.Repositories, prevRepos)) { + LaunchSubScreen(theme, new GameInstanceEditScreen(manager, repoData, manager.CurrentInstance)); + if (!registry.Repositories.DictionaryEquals(prevRepos)) { // Repos changed, need to fetch them UpdateRegistry(theme, false); RefreshList(theme); @@ -506,13 +509,14 @@ private bool SelectInstall(ConsoleTheme theme) GameInstance prevInst = manager.CurrentInstance; var prevRepos = new SortedDictionary(registry.Repositories); var prevVerCrit = prevInst.VersionCriteria(); - LaunchSubScreen(theme, new GameInstanceListScreen(manager)); + LaunchSubScreen(theme, new GameInstanceListScreen(manager, repoData)); if (!prevInst.Equals(manager.CurrentInstance)) { // Game instance changed, reset everything plan.Reset(); - registry = RegistryManager.Instance(manager.CurrentInstance).registry; + regMgr = RegistryManager.Instance(manager.CurrentInstance, repoData); + registry = regMgr.registry; RefreshList(theme); - } else if (!SortedDictionaryEquals(registry.Repositories, prevRepos)) { + } else if (!registry.Repositories.DictionaryEquals(prevRepos)) { // Repos changed, need to fetch them UpdateRegistry(theme, false); RefreshList(theme); @@ -523,15 +527,6 @@ private bool SelectInstall(ConsoleTheme theme) return true; } - private bool SortedDictionaryEquals(SortedDictionary a, SortedDictionary b) - { - return a == null ? b == null - : b == null ? false - : a.Count == b.Count - && a.Keys.All(k => b.ContainsKey(k)) - && b.Keys.All(k => a.ContainsKey(k) && a[k].Equals(b[k])); - } - private bool EditAuthTokens(ConsoleTheme theme) { LaunchSubScreen(theme, new AuthTokenScreen()); @@ -583,8 +578,8 @@ private bool ExportInstalled(ConsoleTheme theme) { try { // Save the mod list as "depends" without the installed versions. - // Beacause that's supposed to work. - RegistryManager.Instance(manager.CurrentInstance).Save(true); + // Because that's supposed to work. + regMgr.Save(true); string path = Path.Combine( manager.CurrentInstance.CkanDir(), $"{Properties.Resources.ModListExportPrefix}-{manager.CurrentInstance.Name}.ckan" @@ -606,7 +601,7 @@ private bool Help(ConsoleTheme theme) private bool ApplyChanges(ConsoleTheme theme) { - LaunchSubScreen(theme, new InstallScreen(manager, plan, debug)); + LaunchSubScreen(theme, new InstallScreen(manager, repoData, plan, debug)); RefreshList(theme); return true; } @@ -661,9 +656,11 @@ private long totalInstalledDownloadSize() return total; } - private GameInstanceManager manager; - private Registry registry; - private bool debug; + private GameInstanceManager manager; + private RegistryManager regMgr; + private Registry registry; + private RepositoryDataManager repoData; + private bool debug; private ConsoleField searchBox; private ConsoleListBox moduleList; diff --git a/ConsoleUI/SplashScreen.cs b/ConsoleUI/SplashScreen.cs index 0a747d4011..ff7601c762 100644 --- a/ConsoleUI/SplashScreen.cs +++ b/ConsoleUI/SplashScreen.cs @@ -13,9 +13,11 @@ public class SplashScreen { /// Initialize the screen /// /// Game instance manager object for getting instances - public SplashScreen(GameInstanceManager mgr) + /// Repository data manager providing info from repos + public SplashScreen(GameInstanceManager mgr, RepositoryDataManager repoData) { manager = mgr; + this.repoData = repoData; } /// @@ -26,7 +28,7 @@ public bool Run(ConsoleTheme theme) { // If there's a default instance, try to get the lock for it. GameInstance ksp = manager.CurrentInstance ?? manager.GetPreferredInstance(); - if (ksp != null && !GameInstanceListScreen.TryGetInstance(theme, ksp, (ConsoleTheme th) => Draw(th, false))) { + if (ksp != null && !GameInstanceListScreen.TryGetInstance(theme, ksp, repoData, (ConsoleTheme th) => Draw(th, false))) { Console.ResetColor(); Console.Clear(); Console.CursorVisible = true; @@ -102,7 +104,8 @@ private void drawCentered(int y, string val) } catch { } } - private GameInstanceManager manager; + private GameInstanceManager manager; + private RepositoryDataManager repoData; } } diff --git a/Core/Configuration/JsonConfiguration.cs b/Core/Configuration/JsonConfiguration.cs index fae03c1a46..5478f20524 100644 --- a/Core/Configuration/JsonConfiguration.cs +++ b/Core/Configuration/JsonConfiguration.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json; -using CKAN.Games; +using CKAN.Games.KerbalSpaceProgram; namespace CKAN.Configuration { diff --git a/Core/Converters/JsonAlwaysEmptyObjectConverter.cs b/Core/Converters/JsonAlwaysEmptyObjectConverter.cs new file mode 100644 index 0000000000..6ff706acaa --- /dev/null +++ b/Core/Converters/JsonAlwaysEmptyObjectConverter.cs @@ -0,0 +1,26 @@ +using System; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace CKAN +{ + /// + /// A converter that ensures an object is always serialized and deserialized as empty, + /// for backwards compatibility with a client that assumes it will never be null + /// + public class JsonAlwaysEmptyObjectConverter : JsonConverter + { + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + => Activator.CreateInstance(objectType); + + public override bool CanWrite => true; + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + serializer.Serialize(writer, new JObject()); + } + + // Only convert when we're an explicit attribute + public override bool CanConvert(Type object_type) => false; + } +} diff --git a/Core/Extensions/DictionaryExtensions.cs b/Core/Extensions/DictionaryExtensions.cs index 62fcb8f18a..5d3dec670b 100644 --- a/Core/Extensions/DictionaryExtensions.cs +++ b/Core/Extensions/DictionaryExtensions.cs @@ -1,10 +1,18 @@ using System; +using System.Linq; using System.Collections.Generic; namespace CKAN.Extensions { public static class DictionaryExtensions { + public static bool DictionaryEquals(this IDictionary a, + IDictionary b) + => a == null ? b == null + : b == null ? false + : a.Count == b.Count + && a.Keys.All(k => b.ContainsKey(k)) + && b.Keys.All(k => a.ContainsKey(k) && a[k].Equals(b[k])); public static V GetOrDefault(this Dictionary dict, K key) { diff --git a/Core/Extensions/EnumerableExtensions.cs b/Core/Extensions/EnumerableExtensions.cs index a16f780c44..d315ba7a6d 100644 --- a/Core/Extensions/EnumerableExtensions.cs +++ b/Core/Extensions/EnumerableExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -25,6 +25,10 @@ public static HashSet ToHashSet(this IEnumerable source) } #endif + public static Dictionary ToDictionary(this IEnumerable> pairs) + => pairs.ToDictionary(kvp => kvp.Key, + kvp => kvp.Value); + public static IEnumerable Memoize(this IEnumerable source) { if (source == null) @@ -100,6 +104,57 @@ public static IEnumerable TraverseNodes(this T start, Func getNext) yield return t; } } + +#if NET45 + /// + /// Make pairs out of the elements of two sequences + /// + /// The first sequence + /// The second sequence + /// Sequence of pairs of one element from seq1 and one from seq2 + public static IEnumerable> Zip(this IEnumerable seq1, IEnumerable seq2) + => seq1.Zip(seq2, (item1, item2) => new Tuple(item1, item2)); +#endif + + /// + /// Enable a `foreach` over a sequence of tuples + /// + /// A tuple to deconstruct + /// Set to the first value from the tuple + /// Set to the second value from the tuple + public static void Deconstruct(this Tuple tuple, out T1 item1, out T2 item2) + { + item1 = tuple.Item1; + item2 = tuple.Item2; + } + + /// + /// Enable a `foreach` over a sequence of tuples + /// + /// A tuple to deconstruct + /// Set to the first value from the tuple + /// Set to the second value from the tuple + public static void Deconstruct(this Tuple tuple, + out T1 item1, + out T2 item2, + out T3 item3) + { + item1 = tuple.Item1; + item2 = tuple.Item2; + item3 = tuple.Item3; + } + + /// + /// Enable a `foreach` over a sequence of key value pairs + /// + /// A tuple to deconstruct + /// Set to the first value from the tuple + /// Set to the second value from the tuple + public static void Deconstruct(this KeyValuePair kvp, out T1 key, out T2 val) + { + key = kvp.Key; + val = kvp.Value; + } } /// diff --git a/Core/Extensions/IOExtensions.cs b/Core/Extensions/IOExtensions.cs index b9ce767910..ab597a8ba7 100644 --- a/Core/Extensions/IOExtensions.cs +++ b/Core/Extensions/IOExtensions.cs @@ -8,7 +8,6 @@ namespace CKAN.Extensions { public static class IOExtensions { - private static bool StringArrayStartsWith(string[] child, string[] parent) { if (parent.Length > child.Length) @@ -97,6 +96,15 @@ public static DriveInfo GetDrive(this DirectoryInfo dir) progress.Report(total); } + public static IEnumerable BytesFromStream(this Stream s) + { + int b; + while ((b = s.ReadByte()) != -1) + { + yield return Convert.ToByte(b); + } + } + private static readonly TimeSpan progressInterval = TimeSpan.FromMilliseconds(200); } } diff --git a/Core/FileIdentifier.cs b/Core/FileIdentifier.cs index 189b959278..3eb7ede37e 100644 --- a/Core/FileIdentifier.cs +++ b/Core/FileIdentifier.cs @@ -1,6 +1,8 @@ using System.IO; using System.Linq; +using CKAN.Extensions; + namespace CKAN { public static class FileIdentifier @@ -73,22 +75,27 @@ private static bool CheckGZip(Stream stream) /// Stream to the file. private static bool CheckTar(Stream stream) { + const int magicOffset = 257; + const int emptyLength = 10240; + if (stream.CanSeek) { // Rewind the stream to the origin of the file. - stream.Seek (0, SeekOrigin.Begin); + stream.Seek(0, SeekOrigin.Begin); } - // Define the buffer and magic types to compare against. - byte[] buffer = new byte[5]; - byte[] tar_identifier = { 0x75, 0x73, 0x74, 0x61, 0x72 }; - + bool allNulls = true; // Advance the stream position to offset 257. This method circumvents stream which can't seek. - for (int i = 0; i < 257; i++) + for (int i = 0; i < magicOffset; ++i) { - stream.ReadByte(); + var b = stream.ReadByte(); + allNulls = allNulls && b == 0; } + // Define the buffer and magic types to compare against. + byte[] tar_identifier = { 0x75, 0x73, 0x74, 0x61, 0x72 }; + byte[] buffer = new byte[tar_identifier.Length]; + // Read 5 bytes into the buffer. int bytes_read = stream.Read(buffer, 0, buffer.Length); @@ -104,6 +111,14 @@ private static bool CheckTar(Stream stream) return true; } + if (allNulls && buffer.SequenceEqual(new byte[] { 0, 0, 0, 0, 0 })) + { + // A tar with no files is 10240 nulls without the magic + var rest = stream.BytesFromStream().ToArray(); + return magicOffset + tar_identifier.Length + rest.Length == emptyLength + && rest.All(b => b == 0); + } + return false; } @@ -217,7 +232,7 @@ public static FileType IdentifyFile(string path) } // Identify the file using the stream method. - using (Stream stream = File.OpenRead (path)) + using (Stream stream = File.OpenRead(path)) { type = IdentifyFile(stream); } diff --git a/Core/GameInstance.cs b/Core/GameInstance.cs index 33a0d97504..122ece9054 100644 --- a/Core/GameInstance.cs +++ b/Core/GameInstance.cs @@ -40,7 +40,9 @@ public class GameInstance : IEquatable /// public string SanitizedName => string.Join("", Name.Split(Path.GetInvalidFileNameChars())); public GameVersion GameVersionWhenCompatibleVersionsWereStored { get; private set; } - public bool CompatibleVersionsAreFromDifferentGameVersion { get { return _compatibleVersions.Count > 0 && GameVersionWhenCompatibleVersionsWereStored != Version(); } } + public bool CompatibleVersionsAreFromDifferentGameVersion + => _compatibleVersions.Count > 0 + && GameVersionWhenCompatibleVersionsWereStored != Version(); private static readonly ILog log = LogManager.GetLogger(typeof(GameInstance)); @@ -53,7 +55,7 @@ public class GameInstance : IEquatable /// Will initialise a CKAN instance in the KSP dir if it does not already exist, /// if the directory contains a valid KSP install. /// - public GameInstance(IGame game, string gameDir, string name, IUser user, bool scan = true) + public GameInstance(IGame game, string gameDir, string name, IUser user) { this.game = game; Name = name; @@ -72,7 +74,7 @@ public GameInstance(IGame game, string gameDir, string name, IUser user, bool sc } if (Valid) { - SetupCkanDirectories(scan); + SetupCkanDirectories(); LoadCompatibleVersions(); } } @@ -95,7 +97,7 @@ public GameInstance(IGame game, string gameDir, string name, IUser user, bool sc /// /// Create the CKAN directory and any supporting files. /// - private void SetupCkanDirectories(bool scan = true) + private void SetupCkanDirectories() { log.InfoFormat("Initialising {0}", CkanDir()); @@ -107,12 +109,6 @@ private void SetupCkanDirectories(bool scan = true) User.RaiseMessage(Properties.Resources.GameInstanceSettingUp); User.RaiseMessage(Properties.Resources.GameInstanceCreatingDir, CkanDir()); txFileMgr.CreateDirectory(CkanDir()); - - if (scan) - { - User.RaiseMessage(Properties.Resources.GameInstanceScanning); - Scan(); - } } playTime = TimeLog.Load(TimeLog.GetPath(CkanDir())) ?? new TimeLog(); @@ -339,68 +335,6 @@ public GameVersionCriteria VersionCriteria() #endregion - #region CKAN/GameData Directory Maintenance - - /// - /// Clears the registry of DLL data, and refreshes it by scanning GameData. - /// This operates as a transaction. - /// This *saves* the registry upon completion. - /// TODO: This would likely be better in the Registry class itself. - /// - /// - /// True if found anything different, false if same as before - /// - public bool Scan() - { - var manager = RegistryManager.Instance(this); - using (TransactionScope tx = CkanTransaction.CreateTransactionScope()) - { - var oldDlls = manager.registry.InstalledDlls.ToHashSet(); - manager.registry.ClearDlls(); - foreach (var dir in Enumerable.Repeat(game.PrimaryModDirectoryRelative, 1) - .Concat(game.AlternateModDirectoriesRelative) - .Select(d => ToAbsoluteGameDir(d))) - { - log.DebugFormat("Scanning for DLLs in {0}", dir); - - if (Directory.Exists(dir)) - { - // EnumerateFiles is *case-sensitive* in its pattern, which causes - // DLL files to be missed under Linux; we have to pick .dll, .DLL, or scanning - // GameData *twice*. - // - // The least evil is to walk it once, and filter it ourselves. - var files = Directory - .EnumerateFiles(dir, "*", SearchOption.AllDirectories) - .Where(file => file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase)) - .Select(CKANPathUtils.NormalizePath) - .Where(absPath => !game.StockFolders.Any(f => - ToRelativeGameDir(absPath).StartsWith($"{f}/"))); - - foreach (string dll in files) - { - manager.registry.RegisterDll(this, dll); - } - } - } - var newDlls = manager.registry.InstalledDlls.ToHashSet(); - bool dllChanged = !oldDlls.SetEquals(newDlls); - bool dlcChanged = manager.ScanDlc(); - - if (dllChanged || dlcChanged) - { - manager.Save(false); - } - - log.Debug("Scan completed, committing transaction"); - tx.Complete(); - - return dllChanged || dlcChanged; - } - } - - #endregion - /// /// Returns path relative to this KSP's GameDir. /// diff --git a/Core/GameInstanceManager.cs b/Core/GameInstanceManager.cs index 67bf3a317a..ca4a1101ee 100644 --- a/Core/GameInstanceManager.cs +++ b/Core/GameInstanceManager.cs @@ -11,6 +11,8 @@ using CKAN.Versioning; using CKAN.Configuration; using CKAN.Games; +using CKAN.Games.KerbalSpaceProgram; +using CKAN.Games.KerbalSpaceProgram2; using CKAN.Extensions; namespace CKAN @@ -111,7 +113,8 @@ internal GameInstance _GetPreferredInstance() if (path != null) { - GameInstance portableInst = new GameInstance(game, path, Properties.Resources.GameInstanceManagerPortable, User); + GameInstance portableInst = new GameInstance( + game, path, Properties.Resources.GameInstanceManagerPortable, User); if (portableInst.Valid) { return portableInst; @@ -256,7 +259,8 @@ public void CloneInstance(GameInstance existingInstance, string newName, string /// The IDlcDetector implementations for the DLCs that should be faked and the requested dlc version as a dictionary. /// Thrown if the instance name is already in use. /// Thrown by AddInstance() if created instance is not valid, e.g. if a write operation didn't complete for whatever reason. - public void FakeInstance(IGame game, string newName, string newPath, GameVersion version, Dictionary dlcs = null) + public void FakeInstance(IGame game, string newName, string newPath, GameVersion version, + Dictionary dlcs = null) { TxFileManager fileMgr = new TxFileManager(); using (TransactionScope transaction = CkanTransaction.CreateTransactionScope()) @@ -266,7 +270,6 @@ public void FakeInstance(IGame game, string newName, string newPath, GameVersion throw new InstanceNameTakenKraken(newName); } - if (!version.InBuildMap(game)) { throw new BadGameVersionKraken(string.Format( @@ -312,8 +315,7 @@ public void FakeInstance(IGame game, string newName, string newPath, GameVersion string.Format(Properties.Resources.GameInstanceFakeDLCNotAllowed, game.ShortName, dlcDetector.ReleaseGameVersion, - dlcDetector.IdentifierBaseName - )); + dlcDetector.IdentifierBaseName)); string dlcDir = Path.Combine(newPath, dlcDetector.InstallPath()); fileMgr.CreateDirectory(dlcDir); @@ -324,7 +326,7 @@ public void FakeInstance(IGame game, string newName, string newPath, GameVersion } // Add the new instance to the config - GameInstance new_instance = new GameInstance(game, newPath, newName, User, false); + GameInstance new_instance = new GameInstance(game, newPath, newName, User); AddInstance(new_instance); transaction.Complete(); } @@ -417,8 +419,9 @@ public void SetCurrentInstance(string name) // Don't try to Dispose a null CurrentInstance. if (CurrentInstance != null && !CurrentInstance.Equals(instances[name])) { - // Dispose of the old registry manager, to release the registry. - RegistryManager.Instance(CurrentInstance)?.Dispose(); + // Dispose of the old registry manager to release the registry + // (without accidentally locking/loading/etc it). + RegistryManager.DisposeInstance(CurrentInstance); } CurrentInstance = instances[name]; } @@ -490,9 +493,7 @@ public void SetAutoStart(string name) } public bool HasInstance(string name) - { - return instances.ContainsKey(name); - } + => instances.ContainsKey(name); public void ClearAutoStart() { diff --git a/Core/Games/IGame.cs b/Core/Games/IGame.cs index 47c21a41bf..b5fe8a72fd 100644 --- a/Core/Games/IGame.cs +++ b/Core/Games/IGame.cs @@ -1,6 +1,10 @@ using System; using System.IO; using System.Collections.Generic; + +using Newtonsoft.Json.Linq; + +using CKAN.DLC; using CKAN.Versioning; namespace CKAN.Games @@ -17,22 +21,25 @@ public interface IGame string MacPath(); // What do we contain? - string PrimaryModDirectoryRelative { get; } - string[] AlternateModDirectoriesRelative { get; } - string PrimaryModDirectory(GameInstance inst); - string[] StockFolders { get; } - string[] ReservedPaths { get; } - string[] CreateableDirs { get; } - string[] AutoRemovableDirs { get; } - bool IsReservedDirectory(GameInstance inst, string path); - bool AllowInstallationIn(string name, out string path); - void RebuildSubdirectories(string absGameRoot); - string DefaultCommandLine { get; } - string[] AdjustCommandLine(string[] args, GameVersion installedVersion); + string PrimaryModDirectoryRelative { get; } + string[] AlternateModDirectoriesRelative { get; } + string PrimaryModDirectory(GameInstance inst); + string[] StockFolders { get; } + string[] ReservedPaths { get; } + string[] CreateableDirs { get; } + string[] AutoRemovableDirs { get; } + bool IsReservedDirectory(GameInstance inst, string path); + bool AllowInstallationIn(string name, out string path); + void RebuildSubdirectories(string absGameRoot); + string DefaultCommandLine { get; } + string[] AdjustCommandLine(string[] args, GameVersion installedVersion); + IDlcDetector[] DlcDetectors { get; } // Which versions exist and which is present? void RefreshVersions(); List KnownVersions { get; } + GameVersion[] EmbeddedGameVersions { get; } + GameVersion[] ParseBuildsJson(JToken json); GameVersion DetectVersion(DirectoryInfo where); string CompatibleVersionsFile { get; } string[] BuildIDFiles { get; } diff --git a/Core/Games/KerbalSpaceProgram.cs b/Core/Games/KerbalSpaceProgram.cs index 292fea4132..51c3ffcfad 100644 --- a/Core/Games/KerbalSpaceProgram.cs +++ b/Core/Games/KerbalSpaceProgram.cs @@ -2,12 +2,19 @@ using System.Linq; using System.IO; using System.Collections.Generic; +using System.Reflection; + using Autofac; using log4net; -using CKAN.GameVersionProviders; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CKAN.DLC; +using CKAN.Games.KerbalSpaceProgram.GameVersionProviders; +using CKAN.Games.KerbalSpaceProgram.DLC; using CKAN.Versioning; -namespace CKAN.Games +namespace CKAN.Games.KerbalSpaceProgram { public class KerbalSpaceProgram : IGame { @@ -184,31 +191,65 @@ public string[] AdjustCommandLine(string[] args, GameVersion installedVersion) return args; } + public IDlcDetector[] DlcDetectors => new IDlcDetector[] + { + new BreakingGroundDlcDetector(), + new MakingHistoryDlcDetector(), + }; + public void RefreshVersions() { ServiceLocator.Container.Resolve().Refresh(); + versions = null; } - public List KnownVersions => - ServiceLocator.Container.Resolve().KnownVersions; + private List versions; - public GameVersion DetectVersion(DirectoryInfo where) + public List KnownVersions { - var buildIdVersionProvider = ServiceLocator.Container - .ResolveKeyed(GameVersionSource.BuildId); - GameVersion version; - if (buildIdVersionProvider.TryGetVersion(where.FullName, out version)) + get { - return version; - } - else - { - var readmeVersionProvider = ServiceLocator.Container - .ResolveKeyed(GameVersionSource.Readme); - return readmeVersionProvider.TryGetVersion(where.FullName, out version) ? version : null; + // There's a lot of duplicate real versions with different build IDs, + // skip all those extra checks when we use these + if (versions == null) + { + versions = ServiceLocator.Container + .Resolve() + .KnownVersions + .Select(v => v.WithoutBuild) + .Distinct() + .ToList(); + } + return versions; } } + public GameVersion[] EmbeddedGameVersions + => JsonConvert.DeserializeObject( + new StreamReader(Assembly.GetExecutingAssembly() + .GetManifestResourceStream("CKAN.builds-ksp.json")) + .ReadToEnd()) + .Builds + .Select(b => GameVersion.Parse(b.Value)) + .ToArray(); + + public GameVersion[] ParseBuildsJson(JToken json) + => json.ToObject() + .Builds + .Select(b => GameVersion.Parse(b.Value)) + .ToArray(); + + public GameVersion DetectVersion(DirectoryInfo where) + => ServiceLocator.Container + .ResolveKeyed(GameVersionSource.BuildId) + .TryGetVersion(where.FullName, out GameVersion verFromId) + ? verFromId + : ServiceLocator.Container + .ResolveKeyed(GameVersionSource.Readme) + .TryGetVersion(where.FullName, out GameVersion verFromReadme) + ? verFromReadme + : null; + public string CompatibleVersionsFile => "compatible_ksp_versions.json"; public string[] BuildIDFiles => new string[] diff --git a/Core/Games/KerbalSpaceProgram/DLC/BreakingGroundDlcDetector.cs b/Core/Games/KerbalSpaceProgram/DLC/BreakingGroundDlcDetector.cs index f8ecd6d6b0..eff74221aa 100644 --- a/Core/Games/KerbalSpaceProgram/DLC/BreakingGroundDlcDetector.cs +++ b/Core/Games/KerbalSpaceProgram/DLC/BreakingGroundDlcDetector.cs @@ -1,7 +1,8 @@ -using System.Collections.Generic; -using CKAN.Games; +using System.Collections.Generic; -namespace CKAN.DLC +using CKAN.Games.KerbalSpaceProgram; + +namespace CKAN.Games.KerbalSpaceProgram.DLC { /// /// Represents an object that can detect the presence of the official Making History DLC in a KSP installation. @@ -9,11 +10,10 @@ namespace CKAN.DLC public sealed class BreakingGroundDlcDetector : StandardDlcDetectorBase { public BreakingGroundDlcDetector() - : base( - new KerbalSpaceProgram(), - "BreakingGround", - "Serenity", - new Versioning.GameVersion(1, 7, 1)) + : base(new KerbalSpaceProgram(), + "BreakingGround", + "Serenity", + new Versioning.GameVersion(1, 7, 1)) { } } } diff --git a/Core/Games/KerbalSpaceProgram/DLC/MakingHistoryDlcDetector.cs b/Core/Games/KerbalSpaceProgram/DLC/MakingHistoryDlcDetector.cs index 40f54e05fd..84965b80da 100644 --- a/Core/Games/KerbalSpaceProgram/DLC/MakingHistoryDlcDetector.cs +++ b/Core/Games/KerbalSpaceProgram/DLC/MakingHistoryDlcDetector.cs @@ -1,7 +1,8 @@ using System.Collections.Generic; -using CKAN.Games; -namespace CKAN.DLC +using CKAN.Games.KerbalSpaceProgram; + +namespace CKAN.Games.KerbalSpaceProgram.DLC { /// /// Represents an object that can detect the presence of the official Making History DLC in a KSP installation. @@ -9,14 +10,13 @@ namespace CKAN.DLC public sealed class MakingHistoryDlcDetector : StandardDlcDetectorBase { public MakingHistoryDlcDetector() - : base( - new KerbalSpaceProgram(), - "MakingHistory", - new Versioning.GameVersion(1, 4, 1), - new Dictionary() - { - { "1.0", "1.0.0" } - }) + : base(new KerbalSpaceProgram(), + "MakingHistory", + new Versioning.GameVersion(1, 4, 1), + new Dictionary() + { + { "1.0", "1.0.0" } + }) { } } } diff --git a/Core/Games/KerbalSpaceProgram/DLC/StandardDlcDetectorBase.cs b/Core/Games/KerbalSpaceProgram/DLC/StandardDlcDetectorBase.cs index 1478e17014..0e8eb946b9 100644 --- a/Core/Games/KerbalSpaceProgram/DLC/StandardDlcDetectorBase.cs +++ b/Core/Games/KerbalSpaceProgram/DLC/StandardDlcDetectorBase.cs @@ -1,11 +1,13 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; + +using CKAN.DLC; using CKAN.Versioning; using CKAN.Games; -namespace CKAN.DLC +namespace CKAN.Games.KerbalSpaceProgram.DLC { /// /// Base class for DLC Detectors that follow standard conventions. diff --git a/Core/Games/KerbalSpaceProgram/GameVersionProviders/IGameVersionProvider.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/IGameVersionProvider.cs index 1025c03c33..1e9f4b675f 100644 --- a/Core/Games/KerbalSpaceProgram/GameVersionProviders/IGameVersionProvider.cs +++ b/Core/Games/KerbalSpaceProgram/GameVersionProviders/IGameVersionProvider.cs @@ -1,6 +1,6 @@ -using CKAN.Versioning; +using CKAN.Versioning; -namespace CKAN.GameVersionProviders +namespace CKAN.Games.KerbalSpaceProgram.GameVersionProviders { public interface IGameVersionProvider { diff --git a/Core/Games/KerbalSpaceProgram/GameVersionProviders/IKspBuildMap.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/IKspBuildMap.cs index 2f2927ab80..3391e5691d 100644 --- a/Core/Games/KerbalSpaceProgram/GameVersionProviders/IKspBuildMap.cs +++ b/Core/Games/KerbalSpaceProgram/GameVersionProviders/IKspBuildMap.cs @@ -1,7 +1,7 @@ -using CKAN.Versioning; +using CKAN.Versioning; using System.Collections.Generic; -namespace CKAN.GameVersionProviders +namespace CKAN.Games.KerbalSpaceProgram.GameVersionProviders { public interface IKspBuildMap { diff --git a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildIdVersionProvider.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildIdVersionProvider.cs index 1b8941e8e5..c3bed58b8e 100644 --- a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildIdVersionProvider.cs +++ b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildIdVersionProvider.cs @@ -1,76 +1,58 @@ -using System.IO; +using System.IO; using System.Linq; using System.Text.RegularExpressions; -using CKAN.Versioning; + using log4net; -namespace CKAN.GameVersionProviders +using CKAN.Versioning; + +namespace CKAN.Games.KerbalSpaceProgram.GameVersionProviders { // ReSharper disable once ClassNeverInstantiated.Global public sealed class KspBuildIdVersionProvider : IGameVersionProvider { - private static readonly Regex BuildIdPattern = new Regex(@"^build id\s+=\s+0*(?\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled - ); - - private static readonly ILog Log = LogManager.GetLogger(typeof(KspBuildIdVersionProvider)); - - private readonly IKspBuildMap _kspBuildMap; - public KspBuildIdVersionProvider(IKspBuildMap kspBuildMap) { _kspBuildMap = kspBuildMap; } - public bool TryGetVersion(string directory, out GameVersion result) + private static readonly string[] buildIDfilenames = { - GameVersion buildIdVersion; - var hasBuildId = TryGetVersionFromFile(Path.Combine(directory, "buildID.txt"), out buildIdVersion); + "buildID.txt", "buildID64.txt" + }; - GameVersion buildId64Version; - var hasBuildId64 = TryGetVersionFromFile(Path.Combine(directory, "buildID64.txt"), out buildId64Version); - - if (hasBuildId && hasBuildId64) - { - result = GameVersion.Max(buildIdVersion, buildId64Version); - - if (buildIdVersion != buildId64Version) - { - Log.WarnFormat( - "Found different KSP versions in buildID.txt ({0}) and buildID64.txt ({1}), assuming {2}.", - buildIdVersion, - buildId64Version, - result - ); - } + public bool TryGetVersion(string directory, out GameVersion result) + { + var foundVersions = buildIDfilenames + .Select(filename => TryGetVersionFromFile(Path.Combine(directory, filename), + out GameVersion v) + ? v : null) + .Where(v => v != null) + .Distinct() + .ToList(); - return true; - } - else if (hasBuildId64) - { - result = buildId64Version; - return true; - } - else if (hasBuildId) - { - result = buildIdVersion; - return true; - } - else + if (foundVersions.Count < 1) { result = default(GameVersion); return false; } + if (foundVersions.Count > 1) + { + Log.WarnFormat("Found different KSP versions in {0}: {1}", + string.Join(" and ", buildIDfilenames), + string.Join(", ", foundVersions)); + } + result = foundVersions.Max(); + return true; } private bool TryGetVersionFromFile(string file, out GameVersion result) { if (File.Exists(file)) { - var match = File - .ReadAllLines(file) - .Select(i => BuildIdPattern.Match(i)) - .FirstOrDefault(i => i.Success); + var match = File.ReadAllLines(file) + .Select(i => BuildIdPattern.Match(i)) + .FirstOrDefault(i => i.Success); if (match != null) { @@ -87,5 +69,13 @@ private bool TryGetVersionFromFile(string file, out GameVersion result) result = default(GameVersion); return false; } + + private readonly IKspBuildMap _kspBuildMap; + + private static readonly Regex BuildIdPattern = + new Regex(@"^build id\s+=\s+0*(?\d+)", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly ILog Log = LogManager.GetLogger(typeof(KspBuildIdVersionProvider)); } } diff --git a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs index 37f634c6db..45aab06f7e 100644 --- a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs +++ b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Collections.Generic; using System.IO; @@ -10,7 +10,7 @@ using CKAN.Versioning; using CKAN.Configuration; -namespace CKAN.GameVersionProviders +namespace CKAN.Games.KerbalSpaceProgram.GameVersionProviders { // // THIS IS NOT THE BUILD MAP! If you are trying to access the build map, diff --git a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspReadmeVersionProvider.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspReadmeVersionProvider.cs index 40371272c2..e484a60d1b 100644 --- a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspReadmeVersionProvider.cs +++ b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspReadmeVersionProvider.cs @@ -1,9 +1,10 @@ -using System.IO; +using System.IO; using System.Linq; using System.Text.RegularExpressions; + using CKAN.Versioning; -namespace CKAN.GameVersionProviders +namespace CKAN.Games.KerbalSpaceProgram.GameVersionProviders { // ReSharper disable once ClassNeverInstantiated.Global public sealed class KspReadmeVersionProvider : IGameVersionProvider diff --git a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspVersionSource.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspVersionSource.cs index 503d894e28..02ef525a0a 100644 --- a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspVersionSource.cs +++ b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspVersionSource.cs @@ -1,4 +1,4 @@ -namespace CKAN.GameVersionProviders +namespace CKAN.Games.KerbalSpaceProgram.GameVersionProviders { public enum GameVersionSource { diff --git a/Core/Games/KerbalSpaceProgram2.cs b/Core/Games/KerbalSpaceProgram2.cs index 437cbeea51..1635860f7f 100644 --- a/Core/Games/KerbalSpaceProgram2.cs +++ b/Core/Games/KerbalSpaceProgram2.cs @@ -8,10 +8,12 @@ using Autofac; using log4net; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using CKAN.DLC; using CKAN.Versioning; -namespace CKAN.Games +namespace CKAN.Games.KerbalSpaceProgram2 { public class KerbalSpaceProgram2 : IGame { @@ -152,6 +154,8 @@ public void RebuildSubdirectories(string absGameRoot) public string[] AdjustCommandLine(string[] args, GameVersion installedVersion) => args; + public IDlcDetector[] DlcDetectors => new IDlcDetector[] { }; + private static readonly Uri BuildMapUri = new Uri("https://raw.githubusercontent.com/KSP-CKAN/KSP2-CKAN-meta/main/builds.json"); private static readonly string cachedBuildMapPath = @@ -183,6 +187,15 @@ public void RefreshVersions() public List KnownVersions => versions; + public GameVersion[] EmbeddedGameVersions + => JsonConvert.DeserializeObject( + new StreamReader(Assembly.GetExecutingAssembly() + .GetManifestResourceStream("CKAN.builds-ksp2.json")) + .ReadToEnd()); + + public GameVersion[] ParseBuildsJson(JToken json) + => json.ToObject(); + public GameVersion DetectVersion(DirectoryInfo where) => VersionFromFile(Path.Combine(where.FullName, "KSP2_x64.exe")); diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 274ba32abd..05c148732b 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -214,18 +214,6 @@ public void InstallList(ICollection modules, RelationshipResolverOpt EnforceCacheSizeLimit(registry_manager.registry); - if (!options.without_enforce_consistency) - { - // We can scan GameData as a separate transaction. Installing the mods - // leaves everything consistent, and this is just gravy. (And ScanGameData - // acts as a Tx, anyway, so we don't need to provide our own.) - User.RaiseProgress( - string.Format(Properties.Resources.ModuleInstallerRescanning, ksp.game.PrimaryModDirectoryRelative), - 90); - log.Debug("Scanning after install"); - ksp.Scan(); - } - User.RaiseProgress(Properties.Resources.ModuleInstallerDone, 100); } @@ -662,8 +650,7 @@ internal static string CopyZipEntry(ZipFile zipfile, ZipEntry entry, string full /// public void UninstallList( IEnumerable mods, ref HashSet possibleConfigOnlyDirs, - RegistryManager registry_manager, bool ConfirmPrompt = true, List installing = null - ) + RegistryManager registry_manager, bool ConfirmPrompt = true, List installing = null) { mods = mods.Memoize(); // Pre-check, have they even asked for things which are installed? @@ -1572,7 +1559,7 @@ public void ImportFiles(HashSet files, IUser user, Action List matches = index[sha1]; foreach (CkanModule mod in matches) { - if (mod.IsCompatibleKSP(ksp.VersionCriteria())) + if (mod.IsCompatible(ksp.VersionCriteria())) { installable.Add(mod); } diff --git a/Core/Net/Net.cs b/Core/Net/Net.cs index 7bdd498722..d12edf7cb6 100644 --- a/Core/Net/Net.cs +++ b/Core/Net/Net.cs @@ -16,10 +16,10 @@ namespace CKAN { /// - /// Doing something with the network? Do it here. + /// Doing something with the network? Do it here. /// - public class Net + public static class Net { // The user agent that we report to web sites public static string UserAgentString = "Mozilla/4.0 (compatible; CKAN)"; @@ -64,21 +64,13 @@ public static string CurrentETag(Uri url) /// console if we detect missing certificates (common on a fresh Linux/mono install) /// public static string Download(Uri url, out string etag, string filename = null, IUser user = null) - { - return Download(url.OriginalString, out etag, filename, user); - } + => Download(url.OriginalString, out etag, filename, user); public static string Download(Uri url, string filename = null, IUser user = null) - { - string etag; - return Download(url, out etag, filename, user); - } + => Download(url, out string etag, filename, user); public static string Download(string url, string filename = null, IUser user = null) - { - string etag; - return Download(url, out etag, filename, user); - } + => Download(url, out string etag, filename, user); public static string Download(string url, out string etag, string filename = null, IUser user = null) { @@ -148,7 +140,7 @@ public class DownloadTarget { public List urls { get; private set; } public string filename { get; private set; } - public long size { get; private set; } + public long size { get; set; } public string mimeType { get; private set; } public DownloadTarget(List urls, string filename = null, long size = 0, string mimeType = "") @@ -165,9 +157,7 @@ public DownloadTarget(List urls, string filename = null, long size = 0, str } public static string DownloadWithProgress(string url, string filename = null, IUser user = null) - { - return DownloadWithProgress(new Uri(url), filename, user); - } + => DownloadWithProgress(new Uri(url), filename, user); public static string DownloadWithProgress(Uri url, string filename = null, IUser user = null) { diff --git a/Core/Net/NetAsyncDownloader.cs b/Core/Net/NetAsyncDownloader.cs index c643cc3191..f44ac5c055 100644 --- a/Core/Net/NetAsyncDownloader.cs +++ b/Core/Net/NetAsyncDownloader.cs @@ -117,7 +117,7 @@ private void ResetAgent() } } - private static readonly ILog log = LogManager.GetLogger(typeof (NetAsyncDownloader)); + private static readonly ILog log = LogManager.GetLogger(typeof(NetAsyncDownloader)); public readonly IUser User; @@ -177,6 +177,8 @@ public void DownloadAndWait(ICollection targets) log.Debug("Waiting for downloads to finish..."); complete_or_canceled.WaitOne(); + log.Debug("Downloads finished"); + var old_download_canceled = download_canceled; // Set up the inter-thread comms for next time. Can not be done at the start // of the method as the thread could pause on the opening line long enough for @@ -185,6 +187,8 @@ public void DownloadAndWait(ICollection targets) download_canceled = false; complete_or_canceled.Reset(); + log.Debug("Completion signal reset"); + // If the user cancelled our progress, then signal that. if (old_download_canceled) { @@ -247,6 +251,7 @@ public void DownloadAndWait(ICollection targets) } // Yay! Everything worked! + log.Debug("Done downloading"); } private static readonly Regex certificatePattern = new Regex( @@ -294,22 +299,25 @@ private void DownloadModule(NetAsyncDownloaderDownloadPart dl) { log.DebugFormat("Beginning download of {0}", string.Join(", ", dl.target.urls)); - if (!downloads.Contains(dl)) + lock (dlMutex) { - // We need a new variable for our closure/lambda, hence index = 1+prev max - int index = downloads.Count; + if (!downloads.Contains(dl)) + { + // We need a new variable for our closure/lambda, hence index = 1+prev max + int index = downloads.Count; - downloads.Add(dl); + downloads.Add(dl); - // Schedule for us to get back progress reports. - dl.Progress += (ProgressPercentage, BytesReceived, TotalBytesToReceive) => - FileProgressReport(index, ProgressPercentage, BytesReceived, TotalBytesToReceive); + // Schedule for us to get back progress reports. + dl.Progress += (ProgressPercentage, BytesReceived, TotalBytesToReceive) => + FileProgressReport(index, ProgressPercentage, BytesReceived, TotalBytesToReceive); - // And schedule a notification if we're done (or if something goes wrong) - dl.Done += (sender, args, etag) => - FileDownloadComplete(index, args.Error, args.Cancelled, etag); + // And schedule a notification if we're done (or if something goes wrong) + dl.Done += (sender, args, etag) => + FileDownloadComplete(index, args.Error, args.Cancelled, etag); + } + queuedDownloads.Remove(dl); } - queuedDownloads.Remove(dl); // Encode spaces to avoid confusing URL parsers User.RaiseMessage(Properties.Resources.NetAsyncDownloaderDownloading, @@ -451,6 +459,8 @@ private void FileDownloadComplete(int index, Exception error, bool canceled, str { log.InfoFormat("Finished downloading {0}", string.Join(", ", dl.target.urls)); dl.bytesLeft = 0; + // Let calling code find out how big this file is + dl.target.size = new FileInfo(dl.target.filename).Length; } PopFromQueue(doneUri.Host); diff --git a/Core/Net/NetFileCache.cs b/Core/Net/NetFileCache.cs index 4190552df7..41f8399485 100644 --- a/Core/Net/NetFileCache.cs +++ b/Core/Net/NetFileCache.cs @@ -356,7 +356,7 @@ public void EnforceSizeLimit(long bytes, Registry registry) // Prune the module lists to only those that are compatible foreach (var kvp in hashMap) { - kvp.Value.RemoveAll(mod => !mod.IsCompatibleKSP(aggregateCriteria)); + kvp.Value.RemoveAll(mod => !mod.IsCompatible(aggregateCriteria)); } // Now get all the files in all the caches, including in progress... diff --git a/Core/Net/Repo.cs b/Core/Net/Repo.cs deleted file mode 100644 index cc69229235..0000000000 --- a/Core/Net/Repo.cs +++ /dev/null @@ -1,335 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; - -using Autofac; -using ChinhDo.Transactions.FileManager; -using ICSharpCode.SharpZipLib.GZip; -using ICSharpCode.SharpZipLib.Tar; -using ICSharpCode.SharpZipLib.Zip; -using log4net; -using Newtonsoft.Json; - -using CKAN.GameVersionProviders; -using CKAN.Versioning; -using CKAN.Extensions; - -namespace CKAN -{ - public enum RepoUpdateResult - { - Failed, - Updated, - NoChanges - } - - /// - /// Class for downloading the CKAN meta-info - /// - public static class Repo - { - /// - /// Download and update the local CKAN meta-info. - /// Optionally takes a URL to the zipfile repo to download. - /// - public static RepoUpdateResult UpdateAllRepositories(RegistryManager registry_manager, GameInstance ksp, NetAsyncDownloader downloader, NetModuleCache cache, IUser user) - { - var repos = registry_manager.registry.Repositories.Values - .DistinctBy(r => r.uri) - // Higher priority repo overwrites lower priority (SortedDictionary just sorts by name) - .OrderByDescending(r => r.priority) - .ToArray(); - - // Get latest copy of the game versions data (remote build map) - user.RaiseMessage(Properties.Resources.NetRepoUpdatingBuildMap); - ksp.game.RefreshVersions(); - - // Check if the ETags have changed, quit if not - user.RaiseProgress(Properties.Resources.NetRepoCheckingForUpdates, 0); - if (repos.All(repo => !string.IsNullOrEmpty(repo.last_server_etag) - && repo.last_server_etag == Net.CurrentETag(repo.uri))) - { - user.RaiseProgress(Properties.Resources.NetRepoAlreadyUpToDate, 100); - user.RaiseMessage(Properties.Resources.NetRepoNoChanges); - return RepoUpdateResult.NoChanges; - } - - // Capture repo etags to be set once we're done - var savedEtags = new Dictionary(); - downloader.onOneCompleted += (url, filename, error, etag) => savedEtags.Add(url, etag); - - // Download metadata from all repos - var targets = repos.Select(r => new Net.DownloadTarget(new List() { r.uri })).ToArray(); - downloader.DownloadAndWait(targets); - - // If we get to this point, the downloads were successful - // Load them - var files = targets.Select(t => t.filename).ToArray(); - var dlCounts = new SortedDictionary(); - var modules = repos - .ZipMany(files, (r, f) => ModulesFromFile(r, f, ref dlCounts, user)) - .ToArray(); - - // Loading done, commit etags - registry_manager.registry.SetETags(savedEtags); - - // Clean up temp files - foreach (var f in files) - { - File.Delete(f); - } - - if (modules.Length > 0) - { - // Save our changes - registry_manager.registry.SetAllAvailable(modules); - registry_manager.registry.SetDownloadCounts(dlCounts); - registry_manager.Save(enforce_consistency: false); - ShowUserInconsistencies(registry_manager.registry, user); - } - - // Report success - return RepoUpdateResult.Updated; - } - - private static IEnumerable ModulesFromFile(Repository repo, string filename, ref SortedDictionary downloadCounts, IUser user) - { - if (!File.Exists(filename)) - { - throw new FileNotFoundKraken(filename); - } - switch (FileIdentifier.IdentifyFile(filename)) - { - case FileType.TarGz: - return ModulesFromTarGz(repo, filename, ref downloadCounts, user); - case FileType.Zip: - return ModulesFromZip(repo, filename, user); - default: - throw new UnsupportedKraken($"Not a .tar.gz or .zip, cannot process: {filename}"); - } - } - - /// - /// Returns available modules from the supplied tar.gz file. - /// - private static List ModulesFromTarGz(Repository repo, string path, ref SortedDictionary downloadCounts, IUser user) - { - log.DebugFormat("Starting registry update from tar.gz file: \"{0}\".", path); - - List modules = new List(); - - // Open the gzip'ed file - using (Stream inputStream = File.OpenRead(path)) - // Create a gzip stream - using (GZipInputStream gzipStream = new GZipInputStream(inputStream)) - // Create a handle for the tar stream - using (TarInputStream tarStream = new TarInputStream(gzipStream, Encoding.UTF8)) - { - user.RaiseMessage(Properties.Resources.NetRepoLoadingModulesFromRepo, repo.name); - TarEntry entry; - int prevPercent = 0; - while ((entry = tarStream.GetNextEntry()) != null) - { - string filename = entry.Name; - - if (filename.EndsWith("download_counts.json")) - { - downloadCounts = JsonConvert.DeserializeObject>( - tarStreamString(tarStream, entry)); - user.RaiseMessage(Properties.Resources.NetRepoLoadedDownloadCounts, repo.name); - } - else if (filename.EndsWith(".ckan")) - { - log.DebugFormat("Reading CKAN data from {0}", filename); - var percent = (int)(100 * inputStream.Position / inputStream.Length); - if (percent > prevPercent) - { - user.RaiseProgress( - string.Format(Properties.Resources.NetRepoLoadingModulesFromRepo, repo.name), - percent); - prevPercent = percent; - } - - // Read each file into a buffer - string metadata_json = tarStreamString(tarStream, entry); - - CkanModule module = ProcessRegistryMetadataFromJSON(metadata_json, filename); - if (module != null) - { - modules.Add(module); - } - } - else - { - // Skip things we don't want - log.DebugFormat("Skipping archive entry {0}", filename); - } - } - } - return modules; - } - - private static string tarStreamString(TarInputStream stream, TarEntry entry) - { - // Read each file into a buffer. - int buffer_size; - - try - { - buffer_size = Convert.ToInt32(entry.Size); - } - catch (OverflowException) - { - log.ErrorFormat("Error processing {0}: Metadata size too large.", entry.Name); - return null; - } - - byte[] buffer = new byte[buffer_size]; - - stream.Read(buffer, 0, buffer_size); - - // Convert the buffer data to a string. - return Encoding.ASCII.GetString(buffer); - } - - /// - /// Returns available modules from the supplied zip file. - /// - private static List ModulesFromZip(Repository repo, string path, IUser user) - { - log.DebugFormat("Starting registry update from zip file: \"{0}\".", path); - - List modules = new List(); - using (var zipfile = new ZipFile(path)) - { - user.RaiseMessage(Properties.Resources.NetRepoLoadingModulesFromRepo, repo.name); - int index = 0; - int prevPercent = 0; - foreach (ZipEntry entry in zipfile) - { - string filename = entry.Name; - - if (filename.EndsWith(".ckan")) - { - log.DebugFormat("Reading CKAN data from {0}", filename); - var percent = (int)(100 * index / zipfile.Count); - if (percent > prevPercent) - { - user.RaiseProgress( - string.Format(Properties.Resources.NetRepoLoadingModulesFromRepo, repo.name), - percent); - } - - // Read each file into a string. - string metadata_json; - using (var stream = new StreamReader(zipfile.GetInputStream(entry))) - { - metadata_json = stream.ReadToEnd(); - stream.Close(); - } - - CkanModule module = ProcessRegistryMetadataFromJSON(metadata_json, filename); - if (module != null) - { - modules.Add(module); - } - } - else - { - // Skip things we don't want. - log.DebugFormat("Skipping archive entry {0}", filename); - } - ++index; - } - - zipfile.Close(); - } - return modules; - } - - private static CkanModule ProcessRegistryMetadataFromJSON(string metadata, string filename) - { - try - { - CkanModule module = CkanModule.FromJson(metadata); - // FromJson can return null for the empty string - if (module != null) - { - log.DebugFormat("Module parsed: {0}", module.ToString()); - } - return module; - } - catch (Exception exception) - { - // Alas, we can get exceptions which *wrap* our exceptions, - // because json.net seems to enjoy wrapping rather than propagating. - // See KSP-CKAN/CKAN-meta#182 as to why we need to walk the whole - // exception stack. - - bool handled = false; - - while (exception != null) - { - if (exception is UnsupportedKraken || exception is BadMetadataKraken) - { - // Either of these can be caused by data meant for future - // clients, so they're not really warnings, they're just - // informational. - - log.InfoFormat("Skipping {0} : {1}", filename, exception.Message); - - // I'd *love a way to "return" from the catch block. - handled = true; - break; - } - - // Look further down the stack. - exception = exception.InnerException; - } - - // If we haven't handled our exception, then it really was exceptional. - if (handled == false) - { - if (exception == null) - { - // Had exception, walked exception tree, reached leaf, got stuck. - log.ErrorFormat("Error processing {0} (exception tree leaf)", filename); - } - else - { - // In case whatever's calling us is lazy in error reporting, we'll - // report that we've got an issue here. - log.ErrorFormat("Error processing {0} : {1}", filename, exception.Message); - } - - throw; - } - return null; - } - } - - private static void ShowUserInconsistencies(Registry registry, IUser user) - { - // However, if there are any sanity errors let's show them to the user so at least they're aware - var sanityErrors = registry.GetSanityErrors(); - if (sanityErrors.Any()) - { - var sanityMessage = new StringBuilder(); - - sanityMessage.AppendLine(Properties.Resources.NetRepoInconsistenciesHeader); - foreach (var sanityError in sanityErrors) - { - sanityMessage.Append("- "); - sanityMessage.AppendLine(sanityError); - } - - user.RaiseMessage(sanityMessage.ToString()); - } - } - - private static readonly ILog log = LogManager.GetLogger(typeof(Repo)); - } -} diff --git a/Core/Net/ResumingWebClient.cs b/Core/Net/ResumingWebClient.cs index c18a66307f..cba0feb490 100644 --- a/Core/Net/ResumingWebClient.cs +++ b/Core/Net/ResumingWebClient.cs @@ -124,15 +124,12 @@ protected override void OnOpenReadCompleted(OpenReadCompletedEventArgs e) log.DebugFormat("OnOpenReadCompleted got open stream, appending to {0}", destination); using (var fileStream = new FileStream(destination, FileMode.Append, FileAccess.Write)) { - try + // file:// URLs don't support timeouts + if (netStream.CanTimeout) { log.DebugFormat("Default stream read timeout is {0}", netStream.ReadTimeout); netStream.ReadTimeout = timeoutMs; } - catch - { - // file:// URLs don't support timeouts - } cancelTokenSrc = new CancellationTokenSource(); netStream.CopyTo(fileStream, new Progress(bytesDownloaded => { diff --git a/Core/Properties/Resources.Designer.cs b/Core/Properties/Resources.Designer.cs index b1d7212b4c..5942655b44 100644 --- a/Core/Properties/Resources.Designer.cs +++ b/Core/Properties/Resources.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 @@ -170,6 +170,9 @@ internal static string RegistryFileConflict { internal static string RegistryFileNotRemoved { get { return (string)(ResourceManager.GetObject("RegistryFileNotRemoved", resourceCulture)); } } + internal static string RegistryDefaultDLCAbstract { + get { return (string)(ResourceManager.GetObject("RegistryDefaultDLCAbstract", resourceCulture)); } + } internal static string RegistryManagerDirectoryNotFound { get { return (string)(ResourceManager.GetObject("RegistryManagerDirectoryNotFound", resourceCulture)); } } diff --git a/Core/Properties/Resources.resx b/Core/Properties/Resources.resx index 210054617e..41d35bb0d6 100644 --- a/Core/Properties/Resources.resx +++ b/Core/Properties/Resources.resx @@ -154,6 +154,7 @@ Install the `mono-complete` package or equivalent for your operating system.`any_of` should not be combined with `{0}` {0} wishes to install {1}, but this file is registered to {2} {0} is registered to {1} but has not been removed! + An official expansion pack Can't find a directory in {0} installed A list of modules installed on the {0} KSP instance diff --git a/Core/Registry/CompatibilitySorter.cs b/Core/Registry/CompatibilitySorter.cs index efa15b0b5c..f785676042 100644 --- a/Core/Registry/CompatibilitySorter.cs +++ b/Core/Registry/CompatibilitySorter.cs @@ -20,14 +20,12 @@ public class CompatibilitySorter /// Dictionary mapping every identifier to the modules providing it /// Collection of found dlls /// Collection of installed DLCs - public CompatibilitySorter( - GameVersionCriteria crit, - Dictionary available, - Dictionary> providers, - Dictionary installed, - HashSet dlls, - IDictionary dlc - ) + public CompatibilitySorter(GameVersionCriteria crit, + IEnumerable> available, + Dictionary> providers, + Dictionary installed, + HashSet dlls, + IDictionary dlc) { CompatibleVersions = crit; this.installed = installed; @@ -47,12 +45,36 @@ IDictionary dlc public readonly SortedDictionary Compatible = new SortedDictionary(); + public ICollection LatestCompatible + { + get + { + if (latestCompatible == null) + { + latestCompatible = Compatible.Values.Select(avail => avail.Latest(CompatibleVersions)).ToList(); + } + return latestCompatible; + } + } + /// /// Mods that are incompatible with our versions /// public readonly SortedDictionary Incompatible = new SortedDictionary(); + public ICollection LatestIncompatible + { + get + { + if (latestIncompatible == null) + { + latestIncompatible = Incompatible.Values.Select(avail => avail.Latest(null)).ToList(); + } + return latestIncompatible; + } + } + /// /// Mods that might be compatible or incompatible based on their dependencies /// @@ -68,6 +90,9 @@ public readonly SortedDictionary Incompatible private readonly HashSet dlls; private readonly IDictionary dlc; + private List latestCompatible; + private List latestIncompatible; + /// /// Filter the provides mapping by compatibility /// @@ -76,23 +101,34 @@ public readonly SortedDictionary Incompatible /// /// Mapping from identifiers to compatible mods providing those identifiers /// - private Dictionary> CompatibleProviders(GameVersionCriteria crit, Dictionary> providers) + private Dictionary> CompatibleProviders( + GameVersionCriteria crit, + Dictionary> providers) { + log.Debug("Calculating compatible provider mapping"); var compat = new Dictionary>(); - foreach (var kvp in providers) + foreach (var (ident, availMods) in providers) { - // Find providing non-DLC modules that are compatible with crit - var compatAvail = kvp.Value.Where(avm => - avm.AllAvailable().Any(ckm => - !ckm.IsDLC && - ckm.ProvidesList.Contains(kvp.Key) && ckm.IsCompatibleKSP(crit)) - ).ToHashSet(); - // Add compatible providers to mapping, if any - if (compatAvail.Any()) + var compatGroups = availMods + .GroupBy(availMod => availMod.AllAvailable() + .Any(ckm => !ckm.IsDLC + && ckm.ProvidesList.Contains(ident) + && ckm.IsCompatible(crit))) + .ToDictionary(grp => grp.Key, + grp => grp); + if (!compatGroups.ContainsKey(false)) { - compat.Add(kvp.Key, compatAvail); + // Everything is compatible, just re-use the same HashSet + compat.Add(ident, availMods); } + else if (compatGroups.TryGetValue(true, out var compatGroup)) + { + // Some are compatible, put them in a new HashSet + compat.Add(ident, compatGroup.ToHashSet()); + } + // Else if nothing compatible, just skip this one } + log.Debug("Done calculating compatible provider mapping"); return compat; } @@ -102,30 +138,36 @@ private Dictionary> CompatibleProviders(GameVer /// /// All mods available from registry /// Mapping from identifiers to mods providing those identifiers - private void PartitionModules(Dictionary available, Dictionary> providers) + private void PartitionModules(IEnumerable> dicts, + Dictionary> providers) { - // First get the ones that are trivially [in]compatible. - foreach (var kvp in available) + log.Debug("Partitioning modules by compatibility"); + foreach (var available in dicts) { - if (kvp.Value.AllAvailable().All(m => !m.IsCompatibleKSP(CompatibleVersions))) + // First get the ones that are trivially [in]compatible. + foreach (var kvp in available) { - // No versions compatible == incompatible - log.DebugFormat("Trivially incompatible: {0}", kvp.Key); - Incompatible.Add(kvp.Key, kvp.Value); - } - else if (kvp.Value.AllAvailable().All(m => m.depends == null)) - { - // No dependencies == compatible - log.DebugFormat("Trivially compatible: {0}", kvp.Key); - Compatible.Add(kvp.Key, kvp.Value); - } - else - { - // Need to investigate this one more later - log.DebugFormat("Trivially indeterminate: {0}", kvp.Key); - Indeterminate.Add(kvp.Key, kvp.Value); + if (kvp.Value.AllAvailable().All(m => !m.IsCompatible(CompatibleVersions))) + { + // No versions compatible == incompatible + log.DebugFormat("Trivially incompatible: {0}", kvp.Key); + Incompatible.Add(kvp.Key, kvp.Value); + } + else if (kvp.Value.AllAvailable().All(m => m.depends == null)) + { + // No dependencies == compatible + log.DebugFormat("Trivially compatible: {0}", kvp.Key); + Compatible.Add(kvp.Key, kvp.Value); + } + else + { + // Need to investigate this one more later + log.DebugFormat("Trivially indeterminate: {0}", kvp.Key); + Indeterminate.Add(kvp.Key, kvp.Value); + } } } + log.Debug("Trivial mods done, moving on to indeterminates"); // We'll be modifying `indeterminate` during this loop, so `foreach` is out while (Indeterminate.Count > 0) { @@ -133,6 +175,7 @@ private void PartitionModules(Dictionary available, Dic log.DebugFormat("Checking: {0}", kvp.Key); CheckDepends(kvp.Key, kvp.Value, providers); } + log.Debug("Done partitioning modules by compatibility"); } /// @@ -145,7 +188,7 @@ private void PartitionModules(Dictionary available, Dic private void CheckDepends(string identifier, AvailableModule am, Dictionary> providers) { Investigating.Push(identifier); - foreach (CkanModule m in am.AllAvailable().Where(m => m.IsCompatibleKSP(CompatibleVersions))) + foreach (CkanModule m in am.AllAvailable().Where(m => m.IsCompatible(CompatibleVersions))) { log.DebugFormat("What about {0}?", m.version); bool installable = true; @@ -228,27 +271,11 @@ private void CheckDepends(string identifier, AvailableModule am, Dictionary private IEnumerable RelationshipIdentifiers(RelationshipDescriptor rel) - { - var modRel = rel as ModuleRelationshipDescriptor; - if (modRel != null) - { - yield return modRel.name; - } - else - { - var anyRel = rel as AnyOfRelationshipDescriptor; - if (anyRel != null) - { - foreach (RelationshipDescriptor subRel in anyRel.any_of) - { - foreach (string name in RelationshipIdentifiers(subRel)) - { - yield return name; - } - } - } - } - } + => rel is ModuleRelationshipDescriptor modRel + ? Enumerable.Repeat(modRel.name, 1) + : rel is AnyOfRelationshipDescriptor anyRel + ? anyRel.any_of.SelectMany(RelationshipIdentifiers) + : Enumerable.Empty(); private static readonly ILog log = LogManager.GetLogger(typeof(CompatibilitySorter)); } diff --git a/Core/Registry/IRegistryQuerier.cs b/Core/Registry/IRegistryQuerier.cs index 1731a7a6a8..14832d4cfa 100644 --- a/Core/Registry/IRegistryQuerier.cs +++ b/Core/Registry/IRegistryQuerier.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Collections.Generic; +using System.Collections.ObjectModel; using log4net; @@ -15,12 +16,11 @@ namespace CKAN /// public interface IRegistryQuerier { + ReadOnlyDictionary Repositories { get; } IEnumerable InstalledModules { get; } IEnumerable InstalledDlls { get; } IDictionary InstalledDlc { get; } - int? DownloadCount(string identifier); - /// /// Returns a simple array of the latest compatible module for each identifier for /// the specified version of KSP. @@ -48,7 +48,7 @@ public interface IRegistryQuerier /// Returns the max game version that is compatible with the given mod. /// /// Name of mod to check - GameVersion LatestCompatibleKSP(string identifier); + GameVersion LatestCompatibleGameVersion(List realVersions, string identifier); /// /// Returns all available versions of a module. @@ -144,9 +144,7 @@ public static class IRegistryQuerierHelpers /// Helper to call /// public static CkanModule GetModuleByVersion(this IRegistryQuerier querier, string ident, string version) - { - return querier.GetModuleByVersion(ident, new ModuleVersion(version)); - } + => querier.GetModuleByVersion(ident, new ModuleVersion(version)); /// /// Check if a mod is installed (either via CKAN, DLL, or virtually) @@ -155,18 +153,15 @@ public static CkanModule GetModuleByVersion(this IRegistryQuerier querier, strin /// /// true, if installedfalse otherwise. public static bool IsInstalled(this IRegistryQuerier querier, string identifier, bool with_provides = true) - { - return querier.InstalledVersion(identifier, with_provides) != null; - } + => querier.InstalledVersion(identifier, with_provides) != null; /// /// Check if a mod is autodetected. /// /// true, if autodetectedfalse otherwise. public static bool IsAutodetected(this IRegistryQuerier querier, string identifier) - { - return querier.IsInstalled(identifier) && querier.InstalledVersion(identifier) is UnmanagedModuleVersion; - } + => querier.IsInstalled(identifier) + && querier.InstalledVersion(identifier) is UnmanagedModuleVersion; /// /// Is the mod installed and does it have a newer version compatible with version @@ -240,7 +235,7 @@ public static string CompatibleGameVersions(this IRegistryQuerier querier, IGame if (releases != null && releases.Count > 0) { ModuleVersion minMod = null, maxMod = null; GameVersion minKsp = null, maxKsp = null; - Registry.GetMinMaxVersions(releases, out minMod, out maxMod, out minKsp, out maxKsp); + CkanModule.GetMinMaxVersions(releases, out minMod, out maxMod, out minKsp, out maxKsp); return GameVersionRange.VersionSpan(game, minKsp, maxKsp); } return ""; @@ -259,7 +254,7 @@ public static string CompatibleGameVersions(this IRegistryQuerier querier, IGame { ModuleVersion minMod = null, maxMod = null; GameVersion minKsp = null, maxKsp = null; - Registry.GetMinMaxVersions( + CkanModule.GetMinMaxVersions( new CkanModule[] { module }, out minMod, out maxMod, out minKsp, out maxKsp @@ -311,7 +306,7 @@ public static ModuleReplacement GetReplacement(this IRegistryQuerier querier, Ck replacement.ReplaceWith = querier.GetModuleByVersion(installedVersion.replaced_by.name, installedVersion.replaced_by.version); if (replacement.ReplaceWith != null) { - if (replacement.ReplaceWith.IsCompatibleKSP(version)) + if (replacement.ReplaceWith.IsCompatible(version)) { return replacement; } @@ -352,12 +347,15 @@ public static ModuleReplacement GetReplacement(this IRegistryQuerier querier, Ck /// Sequence of removable auto-installed modules, if any /// private static IEnumerable FindRemovableAutoInstalled( - this IRegistryQuerier querier, + this IRegistryQuerier querier, List installedModules, HashSet dlls, IDictionary dlc, GameVersionCriteria crit) { + log.DebugFormat("Finding removable autoInstalled for: {0}", + string.Join(", ", installedModules.Select(im => im.identifier))); + var autoInstMods = installedModules.Where(im => im.AutoInstalled).ToList(); var autoInstIds = autoInstMods.Select(im => im.Module.identifier).ToHashSet(); @@ -367,19 +365,18 @@ private static IEnumerable FindRemovableAutoInstalled( opts.without_enforce_consistency = true; opts.proceed_with_inconsistencies = true; var resolver = new RelationshipResolver( - installedModules - // DLC silently crashes the resolver - .Where(im => !im.Module.IsDLC) - .Select(im => im.Module), + // DLC silently crashes the resolver + installedModules.Where(im => !im.Module.IsDLC) + .Select(im => im.Module), null, opts, querier, crit); + var mods = resolver.ModList().ToHashSet(); return autoInstMods.Where( - im => autoInstIds.IsSupersetOf(Registry.FindReverseDependencies( - new List { im.identifier }, - new List(), - resolver.ModList().ToHashSet(), - dlls, dlc))); + im => autoInstIds.IsSupersetOf( + Registry.FindReverseDependencies(new List { im.identifier }, + new List(), + mods, dlls, dlc))); } /// @@ -397,13 +394,11 @@ public static IEnumerable FindRemovableAutoInstalled( this IRegistryQuerier querier, List installedModules, GameVersionCriteria crit) - { - log.DebugFormat("Finding removable autoInstalled for: {0}", string.Join(", ", installedModules.Select(im => im.identifier))); - return querier == null - ? Enumerable.Empty() - : querier.FindRemovableAutoInstalled( - installedModules, querier.InstalledDlls.ToHashSet(), querier.InstalledDlc, crit); - } + => querier?.FindRemovableAutoInstalled(installedModules, + querier.InstalledDlls.ToHashSet(), + querier.InstalledDlc, + crit) + ?? Enumerable.Empty(); private static readonly ILog log = LogManager.GetLogger(typeof(IRegistryQuerierHelpers)); } diff --git a/Core/Registry/InstalledModule.cs b/Core/Registry/InstalledModule.cs index feeb9e319b..a12e74baed 100644 --- a/Core/Registry/InstalledModule.cs +++ b/Core/Registry/InstalledModule.cs @@ -4,6 +4,7 @@ using System.IO; using System.Security.Cryptography; using System.Runtime.Serialization; + using Newtonsoft.Json; namespace CKAN @@ -18,13 +19,7 @@ public class InstalledModuleFile [JsonProperty("sha1_sum", NullValueHandling = NullValueHandling.Ignore)] private string sha1_sum; - public string Sha1 - { - get - { - return sha1_sum; - } - } + public string Sha1 => sha1_sum; public InstalledModuleFile(string path, GameInstance ksp) { @@ -76,9 +71,11 @@ public class InstalledModule { #region Fields and Properties - [JsonProperty] private DateTime install_time; + [JsonProperty] + private DateTime install_time; - [JsonProperty] private CkanModule source_module; + [JsonProperty] + private CkanModule source_module; [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] [DefaultValue(false)] @@ -120,15 +117,18 @@ public InstalledModule(GameInstance ksp, CkanModule module, IEnumerable : new Dictionary(); auto_installed = autoInstalled; - foreach (string file in relative_files) + if (ksp != null) { - if (Path.IsPathRooted(file)) + foreach (string file in relative_files) { - throw new PathErrorKraken(file, "InstalledModule *must* have relative paths"); - } + if (Path.IsPathRooted(file)) + { + throw new PathErrorKraken(file, "InstalledModule *must* have relative paths"); + } - // IMF needs a KSP object so it can compute the SHA1. - installed_files[file] = new InstalledModuleFile(file, ksp); + // IMF needs a KSP object so it can compute the SHA1. + installed_files[file] = new InstalledModuleFile(file, ksp); + } } } diff --git a/Core/Registry/Registry.cs b/Core/Registry/Registry.cs index 6906ef9309..f68cbad9ec 100644 --- a/Core/Registry/Registry.cs +++ b/Core/Registry/Registry.cs @@ -1,13 +1,15 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Runtime.Serialization; using System.Text.RegularExpressions; using System.Transactions; -using log4net; +using Autofac; using Newtonsoft.Json; +using log4net; using CKAN.Extensions; using CKAN.Versioning; @@ -16,7 +18,7 @@ namespace CKAN { /// - /// This is the CKAN registry. All the modules that we know about or have installed + /// This is the CKAN registry. All the modules that we have installed /// are contained in here. /// @@ -35,47 +37,67 @@ public class Registry : IEnlistmentNotification, IRegistryQuerier [JsonProperty("sorted_repositories")] private SortedDictionary repositories; - // TODO: These may be good as custom types, especially those which process - // paths (and flip from absolute to relative, and vice-versa). - [JsonProperty] internal Dictionary available_modules; // name => path [JsonProperty] private Dictionary installed_dlls; [JsonProperty] private Dictionary installed_modules; // filename (case insensitive on Windows) => module [JsonProperty] private Dictionary installed_files; - [JsonProperty] public readonly SortedDictionary download_counts = new SortedDictionary(); - - public int? DownloadCount(string identifier) - => download_counts.TryGetValue(identifier, out int count) ? (int?)count : null; + /// + /// Returns all the activated registries. + /// ReadOnly to ensure calling code can't make changes that + /// should invalidate the available mod caches. + /// + [JsonIgnore] + public ReadOnlyDictionary Repositories + => repositories == null + ? null + : new ReadOnlyDictionary(repositories); - public void SetDownloadCounts(SortedDictionary counts) + /// + /// Wrapper around assignment to this.repositories that invalidates + /// available mod caches + /// + /// The repositories dictionary to replace our current one + public void RepositoriesSet(SortedDictionary value) { - if (counts != null) - { - foreach (var kvp in counts) - { - download_counts[kvp.Key] = kvp.Value; - } - } + EnlistWithTransaction(); + InvalidateAvailableModCaches(); + repositories = value; + } + /// + /// Wrapper around this.repositories.Clear() that invalidates + /// available mod caches + /// + public void RepositoriesClear() + { + EnlistWithTransaction(); + InvalidateAvailableModCaches(); + repositories.Clear(); } - - // Index of which mods provide what, format: - // providers[provided] = { provider1, provider2, ... } - // Built by BuildProvidesIndex, makes LatestAvailableWithProvides much faster. - [JsonIgnore] - private Dictionary> providers - = new Dictionary>(); /// - /// Returns all the activated registries, sorted by name + /// Wrapper around this.repositories.Add() that invalidates + /// available mod caches /// - [JsonIgnore] public SortedDictionary Repositories + /// + public void RepositoriesAdd(Repository repo) { - get => this.repositories; + EnlistWithTransaction(); + InvalidateAvailableModCaches(); + repositories.Add(repo.name, repo); + } - // TODO writable only so it can be initialized, better ideas welcome - set { this.repositories = value; } + /// + /// Wrapper around this.repositories.Remove() that invalidates + /// available mod caches + /// + /// + public void RepositoriesRemove(string name) + { + EnlistWithTransaction(); + InvalidateAvailableModCaches(); + repositories.Remove(name); } /// @@ -113,10 +135,10 @@ [JsonIgnore] public IDictionary InstalledDlc /// Installed modules that are incompatible, if any /// public IEnumerable IncompatibleInstalled(GameVersionCriteria crit) - => installed_modules.Values - .Where(im => !im.Module.IsCompatibleKSP(crit) - && !(GetModuleByVersion(im.identifier, im.Module.version)?.IsCompatibleKSP(crit) - ?? false)); + => installed_modules.Values + .Where(im => !im.Module.IsCompatible(crit) + && !(GetModuleByVersion(im.identifier, im.Module.version)?.IsCompatible(crit) + ?? false)); #region Registry Upgrades @@ -146,7 +168,7 @@ private void DeSerialisationFixes(StreamingContext context) ? new Dictionary(StringComparer.OrdinalIgnoreCase) : new Dictionary(); - foreach (KeyValuePair tuple in installed_files) + foreach (KeyValuePair tuple in installed_files) { string path = CKANPathUtils.NormalizePath(tuple.Key); @@ -251,7 +273,6 @@ private void DeSerialisationFixes(StreamingContext context) } registry_version = LATEST_REGISTRY_VERSION; - BuildProvidesIndex(); } /// @@ -285,44 +306,66 @@ public void Repair() #endregion - #region Constructors + #region Constructors / destructor - public Registry( - Dictionary installed_modules, - Dictionary installed_dlls, - Dictionary available_modules, - Dictionary installed_files, - SortedDictionary repositories) + [JsonConstructor] + private Registry(RepositoryDataManager repoData) + { + if (repoData != null) + { + repoDataMgr = repoData; + repoDataMgr.Updated += RepositoriesUpdated; + } + } + + ~Registry() + { + if (repoDataMgr != null) + { + repoDataMgr.Updated -= RepositoriesUpdated; + } + } + + public Registry(RepositoryDataManager repoData, + Dictionary installed_modules, + Dictionary installed_dlls, + Dictionary installed_files, + SortedDictionary repositories) + : this(repoData) { // Is there a better way of writing constructors than this? Srsly? this.installed_modules = installed_modules; this.installed_dlls = installed_dlls; - this.available_modules = available_modules; this.installed_files = installed_files; this.repositories = repositories; registry_version = LATEST_REGISTRY_VERSION; - BuildProvidesIndex(); } - // If deserialsing, we don't want everything put back directly, - // thus making sure our version number is preserved, letting us - // detect registry version upgrades. - [JsonConstructor] - private Registry() + public Registry(RepositoryDataManager repoData, + params Repository[] repositories) + : this(repoData, + new Dictionary(), + new Dictionary(), + new Dictionary(), + new SortedDictionary( + repositories.ToDictionary(r => r.name, + r => r))) { } - public static Registry Empty() + public Registry(RepositoryDataManager repoData, + IEnumerable repositories) + : this(repoData, repositories.ToArray()) { - return new Registry( - new Dictionary(), - new Dictionary(), - new Dictionary(), - new Dictionary(), - new SortedDictionary() - ); } + public static Registry Empty() + => new Registry(null, + new Dictionary(), + new Dictionary(), + new Dictionary(), + new SortedDictionary()); + #endregion #region Transaction Handling @@ -436,120 +479,94 @@ private void EnlistWithTransaction() #endregion - /// - /// Set the etag values of the repositories - /// Provided in the API so it can enlist us in the transaction - /// - /// Mapping from repo URLs to etags received from servers - public void SetETags(Dictionary savedEtags) - { - log.Debug("Setting repo etags"); - - // Make sure etags get reverted if the transaction fails - EnlistWithTransaction(); + #region Stateful views of data from repo data manager based on which repos we use - foreach (var kvp in savedEtags) - { - var etag = kvp.Value; - foreach (var repo in repositories.Values.Where(r => r.uri == kvp.Key)) - { - log.DebugFormat("Setting etag for {0}: {1}", repo.name, etag); - repo.last_server_etag = etag; - } - } - } + [JsonIgnore] + private RepositoryDataManager repoDataMgr; - public void SetAllAvailable(IEnumerable newAvail) - { - log.DebugFormat( - "Setting all available modules, count {0}", newAvail); - EnlistWithTransaction(); - // Clear current modules - available_modules = new Dictionary(); - providers.Clear(); - // Add the new modules - foreach (CkanModule module in newAvail) - { - AddAvailable(module); - } - } + [JsonIgnore] + private CompatibilitySorter sorter; - /// - /// Check whether the available_modules list is empty - /// - /// - /// True if we have at least one available mod, false otherwise. - /// - public bool HasAnyAvailable() => available_modules.Count > 0; + [JsonIgnore] + private Dictionary tags; - /// - /// Mark a given module as available. - /// - public void AddAvailable(CkanModule module) - { - log.DebugFormat("Adding available module {0}", module); - EnlistWithTransaction(); + [JsonIgnore] + private HashSet untagged; - var identifier = module.identifier; - // If we've never seen this module before, create an entry for it. - if (!available_modules.ContainsKey(identifier)) - { - log.DebugFormat("Adding new available module {0}", identifier); - available_modules[identifier] = new AvailableModule(identifier); - } + // Index of which mods provide what, format: + // providers[provided] = { provider1, provider2, ... } + // Built by BuildProvidesIndex, makes LatestAvailableWithProvides much faster. + [JsonIgnore] + private Dictionary> providers; - // Now register the actual version that we have. - // (It's okay to have multiple versions of the same mod.) + private void InvalidateAvailableModCaches() + { + log.Debug("Invalidating available mod caches"); + // These member variables hold references to data from our repo data manager + // that reflects how the available modules look to this instance. + // Clear them when we have reason to believe the upstream available modules have changed. + providers = null; + sorter = null; + tags = null; + untagged = null; + } - log.DebugFormat("Available: {0} version {1}", identifier, module.version); - available_modules[identifier].Add(module); - BuildProvidesIndexFor(available_modules[identifier]); + private void InvalidateInstalledCaches() + { + log.Debug("Invalidating installed mod caches"); + // These member variables hold references to data that depends on installed modules. + // Clear them when the installed modules have changed. sorter = null; } - /// - /// Remove the given module from the registry of available modules. - /// Does *nothing* if the module was not present to begin with. - /// - public void RemoveAvailable(string identifier, ModuleVersion version) + private void RepositoriesUpdated(Repository[] which) { - AvailableModule availableModule; - if (available_modules.TryGetValue(identifier, out availableModule)) + if (Repositories.Values.Any(r => which.Contains(r))) { - log.DebugFormat("Removing available module {0} {1}", - identifier, version); + // One of our repos changed, old cached data is now junk EnlistWithTransaction(); - availableModule.Remove(version); + InvalidateAvailableModCaches(); } } + public bool HasAnyAvailable() + => repositories != null && repoDataMgr != null + && repoDataMgr.GetAllAvailableModules(repositories.Values).Any(); + /// - /// Removes the given module from the registry of available modules. - /// Does *nothing* if the module was not present to begin with. - public void RemoveAvailable(CkanModule module) + /// Partition all CkanModules in available_modules into + /// compatible and incompatible groups. + /// + /// Version criteria to determine compatibility + public CompatibilitySorter SetCompatibleVersion(GameVersionCriteria versCrit) { - RemoveAvailable(module.identifier, module.version); + if (!versCrit.Equals(sorter?.CompatibleVersions)) + { + if (providers == null) + { + BuildProvidesIndex(); + } + sorter = new CompatibilitySorter( + versCrit, repoDataMgr?.GetAllAvailDicts(repositories.Values), + providers, + installed_modules, InstalledDlls.ToHashSet(), InstalledDlc); + } + return sorter; } /// /// /// - public IEnumerable CompatibleModules(GameVersionCriteria ksp_version) - { + public IEnumerable CompatibleModules(GameVersionCriteria crit) // Set up our compatibility partition - SetCompatibleVersion(ksp_version); - return sorter.Compatible.Values.Select(avail => avail.Latest(ksp_version)).ToList(); - } + => SetCompatibleVersion(crit).LatestCompatible; /// /// /// - public IEnumerable IncompatibleModules(GameVersionCriteria ksp_version) - { + public IEnumerable IncompatibleModules(GameVersionCriteria crit) // Set up our compatibility partition - SetCompatibleVersion(ksp_version); - return sorter.Incompatible.Values.Select(avail => avail.Latest(null)).ToList(); - } + => SetCompatibleVersion(crit).LatestIncompatible; /// /// Check whether any versions of this mod are installable (including dependencies) on the given game versions. @@ -559,34 +576,35 @@ public IEnumerable IncompatibleModules(GameVersionCriteria ksp_versi /// Game versions /// true if any version is recursively compatible, false otherwise public bool IdentifierCompatible(string identifier, GameVersionCriteria crit) - { // Set up our compatibility partition - SetCompatibleVersion(crit); - return sorter.Compatible.ContainsKey(identifier); + => SetCompatibleVersion(crit).Compatible.ContainsKey(identifier); + + private AvailableModule[] getAvail(string identifier) + { + var availMods = (repositories == null || repoDataMgr == null + ? Enumerable.Empty() + : repoDataMgr.GetAvailableModules(repositories.Values, identifier)) + .ToArray(); + if (availMods.Length < 1) + { + throw new ModuleNotFoundKraken(identifier); + } + return availMods; } /// /// /// public CkanModule LatestAvailable( - string module, - GameVersionCriteria ksp_version, - RelationshipDescriptor relationship_descriptor = null) + string identifier, + GameVersionCriteria gameVersion, + RelationshipDescriptor relationshipDescriptor = null) { - // TODO: Consider making this internal, because practically everything should - // be calling LatestAvailableWithProvides() - log.DebugFormat("Finding latest available for {0}", module); - - // TODO: Check user's stability tolerance (stable, unstable, testing, etc) - - try - { - return available_modules[module].Latest(ksp_version, relationship_descriptor); - } - catch (KeyNotFoundException) - { - throw new ModuleNotFoundKraken(module); - } + log.DebugFormat("Finding latest available for {0}", identifier); + return getAvail(identifier)?.Select(am => am.Latest(gameVersion, relationshipDescriptor)) + .Where(m => m != null) + .OrderByDescending(m => m.version) + .FirstOrDefault(); } /// @@ -599,13 +617,27 @@ public CkanModule LatestAvailable( public IEnumerable AvailableByIdentifier(string identifier) { log.DebugFormat("Finding all available versions for {0}", identifier); + return getAvail(identifier).SelectMany(am => am.AllAvailable()) + .OrderByDescending(m => m.version); + } + + /// + /// Returns the specified CkanModule with the version specified, + /// or null if it does not exist. + /// + /// + public CkanModule GetModuleByVersion(string ident, ModuleVersion version) + { + log.DebugFormat("Trying to find {0} version {1}", ident, version); try { - return available_modules[identifier].AllAvailable(); + return getAvail(ident)?.Select(am => am.ByVersion(version)) + .Where(m => m != null) + .FirstOrDefault(); } - catch (KeyNotFoundException) + catch { - throw new ModuleNotFoundKraken(identifier); + return null; } } @@ -617,71 +649,28 @@ public IEnumerable AvailableByIdentifier(string identifier) /// JSON formatted string for all the available versions of the mod /// public string GetAvailableMetadata(string identifier) - { - try - { - return available_modules[identifier].FullMetadata(); - } - catch - { - return null; - } - } + => repoDataMgr == null + ? "" + : string.Join("", + repoDataMgr.GetAvailableModules(repositories.Values, identifier) + .Select(am => am.FullMetadata())); /// /// Return the latest game version compatible with the given mod. /// /// Name of mod to check - public GameVersion LatestCompatibleKSP(string identifier) - => available_modules.TryGetValue(identifier, out AvailableModule availMod) - ? availMod.LatestCompatibleKSP() - : null; - - /// - /// Find the minimum and maximum mod versions and compatible game versions - /// for a list of modules (presumably different versions of the same mod). - /// - /// The modules to inspect - /// Return parameter for the lowest mod version - /// Return parameter for the highest mod version - /// Return parameter for the lowest game version - /// Return parameter for the highest game version - public static void GetMinMaxVersions(IEnumerable modVersions, - out ModuleVersion minMod, out ModuleVersion maxMod, - out GameVersion minKsp, out GameVersion maxKsp) - { - minMod = maxMod = null; - minKsp = maxKsp = null; - foreach (CkanModule rel in modVersions.Where(v => v != null)) - { - if (minMod == null || minMod > rel.version) - { - minMod = rel.version; - } - if (maxMod == null || maxMod < rel.version) - { - maxMod = rel.version; - } - GameVersion relMin = rel.EarliestCompatibleKSP(); - GameVersion relMax = rel.LatestCompatibleKSP(); - if (minKsp == null || !minKsp.IsAny && (minKsp > relMin || relMin.IsAny)) - { - minKsp = relMin; - } - if (maxKsp == null || !maxKsp.IsAny && (maxKsp < relMax || relMax.IsAny)) - { - maxKsp = relMax; - } - } - } + public GameVersion LatestCompatibleGameVersion(List realVersions, + string identifier) + => getAvail(identifier).Select(am => am.LatestCompatibleGameVersion(realVersions)) + .Max(); /// /// Generate the providers index so we can find providing modules quicker /// private void BuildProvidesIndex() { - providers.Clear(); - foreach (AvailableModule am in available_modules.Values) + providers = new Dictionary>(); + foreach (AvailableModule am in repoDataMgr.GetAllAvailableModules(repositories?.Values)) { BuildProvidesIndexFor(am); } @@ -692,6 +681,10 @@ private void BuildProvidesIndex() /// private void BuildProvidesIndexFor(AvailableModule am) { + if (providers == null) + { + providers = new Dictionary>(); + } foreach (CkanModule m in am.AllAvailable()) { foreach (string provided in m.ProvidesList) @@ -704,13 +697,73 @@ private void BuildProvidesIndexFor(AvailableModule am) } } - public void BuildTagIndex(ModuleTagList tags) + [JsonIgnore] + public Dictionary Tags + { + get + { + if (tags == null) + { + BuildTagIndex(); + } + return tags; + } + } + + [JsonIgnore] + public HashSet Untagged { - tags.Tags.Clear(); - tags.Untagged.Clear(); - foreach (AvailableModule am in available_modules.Values) + get { - tags.BuildTagIndexFor(am); + if (untagged == null) + { + BuildTagIndex(); + } + return untagged; + } + } + + /// + /// Assemble a mapping from tags to modules + /// + private void BuildTagIndex() + { + tags = new Dictionary(); + untagged = new HashSet(); + foreach (AvailableModule am in repoDataMgr.GetAllAvailableModules(repositories.Values)) + { + BuildTagIndexFor(am); + } + } + + private void BuildTagIndexFor(AvailableModule am) + { + bool tagged = false; + foreach (CkanModule m in am.AllAvailable()) + { + if (m.Tags != null) + { + tagged = true; + foreach (string tagName in m.Tags) + { + if (tags.TryGetValue(tagName, out ModuleTag tag)) + { + tag.Add(m.identifier); + } + else + { + tags.Add(tagName, new ModuleTag() + { + Name = tagName, + ModuleIdentifiers = new HashSet() { m.identifier }, + }); + } + } + } + } + if (!tagged) + { + untagged.Add(am.AllAvailable().First().identifier); } } @@ -718,23 +771,25 @@ public void BuildTagIndex(ModuleTagList tags) /// /// public List LatestAvailableWithProvides( - string identifier, - GameVersionCriteria ksp_version, - RelationshipDescriptor relationship_descriptor = null, + string identifier, + GameVersionCriteria gameVersion, + RelationshipDescriptor relationship_descriptor = null, IEnumerable installed = null, - IEnumerable toInstall = null - ) + IEnumerable toInstall = null) { + if (providers == null) + { + BuildProvidesIndex(); + } if (providers.TryGetValue(identifier, out HashSet provs)) { // For each AvailableModule, we want the latest one matching our constraints return provs .Select(am => am.Latest( - ksp_version, + gameVersion, relationship_descriptor, installed ?? InstalledModules.Select(im => im.Module), - toInstall - )) + toInstall)) .Where(m => m?.ProvidesList?.Contains(identifier) ?? false) .ToList(); } @@ -745,29 +800,16 @@ public List LatestAvailableWithProvides( } } - /// - /// Returns the specified CkanModule with the version specified, - /// or null if it does not exist. - /// - /// - public CkanModule GetModuleByVersion(string ident, ModuleVersion version) - { - log.DebugFormat("Trying to find {0} version {1}", ident, version); - - if (!available_modules.ContainsKey(ident)) - { - return null; - } - - AvailableModule available = available_modules[ident]; - return available.ByVersion(version); - } + #endregion /// /// Register the supplied module as having been installed, thereby keeping /// track of its metadata and files. /// - public void RegisterModule(CkanModule mod, IEnumerable absolute_files, GameInstance ksp, bool autoInstalled) + public void RegisterModule(CkanModule mod, + IEnumerable absolute_files, + GameInstance inst, + bool autoInstalled) { log.DebugFormat("Registering module {0}", mod); EnlistWithTransaction(); @@ -782,12 +824,12 @@ public void RegisterModule(CkanModule mod, IEnumerable absolute_files, G // We always work with relative files, so let's get some! IEnumerable relative_files = absolute_files - .Select(x => ksp.ToRelativeGameDir(x)) + .Select(x => inst.ToRelativeGameDir(x)) .Memoize(); // For now, it's always cool if a module wants to register a directory. // We have to flip back to absolute paths to actually test this. - foreach (string file in relative_files.Where(file => !Directory.Exists(ksp.ToAbsoluteGameDir(file)))) + foreach (string file in relative_files.Where(file => !Directory.Exists(inst.ToAbsoluteGameDir(file)))) { string owner; if (installed_files.TryGetValue(file, out owner)) @@ -796,8 +838,7 @@ public void RegisterModule(CkanModule mod, IEnumerable absolute_files, G // (Although if it existed, we should have thrown a kraken well before this.) inconsistencies.Add(string.Format( Properties.Resources.RegistryFileConflict, - mod.identifier, file, owner - )); + mod.identifier, file, owner)); } } @@ -819,9 +860,13 @@ public void RegisterModule(CkanModule mod, IEnumerable absolute_files, G installed_files[file] = mod.identifier; } - // Finally, register our module proper. - var installed = new InstalledModule(ksp, mod, relative_files, autoInstalled); - installed_modules.Add(mod.identifier, installed); + // Finally register our module proper + installed_modules.Add(mod.identifier, + new InstalledModule(inst, mod, relative_files, autoInstalled)); + + // Installing and uninstalling mods can change compatibility due to conflicts, + // so we'll need to reset the compatibility sorter + InvalidateInstalledCaches(); } /// @@ -830,16 +875,14 @@ public void RegisterModule(CkanModule mod, IEnumerable absolute_files, G /// /// Throws an InconsistentKraken if not all files have been removed. /// - public void DeregisterModule(GameInstance ksp, string module) + public void DeregisterModule(GameInstance inst, string module) { log.DebugFormat("Deregistering module {0}", module); EnlistWithTransaction(); - sorter = null; - var inconsistencies = new List(); - var absolute_files = installed_modules[module].Files.Select(ksp.ToAbsoluteGameDir); + var absolute_files = installed_modules[module].Files.Select(inst.ToAbsoluteGameDir); // Note, this checks to see if a *file* exists; it doesn't // trigger on directories, which we allow to still be present // (they may be shared by multiple mods. @@ -865,93 +908,69 @@ public void DeregisterModule(GameInstance ksp, string module) // Bye bye, module, it's been nice having you visit. installed_modules.Remove(module); - } - /// - /// Registers the given DLL as having been installed. This provides some support - /// for pre-CKAN modules. - /// - /// Does nothing if the DLL is already part of an installed module. - /// - public void RegisterDll(GameInstance ksp, string absolute_path) - { - log.DebugFormat("Registering DLL {0}", absolute_path); - string relative_path = ksp.ToRelativeGameDir(absolute_path); - - string dllIdentifier = ksp.DllPathToIdentifier(relative_path); - if (dllIdentifier == null) - { - log.WarnFormat("Attempted to index {0} which is not a DLL", relative_path); - return; - } - - string owner; - if (installed_files.TryGetValue(relative_path, out owner)) - { - log.InfoFormat( - "Not registering {0}, it belongs to {1}", - relative_path, - owner - ); - return; - } - - EnlistWithTransaction(); - - log.InfoFormat("Registering {0} from {1}", dllIdentifier, relative_path); - - // We're fine if we overwrite an existing key. - installed_dlls[dllIdentifier] = relative_path; + // Installing and uninstalling mods can change compatibility due to conflicts, + // so we'll need to reset the compatibility sorter + InvalidateInstalledCaches(); } /// - /// Clears knowledge of all DLLs from the registry. + /// Set the list of manually installed DLLs to the given mapping. + /// Files registered to a mod are not allowed and will be ignored. + /// Does nothing if we already have this data. /// - public void ClearDlls() - { - log.Debug("Clearing DLLs"); - EnlistWithTransaction(); - installed_dlls = new Dictionary(); - } - - public void RegisterDlc(string identifier, UnmanagedModuleVersion version) + /// Mapping from identifier to relative path + public bool SetDlls(Dictionary dlls) { - CkanModule dlcModule = null; - if (available_modules.TryGetValue(identifier, out AvailableModule avail)) + var unregistered = dlls.Where(kvp => !installed_files.ContainsKey(kvp.Value)) + .ToDictionary(); + if (!unregistered.DictionaryEquals(installed_dlls)) { - dlcModule = avail.ByVersion(version); - } - if (dlcModule == null) - { - // Don't have the real thing, make a fake one - dlcModule = new CkanModule( - new ModuleVersion("v1.28"), - identifier, - identifier, - "An official expansion pack for KSP", - null, - new List() { "SQUAD" }, - new List() { new License("restricted") }, - version, - null, - "dlc" - ); + EnlistWithTransaction(); + InvalidateInstalledCaches(); + installed_dlls = new Dictionary(unregistered); + return true; } - installed_modules.Add( - identifier, - new InstalledModule(null, dlcModule, new string[] { }, false) - ); + return false; } - public void ClearDlc() + public bool SetDlcs(Dictionary dlcs) { - var installedDlcs = installed_modules.Values - .Where(instMod => instMod.Module.IsDLC) - .ToList(); - foreach (var instMod in installedDlcs) + var installed = InstalledDlc; + if (!dlcs.DictionaryEquals(installed)) { - installed_modules.Remove(instMod.identifier); + EnlistWithTransaction(); + InvalidateInstalledCaches(); + + foreach (var identifier in installed.Keys.Except(dlcs.Keys)) + { + installed_modules.Remove(identifier); + } + + foreach (var kvp in dlcs) + { + var identifier = kvp.Key; + var version = kvp.Value; + // Overwrite everything in case there are version differences + installed_modules[identifier] = + new InstalledModule(null, + GetModuleByVersion(identifier, version) + ?? new CkanModule( + new ModuleVersion("v1.28"), + identifier, + identifier, + Properties.Resources.RegistryDefaultDLCAbstract, + null, + new List() { "SQUAD" }, + new List() { new License("restricted") }, + version, + null, + "dlc"), + Enumerable.Empty(), false); + } + return true; } + return false; } /// @@ -993,11 +1012,9 @@ public Dictionary Installed(bool withProvides = true, boo /// /// public InstalledModule InstalledModule(string module) - { - return installed_modules.TryGetValue(module, out InstalledModule installedModule) + => installed_modules.TryGetValue(module, out InstalledModule installedModule) ? installedModule : null; - } /// /// Find modules provided by currently installed modules @@ -1065,12 +1082,9 @@ public ModuleVersion InstalledVersion(string modIdentifier, bool with_provides=t /// /// public CkanModule GetInstalledVersion(string mod_identifier) - { - InstalledModule installedModule; - return installed_modules.TryGetValue(mod_identifier, out installedModule) + => installed_modules.TryGetValue(mod_identifier, out InstalledModule installedModule) ? installedModule.Module : null; - } /// /// Returns the module which owns this file, or null if not known. @@ -1084,8 +1098,7 @@ public string FileOwner(string file) { throw new PathErrorKraken( file, - "KSPUtils.FileOwner can only work with relative paths." - ); + "KSPUtils.FileOwner can only work with relative paths."); } string fileOwner; @@ -1097,15 +1110,15 @@ public string FileOwner(string file) /// public void CheckSanity() { - IEnumerable installed = from pair in installed_modules select pair.Value.Module; - SanityChecker.EnforceConsistency(installed, installed_dlls.Keys, InstalledDlc); + SanityChecker.EnforceConsistency(installed_modules.Select(pair => pair.Value.Module), + installed_dlls.Keys, InstalledDlc); } public List GetSanityErrors() - { - var installed = from pair in installed_modules select pair.Value.Module; - return SanityChecker.ConsistencyErrors(installed, installed_dlls.Keys, InstalledDlc).ToList(); - } + => SanityChecker.ConsistencyErrors(installed_modules.Select(pair => pair.Value.Module), + installed_dlls.Keys, + InstalledDlc) + .ToList(); /// /// Finds and returns all modules that could not exist without the listed modules installed, including themselves. @@ -1117,7 +1130,7 @@ public List GetSanityErrors() /// Installed DLLs /// Installed DLCs /// List of modules whose dependencies are about to be or already removed. - internal static IEnumerable FindReverseDependencies( + public static IEnumerable FindReverseDependencies( List modulesToRemove, List modulesToInstall, HashSet origInstalled, @@ -1181,7 +1194,9 @@ internal static IEnumerable FindReverseDependencies( if (to_remove.IsSupersetOf(brokenIdents)) { - log.DebugFormat("{0} is a superset of {1}, work done", string.Join(", ", to_remove), string.Join(", ", brokenIdents)); + log.DebugFormat("{0} is a superset of {1}, work done", + string.Join(", ", to_remove), + string.Join(", ", brokenIdents)); break; } @@ -1197,12 +1212,13 @@ internal static IEnumerable FindReverseDependencies( public IEnumerable FindReverseDependencies( List modulesToRemove, List modulesToInstall = null, - Func satisfiedFilter = null - ) - { - var installed = new HashSet(installed_modules.Values.Select(x => x.Module)); - return FindReverseDependencies(modulesToRemove, modulesToInstall, installed, new HashSet(installed_dlls.Keys), InstalledDlc, satisfiedFilter); - } + Func satisfiedFilter = null) + => FindReverseDependencies(modulesToRemove, + modulesToInstall, + new HashSet(installed_modules.Values.Select(x => x.Module)), + new HashSet(installed_dlls.Keys), + InstalledDlc, + satisfiedFilter); /// /// Get a dictionary of all mod versions indexed by their downloads' SHA-1 hash. @@ -1214,9 +1230,8 @@ public IEnumerable FindReverseDependencies( public Dictionary> GetSha1Index() { var index = new Dictionary>(); - foreach (var kvp in available_modules) + foreach (var am in repoDataMgr.GetAllAvailableModules(repositories.Values)) { - AvailableModule am = kvp.Value; foreach (var kvp2 in am.module_version) { CkanModule mod = kvp2.Value; @@ -1246,9 +1261,9 @@ public Dictionary> GetSha1Index() public Dictionary> GetDownloadHashIndex() { var index = new Dictionary>(); - foreach (var kvp in available_modules) + foreach (var am in repoDataMgr?.GetAllAvailableModules(repositories.Values) + ?? Enumerable.Empty()) { - AvailableModule am = kvp.Value; foreach (var kvp2 in am.module_version) { CkanModule mod = kvp2.Value; @@ -1278,39 +1293,33 @@ public Dictionary> GetDownloadHashIndex() /// /// Host strings without duplicates public IEnumerable GetAllHosts() - => available_modules.Values - // Pick all modules where download is not null - .Where(availMod => availMod?.Latest()?.download != null) - // Merge all the URLs into one sequence - .SelectMany(availMod => availMod.Latest().download) - // Skip relative URLs because they don't have hosts - .Where(dlUri => dlUri.IsAbsoluteUri) - // Group the URLs by host - .GroupBy(dlUri => dlUri.Host) - // Put most commonly used hosts first - .OrderByDescending(grp => grp.Count()) - // Alphanumeric sort if same number of usages - .ThenBy(grp => grp.Key) - // Return the host from each group - .Select(grp => grp.Key); - - /// - /// Partition all CkanModules in available_modules into - /// compatible and incompatible groups. - /// - /// Version criteria to determine compatibility - public void SetCompatibleVersion(GameVersionCriteria versCrit) - { - if (!versCrit.Equals(sorter?.CompatibleVersions)) - { - sorter = new CompatibilitySorter( - versCrit, available_modules, providers, - installed_modules, - InstalledDlls.ToHashSet(), InstalledDlc - ); - } - } + => 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 + ?? Enumerable.Empty()) + // Skip relative URLs because they don't have hosts + .Where(dlUri => dlUri.IsAbsoluteUri) + // Group the URLs by host + .GroupBy(dlUri => dlUri.Host) + // Put most commonly used hosts first + .OrderByDescending(grp => grp.Count()) + // Alphanumeric sort if same number of usages + .ThenBy(grp => grp.Key) + // Return the host from each group + .Select(grp => grp.Key); + + + // Older clients expect these properties and can handle them being empty ("{}") but not null + [JsonProperty("available_modules", + NullValueHandling = NullValueHandling.Include)] + [JsonConverter(typeof(JsonAlwaysEmptyObjectConverter))] + private Dictionary legacyAvailableModulesDoNotUse = new Dictionary(); + + [JsonProperty("download_counts", + NullValueHandling = NullValueHandling.Include)] + [JsonConverter(typeof(JsonAlwaysEmptyObjectConverter))] + private Dictionary legacyDownloadCountsDoNotUse = new Dictionary(); - [JsonIgnore] private CompatibilitySorter sorter; } } diff --git a/Core/Registry/RegistryManager.cs b/Core/Registry/RegistryManager.cs index 22f2661353..49198cd8c5 100644 --- a/Core/Registry/RegistryManager.cs +++ b/Core/Registry/RegistryManager.cs @@ -13,7 +13,9 @@ using Newtonsoft.Json; using CKAN.DLC; +using CKAN.Games.KerbalSpaceProgram.DLC; using CKAN.Versioning; +using CKAN.Extensions; namespace CKAN { @@ -22,7 +24,7 @@ public class RegistryManager : IDisposable private static readonly Dictionary registryCache = new Dictionary(); - private static readonly ILog log = LogManager.GetLogger(typeof (RegistryManager)); + private static readonly ILog log = LogManager.GetLogger(typeof(RegistryManager)); private readonly string path; public readonly string lockfilePath; private FileStream lockfileStream = null; @@ -36,6 +38,7 @@ public class RegistryManager : IDisposable /// If loading the registry failed, the parsing error text, else null. /// public string previousCorruptedMessage; + /// /// If loading the registry failed, the location to which we moved it, else null. /// @@ -49,7 +52,7 @@ public static bool IsInstanceMaybeLocked(string ckanDirPath) // We require our constructor to be private so we can // enforce this being an instance (via Instance() above) - private RegistryManager(string path, GameInstance inst) + private RegistryManager(string path, GameInstance inst, RepositoryDataManager repoData) { this.gameInstance = inst; @@ -65,7 +68,7 @@ private RegistryManager(string path, GameInstance inst) try { - LoadOrCreate(); + LoadOrCreate(repoData); } catch { @@ -129,10 +132,7 @@ protected void Dispose(bool safeToAlsoFreeManagedObjects) } log.DebugFormat("Dispose of registry at {0}", directory); - if (!registryCache.Remove(directory)) - { - throw new RegistryInUseKraken(directory); - } + registryCache.Remove(directory); } /// @@ -268,18 +268,26 @@ public void ReleaseLock() /// Returns an instance of the registry manager for the game instance. /// The file `registry.json` is assumed. /// - public static RegistryManager Instance(GameInstance inst) + public static RegistryManager Instance(GameInstance inst, RepositoryDataManager repoData) { string directory = inst.CkanDir(); if (!registryCache.ContainsKey(directory)) { log.DebugFormat("Preparing to load registry at {0}", directory); - registryCache[directory] = new RegistryManager(directory, inst); + registryCache[directory] = new RegistryManager(directory, inst, repoData); } return registryCache[directory]; } + public static void DisposeInstance(GameInstance inst) + { + if (registryCache.TryGetValue(inst.CkanDir(), out RegistryManager regMgr)) + { + regMgr.Dispose(); + } + } + /// /// Call Dispose on all the registry managers in the cache. /// Useful for exiting without Dispose-related exceptions. @@ -293,7 +301,7 @@ public static void DisposeAll() } } - private void Load() + private void Load(RepositoryDataManager repoData) { // Our registry needs to know our game instance when upgrading from older // registry formats. This lets us encapsulate that to make it available @@ -306,29 +314,27 @@ private void Load() log.DebugFormat("Trying to load registry from {0}", path); string json = File.ReadAllText(path); log.Debug("Registry JSON loaded; parsing..."); - // A 0-byte registry.json file loads as null without exceptions - registry = JsonConvert.DeserializeObject(json, settings) - ?? Registry.Empty(); + registry = new Registry(repoData); + JsonConvert.PopulateObject(json, registry, settings); log.Debug("Registry loaded and parsed"); - ScanDlc(); log.InfoFormat("Loaded CKAN registry at {0}", path); } - private void LoadOrCreate() + private void LoadOrCreate(RepositoryDataManager repoData) { try { - Load(); + Load(repoData); } catch (FileNotFoundException) { Create(); - Load(); + Load(repoData); } catch (DirectoryNotFoundException) { Create(); - Load(); + Load(repoData); } catch (JsonException exc) { @@ -338,7 +344,7 @@ private void LoadOrCreate() path, previousCorruptedPath, previousCorruptedMessage); File.Move(path, previousCorruptedPath); Create(); - Load(); + Load(repoData); } catch (Exception ex) { @@ -353,20 +359,21 @@ private void Create() log.InfoFormat("Creating new CKAN registry at {0}", path); registry = Registry.Empty(); AscertainDefaultRepo(); + ScanUnmanagedFiles(); Save(); } private void AscertainDefaultRepo() { - var repositories = registry.Repositories ?? new SortedDictionary(); - - if (repositories.Count == 0) + if (registry.Repositories == null || registry.Repositories.Count == 0) { - repositories.Add(Repository.default_ckan_repo_name, - new Repository(Repository.default_ckan_repo_name, gameInstance.game.DefaultRepositoryURL)); + log.InfoFormat("Fabricating repository: {0}", gameInstance.game.DefaultRepositoryURL); + var name = $"{gameInstance.game.ShortName}-{Repository.default_ckan_repo_name}"; + registry.RepositoriesSet(new SortedDictionary + { + { name, new Repository(name, gameInstance.game.DefaultRepositoryURL) } + }); } - - registry.Repositories = repositories; } private string Serialize() @@ -536,93 +543,70 @@ private RelationshipDescriptor RelationshipWithoutVersion(InstalledModule inst) }; /// - /// Look for DLC installed in GameData + /// Scans the game folder for DLL data and updates the registry. + /// This operates as a transaction. /// /// - /// True if not the same list as last scan, false otherwise + /// True if found anything different, false if same as before /// - public bool ScanDlc() + public bool ScanUnmanagedFiles() { - var dlc = new Dictionary(registry.InstalledDlc); - ModuleVersion foundVer; - bool changed = false; - - registry.ClearDlc(); - - var testDlc = TestDlcScan(); - foreach (var i in testDlc) - { - if (!changed - && (!dlc.TryGetValue(i.Key, out foundVer) - || foundVer != i.Value)) - { - changed = true; - } - registry.RegisterDlc(i.Key, i.Value); - } - - var wellKnownDlc = WellKnownDlcScan(); - foreach (var i in wellKnownDlc) - { - if (!changed - && (!dlc.TryGetValue(i.Key, out foundVer) - || foundVer != i.Value)) - { - changed = true; - } - registry.RegisterDlc(i.Key, i.Value); + log.Info(Properties.Resources.GameInstanceScanning); + using (var tx = CkanTransaction.CreateTransactionScope()) + { + var dlls = Enumerable.Repeat(gameInstance.game.PrimaryModDirectoryRelative, 1) + .Concat(gameInstance.game.AlternateModDirectoriesRelative) + .Select(relDir => gameInstance.ToAbsoluteGameDir(relDir)) + .Where(absDir => Directory.Exists(absDir)) + // EnumerateFiles is *case-sensitive* in its pattern, which causes + // DLL files to be missed under Linux; we have to pick .dll, .DLL, or scanning + // GameData *twice*. + // + // The least evil is to walk it once, and filter it ourselves. + .SelectMany(absDir => Directory.EnumerateFiles(absDir, "*", + SearchOption.AllDirectories)) + .Where(file => file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase)) + .Select(absPath => gameInstance.ToRelativeGameDir(absPath)) + .Where(relPath => !gameInstance.game.StockFolders.Any(f => relPath.StartsWith($"{f}/"))) + .ToDictionary(relPath => gameInstance.DllPathToIdentifier(relPath), + relPath => relPath); + log.DebugFormat("Registering DLLs: {0}", string.Join(", ", dlls.Values)); + var dllChanged = registry.SetDlls(dlls); + + var dlcChanged = ScanDlc(); + + log.Debug("Scan completed, committing transaction"); + tx.Complete(); + + return dllChanged || dlcChanged; } - - // Check if anything got removed - if (!changed) - { - foreach (var i in dlc) - { - if (!registry.InstalledDlc.TryGetValue(i.Key, out foundVer) - || foundVer != i.Value) - { - changed = true; - break; - } - } - } - return changed; - } - - private Dictionary TestDlcScan() - { - var dlc = new Dictionary(); - - var dlcDirectory = Path.Combine(gameInstance.CkanDir(), "dlc"); - if (Directory.Exists(dlcDirectory)) - { - foreach (var f in Directory.EnumerateFiles(dlcDirectory, "*.dlc", SearchOption.TopDirectoryOnly)) - { - var id = $"{Path.GetFileNameWithoutExtension(f)}-DLC"; - var ver = File.ReadAllText(f).Trim(); - - dlc[id] = new UnmanagedModuleVersion(ver); - } - } - - return dlc; } - private Dictionary WellKnownDlcScan() - { - var dlc = new Dictionary(); - - var detectors = new IDlcDetector[] { new BreakingGroundDlcDetector(), new MakingHistoryDlcDetector() }; - - foreach (var d in detectors) - { - if (d.IsInstalled(gameInstance, out var identifier, out var version)) - { - dlc[identifier] = version ?? new UnmanagedModuleVersion(null); - } - } - - return dlc; - } + /// + /// Look for DLC installed in GameData + /// + /// + /// True if not the same list as last scan, false otherwise + /// + public bool ScanDlc() + => registry.SetDlcs(TestDlcScan(Path.Combine(gameInstance.CkanDir(), "dlc")) + .Concat(WellKnownDlcScan()) + .ToDictionary()); + + private IEnumerable> TestDlcScan(string dlcDir) + => (Directory.Exists(dlcDir) + ? Directory.EnumerateFiles(dlcDir, "*.dlc", + SearchOption.TopDirectoryOnly) + : Enumerable.Empty()) + .Select(f => new KeyValuePair( + $"{Path.GetFileNameWithoutExtension(f)}-DLC", + new UnmanagedModuleVersion(File.ReadAllText(f).Trim()))); + + private IEnumerable> WellKnownDlcScan() + => gameInstance.game.DlcDetectors + .Select(d => d.IsInstalled(gameInstance, out string identifier, out UnmanagedModuleVersion version) + ? new KeyValuePair(identifier, version) + : new KeyValuePair(null, null)) + .Where(pair => pair.Key != null); } } diff --git a/Core/Registry/Tags/ModuleTag.cs b/Core/Registry/Tags/ModuleTag.cs index a5bd7979a1..85e2a9e75a 100644 --- a/Core/Registry/Tags/ModuleTag.cs +++ b/Core/Registry/Tags/ModuleTag.cs @@ -5,7 +5,6 @@ namespace CKAN public class ModuleTag { public string Name; - public bool Visible; public HashSet ModuleIdentifiers = new HashSet(); /// diff --git a/Core/Registry/Tags/ModuleTagList.cs b/Core/Registry/Tags/ModuleTagList.cs index 12ae01e5c4..9fdd95805b 100644 --- a/Core/Registry/Tags/ModuleTagList.cs +++ b/Core/Registry/Tags/ModuleTagList.cs @@ -8,47 +8,12 @@ namespace CKAN { public class ModuleTagList { - [JsonIgnore] - public Dictionary Tags = new Dictionary(); - - [JsonIgnore] - public HashSet Untagged = new HashSet(); - [JsonProperty("hidden_tags")] public HashSet HiddenTags = new HashSet(); public static readonly string DefaultPath = Path.Combine(CKANPathUtils.AppDataPath, "tags.json"); - public void BuildTagIndexFor(AvailableModule am) - { - bool tagged = false; - foreach (CkanModule m in am.AllAvailable()) - { - if (m.Tags != null) - { - tagged = true; - foreach (string tagName in m.Tags) - { - ModuleTag tag = null; - if (Tags.TryGetValue(tagName, out tag)) - tag.Add(m.identifier); - else - Tags.Add(tagName, new ModuleTag() - { - Name = tagName, - Visible = !HiddenTags.Contains(tagName), - ModuleIdentifiers = new HashSet() { m.identifier }, - }); - } - } - } - if (!tagged) - { - Untagged.Add(am.AllAvailable().First().identifier); - } - } - public static ModuleTagList Load(string path) { try diff --git a/Core/Relationships/SanityChecker.cs b/Core/Relationships/SanityChecker.cs index bfaf128002..53b12609ee 100644 --- a/Core/Relationships/SanityChecker.cs +++ b/Core/Relationships/SanityChecker.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; using System.Linq; + +using log4net; + using CKAN.Extensions; using CKAN.Versioning; -using log4net; namespace CKAN { @@ -20,8 +22,7 @@ public static class SanityChecker public static ICollection ConsistencyErrors( IEnumerable modules, IEnumerable dlls, - IDictionary dlc - ) + IDictionary dlc) { List> unmetDepends; List> conflicts; @@ -52,8 +53,7 @@ IDictionary dlc public static void EnforceConsistency( IEnumerable modules, IEnumerable dlls = null, - IDictionary dlc = null - ) + IDictionary dlc = null) { List> unmetDepends; List> conflicts; @@ -69,8 +69,7 @@ public static void EnforceConsistency( public static bool IsConsistent( IEnumerable modules, IEnumerable dlls = null, - IDictionary dlc = null - ) + IDictionary dlc = null) { List> unmetDepends; List> conflicts; @@ -82,8 +81,7 @@ private static bool CheckConsistency( IEnumerable dlls, IDictionary dlc, out List> UnmetDepends, - out List> Conflicts - ) + out List> Conflicts) { modules = modules?.Memoize(); var dllSet = dlls?.ToHashSet(); @@ -103,28 +101,15 @@ out List> Conflicts /// Each Key is the depending module, and each Value is the relationship. /// public static List> FindUnsatisfiedDepends( - IEnumerable modules, - HashSet dlls, - IDictionary dlc - ) - { - var unsat = new List>(); - if (modules != null) - { - modules = modules.Memoize(); - foreach (CkanModule m in modules.Where(m => m.depends != null)) - { - foreach (RelationshipDescriptor dep in m.depends) - { - if (!dep.MatchesAny(modules, dlls, dlc)) - { - unsat.Add(new KeyValuePair(m, dep)); - } - } - } - } - return unsat; - } + ICollection modules, + HashSet dlls, + IDictionary dlc) + => (modules?.Where(m => m.depends != null) + .SelectMany(m => m.depends.Select(dep => + new KeyValuePair(m, dep))) + .Where(kvp => !kvp.Value.MatchesAny(modules, dlls, dlc)) + ?? Enumerable.Empty>()) + .ToList(); /// /// Find conflicts among the given modules and DLLs. @@ -139,8 +124,7 @@ IDictionary dlc public static List> FindConflicting( IEnumerable modules, HashSet dlls, - IDictionary dlc - ) + IDictionary dlc) { var confl = new List>(); if (modules != null) @@ -171,11 +155,10 @@ private sealed class ProvidesInfo public ModuleVersion ProvideeVersion { get; } public ProvidesInfo( - string providerIdentifier, + string providerIdentifier, ModuleVersion providerVersion, - string provideeIdentifier, - ModuleVersion provideeVersion - ) + string provideeIdentifier, + ModuleVersion provideeVersion) { ProviderIdentifier = providerIdentifier; ProviderVersion = providerVersion; diff --git a/Core/Registry/AvailableModule.cs b/Core/Repositories/AvailableModule.cs similarity index 82% rename from Core/Registry/AvailableModule.cs rename to Core/Repositories/AvailableModule.cs index d313749405..4a590ca611 100644 --- a/Core/Registry/AvailableModule.cs +++ b/Core/Repositories/AvailableModule.cs @@ -31,20 +31,27 @@ private AvailableModule() { } - [OnDeserialized] - internal void DeserialisationFixes(StreamingContext context) - { - identifier = module_version.Values.LastOrDefault()?.identifier; - Debug.Assert(module_version.Values.All(m => identifier.Equals(m.identifier))); - } - /// The module to keep track of public AvailableModule(string identifier) { this.identifier = identifier; } - private static readonly ILog log = LogManager.GetLogger(typeof (AvailableModule)); + public AvailableModule(string identifier, IEnumerable modules) + : this(identifier) + { + foreach (var module in modules) + { + Add(module); + } + } + + [OnDeserialized] + internal void DeserialisationFixes(StreamingContext context) + { + identifier = module_version.Values.LastOrDefault()?.identifier; + Debug.Assert(module_version.Values.All(m => identifier.Equals(m.identifier))); + } // The map of versions -> modules, that's what we're about! // First element is the oldest version, last is the newest. @@ -70,7 +77,7 @@ public void Add(CkanModule module) throw new ArgumentException( string.Format("This AvailableModule is for tracking {0} not {1}", identifier, module.identifier)); - log.DebugFormat("Adding {0}", module); + log.DebugFormat("Adding to available module: {0}", module); module_version[module.version] = module; } @@ -94,8 +101,7 @@ public CkanModule Latest( GameVersionCriteria ksp_version = null, RelationshipDescriptor relationship = null, IEnumerable installed = null, - IEnumerable toInstall = null - ) + IEnumerable toInstall = null) { IEnumerable modules = module_version.Values.Reverse(); if (relationship != null) @@ -104,7 +110,7 @@ public CkanModule Latest( } if (ksp_version != null) { - modules = modules.Where(m => m.IsCompatibleKSP(ksp_version)); + modules = modules.Where(m => m.IsCompatible(ksp_version)); } if (installed != null) { @@ -169,36 +175,41 @@ private static bool DependsAndConflictsOK(CkanModule module, IEnumerable - public GameVersion LatestCompatibleKSP() + public GameVersion LatestCompatibleGameVersion(List realVersions) { - GameVersion best = null; - foreach (var pair in module_version) + // Cheat slightly for performance: + // Find the CkanModule with the highest ksp_version_max, + // then get the real lastest compatible of just that one mod + GameVersion best = null; + CkanModule bestMod = null; + foreach (var mod in module_version.Values) { - GameVersion v = pair.Value.LatestCompatibleKSP(); + var v = mod.LatestCompatibleGameVersion(); if (v.IsAny) + { // Can't get later than Any, so stop - return v; + return mod.LatestCompatibleRealGameVersion(realVersions); + } else if (best == null || best < v) - best = v; + { + best = v; + bestMod = mod; + } } - return best; + return bestMod.LatestCompatibleRealGameVersion(realVersions); } /// /// Returns the module with the specified version, or null if that does not exist. /// public CkanModule ByVersion(ModuleVersion v) - { - CkanModule module; - module_version.TryGetValue(v, out module); - return module; - } + => module_version.TryGetValue(v, out CkanModule module) ? module : null; + /// + /// Some code may expect this to be sorted in descending order + /// public IEnumerable AllAvailable() - { - // Some code may expect this to be sorted in descending order - return module_version.Values.Reverse(); - } + => module_version.Values.Reverse(); /// /// Return the entire section of registry.json for this mod @@ -209,17 +220,18 @@ public IEnumerable AllAvailable() public string FullMetadata() { StringWriter sw = new StringWriter(new StringBuilder()); - using (JsonTextWriter writer = new JsonTextWriter(sw) { - Formatting = Formatting.Indented, - Indentation = 4, - IndentChar = ' ' - }) + using (JsonTextWriter writer = new JsonTextWriter(sw) + { + Formatting = Formatting.Indented, + Indentation = 4, + IndentChar = ' ' + }) { new JsonSerializer().Serialize(writer, this); } return sw.ToString(); } + private static readonly ILog log = LogManager.GetLogger(typeof(AvailableModule)); } - } diff --git a/Core/Repositories/ProgressFilesOffsetsToPercent.cs b/Core/Repositories/ProgressFilesOffsetsToPercent.cs new file mode 100644 index 0000000000..ba2055969f --- /dev/null +++ b/Core/Repositories/ProgressFilesOffsetsToPercent.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CKAN +{ + /// + /// Accepts progress updates in terms of offsets within a group of files + /// and translates them into percentages across the whole operation. + /// + public class ProgressFilesOffsetsToPercent : IProgress + { + /// + /// Initialize an offset-to-percent progress translator + /// + /// The upstream progress object expecting percentages + /// Sequence of sizes of files in our group + public ProgressFilesOffsetsToPercent(IProgress percentProgress, + IEnumerable sizes) + { + this.percentProgress = percentProgress; + this.sizes = sizes.ToArray(); + totalSize = this.sizes.Sum(); + } + + /// + /// The IProgress member called when we advance within the current file + /// + /// How far into the current file we are + public void Report(long currentFileOffset) + { + var percent = basePercent + (int)(100 * currentFileOffset / totalSize); + // Only report each percentage once, to avoid spamming UI calls + if (percent > lastPercent) + { + percentProgress.Report(percent); + lastPercent = percent; + } + } + + /// + /// Call this when you move on from one file to the next + /// + public void NextFile() + { + doneSize += sizes[currentIndex]; + basePercent = (int)(100 * doneSize / totalSize); + ++currentIndex; + if (basePercent > lastPercent) + { + percentProgress.Report(basePercent); + lastPercent = basePercent; + } + } + + private IProgress percentProgress; + private long[] sizes; + private long totalSize; + private long doneSize = 0; + private int currentIndex = 0; + private int basePercent = 0; + private int lastPercent = -1; + } +} diff --git a/Core/Repositories/ReadProgressStream.cs b/Core/Repositories/ReadProgressStream.cs new file mode 100644 index 0000000000..9011a4676a --- /dev/null +++ b/Core/Repositories/ReadProgressStream.cs @@ -0,0 +1,81 @@ +using System; +using System.IO; + +// https://learn.microsoft.com/en-us/archive/msdn-magazine/2006/december/net-matters-deserialization-progress-and-more + +namespace CKAN +{ + public class ReadProgressStream : ContainerStream + { + public ReadProgressStream(Stream stream, IProgress progress) + : base(stream) + { + if (!stream.CanRead) + { + throw new ArgumentException("stream"); + } + this.progress = progress; + } + + public override int Read(byte[] buffer, int offset, int count) + { + int amountRead = base.Read(buffer, offset, count); + long newProgress = Position; + if (newProgress > lastProgress) + { + progress?.Report(newProgress); + lastProgress = newProgress; + } + return amountRead; + } + + private IProgress progress; + private long lastProgress = 0; + } + + public abstract class ContainerStream : Stream + { + protected ContainerStream(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + inner = stream; + } + + protected Stream ContainedStream => inner; + + public override bool CanRead => inner.CanRead; + public override bool CanSeek => inner.CanSeek; + public override bool CanWrite => inner.CanWrite; + public override long Length => inner.Length; + public override long Position + { + get => inner.Position; + set + { + inner.Position = value; + } + } + public override void Flush() + { + inner.Flush(); + } + public override int Read(byte[] buffer, int offset, int count) + => inner.Read(buffer, offset, count); + + public override void Write(byte[] buffer, int offset, int count) + { + inner.Write(buffer, offset, count); + } + public override void SetLength(long length) + { + inner.SetLength(length); + } + public override long Seek(long offset, SeekOrigin origin) + => inner.Seek(offset, origin); + + private Stream inner; + } +} diff --git a/Core/Repositories/Repository.cs b/Core/Repositories/Repository.cs new file mode 100644 index 0000000000..0b3784192e --- /dev/null +++ b/Core/Repositories/Repository.cs @@ -0,0 +1,60 @@ +using System; +using System.Net; +using System.ComponentModel; + +using Newtonsoft.Json; + +using CKAN.Games; + +namespace CKAN +{ + public class Repository : IEquatable + { + [JsonIgnore] + public static string default_ckan_repo_name => Properties.Resources.RepositoryDefaultName; + + public string name; + public Uri uri; + public int priority = 0; + + // These are only sourced from repositories.json + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(false)] + public bool x_mirror; + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string x_comment; + + public Repository() + { } + + public Repository(string name, Uri uri) + { + this.name = name; + this.uri = uri; + } + + public Repository(string name, string uri) + : this(name, new Uri(uri)) + { } + + public Repository(string name, string uri, int priority) + : this(name, uri) + { + this.priority = priority; + } + + public override bool Equals(Object other) + => Equals(other as Repository); + + public bool Equals(Repository other) + => other != null && uri == other.uri; + + public override int GetHashCode() + => uri.GetHashCode(); + + public override string ToString() + => string.Format("{0} ({1}, {2})", name, priority, uri); + } +} diff --git a/Core/Repositories/RepositoryData.cs b/Core/Repositories/RepositoryData.cs new file mode 100644 index 0000000000..23af21e8bc --- /dev/null +++ b/Core/Repositories/RepositoryData.cs @@ -0,0 +1,303 @@ +using System; +using System.IO; +using System.Text; +using System.Linq; +using System.Collections.Generic; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using ChinhDo.Transactions.FileManager; +using ICSharpCode.SharpZipLib.GZip; +using ICSharpCode.SharpZipLib.Tar; +using ICSharpCode.SharpZipLib.Zip; +using log4net; + +using CKAN.Versioning; +using CKAN.Games; + +namespace CKAN +{ + /// + /// Represents everything we retrieve from one metadata repository + /// + public class RepositoryData + { + /// + /// The available modules from this repository + /// + [JsonProperty("available_modules", NullValueHandling = NullValueHandling.Ignore)] + public readonly Dictionary AvailableModules; + + /// + /// The download counts from this repository's download_counts.json + /// + [JsonProperty("download_counts", NullValueHandling = NullValueHandling.Ignore)] + public readonly SortedDictionary DownloadCounts; + + /// + /// The game versions from this repository's builds.json + /// Currently not used, maybe in the future + /// + [JsonProperty("known_game_versions", NullValueHandling = NullValueHandling.Ignore)] + public readonly GameVersion[] KnownGameVersions; + + /// + /// The other repositories listed in this repo's repositories.json + /// Currently not used, maybe in the future + /// + [JsonProperty("repositories", NullValueHandling = NullValueHandling.Ignore)] + public readonly Repository[] Repositories; + + /// + /// Instantiate a repo data object + /// + /// The available modules contained in this repo + /// Download counts from this repo + /// Game versions in this repo + /// Contents of repositories.json in this repo + public RepositoryData(IEnumerable modules, + SortedDictionary counts, + IEnumerable versions, + IEnumerable repos) + { + AvailableModules = modules?.GroupBy(m => m.identifier) + .ToDictionary(grp => grp.Key, + grp => new AvailableModule(grp.Key, grp)); + DownloadCounts = counts ?? new SortedDictionary(); + KnownGameVersions = (versions ?? Enumerable.Empty()).ToArray(); + Repositories = (repos ?? Enumerable.Empty()).ToArray(); + } + + /// + /// Save this repo data object to a JSON file + /// + /// Filename of the JSON file to create or overwrite + public void SaveTo(string path) + { + StringWriter sw = new StringWriter(new StringBuilder()); + using (JsonTextWriter writer = new JsonTextWriter(sw) + { + Formatting = Formatting.Indented, + Indentation = 0, + }) + { + JsonSerializer serializer = new JsonSerializer(); + serializer.Serialize(writer, this); + } + TxFileManager file_transaction = new TxFileManager(); + file_transaction.WriteAllText(path, sw + Environment.NewLine); + } + + /// + /// Load a previously cached repo data object from JSON on disk + /// + /// Filename of the JSON file to load + /// A repo data object or null if loading fails + public static RepositoryData FromJson(string path, IProgress progress) + { + try + { + log.DebugFormat("Trying to load repository data from {0}", path); + // Ain't OOP grand?! + using (var stream = File.Open(path, FileMode.Open)) + using (var progressStream = new ReadProgressStream(stream, progress)) + using (var reader = new StreamReader(progressStream)) + using (var jStream = new JsonTextReader(reader)) + { + return new JsonSerializer().Deserialize(jStream); + } + } + catch (Exception exc) + { + log.DebugFormat("Valid repository data not found at {0}: {1}", path, exc.Message); + return null; + } + } + + /// + /// Load a repo data object from a downloaded .zip or .tar.gz file + /// downloaded from the repo + /// + /// Filename of the archive to load + /// The game for which this repo has data, used for parsing the game versions because the format can vary + /// Object for reporting progress in loading, since it can take a while + /// A repo data object + public static RepositoryData FromDownload(string path, IGame game, IProgress progress) + { + switch (FileIdentifier.IdentifyFile(path)) + { + case FileType.TarGz: return FromTarGz(path, game, progress); + case FileType.Zip: return FromZip(path, game, progress); + default: throw new UnsupportedKraken($"Not a .tar.gz or .zip, cannot process: {path}"); + } + } + + private static RepositoryData FromTarGz(string path, IGame game, IProgress progress) + { + using (var inputStream = File.OpenRead(path)) + using (var gzipStream = new GZipInputStream(inputStream)) + using (var tarStream = new TarInputStream(gzipStream, Encoding.UTF8)) + { + var modules = new List(); + SortedDictionary counts = null; + GameVersion[] versions = null; + Repository[] repos = null; + + TarEntry entry; + while ((entry = tarStream.GetNextEntry()) != null) + { + ProcessFileEntry(entry.Name, () => tarStreamString(tarStream, entry), + game, inputStream.Position, progress, + ref modules, ref counts, ref versions, ref repos); + } + return new RepositoryData(modules, counts, versions, repos); + } + } + + private static string tarStreamString(TarInputStream stream, TarEntry entry) + { + // Read each file into a buffer. + int buffer_size; + + try + { + buffer_size = Convert.ToInt32(entry.Size); + } + catch (OverflowException) + { + log.ErrorFormat("Error processing {0}: Metadata size too large.", entry.Name); + return null; + } + + byte[] buffer = new byte[buffer_size]; + + stream.Read(buffer, 0, buffer_size); + + // Convert the buffer data to a string. + return Encoding.UTF8.GetString(buffer); + } + + private static RepositoryData FromZip(string path, IGame game, IProgress progress) + { + using (var zipfile = new ZipFile(path)) + { + var modules = new List(); + SortedDictionary counts = null; + GameVersion[] versions = null; + Repository[] repos = null; + + foreach (ZipEntry entry in zipfile) + { + ProcessFileEntry(entry.Name, () => new StreamReader(zipfile.GetInputStream(entry)).ReadToEnd(), + game, entry.Offset, progress, + ref modules, ref counts, ref versions, ref repos); + } + zipfile.Close(); + return new RepositoryData(modules, counts, versions, repos); + } + } + + private static void ProcessFileEntry(string filename, + Func getContents, + IGame game, + long position, + IProgress progress, + ref List modules, + ref SortedDictionary counts, + ref GameVersion[] versions, + ref Repository[] repos) + { + if (filename.EndsWith("download_counts.json")) + { + counts = JsonConvert.DeserializeObject>(getContents()); + } + else if (filename.EndsWith("builds.json")) + { + versions = game.ParseBuildsJson(JToken.Parse(getContents())); + } + else if (filename.EndsWith("repositories.json")) + { + repos = JObject.Parse(getContents())["repositories"] + .ToObject(); + } + else if (filename.EndsWith(".ckan")) + { + log.DebugFormat("Reading CKAN data from {0}", filename); + CkanModule module = ProcessRegistryMetadataFromJSON(getContents(), filename); + if (module != null) + { + modules.Add(module); + } + } + else + { + // Skip things we don't want + log.DebugFormat("Skipping archive entry {0}", filename); + } + progress?.Report(position); + } + + private static CkanModule ProcessRegistryMetadataFromJSON(string metadata, string filename) + { + try + { + CkanModule module = CkanModule.FromJson(metadata); + // FromJson can return null for the empty string + if (module != null) + { + log.DebugFormat("Module parsed: {0}", module.ToString()); + } + return module; + } + catch (Exception exception) + { + // Alas, we can get exceptions which *wrap* our exceptions, + // because json.net seems to enjoy wrapping rather than propagating. + // See KSP-CKAN/CKAN-meta#182 as to why we need to walk the whole + // exception stack. + + bool handled = false; + + while (exception != null) + { + if (exception is UnsupportedKraken || exception is BadMetadataKraken) + { + // Either of these can be caused by data meant for future + // clients, so they're not really warnings, they're just + // informational. + + log.InfoFormat("Skipping {0}: {1}", filename, exception.Message); + + // I'd *love a way to "return" from the catch block. + handled = true; + break; + } + + // Look further down the stack. + exception = exception.InnerException; + } + + // If we haven't handled our exception, then it really was exceptional. + if (handled == false) + { + if (exception == null) + { + // Had exception, walked exception tree, reached leaf, got stuck. + log.ErrorFormat("Error processing {0} (exception tree leaf)", filename); + } + else + { + // In case whatever's calling us is lazy in error reporting, we'll + // report that we've got an issue here. + log.ErrorFormat("Error processing {0}: {1}", filename, exception.Message); + } + + throw; + } + return null; + } + } + + private static readonly ILog log = LogManager.GetLogger(typeof(RepositoryData)); + } +} diff --git a/Core/Repositories/RepositoryDataManager.cs b/Core/Repositories/RepositoryDataManager.cs new file mode 100644 index 0000000000..08b791dab4 --- /dev/null +++ b/Core/Repositories/RepositoryDataManager.cs @@ -0,0 +1,301 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; + +using Newtonsoft.Json; +using ChinhDo.Transactions.FileManager; +using log4net; + +using CKAN.Extensions; +using CKAN.Games; +using CKAN.Versioning; + +namespace CKAN +{ + /// + /// Retrieves data from repositories and provides access to it. + /// Data is cached in memory and on disk to minimize reloading. + /// + public class RepositoryDataManager + { + /// + /// Instantiate a repo data manager + /// + /// Directory to use as cache, defaults to APPDATA/CKAN/repos if null + public RepositoryDataManager(string path = null) + { + reposDir = path ?? defaultReposDir; + Directory.CreateDirectory(reposDir); + loadETags(); + } + + #region Provide access to the data + + /// + /// Return the cached available modules from a given set of repositories + /// for a given identifier + /// + /// The repositories we want to use + /// The identifier to look up + /// Sequence of available modules, if any + public IEnumerable GetAvailableModules(IEnumerable repos, + string identifier) + => GetRepoDatas(repos) + .Where(data => data.AvailableModules != null) + .Select(data => data.AvailableModules.TryGetValue(identifier, out AvailableModule am) + ? am : null) + .Where(am => am != null); + + /// + /// Return the cached available module dictionaries for a given set of repositories. + /// That's a bit low-level for a public function, but the CompatibilitySorter + /// makes some complex use of these dictionaries. + /// + /// The repositories we want to use + /// Sequence of available module dictionaries + public IEnumerable> GetAllAvailDicts(IEnumerable repos) + => GetRepoDatas(repos).Select(data => data.AvailableModules) + .Where(availMods => availMods != null + && availMods.Count > 0); + + /// + /// Return the cached AvailableModule objects from the given repositories. + /// This should not hit the network; only Update() should do that. + /// + /// Sequence of repositories to get modules from + /// Sequence of available modules + public IEnumerable GetAllAvailableModules(IEnumerable repos) + => GetAllAvailDicts(repos).SelectMany(d => d.Values); + + /// + /// Get the cached download count for a given identifier + /// + /// The repositories from which to get download count data + /// The identifier to look up + /// Number if found, else null + public int? GetDownloadCount(IEnumerable repos, string identifier) + => GetRepoDatas(repos) + .Select(data => data.DownloadCounts.TryGetValue(identifier, out int count) + ? (int?)count : null) + .Where(count => count != null) + .FirstOrDefault(); + + #endregion + + #region Manage the repo cache and files + + /// + /// Load the cached data for the given repos, WITHOUT any network calls + /// + /// Repositories for which to load data + /// Progress object for reporting percentage complete + public void Prepopulate(List repos, IProgress percentProgress) + { + // Look up the sizes of repos that have uncached files + var reposAndSizes = repos.Where(r => !repositoriesData.ContainsKey(r)) + .Select(r => new Tuple(r, GetRepoDataPath(r))) + .Where(tuple => File.Exists(tuple.Item2)) + .Select(tuple => new Tuple(tuple.Item1, + new FileInfo(tuple.Item2).Length)) + .ToList(); + // Translate from file group offsets to percent + var progress = new ProgressFilesOffsetsToPercent( + percentProgress, reposAndSizes.Select(tuple => tuple.Item2)); + foreach (var repo in reposAndSizes.Select(tuple => tuple.Item1)) + { + LoadRepoData(repo, progress); + progress.NextFile(); + } + } + + /// + /// Values to describe the result of an attempted repository update. + /// Failure is actually handled by throwing exceptions, so I'm not sure we need that. + /// + public enum UpdateResult + { + Failed, + Updated, + NoChanges, + } + + /// + /// Retrieve repository data from the network and store it in the cache + /// + /// Repositories for which we want to retrieve data + /// The game for which these repo has data, used to get the default URL and for parsing the game versions because the format can vary + /// True to force downloading regardless of the etags, false to skip if no changes on remote + /// The object that will do the actual downloading for us + /// Object for reporting messages and progress to the UI + /// Updated if we changed any of the available modules, NoChanges if already up to date + public UpdateResult Update(Repository[] repos, + IGame game, + bool skipETags, + NetAsyncDownloader downloader, + IUser user) + { + // Get latest copy of the game versions data (remote build map) + user.RaiseMessage(Properties.Resources.NetRepoUpdatingBuildMap); + game.RefreshVersions(); + + // Check if any ETags have changed, quit if not + user.RaiseProgress(Properties.Resources.NetRepoCheckingForUpdates, 0); + var toUpdate = repos.DefaultIfEmpty(new Repository("default", game.DefaultRepositoryURL)) + .DistinctBy(r => r.uri) + .Where(r => r.uri.IsFile + || skipETags + || (etags.TryGetValue(r.uri, out string etag) + ? !File.Exists(GetRepoDataPath(r)) + || etag != Net.CurrentETag(r.uri) + : true)) + .ToArray(); + if (toUpdate.Length < 1) + { + user.RaiseProgress(Properties.Resources.NetRepoAlreadyUpToDate, 100); + user.RaiseMessage(Properties.Resources.NetRepoNoChanges); + return UpdateResult.NoChanges; + } + + downloader.onOneCompleted += setETag; + try + { + // Download metadata + var targets = toUpdate.Select(r => new Net.DownloadTarget(new List() { r.uri })) + .ToArray(); + downloader.DownloadAndWait(targets); + + // If we get to this point, the downloads were successful + // Load them + string msg = ""; + var progress = new ProgressFilesOffsetsToPercent( + new Progress(p => user.RaiseProgress(msg, p)), + targets.Select(t => new FileInfo(t.filename).Length)); + foreach ((Repository repo, Net.DownloadTarget target) in toUpdate.Zip(targets)) + { + user.RaiseMessage(Properties.Resources.NetRepoLoadingModulesFromRepo, repo.name); + var file = target.filename; + msg = string.Format(Properties.Resources.NetRepoLoadingModulesFromRepo, + repo.name); + // Load the file, save to in memory cache + var repoData = repositoriesData[repo] = + RepositoryData.FromDownload(file, game, progress); + // Save parsed data to disk + repoData.SaveTo(GetRepoDataPath(repo)); + // Delete downloaded archive + File.Delete(file); + progress.NextFile(); + } + // Commit these etags to disk + saveETags(); + + // Fire an event so affected registry objects can clear their caches + Updated?.Invoke(toUpdate); + } + catch (DownloadErrorsKraken exc) + { + loadETags(); + throw new DownloadErrorsKraken( + // Renumber the exceptions based on the original repo list + exc.Exceptions.Select(kvp => new KeyValuePair( + Array.IndexOf(repos, toUpdate[kvp.Key]), + kvp.Value)) + .ToList()); + } + catch + { + // Reset etags on errors + loadETags(); + throw; + } + finally + { + // Teardown event handler with or without an exception + downloader.onOneCompleted -= setETag; + } + + return UpdateResult.Updated; + } + + /// + /// Fired when repository data changes so registries can invalidate their + /// caches of available module data + /// + public event Action Updated; + + private void loadETags() + { + try + { + etags = JsonConvert.DeserializeObject>(File.ReadAllText(etagsPath)); + } + catch + { + // We set etags to an empty dictionary at startup, so it won't be null + } + } + + private void saveETags() + { + TxFileManager file_transaction = new TxFileManager(); + file_transaction.WriteAllText(etagsPath, JsonConvert.SerializeObject(etags, Formatting.Indented)); + } + + private void setETag(Uri url, string filename, Exception error, string etag) + { + if (etag != null) + { + etags[url] = etag; + } + else if (etags.ContainsKey(url)) + { + etags.Remove(url); + } + } + + private RepositoryData GetRepoData(Repository repo) + => repositoriesData.TryGetValue(repo, out RepositoryData data) + ? data + : LoadRepoData(repo, null); + + private RepositoryData LoadRepoData(Repository repo, IProgress progress) + { + log.DebugFormat("Looking for data in {0}", GetRepoDataPath(repo)); + var data = RepositoryData.FromJson(GetRepoDataPath(repo), progress); + if (data != null) + { + log.Debug("Found it! Adding..."); + repositoriesData.Add(repo, data); + } + return data; + } + + private IEnumerable GetRepoDatas(IEnumerable repos) + => repos?.OrderBy(repo => repo.priority) + .Select(repo => GetRepoData(repo)) + .Where(data => data != null) + ?? Enumerable.Empty(); + + private string etagsPath => Path.Combine(reposDir, "etags.json"); + private Dictionary etags = new Dictionary(); + + private readonly Dictionary repositoriesData = + new Dictionary(); + + private string GetRepoDataPath(Repository repo) + => GetRepoDataPath(repo, NetFileCache.CreateURLHash(repo.uri)); + + private string GetRepoDataPath(Repository repo, string hash) + => Directory.EnumerateFiles(reposDir) + .Where(path => Path.GetFileName(path).StartsWith(hash)) + .DefaultIfEmpty(Path.Combine(reposDir, $"{hash}-{repo.name}.json")) + .First(); + + private readonly string reposDir; + private static readonly string defaultReposDir = Path.Combine(CKANPathUtils.AppDataPath, "repos"); + + #endregion + + private static readonly ILog log = LogManager.GetLogger(typeof(RepositoryDataManager)); + } +} diff --git a/Core/Repositories/RepositoryList.cs b/Core/Repositories/RepositoryList.cs new file mode 100644 index 0000000000..d5efcca9e8 --- /dev/null +++ b/Core/Repositories/RepositoryList.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; + +using CKAN.Games; + +namespace CKAN +{ + public struct RepositoryList + { + public Repository[] repositories; + + public static RepositoryList DefaultRepositories(IGame game) + { + try + { + return JsonConvert.DeserializeObject( + Net.DownloadText(game.RepositoryListURL)); + } + catch + { + return default(RepositoryList); + } + } + } +} diff --git a/Core/ServiceLocator.cs b/Core/ServiceLocator.cs index 2c0ff55736..3528904c8f 100644 --- a/Core/ServiceLocator.cs +++ b/Core/ServiceLocator.cs @@ -1,6 +1,8 @@ -using System; +using System; + using Autofac; -using CKAN.GameVersionProviders; + +using CKAN.Games.KerbalSpaceProgram.GameVersionProviders; using CKAN.Configuration; namespace CKAN @@ -40,11 +42,13 @@ private static void Init() builder.RegisterType() .As() - .SingleInstance(); // Technically not needed, but makes things easier + // Technically not needed, but makes things easier + .SingleInstance(); builder.RegisterType() .As() - .SingleInstance(); // Since it stores cached data we want to keep it around + // Since it stores cached data we want to keep it around + .SingleInstance(); builder.RegisterType() .As() @@ -54,6 +58,9 @@ private static void Init() .As() .Keyed(GameVersionSource.Readme); + builder.RegisterType() + .SingleInstance(); + _container = builder.Build(); } } diff --git a/Core/Types/CkanModule.cs b/Core/Types/CkanModule.cs index 8e2e13bda7..8b5ea67024 100644 --- a/Core/Types/CkanModule.cs +++ b/Core/Types/CkanModule.cs @@ -35,14 +35,10 @@ public class DownloadHashesDescriptor public class NameComparer : IEqualityComparer { public bool Equals(CkanModule x, CkanModule y) - { - return x.identifier.Equals(y.identifier); - } + => x.identifier.Equals(y.identifier); public int GetHashCode(CkanModule obj) - { - return obj.identifier.GetHashCode(); - } + => obj.identifier.GetHashCode(); } /// @@ -409,9 +405,7 @@ private void CalculateSearchables() } public string serialise() - { - return JsonConvert.SerializeObject(this); - } + => JsonConvert.SerializeObject(this); [OnDeserialized] private void DeSerialisationFixes(StreamingContext like_i_could_care) @@ -456,7 +450,7 @@ public static CkanModule FromIDandVersion(IRegistryQuerier registry, string mod, module = registry.GetModuleByVersion(ident, version); if (module == null - || (ksp_version != null && !module.IsCompatibleKSP(ksp_version))) + || (ksp_version != null && !module.IsCompatible(ksp_version))) throw new ModuleNotFoundKraken(ident, version, string.Format(Properties.Resources.CkanModuleNotAvailable, ident, version)); } @@ -472,23 +466,17 @@ public static CkanModule FromIDandVersion(IRegistryQuerier registry, string mod, return module; } - public static readonly Regex idAndVersionMatcher = new Regex( - @"^(?[^=]*)=(?.*)$", - RegexOptions.Compiled - ); + public static readonly Regex idAndVersionMatcher = + new Regex(@"^(?[^=]*)=(?.*)$", + RegexOptions.Compiled); /// Generates a CKAN.Meta object given a filename - /// TODO: Catch and display errors public static CkanModule FromFile(string filename) - { - string json = File.ReadAllText(filename); - return FromJson(json); - } + => FromJson(File.ReadAllText(filename)); public static void ToFile(CkanModule module, string filename) { - var json = ToJson(module); - File.WriteAllText(filename, json); + File.WriteAllText(filename, ToJson(module)); } public static string ToJson(CkanModule module) @@ -524,29 +512,25 @@ public static CkanModule FromJson(string json) /// Returns true if we conflict with the given module. /// public bool ConflictsWith(CkanModule module) - { // We never conflict with ourselves, since we can't be installed at // the same time as another version of ourselves. - if (module.identifier == this.identifier) return false; - - return UniConflicts(this, module) || UniConflicts(module, this); - } + => module.identifier == identifier + ? false + : UniConflicts(this, module) || UniConflicts(module, this); /// /// Checks if A conflicts with B, but not if B conflicts with A. /// Used by ConflictsWith. /// internal static bool UniConflicts(CkanModule mod1, CkanModule mod2) - { - return mod1?.conflicts?.Any( - conflict => conflict.MatchesAny(new CkanModule[] {mod2}, null, null) - ) ?? false; - } + => mod1?.conflicts?.Any( + conflict => conflict.MatchesAny(new CkanModule[] {mod2}, null, null)) + ?? false; /// /// Returns true if our mod is compatible with the KSP version specified. /// - public bool IsCompatibleKSP(GameVersionCriteria version) + public bool IsCompatible(GameVersionCriteria version) { var compat = _comparator.Compatible(version, this); log.DebugFormat("Checking compat of {0} with game versions {1}: {2}", @@ -554,71 +538,54 @@ public bool IsCompatibleKSP(GameVersionCriteria version) return compat; } - /// - /// Returns a human readable string indicating the highest compatible - /// version of KSP this module will run with. (Eg: 1.0.2, - /// "All versions", etc). - /// - /// This is for *human consumption only*, as the strings may change in the - /// future as we support additional locales. - /// - public string HighestCompatibleKSP() - { - GameVersion v = LatestCompatibleKSP(); - if (v.IsAny) - return Properties.Resources.CkanModuleAllVersions; - else - return v.ToString(); - } - /// /// Returns machine readable object indicating the highest compatible /// version of KSP this module will run with. /// - public GameVersion LatestCompatibleKSP() - { + public GameVersion LatestCompatibleGameVersion() // Find the highest compatible KSP version - if (ksp_version_max != null) - return ksp_version_max; - else if (ksp_version != null) - return ksp_version; - else - // No upper limit. - return GameVersion.Any; - } + => ksp_version_max ?? ksp_version + // No upper limit. + ?? GameVersion.Any; /// /// Returns machine readable object indicating the lowest compatible /// version of KSP this module will run with. /// - public GameVersion EarliestCompatibleKSP() - { + public GameVersion EarliestCompatibleGameVersion() // Find the lowest compatible KSP version - if (ksp_version_min != null) - return ksp_version_min; - else if (ksp_version != null) - return ksp_version; - else - // No lower limit. - return GameVersion.Any; - } + => ksp_version_min ?? ksp_version + // No lower limit. + ?? GameVersion.Any; + + /// + /// Return the latest game version from the given list that is + /// compatible with this module, without the build number. + /// + /// Game versions to test, sorted from earliest to latest + /// The latest game version if any, else null + public GameVersion LatestCompatibleRealGameVersion(List realVers) + => LatestCompatibleRealGameVersion(new GameVersionRange(EarliestCompatibleGameVersion(), + LatestCompatibleGameVersion()), + realVers); + + private GameVersion LatestCompatibleRealGameVersion(GameVersionRange range, + List realVers) + => (realVers?.LastOrDefault(v => range.Contains(v)) + ?? LatestCompatibleGameVersion()); /// /// Returns true if this module provides the functionality requested. /// public bool DoesProvide(string identifier) - { - return this.identifier == identifier || provides.Contains(identifier); - } + => this.identifier == identifier || provides.Contains(identifier); public bool IsMetapackage => kind == "metapackage"; public bool IsDLC => kind == "dlc"; protected bool Equals(CkanModule other) - { - return string.Equals(identifier, other.identifier) && version.Equals(other.version); - } + => string.Equals(identifier, other.identifier) && version.Equals(other.version); public override bool Equals(object obj) { @@ -710,9 +677,7 @@ public override int GetHashCode() } bool IEquatable.Equals(CkanModule other) - { - return Equals(other); - } + => Equals(other); /// /// Returns true if we support at least spec_version of the CKAN spec. @@ -729,18 +694,14 @@ internal static bool IsSpecSupported(ModuleVersion spec_version) /// Returns true if we support the CKAN spec used by this module. /// private bool IsSpecSupported() - { - return IsSpecSupported(spec_version); - } + => IsSpecSupported(spec_version); /// /// Returns a standardised name for this module, in the form /// "identifier-version.zip". For example, `RealSolarSystem-7.3.zip` /// public string StandardName() - { - return StandardName(identifier, version); - } + => StandardName(identifier, version); public static string StandardName(string identifier, ModuleVersion version) { @@ -754,9 +715,7 @@ public static string StandardName(string identifier, ModuleVersion version) } public override string ToString() - { - return string.Format("{0} {1}", identifier, version); - } + => string.Format("{0} {1}", identifier, version); public string DescribeInstallStanzas(IGame game) { @@ -847,6 +806,44 @@ private static HashSet OneDownloadGroupingPass(HashSet u } return found; } + + /// + /// Find the minimum and maximum mod versions and compatible game versions + /// for a list of modules (presumably different versions of the same mod). + /// + /// The modules to inspect + /// Return parameter for the lowest mod version + /// Return parameter for the highest mod version + /// Return parameter for the lowest game version + /// Return parameter for the highest game version + public static void GetMinMaxVersions(IEnumerable modVersions, + out ModuleVersion minMod, out ModuleVersion maxMod, + out GameVersion minGame, out GameVersion maxGame) + { + minMod = maxMod = null; + minGame = maxGame = null; + foreach (CkanModule rel in modVersions.Where(v => v != null)) + { + if (minMod == null || minMod > rel.version) + { + minMod = rel.version; + } + if (maxMod == null || maxMod < rel.version) + { + maxMod = rel.version; + } + GameVersion relMin = rel.EarliestCompatibleGameVersion(); + GameVersion relMax = rel.LatestCompatibleGameVersion(); + if (minGame == null || !minGame.IsAny && (minGame > relMin || relMin.IsAny)) + { + minGame = relMin; + } + if (maxGame == null || !maxGame.IsAny && (maxGame < relMax || relMax.IsAny)) + { + maxGame = relMax; + } + } + } } public class InvalidModuleAttributesException : Exception diff --git a/Core/Types/Repository.cs b/Core/Types/Repository.cs deleted file mode 100644 index 6f0327f248..0000000000 --- a/Core/Types/Repository.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Net; -using Newtonsoft.Json; -using CKAN.Games; - -namespace CKAN -{ - public class Repository : IEquatable - { - [JsonIgnore] public static string default_ckan_repo_name => Properties.Resources.RepositoryDefaultName; - - public string name; - public Uri uri; - public string last_server_etag; - public int priority = 0; - - public Repository() - { - } - - public Repository(string name, string uri) - { - this.name = name; - this.uri = new Uri(uri); - } - - public Repository(string name, string uri, int priority) - { - this.name = name; - this.uri = new Uri(uri); - this.priority = priority; - } - - public Repository(string name, Uri uri) - { - this.name = name; - this.uri = uri; - } - - public override bool Equals(Object other) - { - return Equals(other as Repository); - } - - public bool Equals(Repository other) - { - return other != null - && name == other.name - && uri == other.uri - && priority == other.priority; - } - - public override int GetHashCode() - { - return name.GetHashCode(); - } - - public override string ToString() - { - return String.Format("{0} ({1}, {2})", name, priority, uri); - } - } - - public struct RepositoryList - { - public Repository[] repositories; - - public static RepositoryList DefaultRepositories(IGame game) - { - try - { - return JsonConvert.DeserializeObject( - Net.DownloadText(game.RepositoryListURL) - ); - } - catch - { - return default(RepositoryList); - } - } - - } - -} diff --git a/Core/Versioning/GameVersion.cs b/Core/Versioning/GameVersion.cs index c7976a742b..057fde3d85 100644 --- a/Core/Versioning/GameVersion.cs +++ b/Core/Versioning/GameVersion.cs @@ -1,12 +1,14 @@ -using System; +using System; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Collections.Generic; + using Newtonsoft.Json; using Autofac; -using CKAN.GameVersionProviders; + using CKAN.Games; +using CKAN.Games.KerbalSpaceProgram.GameVersionProviders; namespace CKAN.Versioning { @@ -18,8 +20,7 @@ public sealed partial class GameVersion { private static readonly Regex Pattern = new Regex( @"^(?\d+)(?:\.(?\d+)(?:\.(?\d+)(?:\.(?\d+))?)?)?$", - RegexOptions.Compiled - ); + RegexOptions.Compiled); private const int Undefined = -1; @@ -90,6 +91,11 @@ public sealed partial class GameVersion /// public bool IsAny => !IsMajorDefined && !IsMinorDefined && !IsPatchDefined && !IsBuildDefined; + /// + /// Provide this resource string to other DLLs outside core + /// /// + public static string AnyString => Properties.Resources.GameVersionYalovAny; + /// /// Check whether a version is null or Any. /// We group them here because they mean the same thing. @@ -233,65 +239,6 @@ public GameVersion(int major, int minor, int patch, int build) /// public override string ToString() => _string; - private static Dictionary VersionsMax = new Dictionary(); - - /// - /// Generate version mapping table once for all instances to share - /// - static GameVersion() - { - // Should be sorted - List versions = ServiceLocator.Container.Resolve().KnownVersions; - VersionsMax[""] = versions.Last(); - foreach (var v in versions) - { - // Add or replace - VersionsMax[$"{v.Major}" ] = v; - VersionsMax[$"{v.Major}.{v.Minor}"] = v; - } - } - - /// - /// Get a string to represent a game version. - /// - /// - /// String representing max game version. - /// Partly clamped to real versions, partly rounded up to imaginary versions. - /// - public string ToYalovString() - { - GameVersion value; - - if (!IsMajorDefined - // 2.0.0 - || _major > VersionsMax[""].Major - // 1.99.99 - || (_major == VersionsMax[""].Major && VersionsMax.TryGetValue($"{_major}", out value) && _minor >= UptoNines(value.Minor))) - { - return Properties.Resources.GameVersionYalovAny; - } - else if (IsMinorDefined - && VersionsMax.TryGetValue($"{_major}.{_minor}", out value) - && (!IsPatchDefined || _patch >= UptoNines(value.Patch))) - { - return $"{_major}.{_minor}.{UptoNines(value.Patch)}"; - } - else - { - return ToString(); - } - } - - /// - /// 0 - 9 // 9 - 99 // 99 - 999 - /// 1 - 9 // 10 - 99 // 100 - 999 - /// 8 - 9 // 98 - 99 - /// - private static int UptoNines(int num) - { - return (int)Math.Pow(10, Math.Floor(Math.Log10(num + 1)) + 1) - 1; - } - /// /// Strip off the build number if it's defined /// diff --git a/Core/Versioning/GameVersionRange.cs b/Core/Versioning/GameVersionRange.cs index 21feb44b14..d366c8e8ee 100644 --- a/Core/Versioning/GameVersionRange.cs +++ b/Core/Versioning/GameVersionRange.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text; using CKAN.Games; @@ -44,6 +44,11 @@ public GameVersionRange IntersectWith(GameVersionRange other) return IsEmpty(highestLow, lowestHigh) ? null : new GameVersionRange(highestLow, lowestHigh); } + // Same logic as above but without "new" + private bool Intersects(GameVersionRange other) + => !IsEmpty(GameVersionBound.Highest(Lower, other.Lower), + GameVersionBound.Lowest(Upper, other.Upper)); + public bool IsSupersetOf(GameVersionRange other) { if (ReferenceEquals(other, null)) @@ -60,6 +65,14 @@ public bool IsSupersetOf(GameVersionRange other) return lowerIsOkay && upperIsOkay; } + /// + /// Check whether a given game version is within this range + /// + /// The game version to check + /// True if within bounds, false otherwise + public bool Contains(GameVersion ver) + => Intersects(ver.ToVersionRange()); + private static bool IsEmpty(GameVersionBound lower, GameVersionBound upper) => upper.Value < lower.Value || (lower.Value == upper.Value && (!lower.Inclusive || !upper.Inclusive)); diff --git a/GUI/Controls/EditModpack.cs b/GUI/Controls/EditModpack.cs index 33dfc0b3fb..684016046a 100644 --- a/GUI/Controls/EditModpack.cs +++ b/GUI/Controls/EditModpack.cs @@ -3,9 +3,10 @@ using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; + using Autofac; + using CKAN.Versioning; -using CKAN.GameVersionProviders; using CKAN.Types; namespace CKAN.GUI diff --git a/GUI/Controls/InstallationHistory.cs b/GUI/Controls/InstallationHistory.cs index 5e940396b0..a58fd4f5a8 100644 --- a/GUI/Controls/InstallationHistory.cs +++ b/GUI/Controls/InstallationHistory.cs @@ -18,10 +18,10 @@ public InstallationHistory() InitializeComponent(); } - public void LoadHistory(GameInstance inst, GUIConfiguration config) + public void LoadHistory(GameInstance inst, GUIConfiguration config, RepositoryDataManager repoData) { this.inst = inst; - this.registry = RegistryManager.Instance(inst).registry; + this.registry = RegistryManager.Instance(inst, repoData).registry; this.config = config; Util.Invoke(this, () => { @@ -55,7 +55,7 @@ public void LoadHistory(GameInstance inst, GUIConfiguration config) /// /// Invoked when the user selects a module /// - public event Action OnSelectedModuleChanged; + public event Action OnSelectedModuleChanged; /// /// Invoked when the user clicks the Install toolbar button @@ -204,8 +204,7 @@ private void ModsListView_ItemSelectionChanged(Object sender, ListViewItemSelect .FirstOrDefault(); if (mod != null) { - OnSelectedModuleChanged?.Invoke(new GUIMod( - mod, registry, inst.VersionCriteria())); + OnSelectedModuleChanged?.Invoke(mod); } } diff --git a/GUI/Controls/ManageMods.cs b/GUI/Controls/ManageMods.cs index f2752deb3b..e6f09f4514 100644 --- a/GUI/Controls/ManageMods.cs +++ b/GUI/Controls/ManageMods.cs @@ -7,6 +7,7 @@ using System.Windows.Forms; using System.ComponentModel; +using Autofac; using log4net; using CKAN.Extensions; @@ -38,6 +39,8 @@ public ManageMods() ApplyToolButton.MouseHover += (sender, args) => ApplyToolButton.ShowDropDown(); ApplyToolButton.Enabled = false; + repoData = ServiceLocator.Container.Resolve(); + // History is read-only until the UI is started. We switch // out of it at the end of OnLoad() when we call NavInit(). navHistory = new NavigationHistory { IsReadOnly = true }; @@ -59,6 +62,7 @@ public ManageMods() } private static readonly ILog log = LogManager.GetLogger(typeof(ManageMods)); + private RepositoryDataManager repoData; private DateTime lastSearchTime; private string lastSearchKey; private NavigationHistory navHistory; @@ -175,7 +179,8 @@ private void ConflictsUpdated(Dictionary prevConflicts) { cell.ToolTipText = null; } - mainModList.ReapplyLabels(guiMod, false, inst.Name, inst.game); + var registry = RegistryManager.Instance(Main.Instance.CurrentInstance, repoData).registry; + mainModList.ReapplyLabels(guiMod, false, inst.Name, inst.game, registry); if (row.Visible) { ModGrid.InvalidateRow(row.Index); @@ -206,7 +211,8 @@ private void ConflictsUpdated(Dictionary prevConflicts) private void RefreshToolButton_Click(object sender, EventArgs e) { - Main.Instance.UpdateRepo(); + // If user is holding Shift or Ctrl, force a full update + Main.Instance.UpdateRepo((Control.ModifierKeys & (Keys.Control | Keys.Shift)) != 0); } #region Filter dropdown @@ -220,8 +226,9 @@ private void FilterToolButton_DropDown_Opening(object sender, CancelEventArgs e) private void FilterTagsToolButton_DropDown_Opening(object sender, CancelEventArgs e) { + var registry = RegistryManager.Instance(Main.Instance.CurrentInstance, repoData).registry; FilterTagsToolButton.DropDownItems.Clear(); - foreach (var kvp in mainModList.ModuleTags.Tags.OrderBy(kvp => kvp.Key)) + foreach (var kvp in registry.Tags.OrderBy(kvp => kvp.Key)) { FilterTagsToolButton.DropDownItems.Add(new ToolStripMenuItem( $"{kvp.Key} ({kvp.Value.ModuleIdentifiers.Count})", @@ -234,7 +241,7 @@ private void FilterTagsToolButton_DropDown_Opening(object sender, CancelEventArg } FilterTagsToolButton.DropDownItems.Add(untaggedFilterToolStripSeparator); FilterTagsToolButton.DropDownItems.Add(new ToolStripMenuItem( - string.Format(Properties.Resources.MainLabelsUntagged, mainModList.ModuleTags.Untagged.Count), + string.Format(Properties.Resources.MainLabelsUntagged, registry.Untagged.Count), null, tagFilterButton_Click ) { @@ -302,7 +309,8 @@ private void labelMenuItem_Click(object sender, EventArgs e) mod.HasUpdate && !Main.Instance.LabelsHeld(mod.Identifier)); } var inst = Main.Instance.CurrentInstance; - mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, inst.Name, inst.game); + var registry = RegistryManager.Instance(Main.Instance.CurrentInstance, repoData).registry; + mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, inst.Name, inst.game, registry); mainModList.ModuleLabels.Save(ModuleLabelList.DefaultPath); } @@ -313,9 +321,10 @@ private void editLabelsToolStripMenuItem_Click(object sender, EventArgs e) eld.Dispose(); mainModList.ModuleLabels.Save(ModuleLabelList.DefaultPath); var inst = Main.Instance.CurrentInstance; + var registry = RegistryManager.Instance(Main.Instance.CurrentInstance, repoData).registry; foreach (GUIMod module in mainModList.Modules) { - mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, inst.Name, inst.game); + mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, inst.Name, inst.game, registry); } } @@ -595,13 +604,14 @@ private void ModGrid_HeaderMouseClick(object sender, DataGridViewCellMouseEventA ModListHeaderContextMenuStrip.Items.Add(new ToolStripSeparator()); // Add tags + var registry = RegistryManager.Instance(Main.Instance.CurrentInstance, repoData).registry; ModListHeaderContextMenuStrip.Items.AddRange( - mainModList.ModuleTags.Tags.OrderBy(kvp => kvp.Key) + registry.Tags.OrderBy(kvp => kvp.Key) .Select(kvp => new ToolStripMenuItem() { Name = kvp.Key, Text = kvp.Key, - Checked = kvp.Value.Visible, + Checked = !mainModList.ModuleTags.HiddenTags.Contains(kvp.Key), Tag = kvp.Value, }) .ToArray() @@ -633,8 +643,7 @@ private void ModListHeaderContextMenuStrip_ItemClicked(object sender, ToolStripI } else if (tag != null) { - tag.Visible = !clickedItem.Checked; - if (tag.Visible) + if (!clickedItem.Checked) { mainModList.ModuleTags.HiddenTags.Remove(tag.Name); } @@ -806,7 +815,7 @@ private void ModGrid_CellValueChanged(object sender, DataGridViewCellEventArgs e } UpdateChangeSetAndConflicts( Main.Instance.CurrentInstance, - RegistryManager.Instance(Main.Instance.CurrentInstance).registry); + RegistryManager.Instance(Main.Instance.CurrentInstance, repoData).registry); } } } @@ -859,7 +868,7 @@ private void InstallAllCheckbox_CheckChanged(object sender, EventArgs e) ModGrid.Refresh(); UpdateChangeSetAndConflicts( Main.Instance.CurrentInstance, - RegistryManager.Instance(Main.Instance.CurrentInstance).registry); + RegistryManager.Instance(Main.Instance.CurrentInstance, repoData).registry); } } @@ -874,7 +883,7 @@ public void ClearChangeSet() } else if (mod.InstalledMod != null) { - var registry = RegistryManager.Instance(Main.Instance.CurrentInstance).registry; + var registry = RegistryManager.Instance(Main.Instance.CurrentInstance, repoData).registry; mod.SelectedMod = registry.GetModuleByVersion( mod.InstalledMod.identifier, mod.InstalledMod.Module.version) ?? mod.InstalledMod.Module; @@ -1011,7 +1020,7 @@ private void reinstallToolStripMenuItem_Click(object sender, EventArgs e) if (module == null || !module.IsCKAN) return; - IRegistryQuerier registry = RegistryManager.Instance(Main.Instance.CurrentInstance).registry; + IRegistryQuerier registry = RegistryManager.Instance(Main.Instance.CurrentInstance, repoData).registry; // Find everything we need to re-install var revdep = registry.FindReverseDependencies(new List() { module.Identifier }) @@ -1060,7 +1069,7 @@ private void purgeContentsToolStripMenuItem_Click(object sender, EventArgs e) var selected = SelectedModule; if (selected != null) { - IRegistryQuerier registry = RegistryManager.Instance(Main.Instance.CurrentInstance).registry; + IRegistryQuerier registry = RegistryManager.Instance(Main.Instance.CurrentInstance, repoData).registry; var allAvail = registry.AvailableByIdentifier(selected.Identifier); foreach (CkanModule mod in allAvail) { @@ -1127,12 +1136,13 @@ private void _UpdateFilters() selected_mod = (GUIMod) ModGrid.CurrentRow.Tag; } + var registry = RegistryManager.Instance(Main.Instance.CurrentInstance, repoData).registry; ModGrid.Rows.Clear(); foreach (var row in rows) { var mod = ((GUIMod) row.Tag); var inst = Main.Instance.CurrentInstance; - row.Visible = mainModList.IsVisible(mod, inst.Name, inst.game); + row.Visible = mainModList.IsVisible(mod, inst.Name, inst.game, registry); } ApplyHeaderGlyphs(); @@ -1152,37 +1162,50 @@ private void _UpdateFilters() public void Update(object sender, DoWorkEventArgs e) { - _UpdateModsList(e.Argument as Dictionary); + e.Result = _UpdateModsList(e.Argument as Dictionary); } - private void _UpdateModsList(Dictionary old_modules = null) + private bool _UpdateModsList(Dictionary old_modules = null) { log.Info("Updating the mod list"); - Main.Instance.Wait.AddLogMessage(Properties.Resources.MainRepoScanning); - Main.Instance.CurrentInstance.Scan(); + var regMgr = RegistryManager.Instance(Main.Instance.CurrentInstance, repoData); + IRegistryQuerier registry = regMgr.registry; + + Main.Instance.Wait.AddLogMessage(Properties.Resources.LoadingCachedRepoData); + repoData.Prepopulate( + registry.Repositories.Values.ToList(), + new Progress(p => Main.Instance.currentUser.RaiseProgress( + Properties.Resources.LoadingCachedRepoData, p))); - GameVersionCriteria versionCriteria = Main.Instance.CurrentInstance.VersionCriteria(); - IRegistryQuerier registry = RegistryManager.Instance(Main.Instance.CurrentInstance).registry; + if (!regMgr.registry.HasAnyAvailable()) + { + // Abort the refresh so we can update the repo data + return false; + } + + Main.Instance.Wait.AddLogMessage(Properties.Resources.MainRepoScanning); + regMgr.ScanUnmanagedFiles(); Main.Instance.Wait.AddLogMessage(Properties.Resources.MainModListLoadingInstalled); + var versionCriteria = Main.Instance.CurrentInstance.VersionCriteria(); var gui_mods = new HashSet(); gui_mods.UnionWith( registry.InstalledModules .Where(instMod => !instMod.Module.IsDLC) - .Select(instMod => new GUIMod(instMod, registry, versionCriteria, null, + .Select(instMod => new GUIMod(instMod, repoData, registry, versionCriteria, null, Main.Instance.configuration.HideEpochs, Main.Instance.configuration.HideV))); Main.Instance.Wait.AddLogMessage(Properties.Resources.MainModListLoadingAvailable); gui_mods.UnionWith( registry.CompatibleModules(versionCriteria) .Where(m => !m.IsDLC) - .Select(m => new GUIMod(m, registry, versionCriteria, null, + .Select(m => new GUIMod(m, repoData, registry, versionCriteria, null, Main.Instance.configuration.HideEpochs, Main.Instance.configuration.HideV))); Main.Instance.Wait.AddLogMessage(Properties.Resources.MainModListLoadingIncompatible); gui_mods.UnionWith( registry.IncompatibleModules(versionCriteria) .Where(m => !m.IsDLC) - .Select(m => new GUIMod(m, registry, versionCriteria, true, + .Select(m => new GUIMod(m, repoData, registry, versionCriteria, true, Main.Instance.configuration.HideEpochs, Main.Instance.configuration.HideV))); Main.Instance.Wait.AddLogMessage(Properties.Resources.MainModListPreservingNew); @@ -1224,7 +1247,10 @@ private void _UpdateModsList(Dictionary old_modules = null) Main.Instance.Wait.AddLogMessage(Properties.Resources.MainModListPopulatingList); // Update our mod listing - mainModList.ConstructModList(gui_mods, Main.Instance.CurrentInstance.Name, Main.Instance.CurrentInstance.game, ChangeSet); + mainModList.ConstructModList(gui_mods as IReadOnlyCollection, + Main.Instance.CurrentInstance.Name, + Main.Instance.CurrentInstance.game, + ChangeSet); UpdateChangeSetAndConflicts(Main.Instance.CurrentInstance, registry); @@ -1257,8 +1283,6 @@ private void _UpdateModsList(Dictionary old_modules = null) UpdateAllToolButton.Enabled = has_unheld_updates; }); - (registry as Registry)?.BuildTagIndex(mainModList.ModuleTags); - UpdateFilters(); // Hide update and replacement columns if not needed. @@ -1273,6 +1297,7 @@ private void _UpdateModsList(Dictionary old_modules = null) Main.Instance.Wait.AddLogMessage(Properties.Resources.MainModListUpdatingTray); Util.Invoke(this, () => ModGrid.Focus()); + return true; } public void MarkModForInstall(string identifier, bool uncheck = false) @@ -1667,7 +1692,7 @@ public void UpdateChangeSetAndConflicts(GameInstance inst, IRegistryQuerier regi var tuple = mainModList.ComputeFullChangeSetFromUserChangeSet(registry, user_change_set, gameVersion); full_change_set = tuple.Item1.ToList(); new_conflicts = tuple.Item2.ToDictionary( - item => new GUIMod(item.Key, registry, gameVersion), + item => new GUIMod(item.Key, repoData, registry, gameVersion), item => item.Value); if (new_conflicts.Count > 0) { diff --git a/GUI/Controls/ManageMods.resx b/GUI/Controls/ManageMods.resx index 10b4b635d6..266a5a962e 100644 --- a/GUI/Controls/ManageMods.resx +++ b/GUI/Controls/ManageMods.resx @@ -1,4 +1,4 @@ - +