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(@"^(?