diff --git a/.editorconfig b/.editorconfig index b7f5dce684..da701fb2dd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -42,9 +42,18 @@ dotnet_diagnostic.IDE0055.severity = none # OK to call functions that return values and not use them dotnet_diagnostic.IDE0058.severity = none +# I like seeing when a using block ends +dotnet_diagnostic.IDE0063.severity = none + # A `using` inside a namespace is useful as a typedef dotnet_diagnostic.IDE0065.severity = none +# 'switch' expressions are nice, but I don't need to be alerted about them +dotnet_diagnostic.IDE0066.severity = none + +# Why do 'new' expressions need to be simple? +dotnet_diagnostic.IDE0090.severity = none + # Allow namespaces to be independent of folder names dotnet_diagnostic.IDE0130.severity = none diff --git a/AutoUpdate/CKAN-autoupdate.csproj b/AutoUpdate/CKAN-autoupdate.csproj index 581f14cd2b..b0c4aaec2f 100644 --- a/AutoUpdate/CKAN-autoupdate.csproj +++ b/AutoUpdate/CKAN-autoupdate.csproj @@ -16,10 +16,12 @@ true Debug;Release false - 7 + 9 + enable + true ..\assets\ckan.ico - net48;net7.0-windows - true + net48;net8.0-windows + true true 512 prompt diff --git a/AutoUpdate/Main.cs b/AutoUpdate/Main.cs index df9d915005..4d384dc32f 100644 --- a/AutoUpdate/Main.cs +++ b/AutoUpdate/Main.cs @@ -4,6 +4,9 @@ using System.Runtime.InteropServices; using System.Threading; using System.Windows.Forms; +#if NET5_0_OR_GREATER +using System.Runtime.Versioning; +#endif /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * CKAN AUTO-UPDATE TOOL @@ -145,7 +148,7 @@ private static void MakeExecutable(string path) { UseShellExecute = false }); - permsprocess.WaitForExit(); + permsprocess?.WaitForExit(); } } @@ -158,8 +161,17 @@ private static bool IsOnMono /// /// Are we on Windows? /// - private static bool IsOnWindows - => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + #if NET6_0_OR_GREATER + [SupportedOSPlatformGuard("windows")] + #endif + private static readonly bool IsOnWindows + = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + #if NET8_0_OR_GREATER + [SupportedOSPlatformGuard("windows6.1")] + private static readonly bool IsOnWindows61 + = IsOnWindows && OperatingSystem.IsWindowsVersionAtLeast(6, 1); + #endif /// /// Display unexpected exceptions to user @@ -179,11 +191,20 @@ private static void ReportError(string message, params object[] args) { string err = string.Format(message, args); Console.Error.WriteLine(err); - if (fromGui) + #if NETFRAMEWORK || WINDOWS + if ( + #if NET8_0_OR_GREATER + IsOnWindows61 && + #endif + fromGui) { // Show a popup in case the console isn't open - MessageBox.Show(err, Properties.Resources.FatalErrorTitle, MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show(err, + Properties.Resources.FatalErrorTitle, + MessageBoxButtons.OK, + MessageBoxIcon.Error); } + #endif } private const int maxRetries = 8; diff --git a/AutoUpdate/SingleAssemblyResourceManager.cs b/AutoUpdate/SingleAssemblyResourceManager.cs index 3ee0990a59..b6cf720637 100644 --- a/AutoUpdate/SingleAssemblyResourceManager.cs +++ b/AutoUpdate/SingleAssemblyResourceManager.cs @@ -1,4 +1,3 @@ -using System.IO; using System.Globalization; using System.Resources; using System.Reflection; @@ -14,16 +13,13 @@ public SingleAssemblyResourceManager(string basename, Assembly assembly) : base( { } - protected override ResourceSet InternalGetResourceSet(CultureInfo culture, + protected override ResourceSet? InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents) { - if (!myResourceSets.TryGetValue(culture, out ResourceSet rs) && createIfNotExists) + if (!myResourceSets.TryGetValue(culture, out ResourceSet? rs) && createIfNotExists && MainAssembly != null) { // Lazy-load default language (without caring about duplicate assignment in race conditions, no harm done) - if (neutralResourcesCulture == null) - { - neutralResourcesCulture = GetNeutralResourcesLanguage(MainAssembly); - } + neutralResourcesCulture ??= GetNeutralResourcesLanguage(MainAssembly); // If we're asking for the default language, then ask for the // invariant (non-specific) resources. @@ -33,7 +29,7 @@ protected override ResourceSet InternalGetResourceSet(CultureInfo culture, } string resourceFileName = GetResourceFileName(culture); - Stream store = MainAssembly.GetManifestResourceStream(resourceFileName); + var store = MainAssembly.GetManifestResourceStream(resourceFileName); // If we found the appropriate resources in the local assembly if (store != null) @@ -50,7 +46,7 @@ protected override ResourceSet InternalGetResourceSet(CultureInfo culture, return rs; } - private CultureInfo neutralResourcesCulture; + private CultureInfo? neutralResourcesCulture; private readonly Dictionary myResourceSets = new Dictionary(); } } diff --git a/Cmdline/Action/AuthToken.cs b/Cmdline/Action/AuthToken.cs index 3850a2923b..03edb02694 100644 --- a/Cmdline/Action/AuthToken.cs +++ b/Cmdline/Action/AuthToken.cs @@ -26,7 +26,9 @@ public AuthToken() { } /// /// Exit code /// - public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCommandOptions unparsed) + public int RunSubCommand(GameInstanceManager? manager, + CommonOptions? opts, + SubCommandOptions unparsed) { string[] args = unparsed.options.ToArray(); int exitCode = Exit.OK; @@ -37,12 +39,9 @@ public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCom { CommonOptions options = (CommonOptions)suboptions; options.Merge(opts); - user = new ConsoleUser(options.Headless); - if (manager == null) - { - manager = new GameInstanceManager(user); - } - exitCode = options.Handle(manager, user); + user = new ConsoleUser(options.Headless); + manager ??= new GameInstanceManager(user); + exitCode = options.Handle(manager, user); if (exitCode == Exit.OK) { switch (option) @@ -75,7 +74,7 @@ private int listAuthTokens() foreach (string host in hosts) { longestHostLen = Math.Max(longestHostLen, host.Length); - if (ServiceLocator.Container.Resolve().TryGetAuthToken(host, out string token)) + if (ServiceLocator.Container.Resolve().TryGetAuthToken(host, out string? token)) { longestTokenLen = Math.Max(longestTokenLen, token.Length); } @@ -83,16 +82,15 @@ private int listAuthTokens() // Create format string: {0,-longestHostLen} {1,-longestTokenLen} string fmt = string.Format("{0}0,-{2}{1} {0}1,-{3}{1}", "{", "}", longestHostLen, longestTokenLen); - user.RaiseMessage(fmt, hostHeader, tokenHeader); - user.RaiseMessage(fmt, - new string('-', longestHostLen), - new string('-', longestTokenLen) - ); + user?.RaiseMessage(fmt, hostHeader, tokenHeader); + user?.RaiseMessage(fmt, + new string('-', longestHostLen), + new string('-', longestTokenLen)); foreach (string host in hosts) { - if (ServiceLocator.Container.Resolve().TryGetAuthToken(host, out string token)) + if (ServiceLocator.Container.Resolve().TryGetAuthToken(host, out string? token)) { - user.RaiseMessage(fmt, host, token); + user?.RaiseMessage(fmt, host, token); } } } @@ -101,36 +99,42 @@ private int listAuthTokens() private int addAuthToken(AddAuthTokenOptions opts) { - if (Uri.CheckHostName(opts.host) != UriHostNameType.Unknown) - { - ServiceLocator.Container.Resolve().SetAuthToken(opts.host, opts.token); - } - else + if (opts.host is string h) { - user.RaiseError(Properties.Resources.AuthTokenInvalidHostName, opts.host); + if (Uri.CheckHostName(h) != UriHostNameType.Unknown) + { + ServiceLocator.Container.Resolve().SetAuthToken(h, opts.token); + } + else + { + user?.RaiseError(Properties.Resources.AuthTokenInvalidHostName, h); + } } return Exit.OK; } private int removeAuthToken(RemoveAuthTokenOptions opts) { - ServiceLocator.Container.Resolve().SetAuthToken(opts.host, null); + if (opts.host is string h) + { + ServiceLocator.Container.Resolve().SetAuthToken(h, null); + } return Exit.OK; } - private IUser user; + private IUser? user; } internal class AuthTokenSubOptions : VerbCommandOptions { [VerbOption("list", HelpText = "List auth tokens")] - public CommonOptions ListOptions { get; set; } + public CommonOptions? ListOptions { get; set; } [VerbOption("add", HelpText = "Add an auth token")] - public AddAuthTokenOptions AddOptions { get; set; } + public AddAuthTokenOptions? AddOptions { get; set; } [VerbOption("remove", HelpText = "Delete an auth token")] - public RemoveAuthTokenOptions RemoveOptions { get; set; } + public RemoveAuthTokenOptions? RemoveOptions { get; set; } [HelpVerbOption] public string GetUsage(string verb) @@ -173,13 +177,13 @@ public static IEnumerable GetHelp(string verb) internal class AddAuthTokenOptions : CommonOptions { - [ValueOption(0)] public string host { get; set; } - [ValueOption(1)] public string token { get; set; } + [ValueOption(0)] public string? host { get; set; } + [ValueOption(1)] public string? token { get; set; } } internal class RemoveAuthTokenOptions : CommonOptions { - [ValueOption(0)] public string host { get; set; } + [ValueOption(0)] public string? host { get; set; } } } diff --git a/Cmdline/Action/Cache.cs b/Cmdline/Action/Cache.cs index a8c4faaed5..2db6343dcb 100644 --- a/Cmdline/Action/Cache.cs +++ b/Cmdline/Action/Cache.cs @@ -11,22 +11,22 @@ namespace CKAN.CmdLine public class CacheSubOptions : VerbCommandOptions { [VerbOption("list", HelpText = "List the download cache path")] - public CommonOptions ListOptions { get; set; } + public CommonOptions? ListOptions { get; set; } [VerbOption("set", HelpText = "Set the download cache path")] - public SetOptions SetOptions { get; set; } + public SetOptions? SetOptions { get; set; } [VerbOption("clear", HelpText = "Clear the download cache directory")] - public CommonOptions ClearOptions { get; set; } + public CommonOptions? ClearOptions { get; set; } [VerbOption("reset", HelpText = "Set the download cache path to the default")] - public CommonOptions ResetOptions { get; set; } + public CommonOptions? ResetOptions { get; set; } [VerbOption("showlimit", HelpText = "Show the cache size limit")] - public CommonOptions ShowLimitOptions { get; set; } + public CommonOptions? ShowLimitOptions { get; set; } [VerbOption("setlimit", HelpText = "Set the cache size limit")] - public SetLimitOptions SetLimitOptions { get; set; } + public SetLimitOptions? SetLimitOptions { get; set; } [HelpVerbOption] public string GetUsage(string verb) @@ -77,7 +77,7 @@ public static IEnumerable GetHelp(string verb) public class SetOptions : CommonOptions { [ValueOption(0)] - public string Path { get; set; } + public string? Path { get; set; } } public class SetLimitOptions : CommonOptions @@ -99,7 +99,9 @@ public Cache() { } /// /// Exit code for shell environment /// - public int RunSubCommand(GameInstanceManager mgr, CommonOptions opts, SubCommandOptions unparsed) + public int RunSubCommand(GameInstanceManager? mgr, + CommonOptions? opts, + SubCommandOptions unparsed) { string[] args = unparsed.options.ToArray(); @@ -159,7 +161,7 @@ public int RunSubCommand(GameInstanceManager mgr, CommonOptions opts, SubCommand private int ListCacheDirectory() { IConfiguration cfg = ServiceLocator.Container.Resolve(); - user.RaiseMessage(cfg.DownloadCacheDir); + user?.RaiseMessage("{0}", cfg.DownloadCacheDir ?? ""); printCacheInfo(); return Exit.OK; } @@ -168,44 +170,51 @@ private int SetCacheDirectory(SetOptions options) { if (string.IsNullOrEmpty(options.Path)) { - user.RaiseError(Properties.Resources.ArgumentMissing); + user?.RaiseError(Properties.Resources.ArgumentMissing); PrintUsage("set"); return Exit.BADOPT; } - if (manager.TrySetupCache(options.Path, out string failReason)) + if (manager != null) { - IConfiguration cfg = ServiceLocator.Container.Resolve(); - user.RaiseMessage(Properties.Resources.CacheSet, cfg.DownloadCacheDir); - printCacheInfo(); - return Exit.OK; - } - else - { - user.RaiseError(Properties.Resources.CacheInvalidPath, failReason); - return Exit.BADOPT; + if (manager.TrySetupCache(options.Path, out string? failReason)) + { + IConfiguration cfg = ServiceLocator.Container.Resolve(); + user?.RaiseMessage(Properties.Resources.CacheSet, cfg.DownloadCacheDir ?? ""); + printCacheInfo(); + return Exit.OK; + } + else + { + user?.RaiseError(Properties.Resources.CacheInvalidPath, failReason); + return Exit.BADOPT; + } } + return Exit.ERROR; } private int ClearCacheDirectory() { - manager.Cache.RemoveAll(); - user.RaiseMessage(Properties.Resources.CacheCleared); + manager?.Cache?.RemoveAll(); + user?.RaiseMessage(Properties.Resources.CacheCleared); printCacheInfo(); return Exit.OK; } private int ResetCacheDirectory() { - if (manager.TrySetupCache("", out string failReason)) + if (manager != null) { - IConfiguration cfg = ServiceLocator.Container.Resolve(); - user.RaiseMessage(Properties.Resources.CacheReset, cfg.DownloadCacheDir); - printCacheInfo(); - } - else - { - user.RaiseError(Properties.Resources.CacheResetFailed, failReason); + if (manager.TrySetupCache("", out string? failReason)) + { + IConfiguration cfg = ServiceLocator.Container.Resolve(); + user?.RaiseMessage(Properties.Resources.CacheReset, cfg.DownloadCacheDir ?? ""); + printCacheInfo(); + } + else + { + user?.RaiseError(Properties.Resources.CacheResetFailed, failReason); + } } return Exit.OK; } @@ -215,11 +224,11 @@ private int ShowCacheSizeLimit() IConfiguration cfg = ServiceLocator.Container.Resolve(); if (cfg.CacheSizeLimit.HasValue) { - user.RaiseMessage(CkanModule.FmtSize(cfg.CacheSizeLimit.Value)); + user?.RaiseMessage(CkanModule.FmtSize(cfg.CacheSizeLimit.Value)); } else { - user.RaiseMessage(Properties.Resources.CacheUnlimited); + user?.RaiseMessage(Properties.Resources.CacheUnlimited); } return Exit.OK; } @@ -228,30 +237,33 @@ private int SetCacheSizeLimit(SetLimitOptions options) { IConfiguration cfg = ServiceLocator.Container.Resolve(); cfg.CacheSizeLimit = options.Megabytes < 0 - ? null : - (long?)(options.Megabytes * 1024 * 1024); + ? null + : (options.Megabytes * 1024 * 1024); return ShowCacheSizeLimit(); } private void printCacheInfo() { - manager.Cache.GetSizeInfo(out int fileCount, out long bytes, out long bytesFree); - user.RaiseMessage(Properties.Resources.CacheInfo, - fileCount, - CkanModule.FmtSize(bytes), - CkanModule.FmtSize(bytesFree)); + if (manager?.Cache != null) + { + manager.Cache.GetSizeInfo(out int fileCount, out long bytes, out long bytesFree); + user?.RaiseMessage(Properties.Resources.CacheInfo, + fileCount, + CkanModule.FmtSize(bytes), + CkanModule.FmtSize(bytesFree)); + } } private void PrintUsage(string verb) { foreach (var h in CacheSubOptions.GetHelp(verb)) { - user.RaiseError(h); + user?.RaiseError(h); } } - private IUser user; - private GameInstanceManager manager; + private IUser? user; + private GameInstanceManager? manager; } } diff --git a/Cmdline/Action/Compare.cs b/Cmdline/Action/Compare.cs index 85d6c028b3..495b010693 100644 --- a/Cmdline/Action/Compare.cs +++ b/Cmdline/Action/Compare.cs @@ -65,8 +65,8 @@ internal class CompareOptions : CommonOptions [Option("machine-readable", HelpText = "Output in a machine readable format: -1, 0 or 1")] public bool machine_readable { get; set;} - [ValueOption(0)] public string Left { get; set; } - [ValueOption(1)] public string Right { get; set; } + [ValueOption(0)] public string? Left { get; set; } + [ValueOption(1)] public string? Right { get; set; } } } diff --git a/Cmdline/Action/Compat.cs b/Cmdline/Action/Compat.cs index c6380e97e9..a89673f140 100644 --- a/Cmdline/Action/Compat.cs +++ b/Cmdline/Action/Compat.cs @@ -12,19 +12,19 @@ namespace CKAN.CmdLine public class CompatSubOptions : VerbCommandOptions { [VerbOption("list", HelpText = "List compatible game versions")] - public CompatListOptions List { get; set; } + public CompatListOptions? List { get; set; } [VerbOption("clear", HelpText = "Forget all compatible game versions")] - public CompatClearOptions Clear { get; set; } + public CompatClearOptions? Clear { get; set; } [VerbOption("add", HelpText = "Add versions to compatible game versions list")] - public CompatAddOptions Add { get; set; } + public CompatAddOptions? Add { get; set; } [VerbOption("forget", HelpText = "Forget compatible game versions")] - public CompatForgetOptions Forget { get; set; } + public CompatForgetOptions? Forget { get; set; } [VerbOption("set", HelpText = "Set the compatible game versions list")] - public CompatSetOptions Set { get; set; } + public CompatSetOptions? Set { get; set; } [HelpVerbOption] public string GetUsage(string verb) @@ -76,26 +76,26 @@ public class CompatClearOptions : InstanceSpecificOptions { } public class CompatAddOptions : InstanceSpecificOptions { [ValueList(typeof(List))] - public List Versions { get; set; } + public List? Versions { get; set; } } public class CompatForgetOptions : InstanceSpecificOptions { [ValueList(typeof(List))] - public List Versions { get; set; } + public List? Versions { get; set; } } public class CompatSetOptions : InstanceSpecificOptions { [ValueList(typeof(List))] - public List Versions { get; set; } + public List? Versions { get; set; } } public class Compat : ISubCommand { - public int RunSubCommand(GameInstanceManager mgr, - CommonOptions opts, - SubCommandOptions options) + public int RunSubCommand(GameInstanceManager? mgr, + CommonOptions? opts, + SubCommandOptions options) { var exitCode = Exit.OK; @@ -129,21 +129,21 @@ public int RunSubCommand(GameInstanceManager mgr, break; case "add": - exitCode = Add(suboptions as CompatAddOptions, + exitCode = Add((CompatAddOptions)suboptions, MainClass.GetGameInstance(manager)) ? Exit.OK : Exit.ERROR; break; case "forget": - exitCode = Forget(suboptions as CompatForgetOptions, + exitCode = Forget((CompatForgetOptions)suboptions, MainClass.GetGameInstance(manager)) ? Exit.OK : Exit.ERROR; break; case "set": - exitCode = Set(suboptions as CompatSetOptions, + exitCode = Set((CompatSetOptions)suboptions, MainClass.GetGameInstance(manager)) ? Exit.OK : Exit.ERROR; @@ -162,46 +162,42 @@ private bool List(CKAN.GameInstance instance) { var versionHeader = Properties.Resources.CompatVersionHeader; var actualHeader = Properties.Resources.CompatActualHeader; - var output = Enumerable.Repeat(new - { - Version = instance.Version(), - Actual = true, - }, + var output = (instance.Version() is GameVersion v + ? Enumerable.Repeat((Version: v, + Actual: true), 1) + : Enumerable.Empty<(GameVersion Version, bool Actual)>()) .Concat(instance.GetCompatibleVersions() .OrderByDescending(v => v) - .Select(v => new - { - Version = v, - Actual = false, - })) + .Select(v => (Version: v, + Actual: false))) .ToList(); var versionWidth = Enumerable.Repeat(versionHeader, 1) .Concat(output.Select(i => i.Version.ToString())) - .Max(i => i.Length); + .Max(i => i?.Length ?? 0); var actualWidth = Enumerable.Repeat(actualHeader, 1) .Concat(output.Select(i => i.Actual.ToString())) - .Max(i => i.Length); + .Max(i => i?.Length ?? 0); const string columnFormat = "{0} {1}"; - user.RaiseMessage(columnFormat, - versionHeader.PadRight(versionWidth), - actualHeader.PadRight(actualWidth)); + user?.RaiseMessage(columnFormat, + versionHeader.PadRight(versionWidth), + actualHeader.PadRight(actualWidth)); - user.RaiseMessage(columnFormat, - new string('-', versionWidth), - new string('-', actualWidth)); + user?.RaiseMessage(columnFormat, + new string('-', versionWidth), + new string('-', actualWidth)); foreach (var line in output) { - user.RaiseMessage(columnFormat, - line.Version.ToString() - .PadRight(versionWidth), - line.Actual.ToString() - .PadRight(actualWidth)); + user?.RaiseMessage(columnFormat, + (line.Version.ToString() ?? "") + .PadRight(versionWidth), + line.Actual.ToString() + .PadRight(actualWidth)); } return true; } @@ -216,9 +212,9 @@ private bool Clear(CKAN.GameInstance instance) private bool Add(CompatAddOptions addOptions, CKAN.GameInstance instance) { - if (addOptions.Versions.Count < 1) + if (addOptions.Versions?.Count < 1) { - user.RaiseError(Properties.Resources.CompatMissing); + user?.RaiseError(Properties.Resources.CompatMissing); PrintUsage("add"); return false; } @@ -226,7 +222,7 @@ private bool Add(CompatAddOptions addOptions, out GameVersion[] goodVers, out string[] badVers)) { - user.RaiseError(Properties.Resources.CompatInvalid, + user?.RaiseError(Properties.Resources.CompatInvalid, string.Join(", ", badVers)); return false; } @@ -241,9 +237,9 @@ private bool Add(CompatAddOptions addOptions, private bool Forget(CompatForgetOptions forgetOptions, CKAN.GameInstance instance) { - if (forgetOptions.Versions.Count < 1) + if (forgetOptions.Versions?.Count < 1) { - user.RaiseError(Properties.Resources.CompatMissing); + user?.RaiseError(Properties.Resources.CompatMissing); PrintUsage("forget"); return false; } @@ -251,18 +247,19 @@ private bool Forget(CompatForgetOptions forgetOptions, out GameVersion[] goodVers, out string[] badVers)) { - user.RaiseError(Properties.Resources.CompatInvalid, - string.Join(", ", badVers)); + user?.RaiseError(Properties.Resources.CompatInvalid, + string.Join(", ", badVers)); return false; } - var rmActualVers = goodVers.Intersect(new GameVersion[] { instance.Version(), - instance.Version().WithoutBuild }) + var rmActualVers = goodVers.Intersect(instance.Version() is GameVersion gv + ? new GameVersion[] { gv, gv.WithoutBuild } + : Array.Empty()) .Select(gv => gv.ToString()) .ToArray(); if (rmActualVers.Length > 0) { - user.RaiseError(Properties.Resources.CompatCantForget, - string.Join(", ", rmActualVers)); + user?.RaiseError(Properties.Resources.CompatCantForget, + string.Join(", ", rmActualVers)); return false; } instance.SetCompatibleVersions(instance.GetCompatibleVersions() @@ -275,9 +272,9 @@ private bool Forget(CompatForgetOptions forgetOptions, private bool Set(CompatSetOptions setOptions, CKAN.GameInstance instance) { - if (setOptions.Versions.Count < 1) + if (setOptions.Versions?.Count < 1) { - user.RaiseError(Properties.Resources.CompatMissing); + user?.RaiseError(Properties.Resources.CompatMissing); PrintUsage("set"); return false; } @@ -285,8 +282,8 @@ private bool Set(CompatSetOptions setOptions, out GameVersion[] goodVers, out string[] badVers)) { - user.RaiseError(Properties.Resources.CompatInvalid, - string.Join(", ", badVers)); + user?.RaiseError(Properties.Resources.CompatInvalid, + string.Join(", ", badVers)); return false; } instance.SetCompatibleVersions(goodVers.Distinct().ToList()); @@ -298,18 +295,18 @@ private void PrintUsage(string verb) { foreach (var h in CompatSubOptions.GetHelp(verb)) { - user.RaiseError(h); + user?.RaiseError(h); } } - private static bool TryParseVersions(IEnumerable versions, - out GameVersion[] goodVers, - out string[] badVers) + private static bool TryParseVersions(IEnumerable? versions, + out GameVersion[] goodVers, + out string[] badVers) { - var gameVersions = versions - .Select(v => GameVersion.TryParse(v, out GameVersion gv) - ? new Tuple(v, gv) - : new Tuple(v, null)) + var gameVersions = (versions ?? Enumerable.Empty()) + .Select(v => GameVersion.TryParse(v, out GameVersion? gv) + ? new Tuple(v, gv) + : new Tuple(v, null)) .ToArray(); goodVers = gameVersions.Select(tuple => tuple.Item2) .OfType() @@ -320,7 +317,7 @@ private static bool TryParseVersions(IEnumerable versions, return badVers.Length < 1; } - private IUser user; - private GameInstanceManager manager; + private IUser? user; + private GameInstanceManager? manager; } } diff --git a/Cmdline/Action/Filter.cs b/Cmdline/Action/Filter.cs index 253edd115f..565700ecd6 100644 --- a/Cmdline/Action/Filter.cs +++ b/Cmdline/Action/Filter.cs @@ -28,7 +28,9 @@ public Filter() { } /// /// Exit code /// - public int RunSubCommand(GameInstanceManager mgr, CommonOptions opts, SubCommandOptions unparsed) + public int RunSubCommand(GameInstanceManager? mgr, + CommonOptions? opts, + SubCommandOptions unparsed) { string[] args = unparsed.options.ToArray(); int exitCode = Exit.OK; @@ -74,39 +76,43 @@ public int RunSubCommand(GameInstanceManager mgr, CommonOptions opts, SubCommand private int ListFilters(FilterListOptions opts) { - int exitCode = opts.Handle(manager, user); + int exitCode = manager != null && user != null + ? opts.Handle(manager, user) + : Exit.ERROR; if (exitCode != Exit.OK) { return exitCode; } var cfg = ServiceLocator.Container.Resolve(); - user.RaiseMessage(Properties.Resources.FilterListGlobalHeader); + user?.RaiseMessage(Properties.Resources.FilterListGlobalHeader); foreach (string filter in cfg.GlobalInstallFilters) { - user.RaiseMessage("\t- {0}", filter); + user?.RaiseMessage("\t- {0}", filter); } - user.RaiseMessage(""); + user?.RaiseMessage(""); var instance = MainClass.GetGameInstance(manager); - user.RaiseMessage(Properties.Resources.FilterListInstanceHeader); + user?.RaiseMessage(Properties.Resources.FilterListInstanceHeader); foreach (string filter in instance.InstallFilters) { - user.RaiseMessage("\t- {0}", filter); + user?.RaiseMessage("\t- {0}", filter); } return Exit.OK; } private int AddFilters(FilterAddOptions opts, string verb) { - if (opts.filters.Count < 1) + if (opts.filters?.Count < 1) { - user.RaiseError(Properties.Resources.ArgumentMissing); + user?.RaiseError(Properties.Resources.ArgumentMissing); PrintUsage(verb); return Exit.BADOPT; } - int exitCode = opts.Handle(manager, user); + int exitCode = manager != null && user != null + ? opts.Handle(manager, user) + : Exit.ERROR; if (exitCode != Exit.OK) { return exitCode; @@ -116,20 +122,18 @@ private int AddFilters(FilterAddOptions opts, string verb) { var cfg = ServiceLocator.Container.Resolve(); var duplicates = cfg.GlobalInstallFilters - .Intersect(opts.filters) + .Intersect(opts.filters ?? new List { }) .ToArray(); if (duplicates.Length > 0) { - user.RaiseError( - Properties.Resources.FilterAddGlobalDuplicateError, - string.Join(", ", duplicates) - ); + user?.RaiseError(Properties.Resources.FilterAddGlobalDuplicateError, + string.Join(", ", duplicates)); return Exit.BADOPT; } else { cfg.GlobalInstallFilters = cfg.GlobalInstallFilters - .Concat(opts.filters) + .Concat(opts.filters ?? new List { }) .Distinct() .ToArray(); } @@ -138,20 +142,18 @@ private int AddFilters(FilterAddOptions opts, string verb) { var instance = MainClass.GetGameInstance(manager); var duplicates = instance.InstallFilters - .Intersect(opts.filters) + .Intersect(opts.filters ?? new List { }) .ToArray(); if (duplicates.Length > 0) { - user.RaiseError( - Properties.Resources.FilterAddInstanceDuplicateError, - string.Join(", ", duplicates) - ); + user?.RaiseError(Properties.Resources.FilterAddInstanceDuplicateError, + string.Join(", ", duplicates)); return Exit.BADOPT; } else { instance.InstallFilters = instance.InstallFilters - .Concat(opts.filters) + .Concat(opts.filters ?? new List { }) .Distinct() .ToArray(); } @@ -161,14 +163,16 @@ private int AddFilters(FilterAddOptions opts, string verb) private int RemoveFilters(FilterRemoveOptions opts, string verb) { - if (opts.filters.Count < 1) + if (opts.filters?.Count < 1) { - user.RaiseError(Properties.Resources.ArgumentMissing); + user?.RaiseError(Properties.Resources.ArgumentMissing); PrintUsage(verb); return Exit.BADOPT; } - int exitCode = opts.Handle(manager, user); + int exitCode = manager != null && user != null + ? opts.Handle(manager, user) + : Exit.ERROR; if (exitCode != Exit.OK) { return exitCode; @@ -177,43 +181,40 @@ private int RemoveFilters(FilterRemoveOptions opts, string verb) if (opts.global) { var cfg = ServiceLocator.Container.Resolve(); - var notFound = opts.filters + var notFound = (opts.filters ?? new List { }) .Except(cfg.GlobalInstallFilters) .ToArray(); if (notFound.Length > 0) { - user.RaiseError( + user?.RaiseError( Properties.Resources.FilterRemoveGlobalNotFoundError, - string.Join(", ", notFound) - ); + string.Join(", ", notFound)); return Exit.BADOPT; } else { cfg.GlobalInstallFilters = cfg.GlobalInstallFilters - .Except(opts.filters) + .Except(opts.filters ?? new List { }) .ToArray(); } } else { var instance = MainClass.GetGameInstance(manager); - var notFound = opts.filters + var notFound = (opts.filters ?? new List { }) .Except(instance.InstallFilters) .ToArray(); if (notFound.Length > 0) { - user.RaiseError( - Properties.Resources.FilterRemoveInstanceNotFoundError, - string.Join(", ", notFound) - ); + user?.RaiseError(Properties.Resources.FilterRemoveInstanceNotFoundError, + string.Join(", ", notFound)); return Exit.BADOPT; } else { instance.InstallFilters = instance.InstallFilters - .Except(opts.filters) - .ToArray(); + .Except(opts.filters ?? new List { }) + .ToArray(); } } return Exit.OK; @@ -223,24 +224,24 @@ private void PrintUsage(string verb) { foreach (var h in FilterSubOptions.GetHelp(verb)) { - user.RaiseError(h); + user?.RaiseError(h); } } - private GameInstanceManager manager; - private IUser user; + private GameInstanceManager? manager; + private IUser? user; } internal class FilterSubOptions : VerbCommandOptions { [VerbOption("list", HelpText = "List install filters")] - public FilterListOptions FilterListOptions { get; set; } + public FilterListOptions? FilterListOptions { get; set; } [VerbOption("add", HelpText = "Add install filters")] - public FilterAddOptions FilterAddOptions { get; set; } + public FilterAddOptions? FilterAddOptions { get; set; } [VerbOption("remove", HelpText = "Remove install filters")] - public FilterRemoveOptions FilterRemoveOptions { get; set; } + public FilterRemoveOptions? FilterRemoveOptions { get; set; } [HelpVerbOption] public string GetUsage(string verb) @@ -293,7 +294,7 @@ internal class FilterAddOptions : InstanceSpecificOptions public bool global { get; set; } [ValueList(typeof(List))] - public List filters { get; set; } + public List? filters { get; set; } } internal class FilterRemoveOptions : InstanceSpecificOptions @@ -302,7 +303,7 @@ internal class FilterRemoveOptions : InstanceSpecificOptions public bool global { get; set; } [ValueList(typeof(List))] - public List filters { get; set; } + public List? filters { get; set; } } } diff --git a/Cmdline/Action/GameInstance.cs b/Cmdline/Action/GameInstance.cs index 335d909a98..c2945afe84 100644 --- a/Cmdline/Action/GameInstance.cs +++ b/Cmdline/Action/GameInstance.cs @@ -16,25 +16,25 @@ namespace CKAN.CmdLine internal class InstanceSubOptions : VerbCommandOptions { [VerbOption("list", HelpText = "List game instances")] - public CommonOptions ListOptions { get; set; } + public CommonOptions? ListOptions { get; set; } [VerbOption("add", HelpText = "Add a game instance")] - public AddOptions AddOptions { get; set; } + public AddOptions? AddOptions { get; set; } [VerbOption("clone", HelpText = "Clone an existing game instance")] - public CloneOptions CloneOptions { get; set; } + public CloneOptions? CloneOptions { get; set; } [VerbOption("rename", HelpText = "Rename a game instance")] - public RenameOptions RenameOptions { get; set; } + public RenameOptions? RenameOptions { get; set; } [VerbOption("forget", HelpText = "Forget a game instance")] - public ForgetOptions ForgetOptions { get; set; } + public ForgetOptions? ForgetOptions { get; set; } [VerbOption("default", HelpText = "Set the default game instance")] - public DefaultOptions DefaultOptions { get; set; } + public DefaultOptions? DefaultOptions { get; set; } [VerbOption("fake", HelpText = "Fake a game instance")] - public FakeOptions FakeOptions { get; set; } + public FakeOptions? FakeOptions { get; set; } [HelpVerbOption] public string GetUsage(string verb) @@ -99,8 +99,8 @@ public static IEnumerable GetHelp(string verb) internal class AddOptions : CommonOptions { - [ValueOption(0)] public string name { get; set; } - [ValueOption(1)] public string path { get; set; } + [ValueOption(0)] public string? name { get; set; } + [ValueOption(1)] public string? path { get; set; } } internal class CloneOptions : CommonOptions @@ -108,43 +108,43 @@ internal class CloneOptions : CommonOptions [Option("share-stock", DefaultValue = false, HelpText = "Use junction points (Windows) or symbolic links (Unix) for stock dirs instead of copying")] public bool shareStock { get; set; } - [ValueOption(0)] public string nameOrPath { get; set; } - [ValueOption(1)] public string new_name { get; set; } - [ValueOption(2)] public string new_path { get; set; } + [ValueOption(0)] public string? nameOrPath { get; set; } + [ValueOption(1)] public string? new_name { get; set; } + [ValueOption(2)] public string? new_path { get; set; } } internal class RenameOptions : CommonOptions { [GameInstances] - [ValueOption(0)] public string old_name { get; set; } - [ValueOption(1)] public string new_name { get; set; } + [ValueOption(0)] public string? old_name { get; set; } + [ValueOption(1)] public string? new_name { get; set; } } internal class ForgetOptions : CommonOptions { [GameInstances] - [ValueOption(0)] public string name { get; set; } + [ValueOption(0)] public string? name { get; set; } } internal class DefaultOptions : CommonOptions { [GameInstances] - [ValueOption(0)] public string name { get; set; } + [ValueOption(0)] public string? name { get; set; } } internal class FakeOptions : CommonOptions { - [ValueOption(0)] public string name { get; set; } - [ValueOption(1)] public string path { get; set; } - [ValueOption(2)] public string version { get; set; } + [ValueOption(0)] public string? name { get; set; } + [ValueOption(1)] public string? path { get; set; } + [ValueOption(2)] public string? version { get; set; } [Option("game", DefaultValue = "KSP", HelpText = "The game of the instance to be faked, either KSP or KSP2")] - public string gameId { get; set; } + public string? gameId { get; set; } [Option("MakingHistory", DefaultValue = "none", HelpText = "The version of the Making History DLC to be faked.")] - public string makingHistoryVersion { get; set; } + public string? makingHistoryVersion { get; set; } [Option("BreakingGround", DefaultValue = "none", HelpText = "The version of the Breaking Ground DLC to be faked.")] - public string breakingGroundVersion { get; set; } + public string? breakingGroundVersion { get; set; } [Option("set-default", DefaultValue = false, HelpText = "Set the new instance as the default one.")] public bool setDefault { get; set; } @@ -156,7 +156,9 @@ public GameInstance() { } protected static readonly ILog log = LogManager.GetLogger(typeof(GameInstance)); // This is required by ISubCommand - public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCommandOptions unparsed) + public int RunSubCommand(GameInstanceManager? manager, + CommonOptions? opts, + SubCommandOptions unparsed) { string[] args = unparsed.options.ToArray(); @@ -235,14 +237,14 @@ public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCom return exitCode; } - private IUser user { get; set; } - private GameInstanceManager Manager { get; set; } + private IUser? user { get; set; } + private GameInstanceManager? Manager { get; set; } #region option functions private int ListInstalls() { - var output = Manager.Instances + var output = Manager?.Instances .OrderByDescending(i => i.Value.Name == Manager.AutoStartInstance) .ThenByDescending(i => i.Value.Version() ?? GameVersion.Any) .ThenBy(i => i.Key) @@ -257,70 +259,74 @@ private int ListInstalls() }) .ToList(); - string nameHeader = Properties.Resources.InstanceListNameHeader; - string versionHeader = Properties.Resources.InstanceListVersionHeader; - string defaultHeader = Properties.Resources.InstanceListDefaultHeader; - string pathHeader = Properties.Resources.InstanceListPathHeader; + if (output != null) + { + string nameHeader = Properties.Resources.InstanceListNameHeader; + string versionHeader = Properties.Resources.InstanceListVersionHeader; + string defaultHeader = Properties.Resources.InstanceListDefaultHeader; + string pathHeader = Properties.Resources.InstanceListPathHeader; - var nameWidth = Enumerable.Repeat(nameHeader, 1).Concat(output.Select(i => i.Name)).Max(i => i.Length); - var versionWidth = Enumerable.Repeat(versionHeader, 1).Concat(output.Select(i => i.Version)).Max(i => i.Length); - var defaultWidth = Enumerable.Repeat(defaultHeader, 1).Concat(output.Select(i => i.Default)).Max(i => i.Length); - var pathWidth = Enumerable.Repeat(pathHeader, 1).Concat(output.Select(i => i.Path)).Max(i => i.Length); + var nameWidth = Enumerable.Repeat(nameHeader, 1).Concat(output.Select(i => i.Name)).Max(i => i.Length); + var versionWidth = Enumerable.Repeat(versionHeader, 1).Concat(output.Select(i => i.Version)).Max(i => i.Length); + var defaultWidth = Enumerable.Repeat(defaultHeader, 1).Concat(output.Select(i => i.Default)).Max(i => i.Length); + var pathWidth = Enumerable.Repeat(pathHeader, 1).Concat(output.Select(i => i.Path)).Max(i => i.Length); - const string columnFormat = "{0} {1} {2} {3}"; + const string columnFormat = "{0} {1} {2} {3}"; - user.RaiseMessage(columnFormat, - nameHeader.PadRight(nameWidth), - versionHeader.PadRight(versionWidth), - defaultHeader.PadRight(defaultWidth), - pathHeader.PadRight(pathWidth) - ); + user?.RaiseMessage(columnFormat, + nameHeader.PadRight(nameWidth), + versionHeader.PadRight(versionWidth), + defaultHeader.PadRight(defaultWidth), + pathHeader.PadRight(pathWidth)); - user.RaiseMessage(columnFormat, - new string('-', nameWidth), - new string('-', versionWidth), - new string('-', defaultWidth), - new string('-', pathWidth) - ); + user?.RaiseMessage(columnFormat, + new string('-', nameWidth), + new string('-', versionWidth), + new string('-', defaultWidth), + new string('-', pathWidth)); - foreach (var line in output) - { - user.RaiseMessage(columnFormat, - line.Name.PadRight(nameWidth), - line.Version.PadRight(versionWidth), - line.Default.PadRight(defaultWidth), - line.Path.PadRight(pathWidth) - ); - } + foreach (var line in output) + { + user?.RaiseMessage(columnFormat, + line.Name.PadRight(nameWidth), + line.Version.PadRight(versionWidth), + line.Default.PadRight(defaultWidth), + line.Path.PadRight(pathWidth)); + } - return Exit.OK; + return Exit.OK; + } + return Exit.ERROR; } private int AddInstall(AddOptions options) { if (options.name == null || options.path == null) { - user.RaiseError(Properties.Resources.ArgumentMissing); + user?.RaiseError(Properties.Resources.ArgumentMissing); PrintUsage("add"); return Exit.BADOPT; } - if (Manager.HasInstance(options.name)) + if (Manager?.HasInstance(options.name) ?? false) { - user.RaiseMessage(Properties.Resources.InstanceAddDuplicate, options.name); + user?.RaiseMessage(Properties.Resources.InstanceAddDuplicate, options.name); return Exit.BADOPT; } try { - string path = options.path; - Manager.AddInstance(path, options.name, user); - user.RaiseMessage(Properties.Resources.InstanceAdded, options.name, options.path); + if (user != null) + { + string path = options.path; + Manager?.AddInstance(path, options.name, user); + user.RaiseMessage(Properties.Resources.InstanceAdded, options.name, options.path); + } return Exit.OK; } catch (NotKSPDirKraken ex) { - user.RaiseMessage(Properties.Resources.InstanceNotInstance, ex.path); + user?.RaiseMessage(Properties.Resources.InstanceNotInstance, ex.path); return Exit.BADOPT; } } @@ -329,7 +335,7 @@ private int CloneInstall(CloneOptions options) { if (options.nameOrPath == null || options.new_name == null || options.new_path == null) { - user.RaiseError(Properties.Resources.ArgumentMissing); + user?.RaiseError(Properties.Resources.ArgumentMissing); PrintUsage("clone"); return Exit.BADOPT; } @@ -345,7 +351,7 @@ private int CloneInstall(CloneOptions options) try { // Try instanceNameOrPath as name and search the registry for it. - if (Manager.HasInstance(instanceNameOrPath)) + if (Manager?.HasInstance(instanceNameOrPath) ?? false) { CKAN.GameInstance[] listOfInstances = Manager.Instances.Values.ToArray(); foreach (CKAN.GameInstance instance in listOfInstances) @@ -360,7 +366,7 @@ private int CloneInstall(CloneOptions options) } // Try to use instanceNameOrPath as a path and create a new game instance. // If it's valid, go on. - else if (Manager.InstanceAt(instanceNameOrPath) is CKAN.GameInstance instance && instance.Valid) + else if (Manager?.InstanceAt(instanceNameOrPath) is CKAN.GameInstance instance && instance.Valid) { Manager.CloneInstance(instance, newName, newPath, options.shareStock); } @@ -398,13 +404,13 @@ private int CloneInstall(CloneOptions options) } catch (NoGameInstanceKraken) { - user.RaiseError(Properties.Resources.InstanceCloneNotFound, instanceNameOrPath); + user?.RaiseError(Properties.Resources.InstanceCloneNotFound, instanceNameOrPath); ListInstalls(); return Exit.ERROR; } catch (InstanceNameTakenKraken kraken) { - user.RaiseError(Properties.Resources.InstanceDuplicate, kraken.instName); + user?.RaiseError(Properties.Resources.InstanceDuplicate, kraken.instName); return Exit.BADOPT; } @@ -417,7 +423,7 @@ private int CloneInstall(CloneOptions options) } else { - user.RaiseMessage(Properties.Resources.InstanceCloneFailed); + user?.RaiseMessage(Properties.Resources.InstanceCloneFailed); return Exit.ERROR; } } @@ -426,20 +432,20 @@ private int RenameInstall(RenameOptions options) { if (options.old_name == null || options.new_name == null) { - user.RaiseError(Properties.Resources.ArgumentMissing); + user?.RaiseError(Properties.Resources.ArgumentMissing); PrintUsage("rename"); return Exit.BADOPT; } - if (!Manager.HasInstance(options.old_name)) + if (!Manager?.HasInstance(options.old_name) ?? false) { - user.RaiseMessage(Properties.Resources.InstanceNotFound, options.old_name); + user?.RaiseMessage(Properties.Resources.InstanceNotFound, options.old_name); return Exit.BADOPT; } - Manager.RenameInstance(options.old_name, options.new_name); + Manager?.RenameInstance(options.old_name, options.new_name); - user.RaiseMessage(Properties.Resources.InstanceRenamed, options.old_name, options.new_name); + user?.RaiseMessage(Properties.Resources.InstanceRenamed, options.old_name, options.new_name); return Exit.OK; } @@ -447,94 +453,77 @@ private int ForgetInstall(ForgetOptions options) { if (options.name == null) { - user.RaiseError(Properties.Resources.ArgumentMissing); + user?.RaiseError(Properties.Resources.ArgumentMissing); PrintUsage("forget"); return Exit.BADOPT; } - if (!Manager.HasInstance(options.name)) + if (!Manager?.HasInstance(options.name) ?? false) { - user.RaiseMessage(Properties.Resources.InstanceNotFound, options.name); + user?.RaiseMessage(Properties.Resources.InstanceNotFound, options.name); return Exit.BADOPT; } - Manager.RemoveInstance(options.name); + Manager?.RemoveInstance(options.name); - user.RaiseMessage(Properties.Resources.InstanceForgot, options.name); + user?.RaiseMessage(Properties.Resources.InstanceForgot, options.name); return Exit.OK; } private int SetDefaultInstall(DefaultOptions options) { - string name = options.name; - - if (name == null) + var name = options.name; + // Oh right, this is that one that didn't work AT ALL at one point + if (name == null && Manager != null && user != null) { // No input argument from the user. Present a list of the possible instances. - string message = $"default - {Properties.Resources.InstanceDefaultArgumentMissing}"; - - // Check if there is a default instance. - string defaultInstance = Manager.Configuration.AutoStartInstance; - int defaultInstancePresent = 0; - if (!string.IsNullOrWhiteSpace(defaultInstance)) + try { - defaultInstancePresent = 1; + // If there is a default instance, its index is passed first + var defaultInstance = Manager.Configuration.AutoStartInstance; + int result = user.RaiseSelectionDialog( + $"default - {Properties.Resources.InstanceDefaultArgumentMissing}", + (defaultInstance == null || string.IsNullOrWhiteSpace(defaultInstance) + ? Enumerable.Empty() + : Enumerable.Repeat((object)Manager.Instances.IndexOfKey(defaultInstance), 1)) + .Concat(Manager.Instances.Select(kvp => string.Format("\"{0}\" - {1}", + kvp.Key, kvp.Value.GameDir()))) + .ToArray()); + if (result < 0) + { + return Exit.BADOPT; + } + name = Manager.Instances.ElementAt(result).Key; } - - object[] keys = new object[Manager.Instances.Count + defaultInstancePresent]; - - // Populate the list of instances. - for (int i = 0; i < Manager.Instances.Count; i++) + catch (Kraken) { - var instance = Manager.Instances.ElementAt(i); - - keys[i + defaultInstancePresent] = string.Format("\"{0}\" - {1}", instance.Key, instance.Value.GameDir()); + return Exit.BADOPT; } + } - // Mark the default instance for the user. - if (!string.IsNullOrWhiteSpace(defaultInstance)) + if (name != null) + { + if (!Manager?.Instances.ContainsKey(name) ?? false) { - keys[0] = Manager.Instances.IndexOfKey(defaultInstance); + user?.RaiseMessage(Properties.Resources.InstanceNotFound, name); + return Exit.BADOPT; } - int result; - try { - result = user.RaiseSelectionDialog(message, keys); + Manager?.SetAutoStart(name); } - catch (Kraken) - { - return Exit.BADOPT; - } - - if (result < 0) + catch (NotKSPDirKraken k) { + user?.RaiseMessage(Properties.Resources.InstanceNotInstance, k.path); return Exit.BADOPT; } - name = Manager.Instances.ElementAt(result).Key; - } - - if (!Manager.Instances.ContainsKey(name)) - { - user.RaiseMessage(Properties.Resources.InstanceNotFound, name); - return Exit.BADOPT; - } - - try - { - Manager.SetAutoStart(name); - } - catch (NotKSPDirKraken k) - { - user.RaiseMessage(Properties.Resources.InstanceNotInstance, k.path); - return Exit.BADOPT; + user?.RaiseMessage(Properties.Resources.InstanceDefaultSet, name); + return Exit.OK; } - - user.RaiseMessage(Properties.Resources.InstanceDefaultSet, name); - return Exit.OK; + return Exit.BADOPT; } /// @@ -552,14 +541,14 @@ int error() int badArgument() { log.Debug("Instance faking failed: bad argument(s). See console output for details."); - user.RaiseMessage(Properties.Resources.InstanceFakeBadArguments); + user?.RaiseMessage(Properties.Resources.InstanceFakeBadArguments); return Exit.BADOPT; } if (options.name == null || options.path == null || options.version == null) { - user.RaiseError(Properties.Resources.ArgumentMissing); + user?.RaiseError(Properties.Resources.ArgumentMissing); PrintUsage("fake"); return Exit.BADOPT; } @@ -568,38 +557,38 @@ int badArgument() // Parse all options string installName = options.name; string path = options.path; - GameVersion version; + GameVersion? version; bool setDefault = options.setDefault; - IGame game = KnownGames.GameByShortName(options.gameId); + var game = KnownGames.GameByShortName(options.gameId ?? ""); if (game == null) { - user.RaiseMessage(Properties.Resources.InstanceFakeBadGame, options.gameId); + user?.RaiseMessage(Properties.Resources.InstanceFakeBadGame, options.gameId ?? ""); return badArgument(); } // options.Version is "none" if the DLC should not be simulated. - Dictionary dlcs = new Dictionary(); + var dlcs = new Dictionary(); if (options.makingHistoryVersion != null && options.makingHistoryVersion.ToLower() != "none") { - if (GameVersion.TryParse(options.makingHistoryVersion, out GameVersion ver)) + if (GameVersion.TryParse(options.makingHistoryVersion, out GameVersion? ver)) { dlcs.Add(new MakingHistoryDlcDetector(), ver); } else { - user.RaiseError(Properties.Resources.InstanceFakeMakingHistory); + user?.RaiseError(Properties.Resources.InstanceFakeMakingHistory); return badArgument(); } } if (options.breakingGroundVersion != null && options.breakingGroundVersion.ToLower() != "none") { - if (GameVersion.TryParse(options.breakingGroundVersion, out GameVersion ver)) + if (GameVersion.TryParse(options.breakingGroundVersion, out GameVersion? ver)) { dlcs.Add(new BreakingGroundDlcDetector(), ver); } else { - user.RaiseError(Properties.Resources.InstanceFakeBreakingGround); + user?.RaiseError(Properties.Resources.InstanceFakeBreakingGround); return badArgument(); } } @@ -612,7 +601,7 @@ int badArgument() catch (FormatException) { // Thrown if there is anything besides numbers and points in the version string or a different syntactic error. - user.RaiseError(Properties.Resources.InstanceFakeVersion); + user?.RaiseError(Properties.Resources.InstanceFakeVersion); return badArgument(); } @@ -623,44 +612,49 @@ int badArgument() } catch (BadGameVersionKraken) { - user.RaiseError(Properties.Resources.InstanceFakeBadGameVersion); + user?.RaiseError(Properties.Resources.InstanceFakeBadGameVersion); return badArgument(); } catch (CancelledActionKraken) { - user.RaiseError(Properties.Resources.InstanceFakeCancelled); + user?.RaiseError(Properties.Resources.InstanceFakeCancelled); + return error(); + } + + if (version == null) + { return error(); } - user.RaiseMessage(Properties.Resources.InstanceFakeCreating, - installName, path, version.ToString()); + user?.RaiseMessage(Properties.Resources.InstanceFakeCreating, + installName, path, version.ToString() ?? ""); log.Debug("Faking instance..."); try { // Pass all arguments to CKAN.GameInstanceManager.FakeInstance() and create a new one. - Manager.FakeInstance(game, installName, path, version, dlcs); + Manager?.FakeInstance(game, installName, path, version, dlcs); if (setDefault) { - user.RaiseMessage(Properties.Resources.InstanceFakeDefault); - Manager.SetAutoStart(installName); + user?.RaiseMessage(Properties.Resources.InstanceFakeDefault); + Manager?.SetAutoStart(installName); } } catch (InstanceNameTakenKraken kraken) { - user.RaiseError(Properties.Resources.InstanceDuplicate, kraken.instName); + user?.RaiseError(Properties.Resources.InstanceDuplicate, kraken.instName); return badArgument(); } catch (BadInstallLocationKraken kraken) { // The folder exists and is not empty. - user.RaiseError("{0}", kraken.Message); + user?.RaiseError("{0}", kraken.Message); return badArgument(); } catch (WrongGameVersionKraken kraken) { // Thrown because the specified game instance is too old for one of the selected DLCs. - user.RaiseError("{0}", kraken.Message); + user?.RaiseError("{0}", kraken.Message); return badArgument(); } catch (NotKSPDirKraken kraken) @@ -668,7 +662,7 @@ int badArgument() // Something went wrong adding the new instance to the registry, // most likely because the newly created directory is somehow not valid. log.Error(kraken); - user.RaiseError("{0}", kraken.Message); + user?.RaiseError("{0}", kraken.Message); return error(); } catch (InvalidKSPInstanceKraken) @@ -680,14 +674,14 @@ int badArgument() // Test if the instance was added to the registry. // No need to test if valid, because this is done in AddInstance(). - if (Manager.HasInstance(installName)) + if (Manager?.HasInstance(installName) ?? false) { - user.RaiseMessage(Properties.Resources.InstanceFakeDone); + user?.RaiseMessage(Properties.Resources.InstanceFakeDone); return Exit.OK; } else { - user.RaiseError(Properties.Resources.InstanceFakeFailed); + user?.RaiseError(Properties.Resources.InstanceFakeFailed); return error(); } } @@ -697,7 +691,7 @@ private void PrintUsage(string verb) { foreach (var h in InstanceSubOptions.GetHelp(verb)) { - user.RaiseError(h); + user?.RaiseError(h); } } diff --git a/Cmdline/Action/ISubCommand.cs b/Cmdline/Action/ISubCommand.cs index 0a964a7e88..c00bf1c7bf 100644 --- a/Cmdline/Action/ISubCommand.cs +++ b/Cmdline/Action/ISubCommand.cs @@ -2,6 +2,8 @@ namespace CKAN.CmdLine { internal interface ISubCommand { - int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCommandOptions options); + int RunSubCommand(GameInstanceManager? manager, + CommonOptions? opts, + SubCommandOptions options); } } diff --git a/Cmdline/Action/Import.cs b/Cmdline/Action/Import.cs index 87de99e651..d676d219a6 100644 --- a/Cmdline/Action/Import.cs +++ b/Cmdline/Action/Import.cs @@ -36,8 +36,8 @@ public int RunCommand(CKAN.GameInstance instance, object options) { try { - ImportOptions opts = options as ImportOptions; - HashSet toImport = GetFiles(opts); + var opts = options as ImportOptions; + var toImport = GetFiles(opts?.paths); if (toImport.Count < 1) { user.RaiseError(Properties.Resources.ArgumentMissing); @@ -47,16 +47,16 @@ public int RunCommand(CKAN.GameInstance instance, object options) } return Exit.ERROR; } - else + else if (manager.Cache != null) { log.InfoFormat("Importing {0} files", toImport.Count); 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), regMgr.registry, !opts.Headless); - HashSet possibleConfigOnlyDirs = null; + installer.ImportFiles(toImport, user, mod => toInstall.Add(mod), regMgr.registry, !opts?.Headless ?? false); if (toInstall.Count > 0) { + HashSet? possibleConfigOnlyDirs = null; installer.InstallList(toInstall, new RelationshipResolverOptions(), regMgr, @@ -64,6 +64,7 @@ public int RunCommand(CKAN.GameInstance instance, object options) } return Exit.OK; } + return Exit.ERROR; } catch (Exception ex) { @@ -72,23 +73,26 @@ public int RunCommand(CKAN.GameInstance instance, object options) } } - private HashSet GetFiles(ImportOptions options) + private HashSet GetFiles(List? paths) { HashSet files = new HashSet(); - foreach (string filename in options.paths) + if (paths != null) { - if (Directory.Exists(filename)) + foreach (string filename in paths) { - // Import everything in this folder - log.InfoFormat("{0} is a directory", filename); - foreach (string dirfile in Directory.EnumerateFiles(filename)) + if (Directory.Exists(filename)) { - AddFile(files, dirfile); + // Import everything in this folder + log.InfoFormat("{0} is a directory", filename); + foreach (string dirfile in Directory.EnumerateFiles(filename)) + { + AddFile(files, dirfile); + } + } + else + { + AddFile(files, filename); } - } - else - { - AddFile(files, filename); } } return files; @@ -117,7 +121,7 @@ private void AddFile(HashSet files, string filename) internal class ImportOptions : InstanceSpecificOptions { [ValueList(typeof(List))] - public List paths { get; set; } + public List? paths { get; set; } } } diff --git a/Cmdline/Action/Install.cs b/Cmdline/Action/Install.cs index 0f6df7becd..7d0097dbe2 100644 --- a/Cmdline/Action/Install.cs +++ b/Cmdline/Action/Install.cs @@ -33,7 +33,7 @@ public Install(GameInstanceManager mgr, RepositoryDataManager repoData, IUser us public int RunCommand(CKAN.GameInstance instance, object raw_options) { var options = raw_options as InstallOptions; - if (options.modules.Count == 0 && options.ckan_files == null) + if (options?.modules?.Count == 0 && options.ckan_files == null) { user.RaiseError(Properties.Resources.ArgumentMissing); foreach (var h in Actions.GetHelp("install")) @@ -44,9 +44,9 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) } var regMgr = RegistryManager.Instance(instance, repoData); - List modules = null; + List? modules = null; - if (options.ckan_files != null) + if (options?.ckan_files != null) { // Install from CKAN files try @@ -63,7 +63,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) catch (FileNotFoundKraken kraken) { user.RaiseError(Properties.Resources.InstallNotFound, - kraken.file); + kraken.file ?? ""); return Exit.ERROR; } catch (Kraken kraken) @@ -77,7 +77,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) } else { - var identifiers = options.modules; + var identifiers = options?.modules ?? new List { }; var registry = regMgr.registry; var installed = registry.InstalledModules .Select(im => im.Module) @@ -86,22 +86,28 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) Search.AdjustModulesCase(instance, registry, identifiers); modules = identifiers.Select(arg => CkanModule.FromIDandVersion( registry, arg, - options.allow_incompatible + (options?.allow_incompatible ?? false) ? null : crit) ?? registry.LatestAvailable(arg, crit, null, installed) ?? registry.InstalledModule(arg)?.Module) + .OfType() .ToList(); } + if (manager.Cache == null) + { + return Exit.ERROR; + } + var installer = new ModuleInstaller(instance, manager.Cache, user); var install_ops = new RelationshipResolverOptions { - with_all_suggests = options.with_all_suggests, - with_suggests = options.with_suggests, - with_recommends = !options.no_recommends, - allow_incompatible = options.allow_incompatible, + with_all_suggests = options?.with_all_suggests ?? false, + with_suggests = options?.with_suggests ?? false, + with_recommends = !options?.no_recommends ?? true, + allow_incompatible = options?.allow_incompatible ?? false, without_toomanyprovides_kraken = user.Headless, without_enforce_consistency = user.Headless, }; @@ -111,7 +117,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) // Install everything requested. :) try { - HashSet possibleConfigOnlyDirs = null; + HashSet? possibleConfigOnlyDirs = null; installer.InstallList(modules, install_ops, regMgr, ref possibleConfigOnlyDirs); user.RaiseMessage(""); @@ -128,19 +134,20 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) if (ex.version == null) { user.RaiseError(Properties.Resources.InstallUnversionedDependencyNotSatisfied, - ex.module, instance.game.ShortName); + ex.module, instance.game.ShortName); } else { user.RaiseError(Properties.Resources.InstallVersionedDependencyNotSatisfied, - ex.module, ex.version, instance.game.ShortName); + ex.module, ex.version, instance.game.ShortName); } user.RaiseMessage(Properties.Resources.InstallTryAgain); return Exit.ERROR; } catch (BadMetadataKraken ex) { - user.RaiseError(Properties.Resources.InstallBadMetadata, ex.module, ex.Message); + user.RaiseError(Properties.Resources.InstallBadMetadata, + ex.module?.ToString() ?? "", ex.Message); return Exit.ERROR; } catch (TooManyModsProvideKraken ex) @@ -176,13 +183,13 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) if (ex.owningModule != null) { user.RaiseError(Properties.Resources.InstallFileConflictOwned, - ex.filename, ex.installingModule, ex.owningModule, - Meta.GetVersion(VersionFormat.Full)); + ex.filename, ex.installingModule?.ToString() ?? "", ex.owningModule, + Meta.GetVersion(VersionFormat.Full)); } else { user.RaiseError(Properties.Resources.InstallFileConflictUnowned, - ex.installingModule, ex.filename); + ex.installingModule?.ToString() ?? "", ex.filename); } user.RaiseMessage(Properties.Resources.InstallGamedataReturned, instance.game.PrimaryModDirectoryRelative); @@ -230,8 +237,8 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) { user.RaiseError(Properties.Resources.InstallDLC, kraken.module.name); var res = kraken?.module?.resources; - var storePagesMsg = new Uri[] { res?.store, res?.steamstore } - .Where(u => u != null) + var storePagesMsg = new Uri?[] { res?.store, res?.steamstore } + .OfType() .Aggregate("", (a, b) => $"{a}\r\n- {b}"); if (!string.IsNullOrEmpty(storePagesMsg)) { @@ -261,7 +268,7 @@ private Uri getUri(string arg) internal class InstallOptions : InstanceSpecificOptions { [OptionArray('c', "ckanfiles", HelpText = "Local CKAN files or URLs to process")] - public string[] ckan_files { get; set; } + public string[]? ckan_files { get; set; } [Option("no-recommends", DefaultValue = false, HelpText = "Do not install recommended modules")] public bool no_recommends { get; set; } @@ -277,7 +284,7 @@ internal class InstallOptions : InstanceSpecificOptions [ValueList(typeof(List))] [AvailableIdentifiers] - public List modules { get; set; } + public List? modules { get; set; } } } diff --git a/Cmdline/Action/List.cs b/Cmdline/Action/List.cs index c3fc8cb8f1..9dbe1d76ed 100644 --- a/Cmdline/Action/List.cs +++ b/Cmdline/Action/List.cs @@ -34,18 +34,23 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) if (exportFileType == null) { - user.RaiseError(Properties.Resources.ListUnknownFormat, options.export); + user.RaiseError(Properties.Resources.ListUnknownFormat, + options.export ?? ""); } } - if (!(options.porcelain) && exportFileType == null) + if (!options.porcelain && exportFileType == null) { user.RaiseMessage(""); user.RaiseMessage(Properties.Resources.ListGameFound, instance.game.ShortName, Platform.FormatPath(instance.GameDir())); - user.RaiseMessage(""); - user.RaiseMessage(Properties.Resources.ListGameVersion, instance.game.ShortName, instance.Version()); + if (instance.Version() is GameVersion gv) + { + user.RaiseMessage(""); + user.RaiseMessage(Properties.Resources.ListGameVersion, + instance.game.ShortName, gv); + } user.RaiseMessage(""); user.RaiseMessage(Properties.Resources.ListGameModulesHeader); user.RaiseMessage(""); @@ -82,9 +87,9 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) { // Check if upgrades are available, and show appropriately. log.DebugFormat("Check if upgrades are available for {0}", mod.Key); - CkanModule latest = registry.LatestAvailable(mod.Key, instance.VersionCriteria()); - CkanModule current = registry.GetInstalledVersion(mod.Key); - InstalledModule inst = registry.InstalledModule(mod.Key); + var latest = registry.LatestAvailable(mod.Key, instance.VersionCriteria()); + var current = registry.GetInstalledVersion(mod.Key); + var inst = registry.InstalledModule(mod.Key); if (latest == null) { @@ -95,11 +100,10 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) { log.DebugFormat(" {0} installed version not found in registry", mod.Key); } - // Check if mod is replaceable - if (current.replaced_by != null) + else if (current.replaced_by != null) { - ModuleReplacement replacement = registry.GetReplacement(mod.Key, instance.VersionCriteria()); + var replacement = registry.GetReplacement(mod.Key, instance.VersionCriteria()); if (replacement != null) { // Replaceable! @@ -114,9 +118,9 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) log.InfoFormat("Latest {0} is {1}", mod.Key, latest.version); bullet = (inst?.AutoInstalled ?? false) ? "+" : "-"; // Check if mod is replaceable - if (current.replaced_by != null) + if (current?.replaced_by != null) { - ModuleReplacement replacement = registry.GetReplacement(latest.identifier, instance.VersionCriteria()); + var replacement = registry.GetReplacement(latest.identifier, instance.VersionCriteria()); if (replacement != null) { // Replaceable! @@ -158,11 +162,9 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) return Exit.OK; } - private static ExportFileType? GetExportFileType(string export) + private static ExportFileType? GetExportFileType(string? export) { - export = export.ToLowerInvariant(); - - switch (export) + switch (export?.ToLowerInvariant()) { case "text": return ExportFileType.PlainText; case "markdown": return ExportFileType.Markdown; @@ -185,7 +187,7 @@ internal class ListOptions : InstanceSpecificOptions public bool porcelain { get; set; } [Option("export", HelpText = "Export list of modules in specified format to stdout")] - public string export { get; set; } + public string? export { get; set; } } } diff --git a/Cmdline/Action/Mark.cs b/Cmdline/Action/Mark.cs index d0e7d36eee..ddac8a32aa 100644 --- a/Cmdline/Action/Mark.cs +++ b/Cmdline/Action/Mark.cs @@ -28,7 +28,9 @@ public Mark(RepositoryDataManager repoData) /// /// Exit code /// - public int RunSubCommand(GameInstanceManager mgr, CommonOptions opts, SubCommandOptions unparsed) + public int RunSubCommand(GameInstanceManager? mgr, + CommonOptions? opts, + SubCommandOptions unparsed) { string[] args = unparsed.options.ToArray(); int exitCode = Exit.OK; @@ -40,8 +42,9 @@ public int RunSubCommand(GameInstanceManager mgr, CommonOptions opts, SubCommand { CommonOptions options = (CommonOptions)suboptions; options.Merge(opts); - user = new ConsoleUser(options.Headless); - manager = mgr ?? new GameInstanceManager(user); + var user = new ConsoleUser(options.Headless); + var manager = mgr ?? new GameInstanceManager(user); + exitCode = options.Handle(manager, user); if (exitCode != Exit.OK) { @@ -51,11 +54,21 @@ public int RunSubCommand(GameInstanceManager mgr, CommonOptions opts, SubCommand switch (option) { case "auto": - exitCode = MarkAuto((MarkAutoOptions)suboptions, true, option, Properties.Resources.MarkAutoInstalled); + exitCode = MarkAuto((MarkAutoOptions)suboptions, + true, + option, + Properties.Resources.MarkAutoInstalled, + manager, + user); break; case "user": - exitCode = MarkAuto((MarkAutoOptions)suboptions, false, option, Properties.Resources.MarkUserSelected); + exitCode = MarkAuto((MarkAutoOptions)suboptions, + false, + option, + Properties.Resources.MarkUserSelected, + manager, + user); break; default: @@ -68,12 +81,17 @@ public int RunSubCommand(GameInstanceManager mgr, CommonOptions opts, SubCommand return exitCode; } - private int MarkAuto(MarkAutoOptions opts, bool value, string verb, string descrip) + private int MarkAuto(MarkAutoOptions opts, + bool value, + string verb, + string descrip, + GameInstanceManager manager, + IUser user) { - if (opts.modules.Count < 1) + if (opts.modules == null || opts.modules.Count < 1) { user.RaiseError(Properties.Resources.ArgumentMissing); - PrintUsage(verb); + PrintUsage(user, verb); return Exit.BADOPT; } @@ -86,42 +104,46 @@ private int MarkAuto(MarkAutoOptions opts, bool value, string verb, string descr var instance = MainClass.GetGameInstance(manager); var regMgr = RegistryManager.Instance(instance, repoData); bool needSave = false; - Search.AdjustModulesCase(instance, regMgr.registry, opts.modules); - foreach (string id in opts.modules) + if (opts.modules != null) { - InstalledModule im = regMgr.registry.InstalledModule(id); - if (im == null) - { - user.RaiseError(Properties.Resources.MarkNotInstalled, id); - } - else if (im.AutoInstalled == value) + Search.AdjustModulesCase(instance, regMgr.registry, opts.modules); + foreach (string id in opts.modules) { - user.RaiseError(Properties.Resources.MarkAlready, id, descrip); - } - else - { - user.RaiseMessage(Properties.Resources.Marking, id, descrip); - try + var im = regMgr.registry.InstalledModule(id); + if (im == null) { - im.AutoInstalled = value; - needSave = true; + user.RaiseError(Properties.Resources.MarkNotInstalled, id); } - catch (ModuleIsDLCKraken kraken) + else if (im.AutoInstalled == value) { - user.RaiseMessage(Properties.Resources.MarkDLC, kraken.module.name); - return Exit.BADOPT; + user.RaiseError(Properties.Resources.MarkAlready, id, descrip); + } + else + { + user.RaiseMessage(Properties.Resources.Marking, id, descrip); + try + { + im.AutoInstalled = value; + needSave = true; + } + catch (ModuleIsDLCKraken kraken) + { + user.RaiseMessage(Properties.Resources.MarkDLC, kraken.module.name); + return Exit.BADOPT; + } } } + if (needSave) + { + regMgr.Save(false); + user.RaiseMessage(Properties.Resources.MarkChanged); + } + return Exit.OK; } - if (needSave) - { - regMgr.Save(false); - user.RaiseMessage(Properties.Resources.MarkChanged); - } - return Exit.OK; + return Exit.ERROR; } - private void PrintUsage(string verb) + private void PrintUsage(IUser user, string verb) { foreach (var h in MarkSubOptions.GetHelp(verb)) { @@ -129,18 +151,16 @@ private void PrintUsage(string verb) } } - private GameInstanceManager manager; private readonly RepositoryDataManager repoData; - private IUser user; } internal class MarkSubOptions : VerbCommandOptions { [VerbOption("auto", HelpText = "Mark modules as auto installed")] - public MarkAutoOptions MarkAutoOptions { get; set; } + public MarkAutoOptions? MarkAutoOptions { get; set; } [VerbOption("user", HelpText = "Mark modules as user selected (opposite of auto installed)")] - public MarkAutoOptions MarkUserOptions { get; set; } + public MarkAutoOptions? MarkUserOptions { get; set; } [HelpVerbOption] public string GetUsage(string verb) @@ -183,6 +203,6 @@ internal class MarkAutoOptions : InstanceSpecificOptions { [ValueList(typeof(List))] [InstalledIdentifiers] - public List modules { get; set; } + public List? modules { get; set; } } } diff --git a/Cmdline/Action/Prompt.cs b/Cmdline/Action/Prompt.cs index 1f94d67603..36d083373d 100644 --- a/Cmdline/Action/Prompt.cs +++ b/Cmdline/Action/Prompt.cs @@ -19,7 +19,7 @@ public Prompt(GameInstanceManager mgr, RepositoryDataManager repoData) public int RunCommand(object raw_options) { - CommonOptions opts = raw_options as CommonOptions; + var opts = raw_options as CommonOptions; bool headless = opts?.Headless ?? false; // Print an intro if not in headless mode if (!headless) @@ -43,8 +43,8 @@ public int RunCommand(object raw_options) try { // Get input - string command = ReadLineWithCompletion(headless); - if (command == null || command == exitCommand) + var command = ReadLineWithCompletion(headless); + if (command is null or exitCommand) { done = true; } @@ -91,7 +91,7 @@ private static string[] ParseTextField(string input) private static readonly Regex quotePattern = new Regex( @"(?<="")[^""]*(?="")|[^ ""]+|(?<= )$", RegexOptions.Compiled); - private static string ReadLineWithCompletion(bool headless) + private static string? ReadLineWithCompletion(bool headless) { try { @@ -106,14 +106,14 @@ private static string ReadLineWithCompletion(bool headless) } } - private string[] GetSuggestions(string text, int index) + private string[]? GetSuggestions(string text, int index) { string[] pieces = ParseTextField(text); TypeInfo ti = typeof(Actions).GetTypeInfo(); List extras = new List { exitCommand, "help" }; foreach (string piece in pieces.Take(pieces.Length - 1)) { - PropertyInfo pi = ti.DeclaredProperties + var pi = ti.DeclaredProperties .FirstOrDefault(p => p?.GetCustomAttribute()?.LongName == piece); if (pi == null) { @@ -124,8 +124,8 @@ private string[] GetSuggestions(string text, int index) extras.Clear(); } var lastPiece = pieces.LastOrDefault() ?? ""; - return lastPiece.StartsWith("--") ? GetLongOptions(ti, lastPiece.Substring(2)) - : lastPiece.StartsWith("-") ? GetShortOptions(ti, lastPiece.Substring(1)) + return lastPiece.StartsWith("--") ? GetLongOptions(ti, lastPiece[2..]) + : lastPiece.StartsWith("-") ? GetShortOptions(ti, lastPiece[1..]) : HasVerbs(ti) ? GetVerbs(ti, lastPiece, extras) : WantsAvailIdentifiers(ti) ? GetAvailIdentifiers(lastPiece) : WantsInstIdentifiers(ti) ? GetInstIdentifiers(lastPiece) @@ -157,7 +157,7 @@ private static string[] GetShortOptions(TypeInfo ti, string prefix) private static IEnumerable AllBaseTypes(Type start) { - for (Type t = start; t != null; t = t.BaseType) + for (Type? t = start; t != null; t = t.BaseType) { yield return t; } @@ -170,7 +170,7 @@ private static bool HasVerbs(TypeInfo ti) private static string[] GetVerbs(TypeInfo ti, string prefix, IEnumerable extras) => ti.DeclaredProperties .Select(p => p.GetCustomAttribute()?.LongName) - .Where(v => v != null) + .OfType() .Concat(extras) .Where(v => v.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) .OrderBy(v => v) @@ -204,7 +204,7 @@ private string[] GetInstIdentifiers(string prefix) return registry.Installed(false, false) .Keys .Where(ident => ident.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase) - && !registry.GetInstalledVersion(ident).IsDLC) + && (!registry.GetInstalledVersion(ident)?.IsDLC ?? true)) .ToArray(); } diff --git a/Cmdline/Action/Remove.cs b/Cmdline/Action/Remove.cs index 4a3ce31d80..b79d3db9de 100644 --- a/Cmdline/Action/Remove.cs +++ b/Cmdline/Action/Remove.cs @@ -39,8 +39,6 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) if (options.regex) { log.Debug("Attempting Regex"); - // Parse every "module" as a grumpy regex - var justins = options.modules.Select(s => new Regex(s)); // Modules that have been selected by one regex List selectedModules = new List(); @@ -50,7 +48,9 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) // if it matches, select for removal foreach (string mod in regMgr.registry.InstalledModules.Select(mod => mod.identifier)) { - if (justins.Any(re => re.IsMatch(mod))) + // Parse every "module" as a grumpy regex + if (options.modules?.Select(s => new Regex(s)) + .Any(re => re.IsMatch(mod)) ?? false) { selectedModules.Add(mod); } @@ -65,18 +65,20 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) { log.Debug("Removing all mods"); // Add the list of installed modules to the list that should be uninstalled - options.modules.AddRange( + options.modules?.AddRange( regMgr.registry.InstalledModules .Where(mod => !mod.Module.IsDLC) .Select(mod => mod.identifier) ); } - if (options.modules != null && options.modules.Count > 0) + if (options.modules != null + && options.modules.Count > 0 + && manager.Cache != null) { try { - HashSet possibleConfigOnlyDirs = null; + HashSet? possibleConfigOnlyDirs = null; var installer = new ModuleInstaller(instance, manager.Cache, user); Search.AdjustModulesCase(instance, regMgr.registry, options.modules); installer.UninstallList(options.modules, ref possibleConfigOnlyDirs, regMgr); @@ -91,8 +93,8 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) { user.RaiseMessage(Properties.Resources.RemoveDLC, kraken.module.name); var res = kraken?.module?.resources; - var storePagesMsg = new Uri[] { res?.store, res?.steamstore } - .Where(u => u != null) + var storePagesMsg = new Uri?[] { res?.store, res?.steamstore } + .OfType() .Aggregate("", (a, b) => $"{a}\r\n- {b}"); if (!string.IsNullOrEmpty(storePagesMsg)) { @@ -133,7 +135,7 @@ internal class RemoveOptions : InstanceSpecificOptions [ValueList(typeof(List))] [InstalledIdentifiers] - public List modules { get; set; } + public List? modules { get; set; } [Option("all", DefaultValue = false, HelpText = "Remove all installed mods.")] public bool rmall { get; set; } diff --git a/Cmdline/Action/Repair.cs b/Cmdline/Action/Repair.cs index cd89cccbc3..1b96778cde 100644 --- a/Cmdline/Action/Repair.cs +++ b/Cmdline/Action/Repair.cs @@ -8,7 +8,7 @@ namespace CKAN.CmdLine internal class RepairSubOptions : VerbCommandOptions { [VerbOption("registry", HelpText = "Try to repair the CKAN registry")] - public InstanceSpecificOptions Registry { get; set; } + public InstanceSpecificOptions? Registry { get; set; } [HelpVerbOption] public string GetUsage(string verb) @@ -52,7 +52,9 @@ public Repair(RepositoryDataManager repoData) this.repoData = repoData; } - public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCommandOptions unparsed) + public int RunSubCommand(GameInstanceManager? manager, + CommonOptions? opts, + SubCommandOptions unparsed) { int exitCode = Exit.OK; // Parse and process our sub-verbs @@ -64,10 +66,7 @@ public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCom CommonOptions options = (CommonOptions)suboptions; options.Merge(opts); User = new ConsoleUser(options.Headless); - if (manager == null) - { - manager = new GameInstanceManager(User); - } + manager ??= new GameInstanceManager(User); exitCode = options.Handle(manager, User); if (exitCode != Exit.OK) { @@ -91,7 +90,7 @@ public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCom return exitCode; } - private IUser User { get; set; } + private IUser? User { get; set; } private readonly RepositoryDataManager repoData; /// @@ -101,7 +100,7 @@ private int Registry(RegistryManager regMgr) { regMgr.registry.Repair(); regMgr.Save(); - User.RaiseMessage(Properties.Resources.Repaired); + User?.RaiseMessage(Properties.Resources.Repaired); return Exit.OK; } } diff --git a/Cmdline/Action/Replace.cs b/Cmdline/Action/Replace.cs index 071a69aae9..30f001bceb 100644 --- a/Cmdline/Action/Replace.cs +++ b/Cmdline/Action/Replace.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using CommandLine; using log4net; @@ -23,10 +22,10 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) if (options.ckan_file != null) { - options.modules.Add(MainClass.LoadCkanFromFile(options.ckan_file).identifier); + options.modules?.Add(MainClass.LoadCkanFromFile(options.ckan_file).identifier); } - if (options.modules.Count == 0 && ! options.replace_all) + if (options.modules?.Count == 0 && ! options.replace_all) { user.RaiseError(Properties.Resources.ArgumentMissing); foreach (var h in Actions.GetHelp("replace")) @@ -58,7 +57,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) { ModuleVersion current_version = mod.Value; - if ((current_version is ProvidesModuleVersion) || (current_version is UnmanagedModuleVersion)) + if (current_version is ProvidesModuleVersion or UnmanagedModuleVersion) { continue; } @@ -69,7 +68,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) log.DebugFormat("Testing {0} {1} for possible replacement", mod.Key, mod.Value); // Check if replacement is available - ModuleReplacement replacement = registry.GetReplacement(mod.Key, instance.VersionCriteria()); + var replacement = registry.GetReplacement(mod.Key, instance.VersionCriteria()); if (replacement != null) { // Replaceable @@ -87,21 +86,21 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) } } } - else + else if (options.modules != null) { foreach (string mod in options.modules) { try { log.DebugFormat("Checking that {0} is installed", mod); - CkanModule modToReplace = registry.GetInstalledVersion(mod); + var modToReplace = registry.GetInstalledVersion(mod); if (modToReplace != null) { log.DebugFormat("Testing {0} {1} for possible replacement", modToReplace.identifier, modToReplace.version); try { // Check if replacement is available - ModuleReplacement replacement = registry.GetReplacement(modToReplace.identifier, instance.VersionCriteria()); + var replacement = registry.GetReplacement(modToReplace.identifier, instance.VersionCriteria()); if (replacement != null) { // Replaceable @@ -124,7 +123,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) catch (ModuleNotFoundKraken) { log.InfoFormat("{0} is installed, but its replacement {1} is not in the registry", - mod, modToReplace.replaced_by.name); + mod, modToReplace.replaced_by?.name ?? ""); } } } @@ -134,7 +133,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) } } } - if (to_replace.Count() != 0) + if (to_replace.Count != 0 && manager.Cache != null) { user.RaiseMessage(""); user.RaiseMessage(Properties.Resources.Replacing); @@ -154,17 +153,16 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) return Exit.ERROR; } - // TODO: These instances all need to go. try { - HashSet possibleConfigOnlyDirs = null; + HashSet? possibleConfigOnlyDirs = null; 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, - ex.parent, ex.module, ex.version, instance.game.ShortName); + ex.parent, ex.module, ex.version ?? "", instance.game.ShortName); } } else @@ -186,7 +184,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) internal class ReplaceOptions : InstanceSpecificOptions { [Option('c', "ckanfile", HelpText = "Local CKAN file to process")] - public string ckan_file { get; set; } + public string? ckan_file { get; set; } [Option("no-recommends", HelpText = "Do not install recommended modules")] public bool no_recommends { get; set; } @@ -203,10 +201,9 @@ internal class ReplaceOptions : InstanceSpecificOptions [Option("all", HelpText = "Replace all available replaced modules")] public bool replace_all { get; set; } - // TODO: How do we provide helptext on this? - [ValueList(typeof (List))] + [ValueList(typeof(List))] [InstalledIdentifiers] - public List modules { get; set; } + public List? modules { get; set; } } } diff --git a/Cmdline/Action/Repo.cs b/Cmdline/Action/Repo.cs index 627232d2f9..9293a06780 100644 --- a/Cmdline/Action/Repo.cs +++ b/Cmdline/Action/Repo.cs @@ -1,8 +1,8 @@ using System; using System.Linq; using System.Collections.Generic; +using System.Collections.ObjectModel; -using Newtonsoft.Json; using CommandLine; using CommandLine.Text; using log4net; @@ -12,22 +12,22 @@ namespace CKAN.CmdLine public class RepoSubOptions : VerbCommandOptions { [VerbOption("available", HelpText = "List (canonical) available repositories")] - public RepoAvailableOptions AvailableOptions { get; set; } + public RepoAvailableOptions? AvailableOptions { get; set; } [VerbOption("list", HelpText = "List repositories")] - public RepoListOptions ListOptions { get; set; } + public RepoListOptions? ListOptions { get; set; } [VerbOption("add", HelpText = "Add a repository")] - public RepoAddOptions AddOptions { get; set; } + public RepoAddOptions? AddOptions { get; set; } [VerbOption("priority", HelpText = "Set repository priority")] - public RepoPriorityOptions PriorityOptions { get; set; } + public RepoPriorityOptions? PriorityOptions { get; set; } [VerbOption("forget", HelpText = "Forget a repository")] - public RepoForgetOptions ForgetOptions { get; set; } + public RepoForgetOptions? ForgetOptions { get; set; } [VerbOption("default", HelpText = "Set the default repository")] - public RepoDefaultOptions DefaultOptions { get; set; } + public RepoDefaultOptions? DefaultOptions { get; set; } [HelpVerbOption] public string GetUsage(string verb) @@ -86,24 +86,24 @@ public class RepoListOptions : InstanceSpecificOptions { } public class RepoAddOptions : InstanceSpecificOptions { - [ValueOption(0)] public string name { get; set; } - [ValueOption(1)] public string uri { get; set; } + [ValueOption(0)] public string? name { get; set; } + [ValueOption(1)] public string? uri { get; set; } } public class RepoPriorityOptions : InstanceSpecificOptions { - [ValueOption(0)] public string name { get; set; } + [ValueOption(0)] public string? name { get; set; } [ValueOption(1)] public int priority { get; set; } } public class RepoDefaultOptions : InstanceSpecificOptions { - [ValueOption(0)] public string uri { get; set; } + [ValueOption(0)] public string? uri { get; set; } } public class RepoForgetOptions : InstanceSpecificOptions { - [ValueOption(0)] public string name { get; set; } + [ValueOption(0)] public string? name { get; set; } } public class Repo : ISubCommand @@ -114,7 +114,9 @@ public Repo(RepositoryDataManager repoData) } // This is required by ISubCommand - public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCommandOptions unparsed) + public int RunSubCommand(GameInstanceManager? manager, + CommonOptions? opts, + SubCommandOptions unparsed) { string[] args = unparsed.options.ToArray(); @@ -187,50 +189,39 @@ public int RunSubCommand(GameInstanceManager manager, CommonOptions opts, SubCom return exitCode; } - private RepositoryList FetchMasterRepositoryList(Uri master_uri = null) - { - if (master_uri == null) - { - master_uri = MainClass.GetGameInstance(Manager).game.RepositoryListURL; - } - - string json = Net.DownloadText(master_uri); - return JsonConvert.DeserializeObject(json); - } - private int AvailableRepositories() { - user.RaiseMessage(Properties.Resources.RepoAvailableHeader); - RepositoryList repositories; + user?.RaiseMessage(Properties.Resources.RepoAvailableHeader); try { - repositories = FetchMasterRepositoryList(); + if (RepositoryList.DefaultRepositories( + MainClass.GetGameInstance(Manager).game) + is RepositoryList repoList) + { + var maxNameLen = repoList.repositories + .Select(r => r.name.Length) + .Max(); + foreach (var repository in repoList.repositories) + { + user?.RaiseMessage(" {0}: {1}", + repository.name.PadRight(maxNameLen), + repository.uri); + } + return Exit.OK; + } } catch { - user.RaiseError(Properties.Resources.RepoAvailableFailed, MainClass.GetGameInstance(Manager).game.RepositoryListURL.ToString()); - return Exit.ERROR; + user?.RaiseError(Properties.Resources.RepoAvailableFailed, + MainClass.GetGameInstance(Manager).game.RepositoryListURL.ToString()); } - - int maxNameLen = 0; - foreach (Repository repository in repositories.repositories) - { - maxNameLen = Math.Max(maxNameLen, repository.name.Length); - } - - foreach (Repository repository in repositories.repositories) - { - user.RaiseMessage(" {0}: {1}", repository.name.PadRight(maxNameLen), repository.uri); - } - - return Exit.OK; + return Exit.ERROR; } private int ListRepositories() { var repositories = RegistryManager.Instance(MainClass.GetGameInstance(Manager), repoData).registry.Repositories; - string priorityHeader = Properties.Resources.RepoListPriorityHeader; string nameHeader = Properties.Resources.RepoListNameHeader; string urlHeader = Properties.Resources.RepoListURLHeader; @@ -247,20 +238,20 @@ private int ListRepositories() const string columnFormat = "{0} {1} {2}"; - user.RaiseMessage(columnFormat, - priorityHeader.PadRight(priorityWidth), - nameHeader.PadRight(nameWidth), - urlHeader.PadRight(urlWidth)); - user.RaiseMessage(columnFormat, - new string('-', priorityWidth), - new string('-', nameWidth), - new string('-', urlWidth)); + user?.RaiseMessage(columnFormat, + priorityHeader.PadRight(priorityWidth), + nameHeader.PadRight(nameWidth), + urlHeader.PadRight(urlWidth)); + user?.RaiseMessage(columnFormat, + new string('-', priorityWidth), + new string('-', nameWidth), + new string('-', urlWidth)); foreach (Repository repository in repositories.Values.OrderBy(r => r.priority)) { - user.RaiseMessage(columnFormat, - repository.priority.ToString().PadRight(priorityWidth), - repository.name.PadRight(nameWidth), - repository.uri); + user?.RaiseMessage(columnFormat, + repository.priority.ToString().PadRight(priorityWidth), + repository.name.PadRight(nameWidth), + repository.uri); } return Exit.OK; } @@ -271,121 +262,126 @@ private int AddRepository(RepoAddOptions options) if (options.name == null) { - user.RaiseError(Properties.Resources.ArgumentMissing); + user?.RaiseError(Properties.Resources.ArgumentMissing); PrintUsage("add"); return Exit.BADOPT; } if (options.uri == null) { - RepositoryList repositoryList; - - try + if (RepositoryList.DefaultRepositories( + MainClass.GetGameInstance(Manager).game) + is RepositoryList repoList) { - repositoryList = FetchMasterRepositoryList(); - } - catch - { - user.RaiseError(Properties.Resources.RepoAvailableFailed, Manager.CurrentInstance.game.RepositoryListURL.ToString()); - return Exit.ERROR; - } + foreach (var candidate in repoList.repositories) + { + if (string.Equals(candidate.name, options.name, StringComparison.OrdinalIgnoreCase)) + { + options.name = candidate.name; + options.uri = candidate.uri.ToString(); + } + } - foreach (Repository candidate in repositoryList.repositories) - { - if (string.Equals(candidate.name, options.name, StringComparison.OrdinalIgnoreCase)) + // Nothing found in the master list? + if (options.uri == null) { - options.name = candidate.name; - options.uri = candidate.uri.ToString(); + user?.RaiseMessage(Properties.Resources.RepoAddNotFound, + options.name); + return Exit.BADOPT; } } - // Nothing found in the master list? - if (options.uri == null) + } + if (options.uri != null) + { + log.DebugFormat("About to add repository '{0}' - '{1}'", options.name, options.uri); + if (manager.registry.Repositories.ContainsKey(options.name)) { - user.RaiseMessage(Properties.Resources.RepoAddNotFound, options.name); + user?.RaiseMessage(Properties.Resources.RepoAddDuplicate, + options.name); + return Exit.BADOPT; + } + if (manager.registry.Repositories.Values.Any(r => r.uri.ToString() == options.uri)) + { + user?.RaiseMessage(Properties.Resources.RepoAddDuplicateURL, + options.uri); return Exit.BADOPT; } - } - - log.DebugFormat("About to add repository '{0}' - '{1}'", options.name, options.uri); - var repositories = manager.registry.Repositories; - - if (repositories.ContainsKey(options.name)) - { - user.RaiseMessage(Properties.Resources.RepoAddDuplicate, options.name); - return Exit.BADOPT; - } - if (repositories.Values.Any(r => r.uri.ToString() == options.uri)) - { - user.RaiseMessage(Properties.Resources.RepoAddDuplicateURL, options.uri); - return Exit.BADOPT; - } - manager.registry.RepositoriesAdd(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(); + user?.RaiseMessage(Properties.Resources.RepoAdded, + options.name, + options.uri); + manager.Save(); - return Exit.OK; + return Exit.OK; + } + return Exit.ERROR; } private int SetRepositoryPriority(RepoPriorityOptions options) { if (options.name == null) { - user.RaiseError(Properties.Resources.ArgumentMissing); + user?.RaiseError(Properties.Resources.ArgumentMissing); PrintUsage("priority"); return Exit.BADOPT; } var manager = RegistryManager.Instance(MainClass.GetGameInstance(Manager), repoData); - if (options.priority < 0 || options.priority >= manager.registry.Repositories.Count) + if (manager.registry.Repositories is ReadOnlyDictionary repositories) { - user.RaiseMessage(Properties.Resources.RepoPriorityInvalid, - options.priority, manager.registry.Repositories.Count - 1); - return Exit.BADOPT; - } + if (options.priority < 0 || options.priority >= repositories.Count) + { + user?.RaiseMessage(Properties.Resources.RepoPriorityInvalid, + options.priority, + repositories.Count - 1); + return Exit.BADOPT; + } - if (manager.registry.Repositories.TryGetValue(options.name, out Repository repo)) - { - if (options.priority != repo.priority) + if (repositories.TryGetValue(options.name, out Repository? repo)) { - var sortedRepos = manager.registry.Repositories.Values - .OrderBy(r => r.priority) - .ToList(); - // Shift other repos up or down by 1 to make room in the list - if (options.priority < repo.priority) + if (options.priority != repo.priority) { - for (int i = options.priority; i < repo.priority; ++i) + var sortedRepos = repositories.Values + .OrderBy(r => r.priority) + .ToList(); + // Shift other repos up or down by 1 to make room in the list + if (options.priority < repo.priority) { - sortedRepos[i].priority = i + 1; + for (int i = options.priority; i < repo.priority; ++i) + { + sortedRepos[i].priority = i + 1; + } } - } - else - { - for (int i = repo.priority + 1; i <= options.priority; ++i) + else { - sortedRepos[i].priority = i - 1; + for (int i = repo.priority + 1; i <= options.priority; ++i) + { + sortedRepos[i].priority = i - 1; + } } + // Move chosen repo into new spot and save + repo.priority = options.priority; + manager.Save(); } - // Move chosen repo into new spot and save - repo.priority = options.priority; - manager.Save(); + return ListRepositories(); + } + else + { + user?.RaiseMessage(Properties.Resources.RepoPriorityNotFound, + options.name); } - return ListRepositories(); - } - else - { - user.RaiseMessage(Properties.Resources.RepoPriorityNotFound, options.name); - return Exit.BADOPT; } + return Exit.BADOPT; } private int ForgetRepository(RepoForgetOptions options) { if (options.name == null) { - user.RaiseError(Properties.Resources.ArgumentMissing); + user?.RaiseError(Properties.Resources.ArgumentMissing); PrintUsage("forget"); return Exit.BADOPT; } @@ -393,30 +389,35 @@ private int ForgetRepository(RepoForgetOptions options) RegistryManager manager = RegistryManager.Instance(MainClass.GetGameInstance(Manager), repoData); log.DebugFormat("About to forget repository '{0}'", options.name); - var repos = manager.registry.Repositories; - - string name = options.name; - if (!repos.ContainsKey(options.name)) + if (manager.registry.Repositories is ReadOnlyDictionary repos) { - name = repos.Keys.FirstOrDefault(repo => repo.Equals(options.name, StringComparison.OrdinalIgnoreCase)); - if (name == null) + var name = options.name; + if (!repos.ContainsKey(options.name)) { - user.RaiseMessage(Properties.Resources.RepoForgetNotFound, options.name); - return Exit.BADOPT; + name = repos.Keys.FirstOrDefault(repo => repo.Equals(options.name, StringComparison.OrdinalIgnoreCase)); + if (name == null) + { + user?.RaiseMessage(Properties.Resources.RepoForgetNotFound, + options.name); + return Exit.BADOPT; + } + user?.RaiseMessage(Properties.Resources.RepoForgetRemoving, + name); } - user.RaiseMessage(Properties.Resources.RepoForgetRemoving, name); - } - manager.registry.RepositoriesRemove(name); - var remaining = repos.Values.OrderBy(r => r.priority).ToArray(); - for (int i = 0; i < remaining.Length; ++i) - { - remaining[i].priority = i; - } - user.RaiseMessage(Properties.Resources.RepoForgetRemoved, options.name); - manager.Save(); + manager.registry.RepositoriesRemove(name); + var remaining = repos.Values.OrderBy(r => r.priority).ToArray(); + for (int i = 0; i < remaining.Length; ++i) + { + remaining[i].priority = i; + } + user?.RaiseMessage(Properties.Resources.RepoForgetRemoved, + options.name); + manager.Save(); - return Exit.OK; + return Exit.OK; + } + return Exit.ERROR; } private int DefaultRepository(RepoDefaultOptions options) @@ -426,40 +427,38 @@ private int DefaultRepository(RepoDefaultOptions options) log.DebugFormat("About to add repository '{0}' - '{1}'", Repository.default_ckan_repo_name, uri); RegistryManager manager = RegistryManager.Instance(inst, repoData); - var repositories = manager.registry.Repositories; - - if (repositories.ContainsKey(Repository.default_ckan_repo_name)) + if (manager.registry.Repositories is ReadOnlyDictionary repositories) { - manager.registry.RepositoriesRemove(Repository.default_ckan_repo_name); - } + if (repositories.ContainsKey(Repository.default_ckan_repo_name)) + { + manager.registry.RepositoriesRemove(Repository.default_ckan_repo_name); + } - manager.registry.RepositoriesAdd( - 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(); + user?.RaiseMessage(Properties.Resources.RepoSet, + Repository.default_ckan_repo_name, + uri); + manager.Save(); - return Exit.OK; + return Exit.OK; + } + return Exit.ERROR; } private void PrintUsage(string verb) { foreach (var h in RepoSubOptions.GetHelp(verb)) { - user.RaiseError(h); + user?.RaiseError(h); } } - private GameInstanceManager Manager; + private GameInstanceManager? Manager; private readonly RepositoryDataManager repoData; - private IUser user; + private IUser? user; private static readonly ILog log = LogManager.GetLogger(typeof (Repo)); } - - public struct RepositoryList - { - public Repository[] repositories; - } - } diff --git a/Cmdline/Action/Search.cs b/Cmdline/Action/Search.cs index 6083f67c6d..357e274f4a 100644 --- a/Cmdline/Action/Search.cs +++ b/Cmdline/Action/Search.cs @@ -5,6 +5,8 @@ using CommandLine; +using CKAN.Versioning; + namespace CKAN.CmdLine { public class Search : ICommand @@ -30,8 +32,8 @@ public int RunCommand(CKAN.GameInstance ksp, object raw_options) return Exit.BADOPT; } - List matching_compatible = PerformSearch(ksp, options.search_term, options.author_term, false); - List matching_incompatible = new List(); + var matching_compatible = PerformSearch(ksp, options.search_term, options.author_term, false); + var matching_incompatible = new List(); if (options.all) { matching_incompatible = PerformSearch(ksp, options.search_term, options.author_term, true); @@ -41,34 +43,35 @@ public int RunCommand(CKAN.GameInstance ksp, object raw_options) if (options.all && !string.IsNullOrWhiteSpace(options.author_term)) { user.RaiseMessage(Properties.Resources.SearchFoundByAuthorWithIncompat, - matching_compatible.Count().ToString(), - matching_incompatible.Count().ToString(), - options.search_term, - options.author_term); + matching_compatible.Count.ToString(), + matching_incompatible.Count.ToString(), + options.search_term ?? "", + options.author_term ?? ""); } else if (options.all && string.IsNullOrWhiteSpace(options.author_term)) { user.RaiseMessage(Properties.Resources.SearchFoundWithIncompat, - matching_compatible.Count().ToString(), - matching_incompatible.Count().ToString(), - options.search_term); + matching_compatible.Count.ToString(), + matching_incompatible.Count.ToString(), + options.search_term ?? ""); } else if (!options.all && !string.IsNullOrWhiteSpace(options.author_term)) { user.RaiseMessage(Properties.Resources.SearchFoundByAuthor, - matching_compatible.Count().ToString(), - options.search_term, - options.author_term); + matching_compatible.Count.ToString(), + options.search_term ?? "", + options.author_term ?? ""); } else if (!options.all && string.IsNullOrWhiteSpace(options.author_term)) { user.RaiseMessage(Properties.Resources.SearchFound, - matching_compatible.Count().ToString(), - options.search_term); + matching_compatible.Count.ToString(), + options.search_term ?? ""); } // Present the results. - if (!matching_compatible.Any() && (!options.all || !matching_incompatible.Any())) + if (!matching_compatible.Any() + && (!options.all || !matching_incompatible.Any())) { return Exit.OK; } @@ -79,11 +82,11 @@ public int RunCommand(CKAN.GameInstance ksp, object raw_options) foreach (CkanModule mod in matching_compatible) { user.RaiseMessage(Properties.Resources.SearchCompatibleMod, - mod.identifier, - mod.version, - mod.name, - mod.author == null ? "N/A" : string.Join(", ", mod.author), - mod.@abstract); + mod.identifier, + mod.version, + mod.name, + mod.author == null ? "N/A" : string.Join(", ", mod.author), + mod.@abstract); } if (matching_incompatible.Any()) @@ -92,12 +95,15 @@ public int RunCommand(CKAN.GameInstance ksp, object raw_options) foreach (CkanModule mod in matching_incompatible) { CkanModule.GetMinMaxVersions(new List { mod } , out _, out _, out var minKsp, out var maxKsp); - string GameVersion = Versioning.GameVersionRange.VersionSpan(ksp.game, minKsp, maxKsp).ToString(); + var gv = GameVersionRange.VersionSpan(ksp.game, + minKsp ?? GameVersion.Any, + maxKsp ?? GameVersion.Any) + .ToString(); user.RaiseMessage(Properties.Resources.SearchIncompatibleMod, mod.identifier, mod.version, - GameVersion, + gv, mod.name, mod.author == null ? "N/A" : string.Join(", ", mod.author), mod.@abstract); @@ -127,7 +133,10 @@ public int RunCommand(CKAN.GameInstance ksp, object raw_options) /// The search term. Case insensitive. /// Name of author to find /// True to look for incompatible modules, false (default) to look for compatible - public List PerformSearch(CKAN.GameInstance ksp, string term, string author = null, bool searchIncompatible = false) + public List PerformSearch(CKAN.GameInstance ksp, + string? term, + string? author = null, + bool searchIncompatible = false) { // Remove spaces and special characters from the search term. term = string.IsNullOrWhiteSpace(term) ? string.Empty : CkanModule.nonAlphaNums.Replace(term, ""); @@ -167,9 +176,9 @@ public List PerformSearch(CKAN.GameInstance ksp, string term, string private static string CaseInsensitiveExactMatch(List mods, string module) { // Look for a matching mod with a case insensitive search - CkanModule found = mods.FirstOrDefault( - (CkanModule m) => string.Equals(m.identifier, module, StringComparison.OrdinalIgnoreCase) - ); + var found = mods.FirstOrDefault(m => string.Equals(m.identifier, + module, + StringComparison.OrdinalIgnoreCase)); // If we don't find anything, use the original string so the main code can raise errors return found?.identifier ?? module; } @@ -214,10 +223,10 @@ internal class SearchOptions : InstanceSpecificOptions public bool all { get; set; } [Option("author", HelpText = "Limit search results to mods by matching authors")] - public string author_term { get; set; } + public string? author_term { get; set; } [ValueOption(0)] - public string search_term { get; set; } + public string? search_term { get; set; } } } diff --git a/Cmdline/Action/Show.cs b/Cmdline/Action/Show.cs index f9550cc96a..4f5b7f5c8d 100644 --- a/Cmdline/Action/Show.cs +++ b/Cmdline/Action/Show.cs @@ -39,10 +39,8 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) if (installedModuleToShow != null) { // Show the installed module. - combined_exit_code = CombineExitCodes( - combined_exit_code, - ShowMod(installedModuleToShow, options) - ); + combined_exit_code = CombineExitCodes(combined_exit_code, + ShowMod(installedModuleToShow, options)); if (options.with_versions) { ShowVersionTable(instance, registry.AvailableByIdentifier(installedModuleToShow.identifier).ToList()); @@ -53,12 +51,10 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) // Module was not installed, look for an exact match in the available modules, // either by "name" (the user-friendly display name) or by identifier - CkanModule moduleToShow = registry - .CompatibleModules(instance.VersionCriteria()) - .SingleOrDefault( - mod => mod.name == modName - || mod.identifier == modName - ); + var moduleToShow = registry.CompatibleModules(instance.VersionCriteria()) + .SingleOrDefault( + mod => mod.name == modName + || mod.identifier == modName); if (moduleToShow == null) { // No exact match found. Try to look for a close match for this KSP version. @@ -169,7 +165,7 @@ private int ShowMod(CkanModule module, ShowOptions opts) user.RaiseMessage("{0}", module.name); } - if (!string.IsNullOrEmpty(module.description)) + if (module.description != null && !string.IsNullOrEmpty(module.description)) { user.RaiseMessage(""); user.RaiseMessage("{0}", module.description); @@ -261,67 +257,34 @@ private int ShowMod(CkanModule module, ShowOptions opts) { user.RaiseMessage(""); user.RaiseMessage(Properties.Resources.ShowResourcesHeader); - if (module.resources.homepage != null) - { - user.RaiseMessage(Properties.Resources.ShowHomePage, - Net.NormalizeUri(module.resources.homepage.ToString())); - } - if (module.resources.manual != null) - { - user.RaiseMessage(Properties.Resources.ShowManual, - Net.NormalizeUri(module.resources.manual.ToString())); - } - if (module.resources.spacedock != null) - { - user.RaiseMessage(Properties.Resources.ShowSpaceDock, - Net.NormalizeUri(module.resources.spacedock.ToString())); - } - if (module.resources.repository != null) - { - user.RaiseMessage(Properties.Resources.ShowRepository, - Net.NormalizeUri(module.resources.repository.ToString())); - } - if (module.resources.bugtracker != null) - { - user.RaiseMessage(Properties.Resources.ShowBugTracker, - Net.NormalizeUri(module.resources.bugtracker.ToString())); - } - if (module.resources.discussions != null) - { - user.RaiseMessage(Properties.Resources.ShowDiscussions, - Net.NormalizeUri(module.resources.discussions.ToString())); - } - if (module.resources.curse != null) - { - user.RaiseMessage(Properties.Resources.ShowCurse, - Net.NormalizeUri(module.resources.curse.ToString())); - } - if (module.resources.store != null) - { - user.RaiseMessage(Properties.Resources.ShowStore, - Net.NormalizeUri(module.resources.store.ToString())); - } - if (module.resources.steamstore != null) - { - user.RaiseMessage(Properties.Resources.ShowSteamStore, - Net.NormalizeUri(module.resources.steamstore.ToString())); - } - if (module.resources.remoteAvc != null) - { - user.RaiseMessage(Properties.Resources.ShowVersionFile, - Net.NormalizeUri(module.resources.remoteAvc.ToString())); - } - if (module.resources.remoteSWInfo != null) - { - user.RaiseMessage(Properties.Resources.ShowSpaceWarpInfo, - Net.NormalizeUri(module.resources.remoteSWInfo.ToString())); - } + RaiseResource(Properties.Resources.ShowHomePage, + module.resources.homepage); + RaiseResource(Properties.Resources.ShowManual, + module.resources.manual); + RaiseResource(Properties.Resources.ShowSpaceDock, + module.resources.spacedock); + RaiseResource(Properties.Resources.ShowRepository, + module.resources.repository); + RaiseResource(Properties.Resources.ShowBugTracker, + module.resources.bugtracker); + RaiseResource(Properties.Resources.ShowDiscussions, + module.resources.discussions); + RaiseResource(Properties.Resources.ShowCurse, + module.resources.curse); + RaiseResource(Properties.Resources.ShowStore, + module.resources.store); + RaiseResource(Properties.Resources.ShowSteamStore, + module.resources.steamstore); + RaiseResource(Properties.Resources.ShowVersionFile, + module.resources.remoteAvc); + RaiseResource(Properties.Resources.ShowSpaceWarpInfo, + module.resources.remoteSWInfo); } if (!opts.without_files && !module.IsDLC) { // Compute the CKAN filename. - string file_uri_hash = NetFileCache.CreateURLHash(module.download[0]); + string file_uri_hash = NetFileCache.CreateURLHash(module.download?[0]); string file_name = CkanModule.StandardName(module.identifier, module.version); user.RaiseMessage(""); @@ -331,6 +294,14 @@ private int ShowMod(CkanModule module, ShowOptions opts) return Exit.OK; } + private void RaiseResource(string fmt, Uri? url) + { + if (url is Uri u && Net.NormalizeUri(u.ToString()) is string s) + { + user.RaiseMessage(fmt, s); + } + } + private static int CombineExitCodes(int a, int b) { // Failures should dominate, keep whichever one isn't OK @@ -342,8 +313,10 @@ private void ShowVersionTable(CKAN.GameInstance inst, List modules) var versions = modules.Select(m => m.version.ToString()).ToList(); var gameVersions = modules.Select(m => { - CkanModule.GetMinMaxVersions(new List() { m }, out _, out _, out GameVersion minKsp, out GameVersion maxKsp); - return GameVersionRange.VersionSpan(inst.game, minKsp, maxKsp); + CkanModule.GetMinMaxVersions(new List() { m }, out _, out _, out GameVersion? minKsp, out GameVersion? maxKsp); + return GameVersionRange.VersionSpan(inst.game, + minKsp ?? GameVersion.Any, + maxKsp ?? GameVersion.Any); }).ToList(); string[] headers = new string[] { Properties.Resources.ShowVersionHeader, @@ -409,7 +382,7 @@ internal class ShowOptions : InstanceSpecificOptions [ValueList(typeof(List))] [AvailableIdentifiers] - public List modules { get; set; } + public List? modules { get; set; } } } diff --git a/Cmdline/Action/Update.cs b/Cmdline/Action/Update.cs index 5f59a1c493..8fce5cb3d2 100644 --- a/Cmdline/Action/Update.cs +++ b/Cmdline/Action/Update.cs @@ -5,7 +5,6 @@ using CommandLine; -using CKAN.Versioning; using CKAN.Games; namespace CKAN.CmdLine @@ -46,7 +45,7 @@ public int RunCommand(object raw_options) if (game == null) { user.RaiseError(Properties.Resources.UpdateBadGame, - options.game, + options.game ?? "", string.Join(", ", KnownGames.AllGameShortNames())); return Exit.BADOPT; } @@ -57,41 +56,41 @@ public int RunCommand(object raw_options) getUri(url))) .DefaultIfEmpty(Repository.DefaultGameRepo(game)) .ToArray(); - List availablePrior = null; - if (options.list_changes) - { - availablePrior = repoData.GetAllAvailableModules(repos) - .Select(am => am.Latest()) - .ToList(); - } - UpdateRepositories(game, repos, options.force); if (options.list_changes) { + var availablePrior = repoData.GetAllAvailableModules(repos) + .Select(am => am.Latest()) + .OfType() + .ToList(); + UpdateRepositories(game, repos, options.force); PrintChanges(availablePrior, repoData.GetAllAvailableModules(repos) .Select(am => am.Latest()) + .OfType() .ToList()); } + else + { + UpdateRepositories(game, repos, options.force); + } } else { var instance = MainClass.GetGameInstance(manager); - Registry registry = null; - List compatible_prior = null; - GameVersionCriteria crit = null; if (options.list_changes) { // Get a list of compatible modules prior to the update. - registry = RegistryManager.Instance(instance, repoData).registry; - crit = instance.VersionCriteria(); - compatible_prior = registry.CompatibleModules(crit).ToList(); - } - UpdateRepositories(instance, options.force); - if (options.list_changes) - { + var registry = RegistryManager.Instance(instance, repoData).registry; + var crit = instance.VersionCriteria(); + var compatible_prior = registry.CompatibleModules(crit).ToList(); + UpdateRepositories(instance, options.force); PrintChanges(compatible_prior, registry.CompatibleModules(crit).ToList()); } + else + { + UpdateRepositories(instance, options.force); + } } } catch (MissingCertificateKraken kraken) @@ -127,7 +126,7 @@ private void PrintChanges(List modules_prior, // Print the changes. user.RaiseMessage(Properties.Resources.UpdateChangesSummary, - added.Count(), removed.Count(), updated.Count()); + added.Count, removed.Count, updated.Count); if (added.Count > 0) { @@ -225,10 +224,10 @@ internal class UpdateOptions : InstanceSpecificOptions // Can't specify DefaultValue here because we want to fall back to instance-based updates when omitted [Option('g', "game", HelpText = "Game for which to update repositories")] - public string game { set; get; } + public string? game { set; get; } [OptionArray('u', "urls", HelpText = "URLs of repositories to update")] - public string[] repositoryURLs { get; set; } + public string[]? repositoryURLs { get; set; } [Option('f', "force", DefaultValue = false, HelpText = "Download and parse metadata even if it hasn't changed")] public bool force { get; set; } diff --git a/Cmdline/Action/Upgrade.cs b/Cmdline/Action/Upgrade.cs index 55855c3426..1fd5b9e659 100644 --- a/Cmdline/Action/Upgrade.cs +++ b/Cmdline/Action/Upgrade.cs @@ -8,7 +8,9 @@ using CKAN.Versioning; using CKAN.Configuration; +#if !NET8_0_OR_GREATER using CKAN.Extensions; +#endif namespace CKAN.CmdLine { @@ -40,10 +42,10 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) if (options.ckan_file != null) { - options.modules.Add(MainClass.LoadCkanFromFile(options.ckan_file).identifier); + options.modules?.Add(MainClass.LoadCkanFromFile(options.ckan_file).identifier); } - if (options.modules.Count == 0 && !options.upgrade_all) + if (options.modules?.Count == 0 && !options.upgrade_all) { user.RaiseError(Properties.Resources.ArgumentMissing); foreach (var h in Actions.GetHelp("upgrade")) @@ -53,7 +55,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) return Exit.BADOPT; } - if (!options.upgrade_all && options.modules[0] == "ckan" && AutoUpdate.CanUpdate) + if (!options.upgrade_all && options.modules?[0] == "ckan" && AutoUpdate.CanUpdate) { if (options.dev_build && options.stable_release) { @@ -86,11 +88,14 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) var latestVersion = update.Version; var currentVersion = new ModuleVersion(Meta.GetVersion()); - if (!latestVersion.Equals(currentVersion)) + if (!currentVersion.Equals(latestVersion)) { - user.RaiseMessage(Properties.Resources.UpgradeNewCKANAvailable, latestVersion); - var releaseNotes = update.ReleaseNotes; - user.RaiseMessage(releaseNotes); + user.RaiseMessage(Properties.Resources.UpgradeNewCKANAvailable, + latestVersion?.ToString() ?? ""); + if (update.ReleaseNotes != null) + { + user.RaiseMessage(update.ReleaseNotes); + } user.RaiseMessage(""); user.RaiseMessage(""); @@ -126,15 +131,18 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) { user.RaiseMessage(Properties.Resources.UpgradeAllUpToDate); } - else + else if (manager.Cache != null) { - UpgradeModules(manager, user, instance, to_upgrade); + UpgradeModules(manager.Cache, user, instance, to_upgrade); } } else { - Search.AdjustModulesCase(instance, registry, options.modules); - UpgradeModules(manager, user, instance, options.modules); + if (options.modules != null && manager.Cache != null) + { + Search.AdjustModulesCase(instance, registry, options.modules); + UpgradeModules(manager.Cache, user, instance, options.modules); + } } user.RaiseMessage(""); } @@ -157,8 +165,8 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) { 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) + var storePagesMsg = new Uri?[] { res?.store, res?.steamstore } + .OfType() .Aggregate("", (a, b) => $"{a}\r\n- {b}"); if (!string.IsNullOrEmpty(storePagesMsg)) { @@ -177,14 +185,14 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) /// IUser object for output /// Game instance to use /// List of modules to upgrade - private void UpgradeModules(GameInstanceManager manager, + private void UpgradeModules(NetModuleCache cache, IUser user, CKAN.GameInstance instance, List modules) { UpgradeModules( - manager, user, instance, repoData, - (ModuleInstaller installer, NetAsyncModulesDownloader downloader, RegistryManager regMgr, ref HashSet possibleConfigOnlyDirs) => + cache, user, instance, repoData, + (ModuleInstaller installer, NetAsyncModulesDownloader downloader, RegistryManager regMgr, ref HashSet? possibleConfigOnlyDirs) => installer.Upgrade(modules, downloader, ref possibleConfigOnlyDirs, regMgr, true, true, true), @@ -198,14 +206,14 @@ private void UpgradeModules(GameInstanceManager manager, /// IUser object for output /// Game instance to use /// List of identifier[=version] to upgrade - private void UpgradeModules(GameInstanceManager manager, + private void UpgradeModules(NetModuleCache cache, IUser user, CKAN.GameInstance instance, List identsAndVersions) { UpgradeModules( - manager, user, instance, repoData, - (ModuleInstaller installer, NetAsyncModulesDownloader downloader, RegistryManager regMgr, ref HashSet possibleConfigOnlyDirs) => + cache, user, instance, repoData, + (ModuleInstaller installer, NetAsyncModulesDownloader downloader, RegistryManager regMgr, ref HashSet? possibleConfigOnlyDirs) => { var crit = instance.VersionCriteria(); var registry = regMgr.registry; @@ -220,7 +228,7 @@ private void UpgradeModules(GameInstanceManager manager, () => registry.LatestAvailable(req, crit)) ?? registry.GetInstalledVersion(req)) .Concat(heldIdents.Select(ident => registry.GetInstalledVersion(ident))) - .Where(m => m != null) + .OfType() .ToList(); // Modules allowed by THOSE modules' relationships var upgradeable = registry @@ -233,7 +241,7 @@ private void UpgradeModules(GameInstanceManager manager, foreach (var request in identsAndVersions) { var module = CkanModule.FromIDandVersion(registry, request, crit) - ?? (upgradeable.TryGetValue(request, out CkanModule m) + ?? (upgradeable.TryGetValue(request, out CkanModule? m) ? m : null); if (module == null) @@ -257,11 +265,11 @@ private static string UpToFirst(string orig, char toFind) => UpTo(orig, orig.IndexOf(toFind)); private static string UpTo(string orig, int pos) - => pos >= 0 && pos < orig.Length ? orig.Substring(0, pos) + => pos >= 0 && pos < orig.Length ? orig[..pos] : orig; // Action isn't allowed - private delegate void AttemptUpgradeAction(ModuleInstaller installer, NetAsyncModulesDownloader downloader, RegistryManager regMgr, ref HashSet possibleConfigOnlyDirs); + private delegate void AttemptUpgradeAction(ModuleInstaller installer, NetAsyncModulesDownloader downloader, RegistryManager regMgr, ref HashSet? possibleConfigOnlyDirs); /// /// The core of the module upgrading logic, with callbacks to @@ -274,7 +282,7 @@ private static string UpTo(string orig, int pos) /// 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 void UpgradeModules(GameInstanceManager manager, + private void UpgradeModules(NetModuleCache cache, IUser user, CKAN.GameInstance instance, RepositoryDataManager repoData, @@ -282,10 +290,10 @@ private void UpgradeModules(GameInstanceManager manager, Action addUserChoiceCallback) { using (TransactionScope transact = CkanTransaction.CreateTransactionScope()) { - var installer = new ModuleInstaller(instance, manager.Cache, user); - var downloader = new NetAsyncModulesDownloader(user, manager.Cache); + var installer = new ModuleInstaller(instance, cache, user); + var downloader = new NetAsyncModulesDownloader(user, cache); var regMgr = RegistryManager.Instance(instance, repoData); - HashSet possibleConfigOnlyDirs = null; + HashSet? possibleConfigOnlyDirs = null; bool done = false; while (!done) { @@ -321,7 +329,7 @@ private void UpgradeModules(GameInstanceManager manager, internal class UpgradeOptions : InstanceSpecificOptions { [Option('c', "ckanfile", HelpText = "Local CKAN file to process")] - public string ckan_file { get; set; } + public string? ckan_file { get; set; } [Option("no-recommends", DefaultValue = false, HelpText = "Do not install recommended modules")] public bool no_recommends { get; set; } @@ -345,7 +353,7 @@ internal class UpgradeOptions : InstanceSpecificOptions [ValueList(typeof (List))] [InstalledIdentifiers] - public List modules { get; set; } + public List? modules { get; set; } } } diff --git a/Cmdline/CKAN-cmdline.csproj b/Cmdline/CKAN-cmdline.csproj index 12819a8ecd..dbec83401b 100644 --- a/Cmdline/CKAN-cmdline.csproj +++ b/Cmdline/CKAN-cmdline.csproj @@ -16,10 +16,12 @@ true Debug;Release;NoGUI false - 7.3 + 9 + enable + true CKAN.CmdLine.MainClass ..\assets\ckan.ico - net48;net7.0;net7.0-windows + net48;net8.0;net8.0-windows $(TargetFramework.Replace("-windows", "")) 512 prompt @@ -74,7 +76,7 @@ CKAN-ConsoleUI + Condition=" '$(TargetFramework)' != 'net8.0' "> TargetFramework=$(TargetFramework) {A79F9D54-315C-472B-928F-713A5860B2BE} CKAN-GUI diff --git a/Cmdline/ConsoleUser.cs b/Cmdline/ConsoleUser.cs index 47bf56d6a9..8b1edc204b 100644 --- a/Cmdline/ConsoleUser.cs +++ b/Cmdline/ConsoleUser.cs @@ -164,7 +164,7 @@ public int RaiseSelectionDialog(string message, params object[] args) : string.Format(Properties.Resources.UserSelectionPromptWithoutDefault,1, args.Length)); // Wait for input from the command line. - string input = Console.In.ReadLine(); + var input = Console.In.ReadLine(); if (input == null) { @@ -234,10 +234,10 @@ public void RaiseError(string message, params object[] args) if (Headless) { // Special GitHub Action formatting for mutli-line errors - log.ErrorFormat( - message.Replace("\r\n", "%0A"), - args.Select(a => a.ToString().Replace("\r\n", "%0A")).ToArray() - ); + log.ErrorFormat(message.Replace("\r\n", "%0A"), + args.Select(a => a.ToString()?.Replace("\r\n", "%0A")) + .OfType() + .ToArray()); } else { @@ -290,7 +290,7 @@ public void RaiseProgress(int percent, long bytesPerSecond, long bytesLeft) /// private int previousPercent = -1; - private string lastProgressMessage = null; + private string? lastProgressMessage = null; /// /// Writes a message to the console diff --git a/Cmdline/Main.cs b/Cmdline/Main.cs index 7a7bd36c23..8fa3239854 100644 --- a/Cmdline/Main.cs +++ b/Cmdline/Main.cs @@ -7,7 +7,7 @@ using System.Net; using System.Diagnostics; using System.Linq; -#if NET5_0_OR_GREATER +#if WINDOWS && NET5_0_OR_GREATER using System.Runtime.Versioning; #endif @@ -41,7 +41,10 @@ public static int Main(string[] args) } // Default to GUI if there are no command line args or if the only args are flags rather than commands. - if (args.All(a => a == "--verbose" || a == "--debug" || a == "--asroot" || a == "--show-console")) + if (args.All(a => a is "--verbose" + or "--debug" + or "--asroot" + or "--show-console")) { var guiCommand = args.ToList(); guiCommand.Insert(0, "gui"); @@ -77,7 +80,7 @@ public static int Main(string[] args) } } - public static int Execute(GameInstanceManager manager, CommonOptions opts, 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. @@ -166,10 +169,10 @@ public static int AfterHelp() return Exit.BADOPT; } - public static CKAN.GameInstance GetGameInstance(GameInstanceManager manager) + public static CKAN.GameInstance GetGameInstance(GameInstanceManager? manager) { - CKAN.GameInstance inst = manager.CurrentInstance - ?? manager.GetPreferredInstance(); + var inst = manager?.CurrentInstance + ?? manager?.GetPreferredInstance(); #pragma warning disable IDE0270 if (inst == null) { @@ -323,7 +326,9 @@ private static int Version(IUser user) /// /// Changes the output message if set. /// Exit.OK if instance is consistent, Exit.ERROR otherwise - private static int Scan(CKAN.GameInstance inst, IUser user, string next_command = null) + private static int Scan(CKAN.GameInstance inst, + IUser user, + string? next_command = null) { try { @@ -348,9 +353,9 @@ private static int Scan(CKAN.GameInstance inst, IUser user, string next_command } } - private static int Clean(NetModuleCache cache) + private static int Clean(NetModuleCache? cache) { - cache.RemoveAll(); + cache?.RemoveAll(); return Exit.OK; } } diff --git a/Cmdline/Options.cs b/Cmdline/Options.cs index 53080fb094..a116a1c79d 100644 --- a/Cmdline/Options.cs +++ b/Cmdline/Options.cs @@ -35,6 +35,10 @@ public Options(string[] args) throw new BadCommandKraken(); } ); + // These are just here so the compiler knows they're never null, + // the above callback should always set them + action ??= ""; + options ??= new object(); } } @@ -44,80 +48,80 @@ internal class Actions : VerbCommandOptions { #if NETFRAMEWORK || WINDOWS [VerbOption("gui", HelpText = "Start the CKAN GUI")] - public GuiOptions GuiOptions { get; set; } + public GuiOptions? GuiOptions { get; set; } #endif [VerbOption("consoleui", HelpText = "Start the CKAN console UI")] - public ConsoleUIOptions ConsoleUIOptions { get; set; } + public ConsoleUIOptions? ConsoleUIOptions { get; set; } [VerbOption("prompt", HelpText = "Run CKAN prompt for executing multiple commands in a row")] - public CommonOptions PromptOptions { get; set; } + public CommonOptions? PromptOptions { get; set; } [VerbOption("search", HelpText = "Search for mods")] - public SearchOptions SearchOptions { get; set; } + public SearchOptions? SearchOptions { get; set; } [VerbOption("upgrade", HelpText = "Upgrade an installed mod")] - public UpgradeOptions Upgrade { get; set; } + public UpgradeOptions? Upgrade { get; set; } [VerbOption("update", HelpText = "Update list of available mods")] - public UpdateOptions Update { get; set; } + public UpdateOptions? Update { get; set; } [VerbOption("available", HelpText = "List available mods")] - public AvailableOptions Available { get; set; } + public AvailableOptions? Available { get; set; } [VerbOption("install", HelpText = "Install a mod")] - public InstallOptions Install { get; set; } + public InstallOptions? Install { get; set; } [VerbOption("remove", HelpText = "Remove an installed mod")] - public RemoveOptions Remove { get; set; } + public RemoveOptions? Remove { get; set; } [VerbOption("import", HelpText = "Import manually downloaded mods")] - public ImportOptions Import { get; set; } + public ImportOptions? Import { get; set; } [VerbOption("scan", HelpText = "Scan for manually installed mods")] - public ScanOptions Scan { get; set; } + public ScanOptions? Scan { get; set; } [VerbOption("list", HelpText = "List installed modules")] - public ListOptions List { get; set; } + public ListOptions? List { get; set; } [VerbOption("show", HelpText = "Show information about a mod")] - public ShowOptions Show { get; set; } + public ShowOptions? Show { get; set; } [VerbOption("clean", HelpText = "Clean away downloaded files from the cache")] - public CleanOptions Clean { get; set; } + public CleanOptions? Clean { get; set; } [VerbOption("repair", HelpText = "Attempt various automatic repairs")] - public RepairSubOptions Repair { get; set; } + public RepairSubOptions? Repair { get; set; } [VerbOption("replace", HelpText = "Replace list of replaceable mods")] - public ReplaceOptions Replace { get; set; } + public ReplaceOptions? Replace { get; set; } [VerbOption("repo", HelpText = "Manage CKAN repositories")] - public RepoSubOptions Repo { get; set; } + public RepoSubOptions? Repo { get; set; } [VerbOption("mark", HelpText = "Edit flags on modules")] - public MarkSubOptions Mark { get; set; } + public MarkSubOptions? Mark { get; set; } [VerbOption("instance", HelpText = "Manage game instances")] - public InstanceSubOptions Instance { get; set; } + public InstanceSubOptions? Instance { get; set; } [VerbOption("authtoken", HelpText = "Manage authentication tokens")] - public AuthTokenSubOptions AuthToken { get; set; } + public AuthTokenSubOptions? AuthToken { get; set; } [VerbOption("cache", HelpText = "Manage download cache path")] - public CacheSubOptions Cache { get; set; } + public CacheSubOptions? Cache { get; set; } [VerbOption("compat", HelpText = "Manage game version compatibility")] - public CompatSubOptions Compat { get; set; } + public CompatSubOptions? Compat { get; set; } [VerbOption("compare", HelpText = "Compare version strings")] - public CompareOptions Compare { get; set; } + public CompareOptions? Compare { get; set; } [VerbOption("version", HelpText = "Show the version of the CKAN client being used")] - public VersionOptions Version { get; set; } + public VersionOptions? Version { get; set; } [VerbOption("filter", HelpText = "View or edit installation filters")] - public FilterSubOptions Filter { get; set; } + public FilterSubOptions? Filter { get; set; } [HelpVerbOption] public string GetUsage(string verb) @@ -140,7 +144,7 @@ public static IEnumerable GetHelp(string verb) } else { - string descr = GetDescription(typeof(Action), verb); + var descr = GetDescription(typeof(Action), verb); if (!string.IsNullOrEmpty(descr)) { yield return $"ckan {verb} - {descr}"; @@ -191,14 +195,15 @@ public static IEnumerable GetHelp(string verb) public abstract class VerbCommandOptions { - protected string GetDescription(string verb) + protected string? GetDescription(string verb) => GetDescription(GetType(), verb); - protected static string GetDescription(Type t, string verb) + protected static string? GetDescription(Type t, string verb) => t.GetProperties() - .Select(property => (BaseOptionAttribute)Attribute.GetCustomAttribute( - property, typeof(BaseOptionAttribute), false)) - .FirstOrDefault(attrib => attrib?.LongName == verb) + .Select(property => (BaseOptionAttribute?) + Attribute.GetCustomAttribute(property, typeof(BaseOptionAttribute), false)) + .OfType() + .FirstOrDefault(attrib => attrib.LongName == verb) ?.HelpText; } @@ -216,7 +221,7 @@ public class CommonOptions public bool Debugger { get; set; } [Option("net-useragent", HelpText = "Set the default user-agent string for HTTP requests")] - public string NetUserAgent { get; set; } + public string? NetUserAgent { get; set; } [Option("headless", DefaultValue = false, HelpText = "Set to disable all prompts")] public bool Headless { get; set; } @@ -288,14 +293,14 @@ public virtual int Handle(GameInstanceManager manager, IUser user) /// This is mainly to ensure that --headless carries through for prompt. /// /// Options object to merge into this one - public void Merge(CommonOptions otherOpts) + public void Merge(CommonOptions? otherOpts) { if (otherOpts != null) { Verbose = Verbose || otherOpts.Verbose; Debug = Debug || otherOpts.Debug; Debugger = Debugger || otherOpts.Debugger; - NetUserAgent = NetUserAgent ?? otherOpts.NetUserAgent; + NetUserAgent ??= otherOpts.NetUserAgent; Headless = Headless || otherOpts.Headless; AsRoot = AsRoot || otherOpts.AsRoot; } @@ -318,10 +323,10 @@ private static void CheckMonoVersion(IUser user) public class InstanceSpecificOptions : CommonOptions { [Option("instance", HelpText = "Game instance to use")] - public string Instance { get; set; } + public string? Instance { get; set; } [Option("gamedir", HelpText = "Game dir to use")] - public string Gamedir { get; set; } + public string? Gamedir { get; set; } public override int Handle(GameInstanceManager manager, IUser user) { @@ -337,12 +342,12 @@ public override int Handle(GameInstanceManager manager, IUser user) try { - if (!string.IsNullOrEmpty(Instance)) + if (Instance != null && !string.IsNullOrEmpty(Instance)) { // Set a game directory by its alias. manager.SetCurrentInstance(Instance); } - else if (!string.IsNullOrEmpty(Gamedir)) + else if (Gamedir != null && !string.IsNullOrEmpty(Gamedir)) { // Set a game directory by its path manager.SetCurrentInstanceByPath(Gamedir); @@ -372,7 +377,7 @@ public class SubCommandOptions : CommonOptions [ValueList(typeof(List))] public List options { get; set; } - public SubCommandOptions() { } + // public SubCommandOptions() { } public SubCommandOptions(string[] args) { @@ -399,7 +404,7 @@ internal class GuiOptions : InstanceSpecificOptions internal class ConsoleUIOptions : InstanceSpecificOptions { [Option("theme", HelpText = "Name of color scheme to use, falls back to environment variable CKAN_CONSOLEUI_THEME")] - public string Theme { get; set; } + public string? Theme { get; set; } } [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] diff --git a/Cmdline/SingleAssemblyResourceManager.cs b/Cmdline/SingleAssemblyResourceManager.cs index e13699cd00..4463fda4a0 100644 --- a/Cmdline/SingleAssemblyResourceManager.cs +++ b/Cmdline/SingleAssemblyResourceManager.cs @@ -1,4 +1,3 @@ -using System.IO; using System.Globalization; using System.Resources; using System.Reflection; @@ -14,16 +13,14 @@ public SingleAssemblyResourceManager(string basename, Assembly assembly) : base( { } - protected override ResourceSet InternalGetResourceSet(CultureInfo culture, + protected override ResourceSet? InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents) { - if (!myResourceSets.TryGetValue(culture, out ResourceSet rs) && createIfNotExists) + if (!myResourceSets.TryGetValue(culture, out ResourceSet? rs) + && createIfNotExists && MainAssembly != null) { // Lazy-load default language (without caring about duplicate assignment in race conditions, no harm done) - if (neutralResourcesCulture == null) - { - neutralResourcesCulture = GetNeutralResourcesLanguage(MainAssembly); - } + neutralResourcesCulture ??= GetNeutralResourcesLanguage(MainAssembly); // If we're asking for the default language, then ask for the // invariant (non-specific) resources. @@ -33,7 +30,7 @@ protected override ResourceSet InternalGetResourceSet(CultureInfo culture, } string resourceFileName = GetResourceFileName(culture); - Stream store = MainAssembly.GetManifestResourceStream(resourceFileName); + var store = MainAssembly.GetManifestResourceStream(resourceFileName); // If we found the appropriate resources in the local assembly if (store != null) @@ -50,7 +47,7 @@ protected override ResourceSet InternalGetResourceSet(CultureInfo culture, return rs; } - private CultureInfo neutralResourcesCulture; + private CultureInfo? neutralResourcesCulture; private readonly Dictionary myResourceSets = new Dictionary(); } } diff --git a/ConsoleUI/AuthTokenAddDialog.cs b/ConsoleUI/AuthTokenAddDialog.cs index 5f363ac810..5b8efad00b 100644 --- a/ConsoleUI/AuthTokenAddDialog.cs +++ b/ConsoleUI/AuthTokenAddDialog.cs @@ -15,7 +15,8 @@ public class AuthTokenAddDialog : ConsoleDialog { /// /// Initialize the popup. /// - public AuthTokenAddDialog() : base() + /// The visual theme to use to draw the dialog + public AuthTokenAddDialog(ConsoleTheme theme) : base(theme) { CenterHeader = () => Properties.Resources.AuthTokenAddTitle; @@ -56,10 +57,10 @@ public AuthTokenAddDialog() : base() AddObject(tokenEntry); AddTip(Properties.Resources.Esc, Properties.Resources.Cancel); - AddBinding(Keys.Escape, (object sender, ConsoleTheme theme) => false); + AddBinding(Keys.Escape, (object sender) => false); AddTip(Properties.Resources.Enter, Properties.Resources.Accept, validKey); - AddBinding(Keys.Enter, (object sender, ConsoleTheme theme) => { + AddBinding(Keys.Enter, (object sender) => { if (validKey()) { ServiceLocator.Container.Resolve().SetAuthToken(hostEntry.Value, tokenEntry.Value); return false; diff --git a/ConsoleUI/AuthTokenListScreen.cs b/ConsoleUI/AuthTokenListScreen.cs index e6304e77fa..7ed330888c 100644 --- a/ConsoleUI/AuthTokenListScreen.cs +++ b/ConsoleUI/AuthTokenListScreen.cs @@ -17,9 +17,9 @@ public class AuthTokenScreen : ConsoleScreen { /// /// Initialize the screen. /// - public AuthTokenScreen() : base() + public AuthTokenScreen(ConsoleTheme theme) : base(theme) { - mainMenu = new ConsolePopupMenu(new List() { + mainMenu = new ConsolePopupMenu(new List() { new ConsoleMenuOption(Properties.Resources.AuthTokenListGitHubLink, "", Properties.Resources.AuthTokenListGitHubLinkTip, true, openGitHubURL) @@ -34,20 +34,16 @@ public AuthTokenScreen() : base() 1, 4, -1, -2, new List(ServiceLocator.Container.Resolve().GetAuthTokenHosts()), new List>() { - new ConsoleListBoxColumn() { - Header = Properties.Resources.AuthTokenListHostHeader, - Width = 20, - Renderer = (string s) => s - }, - new ConsoleListBoxColumn() { - Header = Properties.Resources.AuthTokenListTokenHeader, - Width = null, - Renderer = (string s) => { - return ServiceLocator.Container.Resolve().TryGetAuthToken(s, out string token) + new ConsoleListBoxColumn( + Properties.Resources.AuthTokenListHostHeader, + (string s) => s, null, 20), + new ConsoleListBoxColumn( + Properties.Resources.AuthTokenListTokenHeader, + (string s) => { + return ServiceLocator.Container.Resolve().TryGetAuthToken(s, out string? token) ? token : Properties.Resources.AuthTokenListMissingToken; - } - } + }, null, null), }, 0, 0, ListSortDirection.Descending ); @@ -61,19 +57,19 @@ public AuthTokenScreen() : base() )); AddTip(Properties.Resources.Esc, Properties.Resources.Back); - AddBinding(Keys.Escape, (object sender, ConsoleTheme theme) => false); + AddBinding(Keys.Escape, (object sender) => false); tokenList.AddTip("A", Properties.Resources.Add); - tokenList.AddBinding(Keys.A, (object sender, ConsoleTheme theme) => { - AuthTokenAddDialog ad = new AuthTokenAddDialog(); - ad.Run(theme); - DrawBackground(theme); + tokenList.AddBinding(Keys.A, (object sender) => { + var ad = new AuthTokenAddDialog(theme); + ad.Run(); + DrawBackground(); tokenList.SetData(new List(ServiceLocator.Container.Resolve().GetAuthTokenHosts())); return true; }); tokenList.AddTip("R", Properties.Resources.Remove, () => tokenList.Selection != null); - tokenList.AddBinding(Keys.R, (object sender, ConsoleTheme theme) => { + tokenList.AddBinding(Keys.R, (object sender) => { if (tokenList.Selection != null) { ServiceLocator.Container.Resolve().SetAuthToken(tokenList.Selection, null); tokenList.SetData(new List(ServiceLocator.Container.Resolve().GetAuthTokenHosts())); @@ -86,19 +82,15 @@ public AuthTokenScreen() : base() /// Put CKAN 1.25.5 in top left corner /// protected override string LeftHeader() - { - return $"{Meta.GetProductName()} {Meta.GetVersion()}"; - } + => $"{Meta.GetProductName()} {Meta.GetVersion()}"; /// /// Put description in top center /// protected override string CenterHeader() - { - return Properties.Resources.AuthTokenListTitle; - } + => Properties.Resources.AuthTokenListTitle; - private bool openGitHubURL(ConsoleTheme theme) + private bool openGitHubURL() { ModInfoScreen.LaunchURL(theme, githubTokenURL); return true; diff --git a/ConsoleUI/CKAN-ConsoleUI.csproj b/ConsoleUI/CKAN-ConsoleUI.csproj index 685c9dc66d..c872c24ab7 100644 --- a/ConsoleUI/CKAN-ConsoleUI.csproj +++ b/ConsoleUI/CKAN-ConsoleUI.csproj @@ -16,9 +16,10 @@ true Debug;Release;NoGUI false - 7.3 + 9 + enable ..\assets\ckan.ico - net48;net7.0 + net48;net8.0 512 prompt 4 @@ -29,6 +30,8 @@ + + @@ -36,7 +39,7 @@ - + diff --git a/ConsoleUI/CompatibleVersionDialog.cs b/ConsoleUI/CompatibleVersionDialog.cs index c86cc5ae8e..df20684822 100644 --- a/ConsoleUI/CompatibleVersionDialog.cs +++ b/ConsoleUI/CompatibleVersionDialog.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using CKAN.Versioning; using CKAN.Games; @@ -16,7 +17,7 @@ public class CompatibleVersionDialog : ConsoleDialog { /// /// Initialize the popup /// - public CompatibleVersionDialog(IGame game) : base() + public CompatibleVersionDialog(ConsoleTheme theme, IGame game) : base(theme) { int l = GetLeft(), r = GetRight(); @@ -28,18 +29,17 @@ public CompatibleVersionDialog(IGame game) : base() l + 2, t + 2, r - 2, b - 4, options, new List>() { - new ConsoleListBoxColumn() { - Header = Properties.Resources.CompatibleVersionsListHeader, - Width = null, - Renderer = v => v.ToString(), - Comparer = (v1, v2) => v1.CompareTo(v2) - } + new ConsoleListBoxColumn( + Properties.Resources.CompatibleVersionsListHeader, + v => v.ToString() ?? "", + (v1, v2) => v1.CompareTo(v2), + null) }, 0, 0, ListSortDirection.Descending ); AddObject(choices); choices.AddTip(Properties.Resources.Enter, Properties.Resources.CompatibleVersionsListAcceptTip); - choices.AddBinding(Keys.Enter, (object sender, ConsoleTheme theme) => { + choices.AddBinding(Keys.Enter, (object sender) => { choice = choices.Selection; return false; }); @@ -51,7 +51,7 @@ public CompatibleVersionDialog(IGame game) : base() }; AddObject(manualEntry); manualEntry.AddTip(Properties.Resources.Enter, Properties.Resources.CompatibleVersionsEntryAcceptTip, () => GameVersion.TryParse(manualEntry.Value, out choice)); - manualEntry.AddBinding(Keys.Enter, (object sender, ConsoleTheme theme) => { + manualEntry.AddBinding(Keys.Enter, (object sender) => { if (GameVersion.TryParse(manualEntry.Value, out choice)) { // Good value, done running return false; @@ -62,7 +62,7 @@ public CompatibleVersionDialog(IGame game) : base() }); AddTip(Properties.Resources.Esc, Properties.Resources.Cancel); - AddBinding(Keys.Escape, (object sender, ConsoleTheme theme) => { + AddBinding(Keys.Escape, (object StronglyTypedResourceBuilder) => { choice = null; return false; }); @@ -74,16 +74,16 @@ public CompatibleVersionDialog(IGame game) : base() /// Display the dialog and handle its interaction /// /// Function to control the dialog, default is normal user interaction - /// The visual theme to use to draw the dialog /// /// Row user selected /// - public new GameVersion Run(ConsoleTheme theme, Action process = null) + public new GameVersion? Run(Action? process = null) { - base.Run(theme, process); + base.Run(process); return choice; } + [MemberNotNull(nameof(options))] private void loadOptions(IGame game) { options = game.KnownVersions; @@ -92,7 +92,7 @@ private void loadOptions(IGame game) GameVersion v = options[i]; // From GUI/CompatibleGameVersionsDialog.cs GameVersion fullKnownVersion = v.ToVersionRange().Lower.Value; - GameVersion toAdd = new GameVersion(fullKnownVersion.Major, fullKnownVersion.Minor); + var toAdd = new GameVersion(fullKnownVersion.Major, fullKnownVersion.Minor); if (!options.Contains(toAdd)) { options.Add(toAdd); } @@ -103,6 +103,6 @@ private void loadOptions(IGame game) private readonly ConsoleListBox choices; private readonly ConsoleField manualEntry; - private GameVersion choice; + private GameVersion? choice; } } diff --git a/ConsoleUI/ConsoleCKAN.cs b/ConsoleUI/ConsoleCKAN.cs index e2dcec7faf..b9bdd0e748 100644 --- a/ConsoleUI/ConsoleCKAN.cs +++ b/ConsoleUI/ConsoleCKAN.cs @@ -17,9 +17,9 @@ public class ConsoleCKAN { /// Starts with a splash screen, then instance selection if no default, /// then list of mods. /// - public ConsoleCKAN(GameInstanceManager mgr, string themeName, bool debug) + public ConsoleCKAN(GameInstanceManager? mgr, string? themeName, bool debug) { - if (ConsoleTheme.Themes.TryGetValue(themeName ?? "default", out ConsoleTheme theme)) + 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, @@ -35,19 +35,19 @@ public ConsoleCKAN(GameInstanceManager mgr, string themeName, bool debug) if (manager.CurrentInstance == null) { if (manager.Instances.Count == 0) { // No instances, add one - new GameInstanceAddScreen(manager).Run(theme); + new GameInstanceAddScreen(theme, manager).Run(); // Set instance to current if they added one manager.GetPreferredInstance(); } else { // Multiple instances, no default, pick one - new GameInstanceListScreen(manager, repoData).Run(theme); + new GameInstanceListScreen(theme, manager, repoData).Run(); } } if (manager.CurrentInstance != null) { - new ModListScreen(manager, repoData, + new ModListScreen(theme, manager, repoData, RegistryManager.Instance(manager.CurrentInstance, repoData), manager.CurrentInstance.game, - debug, theme).Run(theme); + debug).Run(); } new ExitScreen().Run(theme); diff --git a/ConsoleUI/DeleteDirectoriesScreen.cs b/ConsoleUI/DeleteDirectoriesScreen.cs index 057291b147..3084005cf6 100644 --- a/ConsoleUI/DeleteDirectoriesScreen.cs +++ b/ConsoleUI/DeleteDirectoriesScreen.cs @@ -14,10 +14,13 @@ public class DeleteDirectoriesScreen : ConsoleScreen { /// /// Initialize the screen /// + /// The visual theme to use to draw the dialog /// Game instance /// Deletable stuff the user should see - public DeleteDirectoriesScreen(GameInstance inst, + public DeleteDirectoriesScreen(ConsoleTheme theme, + GameInstance inst, HashSet possibleConfigOnlyDirs) + : base(theme) { instance = inst; toDelete = possibleConfigOnlyDirs.ToHashSet(); @@ -32,29 +35,37 @@ public DeleteDirectoriesScreen(GameInstance inst, 1, 6, listWidth - 1, -2, possibleConfigOnlyDirs.OrderBy(d => d).ToList(), new List>() { - new ConsoleListBoxColumn() { - Header = "", - Width = 1, - Renderer = s => toDelete.Contains(s) ? Symbols.checkmark : "", - }, - new ConsoleListBoxColumn() { - Header = Properties.Resources.DeleteDirectoriesFoldersHeader, - Width = null, + new ConsoleListBoxColumn( + "", + s => toDelete.Contains(s) ? Symbols.checkmark : "", + null, + 1), + new ConsoleListBoxColumn( + Properties.Resources.DeleteDirectoriesFoldersHeader, // The data model holds absolute paths, but the UI shows relative - Renderer = p => Platform.FormatPath(instance.ToRelativeGameDir(p)), - }, + p => Platform.FormatPath(instance.ToRelativeGameDir(p)), + null, + null), }, 0, -1); directories.AddTip("D", Properties.Resources.DeleteDirectoriesDeleteDirTip, - () => !toDelete.Contains(directories.Selection)); - directories.AddBinding(Keys.D, (object sender, ConsoleTheme theme) => { - toDelete.Add(directories.Selection); + () => directories.Selection is not null + && !toDelete.Contains(directories.Selection)); + directories.AddBinding(Keys.D, (object sender) => { + if (directories.Selection is not null) + { + toDelete.Add(directories.Selection); + } return true; }); directories.AddTip("K", Properties.Resources.DeleteDirectoriesKeepDirTip, - () => toDelete.Contains(directories.Selection)); - directories.AddBinding(Keys.K, (object sender, ConsoleTheme theme) => { - toDelete.Remove(directories.Selection); + () => directories.Selection is not null + && toDelete.Contains(directories.Selection)); + directories.AddBinding(Keys.K, (object sender) => { + if (directories.Selection is not null) + { + toDelete.Remove(directories.Selection); + } return true; }); directories.SelectionChanged += PopulateFiles; @@ -64,11 +75,13 @@ public DeleteDirectoriesScreen(GameInstance inst, listWidth + 1, 6, -1, -2, new List() { "" }, new List>() { - new ConsoleListBoxColumn() { - Header = Properties.Resources.DeleteDirectoriesFilesHeader, - Width = null, - Renderer = p => Platform.FormatPath(CKANPathUtils.ToRelative(p, directories.Selection)), - }, + new ConsoleListBoxColumn( + Properties.Resources.DeleteDirectoriesFilesHeader, + p => directories.Selection is null + ? "" + : Platform.FormatPath(CKANPathUtils.ToRelative(p, directories.Selection)), + null, + null), }, 0, -1); AddObject(files); @@ -77,10 +90,10 @@ public DeleteDirectoriesScreen(GameInstance inst, Properties.Resources.DeleteDirectoriesCancelTip); AddBinding(Keys.Escape, // Discard changes - (object sender, ConsoleTheme theme) => false); + (object sender) => false); AddTip("F9", Properties.Resources.DeleteDirectoriesApplyTip); - AddBinding(Keys.F9, (object sender, ConsoleTheme theme) => { + AddBinding(Keys.F9, (object sender) => { foreach (var d in toDelete) { try { Directory.Delete(d, true); @@ -96,11 +109,13 @@ public DeleteDirectoriesScreen(GameInstance inst, private void PopulateFiles() { files.SetData( - Directory.EnumerateFileSystemEntries(directories.Selection, - "*", - SearchOption.AllDirectories) - .OrderBy(f => f) - .ToList(), + directories.Selection is null + ? new List() + : Directory.EnumerateFileSystemEntries(directories.Selection, + "*", + SearchOption.AllDirectories) + .OrderBy(f => f) + .ToList(), true); } diff --git a/ConsoleUI/DependencyScreen.cs b/ConsoleUI/DependencyScreen.cs index b20b53d364..44b8e8c29d 100644 --- a/ConsoleUI/DependencyScreen.cs +++ b/ConsoleUI/DependencyScreen.cs @@ -2,6 +2,7 @@ using System.Linq; using System.ComponentModel; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using CKAN.Versioning; #if NETFRAMEWORK @@ -20,12 +21,13 @@ public class DependencyScreen : ConsoleScreen { /// /// Initialize the screen /// + /// The visual theme to use to draw the dialog /// 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, Registry reg, ChangePlan cp, HashSet rej, bool dbg) : base() + public DependencyScreen(ConsoleTheme theme, GameInstanceManager mgr, Registry reg, ChangePlan cp, HashSet rej, bool dbg) : base(theme) { debug = dbg; manager = mgr; @@ -36,45 +38,48 @@ public DependencyScreen(GameInstanceManager mgr, Registry reg, ChangePlan cp, Ha AddObject(new ConsoleLabel(1, 2, -1, () => Properties.Resources.RecommendationsLabel)); - generateList(new ModuleInstaller(manager.CurrentInstance, manager.Cache, this), - plan.Install - .Concat(ReplacementModules(plan.Replace, - manager.CurrentInstance.VersionCriteria())) - .ToHashSet()); + if (manager.CurrentInstance != null && manager.Cache != null) + { + generateList(new ModuleInstaller(manager.CurrentInstance, manager.Cache, this), + plan.Install + .Concat(ReplacementModules(plan.Replace, + manager.CurrentInstance.VersionCriteria())) + .ToHashSet()); + } dependencyList = new ConsoleListBox( 1, 4, -1, -2, new List(dependencies.Values), new List>() { - new ConsoleListBoxColumn() { - Header = Properties.Resources.RecommendationsInstallHeader, - Width = 7, - Renderer = (Dependency d) => StatusSymbol(d.module), - }, - new ConsoleListBoxColumn() { - Header = Properties.Resources.RecommendationsNameHeader, - Width = null, - Renderer = (Dependency d) => d.module.ToString(), - }, - new ConsoleListBoxColumn() { - Header = Properties.Resources.RecommendationsSourcesHeader, - Width = 42, - Renderer = (Dependency d) => string.Join(", ", d.dependents), - } + new ConsoleListBoxColumn( + Properties.Resources.RecommendationsInstallHeader, + (Dependency d) => StatusSymbol(d.module), + null, + 7), + new ConsoleListBoxColumn( + Properties.Resources.RecommendationsNameHeader, + (Dependency d) => d.module.ToString(), + null, + null), + new ConsoleListBoxColumn( + Properties.Resources.RecommendationsSourcesHeader, + (Dependency d) => string.Join(", ", d.dependents), + null, + 42) }, 1, 0, ListSortDirection.Descending ); dependencyList.AddTip("+", Properties.Resources.Toggle); - dependencyList.AddBinding(Keys.Plus, (object sender, ConsoleTheme theme) => { - var mod = dependencyList.Selection.module; - if (accepted.Contains(mod) || TryWithoutConflicts(accepted.Append(mod))) { + dependencyList.AddBinding(Keys.Plus, (object sender) => { + if (dependencyList.Selection?.module is CkanModule mod + && (accepted.Contains(mod) || TryWithoutConflicts(accepted.Append(mod)))) { ChangePlan.toggleContains(accepted, mod); } return true; }); dependencyList.AddTip($"{Properties.Resources.Ctrl}+A", Properties.Resources.SelectAll); - dependencyList.AddBinding(Keys.CtrlA, (object sender, ConsoleTheme theme) => { + dependencyList.AddBinding(Keys.CtrlA, (object sender) => { if (TryWithoutConflicts(dependencies.Keys)) { foreach (var kvp in dependencies) { if (!accepted.Contains(kvp.Key)) { @@ -86,15 +91,15 @@ public DependencyScreen(GameInstanceManager mgr, Registry reg, ChangePlan cp, Ha }); dependencyList.AddTip($"{Properties.Resources.Ctrl}+D", Properties.Resources.DeselectAll, () => accepted.Count > 0); - dependencyList.AddBinding(Keys.CtrlD, (object sender, ConsoleTheme theme) => { + dependencyList.AddBinding(Keys.CtrlD, (object sender) => { accepted.Clear(); return true; }); dependencyList.AddTip(Properties.Resources.Enter, Properties.Resources.Details); - dependencyList.AddBinding(Keys.Enter, (object sender, ConsoleTheme theme) => { + dependencyList.AddBinding(Keys.Enter, (object sender) => { if (dependencyList.Selection != null) { - LaunchSubScreen(theme, new ModInfoScreen(manager, reg, plan, + LaunchSubScreen(new ModInfoScreen(theme, manager, reg, plan, dependencyList.Selection.module, debug)); } @@ -104,14 +109,14 @@ public DependencyScreen(GameInstanceManager mgr, Registry reg, ChangePlan cp, Ha AddObject(dependencyList); AddTip(Properties.Resources.Esc, Properties.Resources.Cancel); - AddBinding(Keys.Escape, (object sender, ConsoleTheme theme) => { + AddBinding(Keys.Escape, (object sender) => { // Add everything to rejected rejected.UnionWith(dependencies.Keys.Select(m => m.identifier)); return false; }); AddTip("F9", Properties.Resources.Accept); - AddBinding(Keys.F9, (object sender, ConsoleTheme theme) => { + AddBinding(Keys.F9, (object sender) => { if (TryWithoutConflicts(accepted)) { plan.Install.UnionWith(accepted); // Add the rest to rejected @@ -143,28 +148,25 @@ public DependencyScreen(GameInstanceManager mgr, Registry reg, ChangePlan cp, Ha private void generateList(ModuleInstaller installer, HashSet inst) { if (installer.FindRecommendations( - inst, new List(inst), registry as Registry, + inst, new List(inst), registry, out Dictionary>> recommendations, out Dictionary> suggestions, out Dictionary> supporters )) { foreach ((CkanModule mod, Tuple> checkedAndDependents) in recommendations) { - dependencies.Add(mod, new Dependency() { - module = mod, - dependents = checkedAndDependents.Item2.OrderBy(d => d).ToList() - }); + dependencies.Add(mod, new Dependency( + mod, + checkedAndDependents.Item2.OrderBy(d => d).ToList())); } foreach ((CkanModule mod, List dependents) in suggestions) { - dependencies.Add(mod, new Dependency() { - module = mod, - dependents = dependents.OrderBy(d => d).ToList() - }); + dependencies.Add(mod, new Dependency( + mod, + dependents.OrderBy(d => d).ToList())); } foreach ((CkanModule mod, HashSet dependents) in supporters) { - dependencies.Add(mod, new Dependency() { - module = mod, - dependents = dependents.OrderBy(d => d).ToList() - }); + dependencies.Add(mod, new Dependency( + mod, + dependents.OrderBy(d => d).ToList())); } // Check the default checkboxes accepted.UnionWith(recommendations.Where(kvp => kvp.Value.Item1) @@ -175,7 +177,7 @@ out Dictionary> supporters private IEnumerable ReplacementModules(IEnumerable replaced_identifiers, GameVersionCriteria crit) => replaced_identifiers.Select(replaced => registry.GetReplacement(replaced, crit)) - .Where(repl => repl != null) + .OfType() .Select(repl => repl.ReplaceWith); private string StatusSymbol(CkanModule mod) @@ -184,7 +186,7 @@ private string StatusSymbol(CkanModule mod) private bool TryWithoutConflicts(IEnumerable toAdd) { - if (HasConflicts(toAdd, out List conflictDescriptions)) { + if (HasConflicts(toAdd, out List? conflictDescriptions)) { RaiseError("{0}", string.Join(Environment.NewLine, conflictDescriptions)); return false; @@ -192,30 +194,36 @@ private bool TryWithoutConflicts(IEnumerable toAdd) return true; } - private bool HasConflicts(IEnumerable toAdd, - out List descriptions) + private bool HasConflicts(IEnumerable toAdd, + [NotNullWhen(true)] out List? descriptions) { - try + if (manager.CurrentInstance != null) { - var resolver = new RelationshipResolver( - plan.Install.Concat(toAdd).Distinct(), - plan.Remove.Select(ident => registry.InstalledModule(ident)?.Module), - RelationshipResolverOptions.ConflictsOpts(), registry, - manager.CurrentInstance.VersionCriteria()); - descriptions = resolver.ConflictDescriptions.ToList(); - return descriptions.Count > 0; - } - catch (DependencyNotSatisfiedKraken k) - { - descriptions = new List() { k.Message }; - return true; + try + { + var resolver = new RelationshipResolver( + plan.Install.Concat(toAdd).Distinct(), + plan.Remove.Select(ident => registry.InstalledModule(ident)?.Module) + .OfType(), + RelationshipResolverOptions.ConflictsOpts(), registry, + manager.CurrentInstance.VersionCriteria()); + descriptions = resolver.ConflictDescriptions.ToList(); + return descriptions.Count > 0; + } + catch (DependencyNotSatisfiedKraken k) + { + descriptions = new List() { k.Message }; + return true; + } } + descriptions = null; + return false; } private readonly HashSet accepted = new HashSet(); private readonly HashSet rejected; - private readonly IRegistryQuerier registry; + private readonly Registry registry; private readonly GameInstanceManager manager; private readonly ChangePlan plan; private readonly bool debug; @@ -232,6 +240,17 @@ private bool HasConflicts(IEnumerable toAdd, /// public class Dependency { + /// + /// Initialize a dependency + /// + /// The mod + /// Mods that recommend or suggest m + public Dependency(CkanModule m, List d) + { + module = m; + dependents = d; + } + /// /// The mod /// @@ -240,7 +259,7 @@ public class Dependency { /// /// List of mods that recommended or suggested this mod /// - public List dependents = new List(); + public readonly List dependents; } } diff --git a/ConsoleUI/DownloadImportDialog.cs b/ConsoleUI/DownloadImportDialog.cs index 2e8cb9e0c4..87b9b5d49c 100644 --- a/ConsoleUI/DownloadImportDialog.cs +++ b/ConsoleUI/DownloadImportDialog.cs @@ -21,24 +21,26 @@ public static class DownloadImportDialog { /// Change plan object for marking things to be installed public static void ImportDownloads(ConsoleTheme theme, GameInstance gameInst, RepositoryDataManager repoData, NetModuleCache cache, ChangePlan cp) { - ConsoleFileMultiSelectDialog cfmsd = new ConsoleFileMultiSelectDialog( + var cfmsd = new ConsoleFileMultiSelectDialog( + theme, Properties.Resources.ImportSelectTitle, FindDownloadsPath(gameInst), "*.zip", Properties.Resources.ImportSelectHeader, Properties.Resources.ImportSelectHeader ); - HashSet files = cfmsd.Run(theme); + HashSet files = cfmsd.Run(); if (files.Count > 0) { ProgressScreen ps = new ProgressScreen( + theme, Properties.Resources.ImportProgressTitle, Properties.Resources.ImportProgressMessage); ModuleInstaller inst = new ModuleInstaller(gameInst, cache, ps); - ps.Run(theme, (ConsoleTheme th) => inst.ImportFiles(files, ps, + ps.Run(() => inst.ImportFiles(files, ps, (CkanModule mod) => cp.Install.Add(mod), RegistryManager.Instance(gameInst, repoData).registry)); // Don't let the installer re-use old screen references - inst.User = null; + inst.User = new NullUser(); } } diff --git a/ConsoleUI/GameInstanceAddScreen.cs b/ConsoleUI/GameInstanceAddScreen.cs index 3d2764f480..adea587095 100644 --- a/ConsoleUI/GameInstanceAddScreen.cs +++ b/ConsoleUI/GameInstanceAddScreen.cs @@ -13,8 +13,10 @@ public class GameInstanceAddScreen : GameInstanceScreen { /// /// Initialize the Screen /// + /// The visual theme to use to draw the dialog /// Game instance manager containing the instances - public GameInstanceAddScreen(GameInstanceManager mgr) : base(mgr) + public GameInstanceAddScreen(ConsoleTheme theme, GameInstanceManager mgr) + : base(theme, mgr) { AddObject(new ConsoleLabel( labelWidth, pathRow + 1, -1, diff --git a/ConsoleUI/GameInstanceEditScreen.cs b/ConsoleUI/GameInstanceEditScreen.cs index b229e2ea7d..f61ece4f92 100644 --- a/ConsoleUI/GameInstanceEditScreen.cs +++ b/ConsoleUI/GameInstanceEditScreen.cs @@ -15,11 +15,15 @@ public class GameInstanceEditScreen : GameInstanceScreen { /// /// Initialize the Screen /// + /// The visual theme to use to draw the dialog /// Game instance manager containing the instances /// Repository data manager providing info from repos /// Instance to edit - public GameInstanceEditScreen(GameInstanceManager mgr, RepositoryDataManager repoData, GameInstance k) - : base(mgr, k.Name, k.GameDir()) + public GameInstanceEditScreen(ConsoleTheme theme, + GameInstanceManager mgr, + RepositoryDataManager repoData, + GameInstance k) + : base(theme, mgr, k.Name, k.GameDir()) { ksp = k; try { @@ -56,70 +60,84 @@ public GameInstanceEditScreen(GameInstanceManager mgr, RepositoryDataManager rep 3, repoListTop, -3, repoListBottom, new List(repoEditList.Values), new List>() { - new ConsoleListBoxColumn() { - Header = Properties.Resources.InstanceEditRepoIndexHeader, - Renderer = r => r.priority.ToString(), - Width = 7 - }, new ConsoleListBoxColumn() { - Header = Properties.Resources.InstanceEditRepoNameHeader, - Renderer = r => r.name, - Width = 16 - }, new ConsoleListBoxColumn() { - Header = Properties.Resources.InstanceEditRepoURLHeader, - Renderer = r => r.uri.ToString(), - Width = null - } + new ConsoleListBoxColumn( + Properties.Resources.InstanceEditRepoIndexHeader, + r => r.priority.ToString(), + null, + 7), + new ConsoleListBoxColumn( + Properties.Resources.InstanceEditRepoNameHeader, + r => r.name, + null, + 16), + new ConsoleListBoxColumn( + Properties.Resources.InstanceEditRepoURLHeader, + r => r.uri.ToString(), + null, + null) }, 1, 0, ListSortDirection.Ascending ); AddObject(repoList); repoList.AddTip("A", Properties.Resources.Add); - repoList.AddBinding(Keys.A, (object sender, ConsoleTheme theme) => { - LaunchSubScreen(theme, new RepoAddScreen(ksp.game, repoEditList)); + repoList.AddBinding(Keys.A, (object sender) => { + LaunchSubScreen(new RepoAddScreen(theme, ksp.game, repoEditList)); repoList.SetData(new List(repoEditList.Values)); return true; }); repoList.AddTip("R", Properties.Resources.Remove); - repoList.AddBinding(Keys.R, (object sender, ConsoleTheme theme) => { - int oldPrio = repoList.Selection.priority; - repoEditList.Remove(repoList.Selection.name); - // Reshuffle the priorities to fill - foreach (Repository r in repoEditList.Values) { - if (r.priority > oldPrio) { - --r.priority; + repoList.AddBinding(Keys.R, (object sender) => { + if (repoList.Selection is Repository repo) + { + int oldPrio = repo.priority; + repoEditList.Remove(repo.name); + // Reshuffle the priorities to fill + foreach (Repository r in repoEditList.Values) { + if (r.priority > oldPrio) { + --r.priority; + } } + repoList.SetData(new List(repoEditList.Values)); } - repoList.SetData(new List(repoEditList.Values)); return true; }); repoList.AddTip("E", Properties.Resources.Edit); - repoList.AddBinding(Keys.E, (object sender, ConsoleTheme theme) => { - LaunchSubScreen(theme, new RepoEditScreen(ksp.game, repoEditList, repoList.Selection)); - repoList.SetData(new List(repoEditList.Values)); + repoList.AddBinding(Keys.E, (object sender) => { + if (repoList.Selection is Repository repo) + { + LaunchSubScreen(new RepoEditScreen(theme, ksp.game, repoEditList, repo)); + repoList.SetData(new List(repoEditList.Values)); + } return true; }); repoList.AddTip("-", Properties.Resources.Up); - repoList.AddBinding(Keys.Minus, (object sender, ConsoleTheme theme) => { - if (repoList.Selection.priority > 0) { - Repository prev = SortedDictFind(repoEditList, - r => r.priority == repoList.Selection.priority - 1); - if (prev != null) { - ++prev.priority; + repoList.AddBinding(Keys.Minus, (object sender) => { + if (repoList.Selection is Repository repo) + { + if (repo.priority > 0) { + var prev = SortedDictFind(repoEditList, + r => r.priority == repo.priority - 1); + if (prev != null) { + ++prev.priority; + } + --repo.priority; + repoList.SetData(new List(repoEditList.Values)); } - --repoList.Selection.priority; - repoList.SetData(new List(repoEditList.Values)); } return true; }); repoList.AddTip("+", Properties.Resources.Down); - repoList.AddBinding(Keys.Plus, (object sender, ConsoleTheme theme) => { - Repository next = SortedDictFind(repoEditList, - r => r.priority == repoList.Selection.priority + 1); - if (next != null) { - --next.priority; + repoList.AddBinding(Keys.Plus, (object sender) => { + if (repoList.Selection is Repository repo) + { + var next = SortedDictFind(repoEditList, + r => r.priority == repo.priority + 1); + if (next != null) { + --next.priority; + } + ++repo.priority; + repoList.SetData(new List(repoEditList.Values)); } - ++repoList.Selection.priority; - repoList.SetData(new List(repoEditList.Values)); return true; }); @@ -127,22 +145,21 @@ public GameInstanceEditScreen(GameInstanceManager mgr, RepositoryDataManager rep 3, compatListTop, -3, compatListBottom, compatEditList, new List>() { - new ConsoleListBoxColumn() { - Header = Properties.Resources.InstanceEditCompatVersionHeader, - Width = 10, - Renderer = v => v.ToString(), - Comparer = (a, b) => a.CompareTo(b) - } + new ConsoleListBoxColumn( + Properties.Resources.InstanceEditCompatVersionHeader, + v => v.ToString() ?? "", + (a, b) => a.CompareTo(b), + 10) }, 0, 0, ListSortDirection.Descending ); AddObject(compatList); compatList.AddTip("A", Properties.Resources.Add); - compatList.AddBinding(Keys.A, (object sender, ConsoleTheme theme) => { - CompatibleVersionDialog vd = new CompatibleVersionDialog(ksp.game); - GameVersion newVersion = vd.Run(theme); - DrawBackground(theme); + compatList.AddBinding(Keys.A, (object sender) => { + CompatibleVersionDialog vd = new CompatibleVersionDialog(theme, ksp.game); + var newVersion = vd.Run(); + DrawBackground(); if (newVersion != null && !compatEditList.Contains(newVersion)) { compatEditList.Add(newVersion); compatList.SetData(compatEditList); @@ -150,9 +167,12 @@ public GameInstanceEditScreen(GameInstanceManager mgr, RepositoryDataManager rep return true; }); compatList.AddTip("R", Properties.Resources.Remove, () => compatList.Selection != null); - compatList.AddBinding(Keys.R, (object sender, ConsoleTheme theme) => { - compatEditList.Remove(compatList.Selection); - compatList.SetData(compatEditList); + compatList.AddBinding(Keys.R, (object sender) => { + if (compatList.Selection is GameVersion gv) + { + compatEditList.Remove(gv); + compatList.SetData(compatEditList); + } return true; }); @@ -167,7 +187,7 @@ public GameInstanceEditScreen(GameInstanceManager mgr, RepositoryDataManager rep } } - private static V SortedDictFind(SortedDictionary dict, Func pred) + private static V? SortedDictFind(SortedDictionary dict, Func pred) where K: class { foreach (var kvp in dict) { if (pred(kvp.Value)) { @@ -200,8 +220,8 @@ protected override void Save() { if (repoEditList != null) { // Copy the temp list of repositories to the registry - registry.RepositoriesSet(repoEditList); - regMgr.Save(); + registry?.RepositoriesSet(repoEditList); + regMgr?.Save(); } if (compatEditList != null) { ksp.SetCompatibleVersions(compatEditList); @@ -219,14 +239,14 @@ protected override void Save() } } - private readonly GameInstance ksp; - private readonly RegistryManager regMgr; - private readonly Registry registry; + private readonly GameInstance ksp; + private readonly RegistryManager? regMgr; + private readonly Registry? registry; - private readonly SortedDictionary repoEditList; - private readonly ConsoleListBox repoList; - private readonly List compatEditList; - private readonly ConsoleListBox compatList; + private readonly SortedDictionary? repoEditList; + private readonly ConsoleListBox? repoList; + private readonly List? compatEditList; + private readonly ConsoleListBox? compatList; private const int repoFrameTop = pathRow + 2; private const int repoListTop = repoFrameTop + 2; diff --git a/ConsoleUI/GameInstanceListScreen.cs b/ConsoleUI/GameInstanceListScreen.cs index 685c9acd10..e2bec88bc1 100644 --- a/ConsoleUI/GameInstanceListScreen.cs +++ b/ConsoleUI/GameInstanceListScreen.cs @@ -17,10 +17,12 @@ public class GameInstanceListScreen : ConsoleScreen { /// /// Initialize the screen. /// + /// The visual theme to use to draw the dialog /// 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, RepositoryDataManager repoData, bool first = false) + public GameInstanceListScreen(ConsoleTheme theme, GameInstanceManager mgr, RepositoryDataManager repoData, bool first = false) + : base(theme) { manager = mgr; @@ -33,108 +35,121 @@ public GameInstanceListScreen(GameInstanceManager mgr, RepositoryDataManager rep 1, 4, -1, -2, manager.Instances.Values, new List>() { - new ConsoleListBoxColumn() { - Header = Properties.Resources.InstanceListDefaultHeader, - Width = 7, - Renderer = StatusSymbol - }, new ConsoleListBoxColumn() { - Header = Properties.Resources.InstanceListNameHeader, - Width = 20, - Renderer = k => k.Name - }, new ConsoleListBoxColumn() { - Header = Properties.Resources.InstanceListGameHeader, - Width = 5, - Renderer = k => k.game.ShortName - }, new ConsoleListBoxColumn() { - Header = Properties.Resources.InstanceListVersionHeader, - Width = 12, - Renderer = k => k.Version()?.ToString() ?? Properties.Resources.InstanceListNoVersion, - Comparer = (a, b) => a.Version()?.CompareTo(b.Version() ?? GameVersion.Any) ?? 1 - }, new ConsoleListBoxColumn() { - Header = Properties.Resources.InstanceListPathHeader, - Width = null, - Renderer = k => k.GameDir() - } + new ConsoleListBoxColumn( + Properties.Resources.InstanceListDefaultHeader, + StatusSymbol, + null, + 7), + new ConsoleListBoxColumn( + Properties.Resources.InstanceListNameHeader, + k => k.Name, + null, + 20), + new ConsoleListBoxColumn( + Properties.Resources.InstanceListGameHeader, + k => k.game.ShortName, + null, + 5), + new ConsoleListBoxColumn( + Properties.Resources.InstanceListVersionHeader, + k => k.Version()?.ToString() ?? Properties.Resources.InstanceListNoVersion, + (a, b) => a.Version()?.CompareTo(b.Version() ?? GameVersion.Any) ?? 1, + 12), + new ConsoleListBoxColumn( + Properties.Resources.InstanceListPathHeader, + k => k.GameDir(), + null, + null) }, 1, 0, ListSortDirection.Descending ); if (first) { AddTip($"{Properties.Resources.Ctrl}+Q", Properties.Resources.Quit); - AddBinding(Keys.AltX, (object sender, ConsoleTheme theme) => false); - AddBinding(Keys.CtrlQ, (object sender, ConsoleTheme theme) => false); + AddBinding(Keys.AltX, (object sender) => false); + AddBinding(Keys.CtrlQ, (object sender) => false); } else { AddTip(Properties.Resources.Esc, Properties.Resources.Quit); - AddBinding(Keys.Escape, (object sender, ConsoleTheme theme) => false); + AddBinding(Keys.Escape, (object sender) => false); } AddTip(Properties.Resources.Enter, Properties.Resources.Select); - AddBinding(Keys.Enter, (object sender, ConsoleTheme theme) => { - - ConsoleMessageDialog d = new ConsoleMessageDialog( - string.Format(Properties.Resources.InstanceListLoadingInstance, instanceList.Selection.Name), - new List() - ); - - if (TryGetInstance(theme, instanceList.Selection, repoData, - (ConsoleTheme th) => { d.Run(th, (ConsoleTheme thm) => {}); }, - null)) { - try { - manager.SetCurrentInstance(instanceList.Selection.Name); - } catch (Exception ex) { - // This can throw if the previous current instance had an error, - // since it gets destructed when it's replaced. - RaiseError(ex.Message); + AddBinding(Keys.Enter, (object sender) => { + if (instanceList.Selection is GameInstance inst) + { + var d = new ConsoleMessageDialog(theme, string.Format(Properties.Resources.InstanceListLoadingInstance, + inst.Name), + new List()); + + if (TryGetInstance(theme, inst, repoData, + (ConsoleTheme th) => { d.Run(() => {}); }, + null)) { + try { + manager.SetCurrentInstance(inst.Name); + } catch (Exception ex) { + // This can throw if the previous current instance had an error, + // since it gets destructed when it's replaced. + RaiseError(ex.Message); + } + return false; + } else { + return true; } - return false; - } else { - return true; } + return true; }); instanceList.AddTip("A", Properties.Resources.Add); - instanceList.AddBinding(Keys.A, (object sender, ConsoleTheme theme) => { - LaunchSubScreen(theme, new GameInstanceAddScreen(manager)); + instanceList.AddBinding(Keys.A, (object sender) => { + LaunchSubScreen(new GameInstanceAddScreen(theme, manager)); instanceList.SetData(manager.Instances.Values); return true; }); instanceList.AddTip("R", Properties.Resources.Remove); - instanceList.AddBinding(Keys.R, (object sender, ConsoleTheme theme) => { - manager.RemoveInstance(instanceList.Selection.Name); - instanceList.SetData(manager.Instances.Values); + instanceList.AddBinding(Keys.R, (object sender) => { + if (instanceList.Selection is GameInstance inst) + { + manager.RemoveInstance(inst.Name); + instanceList.SetData(manager.Instances.Values); + } return true; }); instanceList.AddTip("E", Properties.Resources.Edit); - instanceList.AddBinding(Keys.E, (object sender, ConsoleTheme theme) => { - - ConsoleMessageDialog d = new ConsoleMessageDialog( - string.Format(Properties.Resources.InstanceListLoadingInstance, instanceList.Selection.Name), - new List() - ); - TryGetInstance(theme, instanceList.Selection, repoData, - (ConsoleTheme th) => { d.Run(theme, (ConsoleTheme thm) => {}); }, - null); - // 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, repoData, instanceList.Selection)); - + instanceList.AddBinding(Keys.E, (object sender) => { + if (instanceList.Selection is GameInstance inst) + { + var d = new ConsoleMessageDialog( + theme, + string.Format(Properties.Resources.InstanceListLoadingInstance, inst.Name), + new List()); + TryGetInstance(theme, inst, repoData, + (ConsoleTheme th) => { d.Run(() => {}); }, + null); + // Still launch the screen even if the load fails, + // because you need to be able to fix the name/path. + LaunchSubScreen(new GameInstanceEditScreen(theme, manager, repoData, instanceList.Selection)); + } return true; }); instanceList.AddTip("D", Properties.Resources.InstanceListDefaultToggle); - instanceList.AddBinding(Keys.D, (object sender, ConsoleTheme theme) => { - string name = instanceList.Selection.Name; - if (name == manager.AutoStartInstance) { - manager.ClearAutoStart(); - } else { - try { - manager.SetAutoStart(name); - } catch (NotKSPDirKraken k) { - ConsoleMessageDialog errd = new ConsoleMessageDialog( - string.Format(Properties.Resources.InstanceListLoadingError, k.path, k.Message), - new List() { Properties.Resources.OK } - ); - errd.Run(theme); + instanceList.AddBinding(Keys.D, (object sender) => { + if (instanceList.Selection is GameInstance inst) + { + string name = inst.Name; + if (name == manager.AutoStartInstance) { + manager.ClearAutoStart(); + } else { + try { + manager.SetAutoStart(name); + } catch (NotKSPDirKraken k) { + var errd = new ConsoleMessageDialog( + theme, + string.Format(Properties.Resources.InstanceListLoadingError, k.path, k.Message), + new List() { Properties.Resources.OK } + ); + errd.Run(); + } } } return true; @@ -148,25 +163,19 @@ public GameInstanceListScreen(GameInstanceManager mgr, RepositoryDataManager rep /// Put CKAN 1.25.5 in top left corner /// protected override string LeftHeader() - { - return $"{Meta.GetProductName()} {Meta.GetVersion()}"; - } + => $"{Meta.GetProductName()} {Meta.GetVersion()}"; /// /// Put description in top center /// protected override string CenterHeader() - { - return Properties.Resources.InstanceListTitle; - } + => Properties.Resources.InstanceListTitle; /// /// Label the menu as Sort /// protected override string MenuTip() - { - return Properties.Resources.Sort; - } + => Properties.Resources.Sort; /// /// Try to load the registry of an instance @@ -183,7 +192,7 @@ public static bool TryGetInstance(ConsoleTheme theme, GameInstance ksp, RepositoryDataManager repoData, Action render, - IProgress progress) + IProgress? progress) { bool retry; do { @@ -201,13 +210,14 @@ public static bool TryGetInstance(ConsoleTheme theme, } catch (RegistryInUseKraken k) { ConsoleMessageDialog md = new ConsoleMessageDialog( + theme, k.ToString(), new List() { Properties.Resources.Cancel, Properties.Resources.Force } ); - if (md.Run(theme) == 1) { + if (md.Run() == 1) { // Delete it File.Delete(k.lockfilePath); retry = true; @@ -219,21 +229,23 @@ public static bool TryGetInstance(ConsoleTheme theme, } catch (NotKSPDirKraken k) { ConsoleMessageDialog errd = new ConsoleMessageDialog( + theme, string.Format(Properties.Resources.InstanceListLoadingError, ksp.GameDir(), k.Message), new List() { Properties.Resources.OK } ); - errd.Run(theme); + errd.Run(); return false; } catch (Exception e) { ConsoleMessageDialog errd = new ConsoleMessageDialog( + theme, string.Format(Properties.Resources.InstanceListLoadingError, Platform.FormatPath(Path.Combine(ksp.CkanDir(), "registry.json")), e.ToString()), new List() { Properties.Resources.OK } ); - errd.Run(theme); + errd.Run(); return false; } diff --git a/ConsoleUI/GameInstanceScreen.cs b/ConsoleUI/GameInstanceScreen.cs index efd81ae92a..3af3d63ea9 100644 --- a/ConsoleUI/GameInstanceScreen.cs +++ b/ConsoleUI/GameInstanceScreen.cs @@ -13,15 +13,16 @@ public abstract class GameInstanceScreen : ConsoleScreen { /// /// Initialize the screen /// + /// The visual theme to use to draw the dialog /// Game instance manager containing the instances, needed for saving changes /// Initial value of name field /// Initial value of path field - protected GameInstanceScreen(GameInstanceManager mgr, string initName = "", string initPath = "") : base() + protected GameInstanceScreen(ConsoleTheme theme, GameInstanceManager mgr, string initName = "", string initPath = "") : base(theme) { manager = mgr; AddTip("F2", Properties.Resources.Accept); - AddBinding(Keys.F2, (object sender, ConsoleTheme theme) => { + AddBinding(Keys.F2, (object sender) => { if (Valid()) { Save(); // Close screen @@ -33,7 +34,7 @@ protected GameInstanceScreen(GameInstanceManager mgr, string initName = "", stri }); AddTip(Properties.Resources.Esc, Properties.Resources.Cancel); - AddBinding(Keys.Escape, (object sender, ConsoleTheme theme) => { + AddBinding(Keys.Escape, (object sender) => { // Discard changes return false; }); @@ -55,9 +56,7 @@ protected GameInstanceScreen(GameInstanceManager mgr, string initName = "", stri /// Put CKAN 1.25.5 in top left corner /// protected override string LeftHeader() - { - return $"{Meta.GetProductName()} {Meta.GetVersion()}"; - } + => $"{Meta.GetProductName()} {Meta.GetVersion()}"; /// /// Return whether the fields currently are valid. diff --git a/ConsoleUI/InstallFilterAddDialog.cs b/ConsoleUI/InstallFilterAddDialog.cs index bd7f450ac8..a018b865c7 100644 --- a/ConsoleUI/InstallFilterAddDialog.cs +++ b/ConsoleUI/InstallFilterAddDialog.cs @@ -12,7 +12,8 @@ public class InstallFilterAddDialog : ConsoleDialog { /// /// Initialize the popup /// - public InstallFilterAddDialog() : base() + /// The visual theme to use to draw the dialog + public InstallFilterAddDialog(ConsoleTheme theme) : base(theme) { int l = GetLeft(), r = GetRight(); @@ -27,13 +28,13 @@ public InstallFilterAddDialog() : base() }; AddObject(manualEntry); manualEntry.AddTip(Properties.Resources.Enter, Properties.Resources.FilterAddAcceptTip); - manualEntry.AddBinding(Keys.Enter, (object sender, ConsoleTheme theme) => { + manualEntry.AddBinding(Keys.Enter, (object sender) => { choice = manualEntry.Value; return false; }); AddTip(Properties.Resources.Esc, Properties.Resources.Cancel); - AddBinding(Keys.Escape, (object sender, ConsoleTheme theme) => { + AddBinding(Keys.Escape, (object sender) => { choice = null; return false; }); @@ -45,17 +46,16 @@ public InstallFilterAddDialog() : base() /// Display the dialog and handle its interaction /// /// Function to control the dialog, default is normal user interaction - /// The visual theme to use to draw the dialog /// /// User input /// - public new string Run(ConsoleTheme theme, Action process = null) + public new string? Run(Action? process = null) { - base.Run(theme, process); + base.Run(process); return choice; } private readonly ConsoleField manualEntry; - private string choice; + private string? choice; } } diff --git a/ConsoleUI/InstallFiltersScreen.cs b/ConsoleUI/InstallFiltersScreen.cs index 0cf018885e..5edbb8091d 100644 --- a/ConsoleUI/InstallFiltersScreen.cs +++ b/ConsoleUI/InstallFiltersScreen.cs @@ -15,9 +15,11 @@ public class InstallFiltersScreen : ConsoleScreen { /// /// Initialize the screen /// + /// The visual theme to use to draw the dialog /// Object holding the global configuration /// The current instance - public InstallFiltersScreen(IConfiguration globalConfig, GameInstance instance) + public InstallFiltersScreen(ConsoleTheme theme, IConfiguration globalConfig, GameInstance instance) + : base(theme) { this.globalConfig = globalConfig; this.instance = instance; @@ -25,18 +27,18 @@ public InstallFiltersScreen(IConfiguration globalConfig, GameInstance instance) instanceFilters = instance.InstallFilters.ToList(); AddTip("F2", Properties.Resources.Accept); - AddBinding(Keys.F2, (object sender, ConsoleTheme theme) => { + AddBinding(Keys.F2, (object sender) => { Save(); // Close screen return false; }); AddTip(Properties.Resources.Esc, Properties.Resources.Cancel); - AddBinding(Keys.Escape, (object sender, ConsoleTheme theme) => { + AddBinding(Keys.Escape, (object sender) => { // Discard changes return false; }); - mainMenu = new ConsolePopupMenu(new List() { + mainMenu = new ConsolePopupMenu(new List() { new ConsoleMenuOption(Properties.Resources.FiltersAddMiniAVCMenu, "", Properties.Resources.FiltersAddMiniAVCMenuTip, true, AddMiniAVC), @@ -48,22 +50,22 @@ public InstallFiltersScreen(IConfiguration globalConfig, GameInstance instance) 2, 2, -2, vMid - 1, globalFilters, new List>() { - new ConsoleListBoxColumn() { - Header = Properties.Resources.FiltersGlobalHeader, - Width = null, - Renderer = f => f, - } + new ConsoleListBoxColumn( + Properties.Resources.FiltersGlobalHeader, + f => f, + null, + null) }, 0 ); AddObject(globalList); globalList.AddTip("A", Properties.Resources.Add); - globalList.AddBinding(Keys.A, (object sender, ConsoleTheme theme) => { + globalList.AddBinding(Keys.A, (object sender) => { AddFilter(theme, globalList, globalFilters); return true; }); globalList.AddTip("R", Properties.Resources.Remove); - globalList.AddBinding(Keys.R, (object sender, ConsoleTheme theme) => { + globalList.AddBinding(Keys.R, (object sender) => { RemoveFilter(globalList, globalFilters); return true; }); @@ -71,22 +73,22 @@ public InstallFiltersScreen(IConfiguration globalConfig, GameInstance instance) 2, vMid + 1, -2, -2, instanceFilters, new List>() { - new ConsoleListBoxColumn() { - Header = Properties.Resources.FiltersInstanceHeader, - Width = null, - Renderer = f => f, - } + new ConsoleListBoxColumn( + Properties.Resources.FiltersInstanceHeader, + f => f, + null, + null) }, 0 ); AddObject(instanceList); instanceList.AddTip("A", Properties.Resources.Add); - instanceList.AddBinding(Keys.A, (object sender, ConsoleTheme theme) => { + instanceList.AddBinding(Keys.A, (object sender) => { AddFilter(theme, instanceList, instanceFilters); return true; }); instanceList.AddTip("R", Properties.Resources.Remove); - instanceList.AddBinding(Keys.R, (object sender, ConsoleTheme theme) => { + instanceList.AddBinding(Keys.R, (object sender) => { RemoveFilter(instanceList, instanceFilters); return true; }); @@ -96,19 +98,15 @@ public InstallFiltersScreen(IConfiguration globalConfig, GameInstance instance) /// Put CKAN 1.25.5 in top left corner /// protected override string LeftHeader() - { - return $"{Meta.GetProductName()} {Meta.GetVersion()}"; - } + => $"{Meta.GetProductName()} {Meta.GetVersion()}"; /// /// Put description in top center /// protected override string CenterHeader() - { - return Properties.Resources.FiltersTitle; - } + => Properties.Resources.FiltersTitle; - private bool AddMiniAVC(ConsoleTheme theme) + private bool AddMiniAVC() { globalFilters = globalFilters .Concat(miniAVC) @@ -120,9 +118,9 @@ private bool AddMiniAVC(ConsoleTheme theme) private void AddFilter(ConsoleTheme theme, ConsoleListBox box, List filters) { - string filter = new InstallFilterAddDialog().Run(theme); - DrawBackground(theme); - if (!string.IsNullOrEmpty(filter) && !filters.Contains(filter)) { + var filter = new InstallFilterAddDialog(theme).Run(); + DrawBackground(); + if (filter != null && !string.IsNullOrEmpty(filter) && !filters.Contains(filter)) { filters.Add(filter); box.SetData(filters); } @@ -130,8 +128,11 @@ private void AddFilter(ConsoleTheme theme, ConsoleListBox box, List box, List filters) { - filters.Remove(box.Selection); - box.SetData(filters); + if (box.Selection is not null) + { + filters.Remove(box.Selection); + box.SetData(filters); + } } private void Save() diff --git a/ConsoleUI/InstallFromCkanDialog.cs b/ConsoleUI/InstallFromCkanDialog.cs index ea69db840d..250fee23a5 100644 --- a/ConsoleUI/InstallFromCkanDialog.cs +++ b/ConsoleUI/InstallFromCkanDialog.cs @@ -20,12 +20,13 @@ public static CkanModule[] ChooseCkanFiles(ConsoleTheme theme, GameInstance gameInst) { var cfmsd = new ConsoleFileMultiSelectDialog( + theme, Properties.Resources.CkanFileSelectTitle, FindDownloadsPath(gameInst), "*.ckan", Properties.Resources.CkanFileSelectHeader, Properties.Resources.CkanFileSelectHeader); - return cfmsd.Run(theme) + return cfmsd.Run() .Select(f => CkanModule.FromFile(f.FullName)) .ToArray(); } diff --git a/ConsoleUI/InstallScreen.cs b/ConsoleUI/InstallScreen.cs index d9fd9bcb1c..b5c9d707df 100644 --- a/ConsoleUI/InstallScreen.cs +++ b/ConsoleUI/InstallScreen.cs @@ -16,15 +16,16 @@ public class InstallScreen : ProgressScreen { /// /// Initialize the Screen /// + /// The visual theme to use to draw the dialog /// 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, RepositoryDataManager repoData, ChangePlan cp, bool dbg) + public InstallScreen(ConsoleTheme theme, GameInstanceManager mgr, RepositoryDataManager repoData, ChangePlan cp, bool dbg) : base( + theme, Properties.Resources.InstallTitle, - Properties.Resources.InstallMessage - ) + Properties.Resources.InstallMessage) { debug = dbg; manager = mgr; @@ -35,146 +36,149 @@ public InstallScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Ch /// /// Run the screen /// - /// The visual theme to use to draw the dialog /// Framework parameter not used by this object - public override void Run(ConsoleTheme theme, Action process = null) + public override void Run(Action? process = null) { HashSet rejected = new HashSet(); - DrawBackground(theme); - using (TransactionScope trans = CkanTransaction.CreateTransactionScope()) { - bool retry = false; - do { - Draw(theme); - try { - // Reset this so we stop unless an exception sets it to true - retry = false; - - var regMgr = RegistryManager.Instance(manager.CurrentInstance, repoData); - var registry = regMgr.registry; - - // 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, registry, plan, rejected, debug); - if (ds.HaveOptions()) { - LaunchSubScreen(theme, ds); + DrawBackground(); + if (manager.CurrentInstance != null && manager.Cache != null) + { + using (TransactionScope trans = CkanTransaction.CreateTransactionScope()) { + bool retry = false; + do { + Draw(); + try { + // Reset this so we stop unless an exception sets it to true + retry = false; + + var regMgr = RegistryManager.Instance(manager.CurrentInstance, repoData); + var registry = regMgr.registry; + + // GUI prompts user to choose recs/sugs, + // CmdLine assumes recs and ignores sugs + if (plan.Install.Count > 0) { + // Track previously rejected optional dependencies and don't prompt for them again. + DependencyScreen ds = new DependencyScreen(theme, manager, registry, plan, rejected, debug); + if (ds.HaveOptions()) { + LaunchSubScreen(ds); + } } - } - // FUTURE: BackgroundWorker + // FUTURE: BackgroundWorker - HashSet possibleConfigOnlyDirs = null; + HashSet? possibleConfigOnlyDirs = null; - ModuleInstaller inst = new ModuleInstaller(manager.CurrentInstance, manager.Cache, this); - inst.onReportModInstalled += OnModInstalled; - if (plan.Remove.Count > 0) { - inst.UninstallList(plan.Remove, ref possibleConfigOnlyDirs, regMgr, true, new List(plan.Install)); - plan.Remove.Clear(); - } - NetAsyncModulesDownloader dl = new NetAsyncModulesDownloader(this, manager.Cache); - if (plan.Install.Count > 0) { - var iList = plan.Install - .Select(m => Utilities.DefaultIfThrows(() => - registry.LatestAvailable(m.identifier, - manager.CurrentInstance.VersionCriteria(), - null, - registry.InstalledModules - .Select(im => im.Module) - .ToArray(), - plan.Install)) - ?? m) - .ToArray(); - inst.InstallList(iList, resolvOpts, regMgr, ref possibleConfigOnlyDirs, dl); - plan.Install.Clear(); - } - if (plan.Upgrade.Count > 0) { - var upgGroups = registry - .CheckUpgradeable(manager.CurrentInstance, - // Hold identifiers not chosen for upgrading - registry.Installed(false) - .Keys - .Except(plan.Upgrade) - .ToHashSet()); - inst.Upgrade(upgGroups[true], dl, ref possibleConfigOnlyDirs, regMgr); - plan.Upgrade.Clear(); - } - if (plan.Replace.Count > 0) { - inst.Replace(AllReplacements(plan.Replace), resolvOpts, dl, ref possibleConfigOnlyDirs, regMgr, true); - } + ModuleInstaller inst = new ModuleInstaller(manager.CurrentInstance, manager.Cache, this); + inst.onReportModInstalled += OnModInstalled; + if (plan.Remove.Count > 0) { + inst.UninstallList(plan.Remove, ref possibleConfigOnlyDirs, regMgr, true, new List(plan.Install)); + plan.Remove.Clear(); + } + NetAsyncModulesDownloader dl = new NetAsyncModulesDownloader(this, manager.Cache); + if (plan.Install.Count > 0) { + var iList = plan.Install + .Select(m => Utilities.DefaultIfThrows(() => + registry.LatestAvailable(m.identifier, + manager.CurrentInstance.VersionCriteria(), + null, + registry.InstalledModules + .Select(im => im.Module) + .ToArray(), + plan.Install)) + ?? m) + .ToArray(); + inst.InstallList(iList, resolvOpts, regMgr, ref possibleConfigOnlyDirs, dl); + plan.Install.Clear(); + } + if (plan.Upgrade.Count > 0) { + var upgGroups = registry + .CheckUpgradeable(manager.CurrentInstance, + // Hold identifiers not chosen for upgrading + registry.Installed(false) + .Keys + .Except(plan.Upgrade) + .ToHashSet()); + inst.Upgrade(upgGroups[true], dl, ref possibleConfigOnlyDirs, regMgr); + plan.Upgrade.Clear(); + } + if (plan.Replace.Count > 0) { + inst.Replace(AllReplacements(plan.Replace), resolvOpts, dl, ref possibleConfigOnlyDirs, regMgr, true); + } - trans.Complete(); - inst.onReportModInstalled -= OnModInstalled; - // Don't let the installer re-use old screen references - inst.User = null; - - HandlePossibleConfigOnlyDirs(theme, registry, possibleConfigOnlyDirs); - - } catch (CancelledActionKraken) { - // Don't need to tell the user they just cancelled out. - } catch (FileNotFoundKraken ex) { - // Possible file corruption - RaiseError(ex.Message); - } catch (DirectoryNotFoundKraken ex) { - RaiseError(ex.Message); - } catch (FileExistsKraken ex) { - if (ex.owningModule != null) { - RaiseMessage(Properties.Resources.InstallOwnedFileConflict, ex.installingModule, ex.filename, ex.owningModule); - } else { - RaiseMessage(Properties.Resources.InstallUnownedFileConflict, ex.installingModule, ex.filename, ex.installingModule); - } - RaiseError(Properties.Resources.InstallFilesReverted); - } catch (DownloadErrorsKraken ex) { - RaiseError(ex.ToString()); - } catch (ModuleDownloadErrorsKraken ex) { - RaiseError(ex.ToString()); - } catch (DownloadThrottledKraken ex) { - if (RaiseYesNoDialog(string.Format(Properties.Resources.InstallAuthTokenPrompt, ex.ToString()))) { - if (ex.infoUrl != null) { - ModInfoScreen.LaunchURL(theme, ex.infoUrl); + trans.Complete(); + inst.onReportModInstalled -= OnModInstalled; + // Don't let the installer re-use old screen references + inst.User = new NullUser(); + + HandlePossibleConfigOnlyDirs(theme, registry, possibleConfigOnlyDirs); + + } catch (CancelledActionKraken) { + // Don't need to tell the user they just cancelled out. + } catch (FileNotFoundKraken ex) { + // Possible file corruption + RaiseError(ex.Message); + } catch (DirectoryNotFoundKraken ex) { + RaiseError(ex.Message); + } catch (FileExistsKraken ex) { + if (ex.owningModule != null) { + RaiseMessage(Properties.Resources.InstallOwnedFileConflict, ex.installingModule?.ToString() ?? "", ex.filename, ex.owningModule); + } else { + RaiseMessage(Properties.Resources.InstallUnownedFileConflict, ex.installingModule?.ToString() ?? "", ex.filename, ex.installingModule?.ToString() ?? ""); + } + RaiseError(Properties.Resources.InstallFilesReverted); + } catch (DownloadErrorsKraken ex) { + RaiseError(ex.ToString()); + } catch (ModuleDownloadErrorsKraken ex) { + RaiseError(ex.ToString()); + } catch (DownloadThrottledKraken ex) { + if (RaiseYesNoDialog(string.Format(Properties.Resources.InstallAuthTokenPrompt, ex.ToString()))) { + if (ex.infoUrl != null) { + ModInfoScreen.LaunchURL(theme, ex.infoUrl); + } + LaunchSubScreen(new AuthTokenScreen(theme)); + } + } catch (MissingCertificateKraken ex) { + RaiseError(ex.ToString()); + } catch (InconsistentKraken ex) { + RaiseError(ex.Message); + } catch (TooManyModsProvideKraken ex) { + + var ch = new ConsoleChoiceDialog( + theme, + ex.Message, + Properties.Resources.InstallTooManyModsNameHeader, + ex.modules, + (CkanModule mod) => mod.ToString() + ); + var chosen = ch.Run(); + DrawBackground(); + if (chosen != null) { + // Use chosen to continue installing + plan.Install.Add(chosen); + retry = true; } - LaunchSubScreen(theme, new AuthTokenScreen()); - } - } catch (MissingCertificateKraken ex) { - RaiseError(ex.ToString()); - } catch (InconsistentKraken ex) { - RaiseError(ex.Message); - } catch (TooManyModsProvideKraken ex) { - - ConsoleChoiceDialog ch = new ConsoleChoiceDialog( - ex.Message, - Properties.Resources.InstallTooManyModsNameHeader, - ex.modules, - (CkanModule mod) => mod.ToString() - ); - CkanModule chosen = ch.Run(theme); - DrawBackground(theme); - if (chosen != null) { - // Use chosen to continue installing - plan.Install.Add(chosen); - retry = true; - } - } catch (BadMetadataKraken ex) { - RaiseError(Properties.Resources.InstallBadMetadata, ex.module, ex.Message); - } catch (DependencyNotSatisfiedKraken ex) { - RaiseError(Properties.Resources.InstallUnsatisfiedDependency, ex.parent, ex.module, ex.Message); - } catch (ModuleNotFoundKraken ex) { - RaiseError(Properties.Resources.InstallModuleNotFound, ex.module, ex.Message); - } catch (ModNotInstalledKraken ex) { - RaiseError(Properties.Resources.InstallNotInstalled, ex.mod); - } catch (DllLocationMismatchKraken ex) { - RaiseError(ex.Message); - } - } while (retry); + } catch (BadMetadataKraken ex) { + RaiseError(Properties.Resources.InstallBadMetadata, ex.module?.ToString() ?? "", ex.Message); + } catch (DependencyNotSatisfiedKraken ex) { + RaiseError(Properties.Resources.InstallUnsatisfiedDependency, ex.parent, ex.module, ex.Message); + } catch (ModuleNotFoundKraken ex) { + RaiseError(Properties.Resources.InstallModuleNotFound, ex.module, ex.Message); + } catch (ModNotInstalledKraken ex) { + RaiseError(Properties.Resources.InstallNotInstalled, ex.mod); + } catch (DllLocationMismatchKraken ex) { + RaiseError(ex.Message); + } + } while (retry); + } } } - private void HandlePossibleConfigOnlyDirs(ConsoleTheme theme, - Registry registry, - HashSet possibleConfigOnlyDirs) + private void HandlePossibleConfigOnlyDirs(ConsoleTheme theme, + Registry registry, + HashSet? possibleConfigOnlyDirs) { - if (possibleConfigOnlyDirs != null) + if (possibleConfigOnlyDirs != null && manager.CurrentInstance != null) { // Check again for registered files, since we may // just have installed or upgraded some @@ -185,7 +189,7 @@ private void HandlePossibleConfigOnlyDirs(ConsoleTheme theme, .Any(relF => registry.FileOwner(relF) != null)); if (possibleConfigOnlyDirs.Count > 0) { - LaunchSubScreen(theme, new DeleteDirectoriesScreen(manager.CurrentInstance, possibleConfigOnlyDirs)); + LaunchSubScreen(new DeleteDirectoriesScreen(theme, manager.CurrentInstance, possibleConfigOnlyDirs)); } } } @@ -197,14 +201,16 @@ private void OnModInstalled(CkanModule mod) private IEnumerable AllReplacements(IEnumerable identifiers) { - IRegistryQuerier registry = RegistryManager.Instance(manager.CurrentInstance, repoData).registry; - - foreach (string id in identifiers) { - ModuleReplacement repl = registry.GetReplacement( - id, manager.CurrentInstance.VersionCriteria() - ); - if (repl != null) { - yield return repl; + if (manager.CurrentInstance != null) + { + IRegistryQuerier registry = RegistryManager.Instance(manager.CurrentInstance, repoData).registry; + + foreach (string id in identifiers) { + var repl = registry.GetReplacement( + id, manager.CurrentInstance.VersionCriteria()); + if (repl != null) { + yield return repl; + } } } } diff --git a/ConsoleUI/ModInfoScreen.cs b/ConsoleUI/ModInfoScreen.cs index a8370035dc..46c0d02414 100644 --- a/ConsoleUI/ModInfoScreen.cs +++ b/ConsoleUI/ModInfoScreen.cs @@ -16,12 +16,14 @@ public class ModInfoScreen : ConsoleScreen { /// /// Initialize the Screen /// + /// The visual theme to use to draw the dialog /// 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, Registry registry, ChangePlan cp, CkanModule m, bool dbg) + public ModInfoScreen(ConsoleTheme theme, GameInstanceManager mgr, Registry registry, ChangePlan cp, CkanModule m, bool dbg) + : base(theme) { debug = dbg; mod = m; @@ -103,7 +105,7 @@ public ModInfoScreen(GameInstanceManager mgr, Registry registry, ChangePlan cp, th => th.LabelFg ); tb.AddLine(mod.@abstract); - if (!string.IsNullOrEmpty(mod.description) + if (mod.description != null && !string.IsNullOrEmpty(mod.description) && mod.description != mod.@abstract) { tb.AddLine(mod.description); } @@ -111,87 +113,78 @@ public ModInfoScreen(GameInstanceManager mgr, Registry registry, ChangePlan cp, if (!ChangePlan.IsAnyAvailable(registry, mod.identifier)) { tb.AddLine(Properties.Resources.ModInfoUnavailableWarning); } - tb.AddScrollBindings(this); + tb.AddScrollBindings(this, theme); AddTip(Properties.Resources.Esc, Properties.Resources.Back); - AddBinding(Keys.Escape, (object sender, ConsoleTheme theme) => false); + AddBinding(Keys.Escape, (object sender) => false); AddTip($"{Properties.Resources.Ctrl}+D", Properties.Resources.ModInfoDownloadToCache, - () => !manager.Cache.IsMaybeCachedZip(mod) && !mod.IsDLC + () => manager.Cache != null && !manager.Cache.IsMaybeCachedZip(mod) && !mod.IsDLC ); - AddBinding(Keys.CtrlD, (object sender, ConsoleTheme theme) => { + AddBinding(Keys.CtrlD, (object sender) => { if (!mod.IsDLC) { - Download(theme); + Download(); } return true; }); if (mod.resources != null) { - List opts = new List(); + var opts = new List(); if (mod.resources.homepage != null) { opts.Add(new ConsoleMenuOption( Properties.Resources.ModInfoHomePage, "", Properties.Resources.ModInfoHomePageTip, true, - th => LaunchURL(th, mod.resources.homepage) - )); + () => LaunchURL(theme, mod.resources.homepage))); } if (mod.resources.repository != null) { opts.Add(new ConsoleMenuOption( Properties.Resources.ModInfoRepository, "", Properties.Resources.ModInfoRepositoryTip, true, - th => LaunchURL(th, mod.resources.repository) - )); + () => LaunchURL(theme, mod.resources.repository))); } if (mod.resources.bugtracker != null) { opts.Add(new ConsoleMenuOption( Properties.Resources.ModInfoBugtracker, "", Properties.Resources.ModInfoBugtrackerTip, true, - th => LaunchURL(th, mod.resources.bugtracker) - )); + () => LaunchURL(theme, mod.resources.bugtracker))); } if (mod.resources.discussions != null) { opts.Add(new ConsoleMenuOption( Properties.Resources.ModInfoDiscussions, "", Properties.Resources.ModInfoDiscussionsTip, true, - th => LaunchURL(th, mod.resources.discussions) - )); + () => LaunchURL(theme, mod.resources.discussions))); } if (mod.resources.spacedock != null) { opts.Add(new ConsoleMenuOption( Properties.Resources.ModInfoSpaceDock, "", Properties.Resources.ModInfoSpaceDockTip, true, - th => LaunchURL(th, mod.resources.spacedock) - )); + () => LaunchURL(theme, mod.resources.spacedock))); } if (mod.resources.curse != null) { opts.Add(new ConsoleMenuOption( Properties.Resources.ModInfoCurse, "", Properties.Resources.ModInfoCurseTip, true, - th => LaunchURL(th, mod.resources.curse) - )); + () => LaunchURL(theme, mod.resources.curse))); } if (mod.resources.store != null) { opts.Add(new ConsoleMenuOption( Properties.Resources.ModInfoStore, "", Properties.Resources.ModInfoStoreTip, true, - th => LaunchURL(th, mod.resources.store) - )); + () => LaunchURL(theme, mod.resources.store))); } if (mod.resources.steamstore != null) { opts.Add(new ConsoleMenuOption( Properties.Resources.ModInfoSteamStore, "", Properties.Resources.ModInfoSteamStoreTip, true, - th => LaunchURL(th, mod.resources.steamstore) - )); + () => LaunchURL(theme, mod.resources.steamstore))); } if (debug) { opts.Add(null); opts.Add(new ConsoleMenuOption( Properties.Resources.ModInfoViewMetadata, "", Properties.Resources.ModInfoViewMetadataTip, true, - ViewMetadata - )); + ViewMetadata)); } if (opts.Count > 0) { @@ -204,36 +197,30 @@ public ModInfoScreen(GameInstanceManager mgr, Registry registry, ChangePlan cp, /// Put CKAN 1.25.5 in top left corner /// protected override string LeftHeader() - { - return $"{Meta.GetProductName()} {Meta.GetVersion()}"; - } + => $"{Meta.GetProductName()} {Meta.GetVersion()}"; /// /// Put description in top center /// protected override string CenterHeader() - { - return Properties.Resources.ModInfoTitle; - } + => Properties.Resources.ModInfoTitle; /// /// Label menu as Links /// protected override string MenuTip() - { - return Properties.Resources.ModInfoMenuTip; - } + => Properties.Resources.ModInfoMenuTip; - private bool ViewMetadata(ConsoleTheme theme) + private bool ViewMetadata() { - ConsoleMessageDialog md = new ConsoleMessageDialog( + var md = new ConsoleMessageDialog( + theme, $"\"{mod.identifier}\": {registry.GetAvailableMetadata(mod.identifier)}", new List { Properties.Resources.OK }, () => string.Format(Properties.Resources.ModInfoViewMetadataTitle, mod.name), - TextAlign.Left - ); - md.Run(theme); - DrawBackground(theme); + TextAlign.Left); + md.Run(); + DrawBackground(); return true; } @@ -254,8 +241,8 @@ public static bool LaunchURL(ConsoleTheme theme, Uri u) // support launching URLs! .NET's API design has painted us into a corner. // So instead we display a popup dialog for the garbage to print all over, // then wait 1.5 seconds and refresh the screen when it closes. - ConsoleMessageDialog d = new ConsoleMessageDialog(Properties.Resources.ModInfoURLLaunching, new List()); - d.Run(theme, (ConsoleTheme th) => { + var d = new ConsoleMessageDialog(theme, Properties.Resources.ModInfoURLLaunching, new List()); + d.Run(() => { Utilities.ProcessStartURL(u.ToString()); Thread.Sleep(1500); }); @@ -264,85 +251,85 @@ public static bool LaunchURL(ConsoleTheme theme, Uri u) private int addDependencies(int top = 8) { - int numDeps = mod.depends?.Count ?? 0; - int numConfs = mod.conflicts?.Count ?? 0; - - if (numDeps + numConfs > 0) { - int midL = (Console.WindowWidth / 2) - 1; - int h = Math.Min(11, numDeps + numConfs + 2); + if (manager.CurrentInstance != null) + { const int lblW = 16; + int midL = (Console.WindowWidth / 2) - 1; int nameW = midL - 2 - lblW - 2 - 1; - int depsH = (h - 2) * numDeps / (numDeps + numConfs); - var upgradeableGroups = registry - .CheckUpgradeable(manager.CurrentInstance, - new HashSet()); - - AddObject(new ConsoleFrame( - 1, top, midL, top + h - 1, - () => Properties.Resources.ModInfoDependenciesFrame, - th => th.NormalFrameFg, - false - )); - if (numDeps > 0) { - AddObject(new ConsoleLabel( - 3, top + 1, 3 + lblW - 1, - () => string.Format(Properties.Resources.ModInfoRequiredLabel, numDeps), - null, - th => th.DimLabelFg - )); - ConsoleTextBox tb = new ConsoleTextBox( - 3 + lblW, top + 1, midL - 2, top + 1 + depsH - 1, false, - TextAlign.Left, - th => th.MainBg, - th => th.LabelFg - ); - AddObject(tb); - foreach (RelationshipDescriptor rd in mod.depends) { - tb.AddLine(ScreenObject.TruncateLength( - // Show install status - ModListScreen.StatusSymbol(plan.GetModStatus(manager, registry, rd.ToString(), - upgradeableGroups[true])) - + rd.ToString(), - nameW - )); + var upgradeable = registry.CheckUpgradeable(manager.CurrentInstance, + new HashSet()) + [true]; + var depends = (mod.depends?.Select(dep => RelationshipString(dep, upgradeable, nameW)) + ?? Enumerable.Empty()) + .ToArray(); + var conflicts = (mod.conflicts?.Select(con => RelationshipString(con, upgradeable, nameW)) + ?? Enumerable.Empty()) + .ToArray(); + + if (depends.Length + conflicts.Length > 0) { + int h = Math.Min(11, depends.Length + conflicts.Length + 2); + int depsH = (h - 2) * depends.Length / (depends.Length + conflicts.Length); + + AddObject(new ConsoleFrame( + 1, top, midL, top + h - 1, + () => Properties.Resources.ModInfoDependenciesFrame, + th => th.NormalFrameFg, + false)); + if (depends.Length > 0) { + AddObject(new ConsoleLabel( + 3, top + 1, 3 + lblW - 1, + () => string.Format(Properties.Resources.ModInfoRequiredLabel, depends.Length), + null, + th => th.DimLabelFg)); + var tb = new ConsoleTextBox( + 3 + lblW, top + 1, midL - 2, top + 1 + depsH - 1, false, + TextAlign.Left, + th => th.MainBg, + th => th.LabelFg); + AddObject(tb); + foreach (var d in depends) { + tb.AddLine(d); + } } - } - if (numConfs > 0) { - AddObject(new ConsoleLabel( - 3, top + 1 + depsH, 3 + lblW - 1, - () => string.Format(Properties.Resources.ModInfoConflictsLabel, numConfs), - null, - th => th.DimLabelFg - )); - ConsoleTextBox tb = new ConsoleTextBox( - 3 + lblW, top + 1 + depsH, midL - 2, top + h - 2, false, - TextAlign.Left, - th => th.MainBg, - th => th.LabelFg - ); - AddObject(tb); - // FUTURE: Find mods that conflict with this one - // See GUI/MainModList.cs::ComputeConflictsFromModList - foreach (RelationshipDescriptor rd in mod.conflicts) { - tb.AddLine(ScreenObject.TruncateLength( - // Show install status - ModListScreen.StatusSymbol(plan.GetModStatus(manager, registry, rd.ToString(), - upgradeableGroups[true])) - + rd.ToString(), - nameW - )); + if (conflicts.Length > 0) { + AddObject(new ConsoleLabel( + 3, top + 1 + depsH, 3 + lblW - 1, + () => string.Format(Properties.Resources.ModInfoConflictsLabel, conflicts.Length), + null, + th => th.DimLabelFg)); + var tb = new ConsoleTextBox( + 3 + lblW, top + 1 + depsH, midL - 2, top + h - 2, false, + TextAlign.Left, + th => th.MainBg, + th => th.LabelFg); + AddObject(tb); + // FUTURE: Find mods that conflict with this one + // See GUI/MainModList.cs::ComputeConflictsFromModList + foreach (var c in conflicts) { + tb.AddLine(c); + } } + return top + h - 1; } - return top + h - 1; } return top - 1; } + private string RelationshipString(RelationshipDescriptor rel, + List upgradeable, + int width) + => ScreenObject.TruncateLength((ModListScreen.StatusSymbol( + rel is ModuleRelationshipDescriptor mrd + // Show install status + ? plan.GetModStatus(manager, registry, + mrd.name, upgradeable) + : InstallStatus.NotInstalled)) + + rel.ToString(), + width); + private DateTime? InstalledOn(string identifier) - { // This can be null for manually installed mods - return registry.InstalledModule(identifier)?.InstallTime; - } + => registry.InstalledModule(identifier)?.InstallTime; private int addVersionDisplay() { @@ -351,17 +338,18 @@ private int addVersionDisplay() const int boxRight = -1, boxH = 5; - if (ChangePlan.IsAnyAvailable(registry, mod.identifier)) { + if (manager.CurrentInstance != null + && ChangePlan.IsAnyAvailable(registry, mod.identifier)) { - List avail = registry.AvailableByIdentifier(mod.identifier).ToList(); - CkanModule inst = registry.GetInstalledVersion( mod.identifier); - CkanModule latest = registry.LatestAvailable( mod.identifier, null); - bool installed = registry.IsInstalled(mod.identifier, false); - bool latestIsInstalled = inst?.Equals(latest) ?? false; - List others = avail; + var inst = registry.GetInstalledVersion(mod.identifier); + var latest = registry.LatestAvailable(mod.identifier, null); + var others = registry.AvailableByIdentifier(mod.identifier) + .Except(new[] { inst, latest }.OfType()) + .OfType() + .ToList(); - others.Remove(inst); - others.Remove(latest); + bool installed = registry.IsInstalled(mod.identifier, false); + bool latestIsInstalled = inst?.Equals(latest) ?? false; if (installed) { @@ -369,12 +357,9 @@ private int addVersionDisplay() if (latestIsInstalled) { - ModuleReplacement mr = registry.GetReplacement( - mod.identifier, - manager.CurrentInstance.VersionCriteria() - ); - - if (mr != null) { + if (registry.GetReplacement(mod.identifier, + manager.CurrentInstance.VersionCriteria()) + is ModuleReplacement mr) { // Show replaced_by addVersionBox( @@ -382,19 +367,18 @@ private int addVersionDisplay() () => string.Format(Properties.Resources.ModInfoReplacedBy, mr.ReplaceWith.identifier), th => th.AlertFrameFg, false, - new List() {mr.ReplaceWith} - ); + new List() { mr.ReplaceWith }); boxTop += boxH; addVersionBox( boxLeft, boxTop, boxRight, boxTop + boxH - 1, () => instTime.HasValue - ? string.Format(Properties.Resources.ModInfoInstalledOn, instTime.Value.ToString("d")) + ? string.Format(Properties.Resources.ModInfoInstalledOn, + instTime.Value.ToString("d")) : Properties.Resources.ModInfoInstalledManually, th => th.ActiveFrameFg, true, - new List() {inst} - ); + new List() { inst }); boxTop += boxH; } else { @@ -402,17 +386,16 @@ private int addVersionDisplay() addVersionBox( boxLeft, boxTop, boxRight, boxTop + boxH - 1, () => instTime.HasValue - ? string.Format(Properties.Resources.ModInfoLatestInstalledOn, instTime.Value.ToString("d")) + ? string.Format(Properties.Resources.ModInfoLatestInstalledOn, + instTime.Value.ToString("d")) : Properties.Resources.ModInfoLatestInstalledManually, th => th.ActiveFrameFg, true, - new List() {inst} - ); + new List() { inst }); boxTop += boxH; } - } else { addVersionBox( @@ -420,19 +403,18 @@ private int addVersionDisplay() () => Properties.Resources.ModInfoLatestVersion, th => th.AlertFrameFg, false, - new List() {latest} - ); + new List() { latest }); boxTop += boxH; addVersionBox( boxLeft, boxTop, boxRight, boxTop + boxH - 1, - () => instTime.HasValue - ? string.Format(Properties.Resources.ModInfoInstalledOn, instTime.Value.ToString("d")) - : Properties.Resources.ModInfoInstalledManually, + () => instTime.HasValue + ? string.Format(Properties.Resources.ModInfoInstalledOn, + instTime.Value.ToString("d")) + : Properties.Resources.ModInfoInstalledManually, th => th.ActiveFrameFg, true, - new List() {inst} - ); + new List() { inst }); boxTop += boxH; } @@ -440,11 +422,10 @@ private int addVersionDisplay() addVersionBox( boxLeft, boxTop, boxRight, boxTop + boxH - 1, - () => Properties.Resources.ModInfoLatestVersion, + () => Properties.Resources.ModInfoLatestVersion, th => th.NormalFrameFg, false, - new List() {latest} - ); + new List() { latest }); boxTop += boxH; } @@ -456,8 +437,7 @@ private int addVersionDisplay() () => Properties.Resources.ModInfoOtherVersions, th => th.NormalFrameFg, false, - others - ); + others); boxTop += boxH; } @@ -474,16 +454,18 @@ private int addVersionDisplay() : Properties.Resources.ModInfoUnavailableInstalledManually, th => th.AlertFrameFg, true, - new List() {mod} - ); + new List() { mod }); boxTop += boxH; } - return boxTop - 1; } - private void addVersionBox(int l, int t, int r, int b, Func title, Func color, bool doubleLine, List releases) + private void addVersionBox(int l, int t, int r, int b, + Func title, + Func color, + bool doubleLine, + List releases) { AddObject(new ConsoleFrame( l, t, r, b, @@ -494,7 +476,9 @@ private void addVersionBox(int l, int t, int r, int b, Func title, Func< if (releases != null && releases.Count > 0) { - CkanModule.GetMinMaxVersions(releases, out ModuleVersion minMod, out ModuleVersion maxMod, out GameVersion minKsp, out GameVersion maxKsp); + CkanModule.GetMinMaxVersions(releases, + out ModuleVersion? minMod, out ModuleVersion? maxMod, + out GameVersion? minKsp, out GameVersion? maxKsp); AddObject(new ConsoleLabel( l + 2, t + 1, r - 2, () => minMod == maxMod @@ -511,7 +495,11 @@ private void addVersionBox(int l, int t, int r, int b, Func title, Func< )); AddObject(new ConsoleLabel( l + 4, t + 3, r - 2, - () => GameVersionRange.VersionSpan(manager.CurrentInstance.game, minKsp, maxKsp), + () => manager.CurrentInstance == null + ? "" + : GameVersionRange.VersionSpan(manager.CurrentInstance.game, + minKsp ?? GameVersion.Any, + maxKsp ?? GameVersion.Any), null, color )); @@ -526,7 +514,7 @@ private string HostedOn() var downloadHosts = mod.download .Select(dlUri => dlUri.Host) .Select(host => - hostDomains.TryGetValue(host, out string name) + hostDomains.TryGetValue(host, out string? name) ? name : host); return string.Format(Properties.Resources.ModInfoHostedOn, @@ -567,27 +555,29 @@ private string HostedOn() return ""; } - private void Download(ConsoleTheme theme) + private void Download() { - ProgressScreen ps = new ProgressScreen(string.Format(Properties.Resources.ModInfoDownloading, mod.identifier)); - NetAsyncModulesDownloader dl = new NetAsyncModulesDownloader(ps, manager.Cache); - ModuleInstaller inst = new ModuleInstaller(manager.CurrentInstance, manager.Cache, ps); - LaunchSubScreen( - theme, - ps, - (ConsoleTheme th) => { - try { - dl.DownloadModules(new List {mod}); - if (!manager.Cache.IsMaybeCachedZip(mod)) { - ps.RaiseError(Properties.Resources.ModInfoDownloadFailed); + if (manager.CurrentInstance != null && manager.Cache != null) + { + var ps = new ProgressScreen(theme, string.Format(Properties.Resources.ModInfoDownloading, mod.identifier)); + var dl = new NetAsyncModulesDownloader(ps, manager.Cache); + var inst = new ModuleInstaller(manager.CurrentInstance, manager.Cache, ps); + LaunchSubScreen( + ps, + () => { + try { + dl.DownloadModules(new List {mod}); + if (!manager.Cache.IsMaybeCachedZip(mod)) { + ps.RaiseError(Properties.Resources.ModInfoDownloadFailed); + } + } catch (Exception ex) { + ps.RaiseError(Properties.Resources.ModInfoDownloadFailed, ex); } - } catch (Exception ex) { - ps.RaiseError(Properties.Resources.ModInfoDownloadFailed, ex); } - } - ); - // Don't let the installer re-use old screen references - inst.User = null; + ); + // Don't let the installer re-use old screen references + inst.User = new NullUser(); + } } private static readonly Dictionary hostDomains = new Dictionary() { diff --git a/ConsoleUI/ModListHelpDialog.cs b/ConsoleUI/ModListHelpDialog.cs index eb9d4b0a49..ae51118f00 100644 --- a/ConsoleUI/ModListHelpDialog.cs +++ b/ConsoleUI/ModListHelpDialog.cs @@ -15,7 +15,7 @@ public class ModListHelpDialog : ConsoleDialog { /// /// Initialize the screen /// - public ModListHelpDialog() : base() + public ModListHelpDialog(ConsoleTheme theme) : base(theme) { SetDimensions(9, 3, -9, -3); diff --git a/ConsoleUI/ModListScreen.cs b/ConsoleUI/ModListScreen.cs index e633f34d2e..048c9283fa 100644 --- a/ConsoleUI/ModListScreen.cs +++ b/ConsoleUI/ModListScreen.cs @@ -10,6 +10,7 @@ using CKAN.Extensions; using CKAN.Games; using CKAN.Configuration; +using CKAN.Versioning; namespace CKAN.ConsoleUI { @@ -21,13 +22,19 @@ public class ModListScreen : ConsoleScreen { /// /// Initialize the screen /// + /// The visual theme to use to draw the dialog /// 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, RepositoryDataManager repoData, RegistryManager regMgr, IGame game, bool dbg, ConsoleTheme regTheme) + public ModListScreen(ConsoleTheme theme, + GameInstanceManager mgr, + RepositoryDataManager repoData, + RegistryManager regMgr, + IGame game, + bool dbg) + : base(theme) { debug = dbg; manager = mgr; @@ -37,41 +44,41 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re moduleList = new ConsoleListBox( 1, 4, -1, -2, - GetAllMods(regTheme), + GetAllMods(), new List>() { - new ConsoleListBoxColumn() { - Header = "", - Width = 1, - Renderer = StatusSymbol - }, new ConsoleListBoxColumn() { - Header = Properties.Resources.ModListNameHeader, - Width = null, - Renderer = m => m.name ?? "" - }, new ConsoleListBoxColumn() { - Header = Properties.Resources.ModListVersionHeader, - Width = 10, - Renderer = m => ModuleInstaller.StripEpoch(m.version?.ToString() ?? ""), - Comparer = (a, b) => a.version.CompareTo(b.version) - }, new ConsoleListBoxColumn() { - Header = Properties.Resources.ModListMaxGameVersionHeader, - Width = 20, - 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)) - }, new ConsoleListBoxColumn() { - Header = Properties.Resources.ModListDownloadsHeader, - Width = 12, - Renderer = m => repoData.GetDownloadCount(registry.Repositories.Values, m.identifier) + new ConsoleListBoxColumn( + "", StatusSymbol, null, 1), + new ConsoleListBoxColumn( + Properties.Resources.ModListNameHeader, + m => m.name ?? "", + null, null), + new ConsoleListBoxColumn( + Properties.Resources.ModListVersionHeader, + m => ModuleInstaller.StripEpoch(m.version?.ToString() ?? ""), + (a, b) => a.version.CompareTo(b.version), + 10), + new ConsoleListBoxColumn( + Properties.Resources.ModListMaxGameVersionHeader, + m => registry.LatestCompatibleGameVersion(game.KnownVersions, m.identifier)?.ToString() ?? "", + (a, b) => registry.LatestCompatibleGameVersion(game.KnownVersions, a.identifier) is GameVersion gvA + && registry.LatestCompatibleGameVersion(game.KnownVersions, b.identifier) is GameVersion gvB + ? gvA.CompareTo(gvB) + : 0, + 20), + new ConsoleListBoxColumn( + Properties.Resources.ModListDownloadsHeader, + m => repoData.GetDownloadCount(registry.Repositories.Values, m.identifier) ?.ToString() ?? "", - Comparer = (a, b) => (repoData.GetDownloadCount(registry.Repositories.Values, a.identifier) ?? 0) + (a, b) => (repoData.GetDownloadCount(registry.Repositories.Values, a.identifier) ?? 0) .CompareTo(repoData.GetDownloadCount(registry.Repositories.Values, b.identifier) ?? 0), - } + 12), }, 1, 0, ListSortDirection.Descending, (CkanModule m, string filter) => { // Search for author if (filter.StartsWith("@")) { - string authorFilt = filter.Substring(1); + string authorFilt = filter[1..]; if (string.IsNullOrEmpty(authorFilt)) { return true; } else { @@ -84,8 +91,7 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re if (filter.Length <= 1) { // Don't blank the list for just "~" by itself return true; - } else - { + } else { switch (filter.Substring(1, 1)) { case "i": return registry.IsInstalled(m.identifier, false); @@ -96,7 +102,7 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re return recent.Contains(m.identifier); case "c": if (m.conflicts != null) { - string conflictsWith = filter.Substring(2); + string conflictsWith = filter[2..]; // Search for mods depending on a given mod foreach (var rel in m.conflicts) { if (rel.StartsWith(conflictsWith)) { @@ -107,7 +113,7 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re return false; case "d": if (m.depends != null) { - string dependsOn = filter.Substring(2); + string dependsOn = filter[2..]; // Search for mods depending on a given mod foreach (var rel in m.depends) { if (rel.StartsWith(dependsOn)) { @@ -116,7 +122,7 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re } } return false; - } + } } return false; @@ -147,34 +153,34 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re AddObject(searchBox); AddObject(moduleList); - AddBinding(Keys.CtrlP, (object sender, ConsoleTheme theme) => PlayGame()); - AddBinding(Keys.CtrlQ, (object sender, ConsoleTheme theme) => false); - AddBinding(Keys.AltX, (object sender, ConsoleTheme theme) => false); - AddBinding(Keys.F1, (object sender, ConsoleTheme theme) => Help(theme)); - AddBinding(Keys.AltH, (object sender, ConsoleTheme theme) => Help(theme)); - AddBinding(Keys.F5, (object sender, ConsoleTheme theme) => UpdateRegistry(theme)); - AddBinding(Keys.CtrlR, (object sender, ConsoleTheme theme) => UpdateRegistry(theme)); - AddBinding(Keys.CtrlU, (object sender, ConsoleTheme theme) => UpgradeAll(theme)); + AddBinding(Keys.CtrlP, (object sender) => PlayGame()); + AddBinding(Keys.CtrlQ, (object sender) => false); + AddBinding(Keys.AltX, (object sender) => false); + AddBinding(Keys.F1, (object sender) => Help()); + AddBinding(Keys.AltH, (object sender) => Help()); + AddBinding(Keys.F5, (object sender) => UpdateRegistry()); + AddBinding(Keys.CtrlR, (object sender) => UpdateRegistry()); + AddBinding(Keys.CtrlU, (object sender) => UpgradeAll()); // Now a bunch of convenience shortcuts so you don't get stuck in the search box - searchBox.AddBinding(Keys.PageUp, (object sender, ConsoleTheme theme) => { + searchBox.AddBinding(Keys.PageUp, (object sender) => { SetFocus(moduleList); return true; }); - searchBox.AddBinding(Keys.PageDown, (object sender, ConsoleTheme theme) => { + searchBox.AddBinding(Keys.PageDown, (object sender) => { SetFocus(moduleList); return true; }); - searchBox.AddBinding(Keys.Enter, (object sender, ConsoleTheme theme) => { + searchBox.AddBinding(Keys.Enter, (object sender) => { SetFocus(moduleList); return true; }); - moduleList.AddBinding(Keys.CtrlF, (object sender, ConsoleTheme theme) => { + moduleList.AddBinding(Keys.CtrlF, (object sender) => { SetFocus(searchBox); return true; }); - moduleList.AddBinding(Keys.Escape, (object sender, ConsoleTheme theme) => { + moduleList.AddBinding(Keys.Escape, (object sender) => { searchBox.Clear(); return true; }); @@ -182,9 +188,9 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re moduleList.AddTip(Properties.Resources.Enter, Properties.Resources.Details, () => moduleList.Selection != null ); - moduleList.AddBinding(Keys.Enter, (object sender, ConsoleTheme theme) => { + moduleList.AddBinding(Keys.Enter, (object sender) => { if (moduleList.Selection != null) { - LaunchSubScreen(theme, new ModInfoScreen(manager, registry, plan, moduleList.Selection, debug)); + LaunchSubScreen(new ModInfoScreen(theme, manager, registry, plan, moduleList.Selection, debug)); } return true; }); @@ -201,10 +207,11 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re ); moduleList.AddTip("+", Properties.Resources.ModListReplaceTip, () => moduleList.Selection != null - && registry.GetReplacement(moduleList.Selection.identifier, manager.CurrentInstance.VersionCriteria()) != null + && manager.CurrentInstance != null + && registry.GetReplacement(moduleList.Selection.identifier, manager.CurrentInstance.VersionCriteria()) != null ); - moduleList.AddBinding(Keys.Plus, (object sender, ConsoleTheme theme) => { - if (moduleList.Selection != null && !moduleList.Selection.IsDLC) { + moduleList.AddBinding(Keys.Plus, (object sender) => { + if (moduleList.Selection != null && !moduleList.Selection.IsDLC && manager.CurrentInstance != null) { if (!registry.IsInstalled(moduleList.Selection.identifier, false)) { plan.ToggleInstall(moduleList.Selection); } else if (registry.IsInstalled(moduleList.Selection.identifier, false) @@ -222,7 +229,7 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re && registry.IsInstalled(moduleList.Selection.identifier, false) && !registry.IsAutodetected(moduleList.Selection.identifier) ); - moduleList.AddBinding(Keys.Minus, (object sender, ConsoleTheme theme) => { + moduleList.AddBinding(Keys.Minus, (object sender) => { if (moduleList.Selection != null && !moduleList.Selection.IsDLC && registry.IsInstalled(moduleList.Selection.identifier, false) && !registry.IsAutodetected(moduleList.Selection.identifier)) { @@ -239,18 +246,21 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re () => moduleList.Selection != null && !moduleList.Selection.IsDLC && (registry.InstalledModule(moduleList.Selection.identifier)?.AutoInstalled ?? false) ); - moduleList.AddBinding(Keys.F8, (object sender, ConsoleTheme theme) => { - InstalledModule im = registry.InstalledModule(moduleList.Selection.identifier); - if (im != null && !moduleList.Selection.IsDLC) { - im.AutoInstalled = !im.AutoInstalled; - regMgr.Save(false); + moduleList.AddBinding(Keys.F8, (object sender) => { + if (moduleList.Selection is CkanModule m) + { + var im = registry.InstalledModule(m.identifier); + if (im != null && !m.IsDLC) { + im.AutoInstalled = !im.AutoInstalled; + regMgr.Save(false); + } } return true; }); AddTip("F9", Properties.Resources.ModListApplyChangesTip, plan.NonEmpty); - AddBinding(Keys.F9, (object sender, ConsoleTheme theme) => { - ApplyChanges(theme); + AddBinding(Keys.F9, (object sender) => { + ApplyChanges(); return true; }); @@ -278,22 +288,24 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re } )); - List opts = new List() { + var opts = new List() { new ConsoleMenuOption(Properties.Resources.ModListPlayMenu, "", Properties.Resources.ModListPlayMenuTip, true, null, null, null, true, () => new ConsolePopupMenu( - manager.CurrentInstance - .game - .DefaultCommandLines(manager.SteamLibrary, - new DirectoryInfo(manager.CurrentInstance.GameDir())) - .Select((cmd, i) => new ConsoleMenuOption( - cmd, - i == 0 ? $"{Properties.Resources.Ctrl}+P" - : "", - cmd, true, - th => PlayGame(cmd))) - .ToList())), + (manager.CurrentInstance + ?.game + .DefaultCommandLines(manager.SteamLibrary, + new DirectoryInfo(manager.CurrentInstance.GameDir())) + ?? Enumerable.Empty()) + .Select((cmd, i) => new ConsoleMenuOption( + cmd, + i == 0 ? $"{Properties.Resources.Ctrl}+P" + : "", + cmd, true, + () => PlayGame(cmd))) + .OfType() + .ToList())), null, new ConsoleMenuOption(Properties.Resources.ModListSortMenu, "", Properties.Resources.ModListSortMenuTip, @@ -301,7 +313,7 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re null, new ConsoleMenuOption(Properties.Resources.ModListRefreshMenu, $"F5, {Properties.Resources.Ctrl}+R", Properties.Resources.ModListRefreshMenuTip, - true, (ConsoleTheme th) => UpdateRegistry(th)), + true, () => UpdateRegistry()), new ConsoleMenuOption(Properties.Resources.ModListUpgradeMenu, $"{Properties.Resources.Ctrl}+U", Properties.Resources.ModListUpgradeMenuTip, true, UpgradeAll, null, null, HasAnyUpgradeable()), @@ -337,7 +349,7 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re null, new ConsoleMenuOption(Properties.Resources.ModListQuitMenu, $"{Properties.Resources.Ctrl}+Q", Properties.Resources.ModListQuitMenuTip, - true, (ConsoleTheme th) => false) + true, () => false) }; if (debug) { opts.Add(null); @@ -352,17 +364,13 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re /// Put CKAN 1.25.5 in top left corner /// protected override string LeftHeader() - { - return $"{Meta.GetProductName()} {Meta.GetVersion()}"; - } + => $"{Meta.GetProductName()} {Meta.GetVersion()}"; /// /// Put description in top center /// protected override string CenterHeader() - { - return $"{manager.CurrentInstance.game.ShortName} {manager.CurrentInstance.Version()} ({manager.CurrentInstance.Name})"; - } + => $"{manager.CurrentInstance?.game.ShortName} {manager.CurrentInstance?.Version()} ({manager.CurrentInstance?.Name})"; // Alt+H doesn't work on Mac, but F1 does, and we need // an option other than F1 for terminals that open their own help. @@ -370,47 +378,49 @@ protected override string CenterHeader() ? "F1" : $"F1, {Properties.Resources.Alt}+H"; - private bool ImportDownloads(ConsoleTheme theme) + private bool ImportDownloads() { - DownloadImportDialog.ImportDownloads(theme, manager.CurrentInstance, repoData, manager.Cache, plan); - RefreshList(theme); + if (manager.CurrentInstance != null && manager.Cache != null) + { + DownloadImportDialog.ImportDownloads(theme, manager.CurrentInstance, repoData, manager.Cache, plan); + RefreshList(); + } return true; } - private bool CaptureKey(ConsoleTheme theme) + private bool CaptureKey() { ConsoleKeyInfo k = default; - ConsoleMessageDialog keyprompt = new ConsoleMessageDialog(Properties.Resources.ModListPressAKey, new List()); - keyprompt.Run(theme, (ConsoleTheme th) => { + ConsoleMessageDialog keyprompt = new ConsoleMessageDialog(theme, Properties.Resources.ModListPressAKey, new List()); + keyprompt.Run(() => { k = Console.ReadKey(true); }); ConsoleMessageDialog output = new ConsoleMessageDialog( + theme, $"Key: {k.Key,18}\nKeyChar: 0x{(int)k.KeyChar:x2}\nModifiers: {k.Modifiers,12}", new List { Properties.Resources.OK } ); - output.Run(theme); + output.Run(); return true; } private bool HasAnyUpgradeable() - { - return (upgradeableGroups?[true].Count ?? 0) > 0; - } + => (upgradeableGroups?[true].Count ?? 0) > 0; - private bool UpgradeAll(ConsoleTheme theme) + private bool UpgradeAll() { plan.Upgrade.UnionWith(upgradeableGroups?[true].Select(m => m.identifier) ?? Enumerable.Empty()); return true; } - private bool ViewSuggestions(ConsoleTheme theme) + private bool ViewSuggestions() { ChangePlan reinstall = new ChangePlan(); foreach (InstalledModule im in registry.InstalledModules) { // Only check mods that are still available try { - if (registry.LatestAvailable(im.identifier, manager.CurrentInstance.VersionCriteria()) != null) { + if (registry.LatestAvailable(im.identifier, manager.CurrentInstance?.VersionCriteria()) != null) { reinstall.Install.Add(im.Module); } } catch { @@ -418,9 +428,9 @@ private bool ViewSuggestions(ConsoleTheme theme) } } try { - DependencyScreen ds = new DependencyScreen(manager, registry, reinstall, new HashSet(), debug); + DependencyScreen ds = new DependencyScreen(theme, manager, registry, reinstall, new HashSet(), debug); if (ds.HaveOptions()) { - LaunchSubScreen(theme, ds); + LaunchSubScreen(ds); bool needRefresh = false; // Copy the right ones into our real plan foreach (CkanModule mod in reinstall.Install) { @@ -430,7 +440,7 @@ private bool ViewSuggestions(ConsoleTheme theme) } } if (needRefresh) { - RefreshList(theme); + RefreshList(); } } else { RaiseError(Properties.Resources.ModListAuditNotFound); @@ -442,50 +452,49 @@ private bool ViewSuggestions(ConsoleTheme theme) } private bool PlayGame() - => PlayGame(manager.CurrentInstance - .game - .DefaultCommandLines(manager.SteamLibrary, - new DirectoryInfo(manager.CurrentInstance.GameDir())) - .FirstOrDefault()); + => manager.CurrentInstance != null + && PlayGame(manager.CurrentInstance + .game + .DefaultCommandLines(manager.SteamLibrary, + new DirectoryInfo(manager.CurrentInstance.GameDir())) + .First()); private bool PlayGame(string commandLine) { - manager.CurrentInstance.PlayGame(commandLine); + manager.CurrentInstance?.PlayGame(commandLine); return true; } - private bool UpdateRegistry(ConsoleTheme theme, bool showNewModsPrompt = true) + private bool UpdateRegistry(bool showNewModsPrompt = true) { ProgressScreen ps = new ProgressScreen( + theme, Properties.Resources.ModListUpdateRegistryTitle, - Properties.Resources.ModListUpdateRegistryMessage - ); - LaunchSubScreen(theme, ps, (ConsoleTheme th) => { - HashSet availBefore = new HashSet( - Array.ConvertAll( - registry.CompatibleModules( + Properties.Resources.ModListUpdateRegistryMessage); + LaunchSubScreen(ps, () => { + if (manager.CurrentInstance != null) + { + var availBefore = registry.CompatibleModules(manager.CurrentInstance.VersionCriteria()) + .Select(l => l.identifier) + .ToHashSet(); + recent.Clear(); + try { + 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); + } + // Update recent with mods that were updated in this pass + foreach (CkanModule mod in registry.CompatibleModules( manager.CurrentInstance.VersionCriteria() - ).ToArray(), - l => l.identifier - ) - ); - recent.Clear(); - try { - 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); - } - // Update recent with mods that were updated in this pass - foreach (CkanModule mod in registry.CompatibleModules( - manager.CurrentInstance.VersionCriteria() - )) { - if (!availBefore.Contains(mod.identifier)) { - recent.Add(mod.identifier); + )) { + if (!availBefore.Contains(mod.identifier)) { + recent.Add(mod.identifier); + } } } }); @@ -493,7 +502,7 @@ private bool UpdateRegistry(ConsoleTheme theme, bool showNewModsPrompt = true) searchBox.Clear(); moduleList.FilterString = searchBox.Value = "~n"; } - RefreshList(theme); + RefreshList(); return true; } @@ -515,148 +524,167 @@ private bool ScanForMods() return true; } - private bool InstanceSettings(ConsoleTheme theme) + private bool InstanceSettings() { - var prevRepos = new SortedDictionary(registry.Repositories); - var prevVerCrit = manager.CurrentInstance.VersionCriteria(); - LaunchSubScreen(theme, new GameInstanceEditScreen(manager, repoData, manager.CurrentInstance)); - if (!registry.Repositories.DictionaryEquals(prevRepos)) { - // Repos changed, need to fetch them - UpdateRegistry(theme, false); - RefreshList(theme); - } else if (!manager.CurrentInstance.VersionCriteria().Equals(prevVerCrit)) { - // VersionCriteria changed, need to re-check what is compatible - RefreshList(theme); + if (manager.CurrentInstance != null) + { + var prevRepos = new SortedDictionary(registry.Repositories); + var prevVerCrit = manager.CurrentInstance.VersionCriteria(); + LaunchSubScreen(new GameInstanceEditScreen(theme, manager, repoData, manager.CurrentInstance)); + if (!registry.Repositories.DictionaryEquals(prevRepos)) { + // Repos changed, need to fetch them + UpdateRegistry(false); + RefreshList(); + } else if (!manager.CurrentInstance.VersionCriteria().Equals(prevVerCrit)) { + // VersionCriteria changed, need to re-check what is compatible + RefreshList(); + } } return true; } - private bool SelectInstall(ConsoleTheme theme) + private bool SelectInstall() { - GameInstance prevInst = manager.CurrentInstance; - var prevRepos = new SortedDictionary(registry.Repositories); - var prevVerCrit = prevInst.VersionCriteria(); - LaunchSubScreen(theme, new GameInstanceListScreen(manager, repoData)); - if (!prevInst.Equals(manager.CurrentInstance)) { - // Game instance changed, reset everything - plan.Reset(); - regMgr = RegistryManager.Instance(manager.CurrentInstance, repoData); - registry = regMgr.registry; - RefreshList(theme); - } else if (!registry.Repositories.DictionaryEquals(prevRepos)) { - // Repos changed, need to fetch them - UpdateRegistry(theme, false); - RefreshList(theme); - } else if (!manager.CurrentInstance.VersionCriteria().Equals(prevVerCrit)) { - // VersionCriteria changed, need to re-check what is compatible - RefreshList(theme); + if (manager.CurrentInstance != null) + { + var prevInst = manager.CurrentInstance; + var prevRepos = new SortedDictionary(registry.Repositories); + var prevVerCrit = prevInst.VersionCriteria(); + LaunchSubScreen(new GameInstanceListScreen(theme, manager, repoData)); + if (!prevInst.Equals(manager.CurrentInstance)) { + // Game instance changed, reset everything + plan.Reset(); + regMgr = RegistryManager.Instance(manager.CurrentInstance, repoData); + registry = regMgr.registry; + RefreshList(); + } else if (!registry.Repositories.DictionaryEquals(prevRepos)) { + // Repos changed, need to fetch them + UpdateRegistry(false); + RefreshList(); + } else if (!manager.CurrentInstance.VersionCriteria().Equals(prevVerCrit)) { + // VersionCriteria changed, need to re-check what is compatible + RefreshList(); + } } return true; } - private bool EditAuthTokens(ConsoleTheme theme) + private bool EditAuthTokens() { - LaunchSubScreen(theme, new AuthTokenScreen()); + LaunchSubScreen(new AuthTokenScreen(theme)); return true; } - private bool EditInstallFilters(ConsoleTheme theme) + private bool EditInstallFilters() { - LaunchSubScreen(theme, new InstallFiltersScreen( - ServiceLocator.Container.Resolve(), - manager.CurrentInstance - )); + if (manager.CurrentInstance != null) + { + LaunchSubScreen(new InstallFiltersScreen( + theme, + ServiceLocator.Container.Resolve(), + manager.CurrentInstance)); + } return true; } - private void RefreshList(ConsoleTheme theme) + private void RefreshList() { // In the constructor this is called while moduleList is being populated, just do nothing in this case. // ModListScreen -> moduleList = (GetAllMods ...) -> UpdateRegistry -> RefreshList - moduleList?.SetData(GetAllMods(theme, true)); + moduleList?.SetData(GetAllMods(true)); } - private List allMods = null; + private List? allMods = null; - private List GetAllMods(ConsoleTheme theme, bool force = false) + private List GetAllMods(bool force = false) { - timeSinceUpdate = repoData.LastUpdate(registry.Repositories.Values); - ScanForMods(); - if (allMods == null || force) { - if (!registry?.HasAnyAvailable() ?? false) - { - UpdateRegistry(theme, false); - } - var crit = manager.CurrentInstance.VersionCriteria(); - allMods = new List(registry.CompatibleModules(crit)); - foreach (InstalledModule im in registry.InstalledModules) { - var m = Utilities.DefaultIfThrows(() => registry.LatestAvailable(im.identifier, crit)); - if (m == null) { - // Add unavailable installed mods to the list - allMods.Add(im.Module); + if (manager.CurrentInstance != null) + { + timeSinceUpdate = repoData.LastUpdate(registry.Repositories.Values); + ScanForMods(); + if (allMods == null || force) { + if (!registry.HasAnyAvailable()) + { + UpdateRegistry(false); + } + var crit = manager.CurrentInstance.VersionCriteria(); + allMods = new List(registry.CompatibleModules(crit)); + foreach (InstalledModule im in registry.InstalledModules) { + var m = Utilities.DefaultIfThrows(() => registry.LatestAvailable(im.identifier, crit)); + if (m == null) { + // Add unavailable installed mods to the list + allMods.Add(im.Module); + } } + upgradeableGroups = registry + .CheckUpgradeable(manager.CurrentInstance, new HashSet()); } - upgradeableGroups = registry - .CheckUpgradeable(manager.CurrentInstance, new HashSet()); + return allMods; } - return allMods; + return new List(); } - private bool ExportInstalled(ConsoleTheme theme) + private bool ExportInstalled() { try { - // Save the mod list as "depends" without the installed versions. - // Because that's supposed to work. - regMgr.Save(true); - string path = Path.Combine( - Platform.FormatPath(manager.CurrentInstance.CkanDir()), - $"{Properties.Resources.ModListExportPrefix}-{manager.CurrentInstance.Name}.ckan" - ); - RaiseError(Properties.Resources.ModListExported, path); + if (manager.CurrentInstance != null) + { + // Save the mod list as "depends" without the installed versions. + // Because that's supposed to work. + regMgr.Save(true); + string path = Path.Combine( + Platform.FormatPath(manager.CurrentInstance.CkanDir()), + $"{Properties.Resources.ModListExportPrefix}-{manager.CurrentInstance.Name}.ckan"); + RaiseError(Properties.Resources.ModListExported, path); + } } catch (Exception ex) { RaiseError(Properties.Resources.ModListExportFailed, ex.Message); } return true; } - private bool InstallFromCkan(ConsoleTheme theme) + private bool InstallFromCkan() { - var modules = InstallFromCkanDialog.ChooseCkanFiles(theme, manager.CurrentInstance); - if (modules.Length > 0) { - var crit = manager.CurrentInstance.VersionCriteria(); - var installed = regMgr.registry.InstalledModules.Select(inst => inst.Module).ToList(); - var cp = new ChangePlan(); - cp.Install.UnionWith( - modules.Concat( - modules.Where(m => m.IsMetapackage && m.depends != null) - .SelectMany(m => m.depends.Where(rel => !rel.MatchesAny(installed, null, null)) - .Select(rel => - // If there's a compatible match, return it - // Metapackages aren't intending to prompt users to choose providing mods - rel.ExactMatch(regMgr.registry, crit, installed, modules) - // Otherwise look for incompatible - ?? rel.ExactMatch(regMgr.registry, null, installed, modules)) - .Where(mod => mod != null)))); - LaunchSubScreen(theme, new InstallScreen(manager, repoData, cp, debug)); - RefreshList(theme); + if (manager.CurrentInstance != null) + { + var modules = InstallFromCkanDialog.ChooseCkanFiles(theme, manager.CurrentInstance); + if (modules.Length > 0) { + var crit = manager.CurrentInstance.VersionCriteria(); + var installed = regMgr.registry.InstalledModules.Select(inst => inst.Module).ToList(); + var cp = new ChangePlan(); + cp.Install.UnionWith( + modules.Concat( + modules.Where(m => m.IsMetapackage && m.depends != null) + .SelectMany(m => m.depends?.Where(rel => !rel.MatchesAny(installed, null, null)) + .Select(rel => + // If there's a compatible match, return it + // Metapackages aren't intending to prompt users to choose providing mods + rel.ExactMatch(regMgr.registry, crit, installed, modules) + // Otherwise look for incompatible + ?? rel.ExactMatch(regMgr.registry, null, installed, modules)) + .OfType() + ?? Enumerable.Empty()))); + LaunchSubScreen(new InstallScreen(theme, manager, repoData, cp, debug)); + RefreshList(); + } } return true; } - private bool Help(ConsoleTheme theme) + private bool Help() { - ModListHelpDialog hd = new ModListHelpDialog(); - hd.Run(theme); - DrawBackground(theme); + ModListHelpDialog hd = new ModListHelpDialog(theme); + hd.Run(); + DrawBackground(); return true; } - private bool ApplyChanges(ConsoleTheme theme) + private bool ApplyChanges() { if (plan.NonEmpty()) { - LaunchSubScreen(theme, new InstallScreen(manager, repoData, plan, debug)); - RefreshList(theme); + LaunchSubScreen(new InstallScreen(theme, manager, repoData, plan, debug)); + RefreshList(); } return true; } @@ -670,10 +698,8 @@ private bool ApplyChanges(ConsoleTheme theme) /// String containing symbol to use /// public string StatusSymbol(CkanModule m) - { - return StatusSymbol(plan.GetModStatus(manager, registry, m.identifier, - upgradeableGroups?[true] ?? new List())); - } + => StatusSymbol(plan.GetModStatus(manager, registry, m.identifier, + upgradeableGroups?[true] ?? new List())); /// /// Return the symbol to use to represent a mod's StatusSymbol. @@ -712,13 +738,13 @@ private long totalInstalledDownloadSize() return total; } - private readonly GameInstanceManager manager; - private RegistryManager regMgr; - private Registry registry; - private readonly RepositoryDataManager repoData; - private Dictionary> upgradeableGroups; - private readonly bool debug; - private TimeSpan timeSinceUpdate = TimeSpan.Zero; + private readonly GameInstanceManager manager; + private RegistryManager regMgr; + private Registry registry; + private readonly RepositoryDataManager repoData; + private Dictionary>? upgradeableGroups; + private readonly bool debug; + private TimeSpan timeSinceUpdate = TimeSpan.Zero; private readonly ConsoleField searchBox; private readonly ConsoleListBox moduleList; @@ -846,7 +872,8 @@ public InstallStatus GetModStatus(GameInstanceManager manager, return InstallStatus.AutoDetected; } else if (Replace.Contains(identifier)) { return InstallStatus.Replacing; - } else if (registry.GetReplacement(identifier, manager.CurrentInstance.VersionCriteria()) != null) { + } else if (manager.CurrentInstance != null + && registry.GetReplacement(identifier, manager.CurrentInstance.VersionCriteria()) != null) { return InstallStatus.Replaceable; } else if (!IsAnyAvailable(registry, identifier)) { return InstallStatus.Unavailable; diff --git a/ConsoleUI/Program.cs b/ConsoleUI/Program.cs index da14b854ed..d30504353f 100644 --- a/ConsoleUI/Program.cs +++ b/ConsoleUI/Program.cs @@ -31,7 +31,7 @@ public static void Main(string[] args) /// /// Process exit status /// - public static int Main_(GameInstanceManager manager, string themeName, bool debug = false) + public static int Main_(GameInstanceManager? manager, string? themeName, bool debug = false) { Logging.Initialize(); diff --git a/ConsoleUI/ProgressScreen.cs b/ConsoleUI/ProgressScreen.cs index f684f03f4c..54f1f65e56 100644 --- a/ConsoleUI/ProgressScreen.cs +++ b/ConsoleUI/ProgressScreen.cs @@ -15,9 +15,11 @@ public class ProgressScreen : ConsoleScreen { /// /// Initialize the Screen /// + /// The visual theme to use to draw the dialog /// Description of the task being done for the header /// Starting string to put in the progress bar - public ProgressScreen(string descrip, string initMsg = "") + public ProgressScreen(ConsoleTheme theme, string descrip, string initMsg = "") + : base(theme) { // A nice frame to take up some of the blank space at the top AddObject(new ConsoleDoubleFrame( @@ -25,16 +27,13 @@ public ProgressScreen(string descrip, string initMsg = "") () => Properties.Resources.ProgressTitle, () => Properties.Resources.ProgressMessages, // Cheating because our IUser handler needs a theme context - th => { yesNoTheme = th; return th.NormalFrameFg; } - )); + th => th.NormalFrameFg)); progress = new ConsoleProgressBar( 3, 5, -3, () => topMessage, - () => percent - ); + () => percent); messages = new ConsoleTextBox( - 3, 10, -3, -3 - ); + 3, 10, -3, -3); AddObject(progress); AddObject(messages); @@ -47,17 +46,13 @@ public ProgressScreen(string descrip, string initMsg = "") /// Put CKAN 1.25.5 in upper left corner /// protected override string LeftHeader() - { - return $"{Meta.GetProductName()} {Meta.GetVersion()}"; - } + => $"{Meta.GetProductName()} {Meta.GetVersion()}"; /// /// Put description of what we're doing in top center /// protected override string CenterHeader() - { - return taskDescription; - } + => taskDescription; // IUser stuff for managing the progress bar and message box @@ -72,7 +67,8 @@ public override bool RaiseYesNoDialog(string question) { // Show the popup at the top of the screen // to overwrite the progress bar instead of the messages - ConsoleMessageDialog d = new ConsoleMessageDialog( + var d = new ConsoleMessageDialog( + theme, // The installer's questions include embedded newlines for spacing in CmdLine question.Trim(), new List() { @@ -83,22 +79,22 @@ public override bool RaiseYesNoDialog(string question) TextAlign.Center, -Console.WindowHeight / 2 ); - d.AddBinding(Keys.Y, (object sender, ConsoleTheme theme) => { + d.AddBinding(Keys.Y, (object sender) => { d.PressButton(0); return false; }); - d.AddBinding(Keys.N, (object sender, ConsoleTheme theme) => { + d.AddBinding(Keys.N, (object sender) => { d.PressButton(1); return false; }); // Scroll messages d.AddTip(Properties.Resources.CursorKeys, Properties.Resources.ScrollMessages); - messages.AddScrollBindings(d, true); + messages.AddScrollBindings(d, theme, true); - bool val = d.Run(yesNoTheme) == 0; - DrawBackground(yesNoTheme); - Draw(yesNoTheme); + bool val = d.Run() == 0; + DrawBackground(); + Draw(); return val; } @@ -126,8 +122,6 @@ protected override void Progress(string message, int percent) private readonly ConsoleProgressBar progress; private readonly ConsoleTextBox messages; - private ConsoleTheme yesNoTheme; - private string topMessage = ""; private readonly string taskDescription; private double percent = 0; diff --git a/ConsoleUI/RepoAddScreen.cs b/ConsoleUI/RepoAddScreen.cs index 90e79c5c14..ab83a15b26 100644 --- a/ConsoleUI/RepoAddScreen.cs +++ b/ConsoleUI/RepoAddScreen.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; - +using CKAN.ConsoleUI.Toolkit; using CKAN.Games; namespace CKAN.ConsoleUI { @@ -12,10 +12,11 @@ public class RepoAddScreen : RepoScreen { /// /// Construct the screen /// + /// The visual theme to use to draw the dialog /// Game from which to get repos /// Collection of Repository objects - public RepoAddScreen(IGame game, SortedDictionary reps) - : base(game, reps, "", "") { } + public RepoAddScreen(ConsoleTheme theme, IGame game, SortedDictionary reps) + : base(theme, game, reps, "", "") { } /// /// Check whether the fields are valid diff --git a/ConsoleUI/RepoEditScreen.cs b/ConsoleUI/RepoEditScreen.cs index 32349fab2d..288f199b43 100644 --- a/ConsoleUI/RepoEditScreen.cs +++ b/ConsoleUI/RepoEditScreen.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using CKAN.ConsoleUI.Toolkit; using CKAN.Games; namespace CKAN.ConsoleUI { @@ -12,11 +13,12 @@ public class RepoEditScreen : RepoScreen { /// /// Construct the Screen /// + /// The visual theme to use to draw the dialog /// Game from which to get repos /// Collection of Repository objects /// The object to edit - public RepoEditScreen(IGame game, SortedDictionary reps, Repository repo) - : base(game, reps, repo.name, repo.uri.ToString()) + public RepoEditScreen(ConsoleTheme theme, IGame game, SortedDictionary reps, Repository repo) + : base(theme, game, reps, repo.name, repo.uri.ToString()) { repository = repo; } diff --git a/ConsoleUI/RepoScreen.cs b/ConsoleUI/RepoScreen.cs index 601dbb7bd3..5739423d37 100644 --- a/ConsoleUI/RepoScreen.cs +++ b/ConsoleUI/RepoScreen.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; - +using System.Linq; using CKAN.ConsoleUI.Toolkit; using CKAN.Games; @@ -14,14 +14,15 @@ public abstract class RepoScreen : ConsoleScreen { /// /// Construct the screens /// + /// The visual theme to use to draw the dialog /// Game from which to get repos /// Collection of Repository objects /// Initial value of the Name field /// Iniital value of the URL field - protected RepoScreen(IGame game, SortedDictionary reps, string initName, string initUrl) : base() + protected RepoScreen(ConsoleTheme theme, IGame game, SortedDictionary reps, string initName, string initUrl) + : base(theme) { editList = reps; - defaultRepos = RepositoryList.DefaultRepositories(game); name = new ConsoleField(labelWidth, nameRow, -1, initName) { GhostText = () => Properties.Resources.RepoNameGhostText @@ -36,7 +37,7 @@ protected RepoScreen(IGame game, SortedDictionary reps, stri AddObject(url); AddTip("F2", Properties.Resources.Accept); - AddBinding(Keys.F2, (object sender, ConsoleTheme theme) => { + AddBinding(Keys.F2, (object sender) => { if (Valid()) { Save(); return false; @@ -46,46 +47,40 @@ protected RepoScreen(IGame game, SortedDictionary reps, stri }); AddTip(Properties.Resources.Esc, Properties.Resources.Cancel); - AddBinding(Keys.Escape, (object sender, ConsoleTheme theme) => { - return false; - }); + AddBinding(Keys.Escape, (object sender) => false); // mainMenu = list of default options - if (defaultRepos.repositories != null && defaultRepos.repositories.Length > 0) { - List opts = new List(); - foreach (Repository r in defaultRepos.repositories) { - // This variable will be remembered correctly in our lambdas later - Repository repo = r; - opts.Add(new ConsoleMenuOption( - repo.name, "", string.Format(Properties.Resources.RepoImportTip, repo.name), - true, (ConsoleTheme theme) => { - name.Value = repo.name; - name.Position = name.Value.Length; - url.Value = repo.uri.ToString(); - url.Position = url.Value.Length; - return true; - } - )); - } - mainMenu = new ConsolePopupMenu(opts); - } + mainMenu = (RepositoryList.DefaultRepositories(game) is RepositoryList repoList) + ? new ConsolePopupMenu( + repoList.repositories + .Select(r => new ConsoleMenuOption(r.name, + "", + string.Format(Properties.Resources.RepoImportTip, + r.name), + true, + () => { + name.Value = r.name; + name.Position = name.Value.Length; + url.Value = r.uri.ToString(); + url.Position = url.Value.Length; + return true; + })) + .OfType() + .ToList()) + : null; } /// /// Put CKAN 1.25.5 in top left corner /// protected override string LeftHeader() - { - return $"{Meta.GetProductName()} {Meta.GetVersion()}"; - } + => $"{Meta.GetProductName()} {Meta.GetVersion()}"; /// /// Put description in top center /// protected override string CenterHeader() - { - return Properties.Resources.RepoTitle; - } + => Properties.Resources.RepoTitle; /// /// Report whether the fields are Valid @@ -153,12 +148,9 @@ protected bool urlValid() /// protected SortedDictionary editList; - private RepositoryList defaultRepos; - private int labelWidth => Math.Max(8, Math.Max( Properties.Resources.RepoNameLabel.Length, - Properties.Resources.RepoURLLabel.Length - )); + Properties.Resources.RepoURLLabel.Length)); private const int nameRow = 3; private const int urlRow = 5; } diff --git a/ConsoleUI/SingleAssemblyResourceManager.cs b/ConsoleUI/SingleAssemblyResourceManager.cs index ecdf62c50d..9fa6456806 100644 --- a/ConsoleUI/SingleAssemblyResourceManager.cs +++ b/ConsoleUI/SingleAssemblyResourceManager.cs @@ -1,4 +1,3 @@ -using System.IO; using System.Globalization; using System.Resources; using System.Reflection; @@ -31,16 +30,13 @@ public SingleAssemblyResourceManager(string basename, Assembly assembly) /// Set to false to avoid loading if not already cached /// Just gets passed to base class implementation /// - protected override ResourceSet InternalGetResourceSet(CultureInfo culture, + protected override ResourceSet? InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents) { - if (!myResourceSets.TryGetValue(culture, out ResourceSet rs) && createIfNotExists) + if (!myResourceSets.TryGetValue(culture, out ResourceSet? rs) && createIfNotExists && MainAssembly != null) { // Lazy-load default language (without caring about duplicate assignment in race conditions, no harm done) - if (neutralResourcesCulture == null) - { - neutralResourcesCulture = GetNeutralResourcesLanguage(MainAssembly); - } + neutralResourcesCulture ??= GetNeutralResourcesLanguage(MainAssembly); // If we're asking for the default language, then ask for the // invariant (non-specific) resources. @@ -50,7 +46,7 @@ protected override ResourceSet InternalGetResourceSet(CultureInfo culture, } string resourceFileName = GetResourceFileName(culture); - Stream store = MainAssembly.GetManifestResourceStream(resourceFileName); + var store = MainAssembly.GetManifestResourceStream(resourceFileName); // If we found the appropriate resources in the local assembly if (store != null) @@ -67,7 +63,7 @@ protected override ResourceSet InternalGetResourceSet(CultureInfo culture, return rs; } - private CultureInfo neutralResourcesCulture; + private CultureInfo? neutralResourcesCulture; private readonly Dictionary myResourceSets = new Dictionary(); } } diff --git a/ConsoleUI/SplashScreen.cs b/ConsoleUI/SplashScreen.cs index 4e56aa11ea..d76f0a01cb 100644 --- a/ConsoleUI/SplashScreen.cs +++ b/ConsoleUI/SplashScreen.cs @@ -28,7 +28,7 @@ public SplashScreen(GameInstanceManager mgr, RepositoryDataManager repoData) public bool Run(ConsoleTheme theme) { // If there's a default instance, try to get the lock for it. - GameInstance ksp = manager.CurrentInstance ?? manager.GetPreferredInstance(); + GameInstance? ksp = manager.CurrentInstance ?? manager.GetPreferredInstance(); if (ksp != null && !GameInstanceListScreen.TryGetInstance(theme, ksp, repoData, (ConsoleTheme th) => Draw(th, false), diff --git a/ConsoleUI/Toolkit/ConsoleChoiceDialog.cs b/ConsoleUI/Toolkit/ConsoleChoiceDialog.cs index 28c5170bd9..6c38d78e7d 100644 --- a/ConsoleUI/Toolkit/ConsoleChoiceDialog.cs +++ b/ConsoleUI/Toolkit/ConsoleChoiceDialog.cs @@ -12,13 +12,14 @@ public class ConsoleChoiceDialog : ConsoleDialog { /// /// Initialize the Dialog /// + /// The visual theme to use to draw the dialog /// Message to show /// Text for column header of list box /// List of objects to put in the list box /// Function to generate text for each option /// Optional function to sort the rows - public ConsoleChoiceDialog(string m, string hdr, List c, Func renderer, Comparison comparer = null) - : base() + public ConsoleChoiceDialog(ConsoleTheme theme, string m, string hdr, List c, Func renderer, Comparison? comparer = null) + : base(theme) { int l = GetLeft(), r = GetRight(); @@ -34,7 +35,7 @@ public ConsoleChoiceDialog(string m, string hdr, List c, Func c, Func>() { - new ConsoleListBoxColumn() { - Header = hdr, - Width = null, - Renderer = renderer, - Comparer = comparer - } + new ConsoleListBoxColumn(hdr, renderer, comparer, null) }, 0, 0, ListSortDirection.Ascending ); choices.AddTip(Properties.Resources.Enter, Properties.Resources.Accept); - choices.AddBinding(Keys.Enter, (object sender, ConsoleTheme theme) => { + choices.AddBinding(Keys.Enter, (object sender) => { return false; }); choices.AddTip(Properties.Resources.Esc, Properties.Resources.Cancel); - choices.AddBinding(Keys.Escape, (object sender, ConsoleTheme theme) => { + choices.AddBinding(Keys.Escape, (object sender) => { cancelled = true; return false; }); @@ -76,14 +72,13 @@ public ConsoleChoiceDialog(string m, string hdr, List c, Func /// Display the dialog and handle its interaction /// - /// The visual theme to use to draw the dialog /// Function to control the dialog, default is normal user interaction /// /// Row user selected /// - public new ChoiceT Run(ConsoleTheme theme, Action process = null) + public new ChoiceT? Run(Action? process = null) { - base.Run(theme, process); + base.Run(process); return cancelled ? default : choices.Selection; } diff --git a/ConsoleUI/Toolkit/ConsoleDialog.cs b/ConsoleUI/Toolkit/ConsoleDialog.cs index 6bbd02b539..914c77d28a 100644 --- a/ConsoleUI/Toolkit/ConsoleDialog.cs +++ b/ConsoleUI/Toolkit/ConsoleDialog.cs @@ -11,7 +11,9 @@ public abstract class ConsoleDialog : ScreenContainer { /// Initialize the dialog. /// By default sets size and position to middle 50%. /// - protected ConsoleDialog() + /// The visual theme to use to draw the dialog + protected ConsoleDialog(ConsoleTheme theme) + : base(theme) { left = Console.WindowWidth / 4; top = Console.WindowHeight / 4; @@ -63,19 +65,19 @@ public static void DrawShadow(ConsoleTheme theme, int l, int t, int r, int b) /// /// X coordinate of left edge of dialog /// - protected int GetLeft() { return Formatting.ConvertCoord(left, Console.WindowWidth); } + protected int GetLeft() => Formatting.ConvertCoord(left, Console.WindowWidth); /// /// Y coordinate of top edge of dialog /// - protected int GetTop() { return Formatting.ConvertCoord(top, Console.WindowHeight); } + protected int GetTop() => Formatting.ConvertCoord(top, Console.WindowHeight); /// /// X coordinate of right edge of dialog /// - protected int GetRight() { return Formatting.ConvertCoord(right, Console.WindowWidth); } + protected int GetRight() => Formatting.ConvertCoord(right, Console.WindowWidth); /// /// Y coordinate of bottom edge of dialog /// - protected int GetBottom() { return Formatting.ConvertCoord(bottom, Console.WindowHeight); } + protected int GetBottom() => Formatting.ConvertCoord(bottom, Console.WindowHeight); /// /// Set position of dialog @@ -95,7 +97,7 @@ protected void SetDimensions(int l, int t, int r, int b) /// /// Draw the outline of the dialog and clear the footer /// - protected override void DrawBackground(ConsoleTheme theme) + protected override void DrawBackground() { int w = GetRight() - GetLeft() + 1; string fullHorizLineDouble = new string(Symbols.horizLineDouble, w - 2); diff --git a/ConsoleUI/Toolkit/ConsoleDoubleFrame.cs b/ConsoleUI/Toolkit/ConsoleDoubleFrame.cs index f77d3e8e94..b0993479ba 100644 --- a/ConsoleUI/Toolkit/ConsoleDoubleFrame.cs +++ b/ConsoleUI/Toolkit/ConsoleDoubleFrame.cs @@ -76,7 +76,7 @@ private void writeTitleRow(string title, int w) if (leftSidePad < 0 || rightSidePad < 0) { leftSidePad = 0; rightSidePad = 0; - title = title.Substring(0, w - 4); + title = title[..(w - 4)]; } Console.Write(new string(doubleBorder ? Symbols.horizLineDouble : Symbols.horizLine, leftSidePad)); Console.Write($" {title} "); diff --git a/ConsoleUI/Toolkit/ConsoleField.cs b/ConsoleUI/Toolkit/ConsoleField.cs index 885030eb8e..109b4983da 100644 --- a/ConsoleUI/Toolkit/ConsoleField.cs +++ b/ConsoleUI/Toolkit/ConsoleField.cs @@ -45,7 +45,7 @@ public ConsoleField(int l, int t, int r, string val = "") /// /// Event to notify that the text has changed /// - public event ChangeListener OnChange; + public event ChangeListener? OnChange; private void Changed() { OnChange?.Invoke(this, Value); @@ -89,7 +89,7 @@ public override void Draw(ConsoleTheme theme, bool focused) Console.ForegroundColor = focused ? theme.FieldFocusedFg : theme.FieldBlurredFg; - Console.Write(FormatExactWidth(Value.Substring(leftPos), w)); + Console.Write(FormatExactWidth(Value[leftPos..], w)); } } @@ -110,11 +110,11 @@ public override void OnKeyPress(ConsoleKeyInfo k) if (!k.Modifiers.HasFlag(ConsoleModifiers.Control)) { if (Position > 0) { --Position; - Value = Value.Substring(0, Position) + Value.Substring(Position + 1); + Value = Value[..Position] + Value[(Position + 1)..]; Changed(); } } else if (!string.IsNullOrEmpty(Value)) { - Value = Value.Substring(Position); + Value = Value[Position..]; Position = 0; Changed(); } @@ -122,8 +122,8 @@ public override void OnKeyPress(ConsoleKeyInfo k) case ConsoleKey.Delete: if (Position < Value.Length) { Value = k.Modifiers.HasFlag(ConsoleModifiers.Control) - ? Value.Substring(0, Position) - : Value.Substring(0, Position) + Value.Substring(Position + 1); + ? Value[..Position] + : Value[..Position] + Value[(Position + 1)..]; Changed(); } break; @@ -155,7 +155,7 @@ public override void OnKeyPress(ConsoleKeyInfo k) default: if (!char.IsControl(k.KeyChar)) { if (Position < Value.Length) { - Value = Value.Substring(0, Position) + k.KeyChar + Value.Substring(Position); + Value = Value[..Position] + k.KeyChar + Value[Position..]; } else { Value += k.KeyChar; } diff --git a/ConsoleUI/Toolkit/ConsoleFileMultiSelectDialog.cs b/ConsoleUI/Toolkit/ConsoleFileMultiSelectDialog.cs index 613dc99583..20ed445d17 100644 --- a/ConsoleUI/Toolkit/ConsoleFileMultiSelectDialog.cs +++ b/ConsoleUI/Toolkit/ConsoleFileMultiSelectDialog.cs @@ -2,6 +2,7 @@ using System.IO; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; namespace CKAN.ConsoleUI.Toolkit { @@ -13,17 +14,19 @@ public class ConsoleFileMultiSelectDialog : ConsoleDialog { /// /// Initialize the popup. /// + /// The visual theme to use to draw the dialog /// String to be shown in the top center of the popup /// Path of directory to start in /// Glob-style wildcard string for matching files to show /// Header for the column with checkmarks for selected files /// Description of the F9 action to accept selections - public ConsoleFileMultiSelectDialog(string title, + public ConsoleFileMultiSelectDialog(ConsoleTheme theme, + string title, string startPath, string filPat, string toggleHeader, string acceptTip) - : base() + : base(theme) { CenterHeader = () => title; curDir = new DirectoryInfo(startPath); @@ -63,57 +66,51 @@ public ConsoleFileMultiSelectDialog(string title, left + 2, top + 4, right - 2, bottom - 2, getFileList(), new List>() { - new ConsoleListBoxColumn() { - Header = toggleHeader, - Width = 8, - Renderer = getRowSymbol - }, new ConsoleListBoxColumn() { - Header = Properties.Resources.FileSelectNameHeader, - Width = null, - Renderer = getRowName, - Comparer = compareNames - }, new ConsoleListBoxColumn() { - Header = Properties.Resources.FileSelectSizeHeader, - // Longest: "1023.1 KB" - Width = 9, - Renderer = (FileSystemInfo fi) => getLength(fi), - Comparer = (a, b) => { - FileInfo fb = b as FileInfo; - return !(a is FileInfo fa) + new ConsoleListBoxColumn( + toggleHeader, getRowSymbol, null, 8), + new ConsoleListBoxColumn( + Properties.Resources.FileSelectNameHeader, + getRowName, compareNames, null), + new ConsoleListBoxColumn( + Properties.Resources.FileSelectSizeHeader, + (FileSystemInfo fi) => getLength(fi), + (a, b) => { + var fb = b as FileInfo; + return a is not FileInfo fa ? (fb == null ? 0 : -1) : (fb == null ? 1 : fa.Length.CompareTo(fb.Length)); - } - }, new ConsoleListBoxColumn() { - Header = Properties.Resources.FileSelectTimestampHeader, - Width = 10, - Renderer = (FileSystemInfo fi) => fi.LastWriteTime.ToString("yyyy-MM-dd"), - Comparer = (a, b) => a.LastWriteTime.CompareTo(b.LastWriteTime) - } + }, + 9), + new ConsoleListBoxColumn( + Properties.Resources.FileSelectTimestampHeader, + (FileSystemInfo fi) => fi.LastWriteTime.ToString("yyyy-MM-dd"), + (a, b) => a.LastWriteTime.CompareTo(b.LastWriteTime), + 10) }, 1, 1, ListSortDirection.Ascending ); AddObject(fileList); AddTip(Properties.Resources.Esc, Properties.Resources.Cancel); - AddBinding(Keys.Escape, (object sender, ConsoleTheme theme) => { + AddBinding(Keys.Escape, (object sender) => { chosenFiles.Clear(); return false; }); AddTip("F10", Properties.Resources.Sort); - AddBinding(Keys.F10, (object sender, ConsoleTheme theme) => { + AddBinding(Keys.F10, (object sender) => { fileList.SortMenu().Run(theme, right - 2, top + 2); - DrawBackground(theme); + DrawBackground(); return true; }); AddTip(Properties.Resources.Enter, Properties.Resources.FileSelectChangeDirectory, () => fileList.Selection != null && isDir(fileList.Selection)); AddTip(Properties.Resources.Enter, Properties.Resources.FileSelectSelect, () => fileList.Selection != null && !isDir(fileList.Selection)); - AddBinding(Keys.Enter, (object sender, ConsoleTheme theme) => selectRow()); - AddBinding(Keys.Space, (object sender, ConsoleTheme theme) => selectRow()); + AddBinding(Keys.Enter, (object sender) => selectRow()); + AddBinding(Keys.Space, (object sender) => selectRow()); AddTip($"{Properties.Resources.Ctrl}+A", Properties.Resources.SelectAll); - AddBinding(Keys.CtrlA, (object sender, ConsoleTheme theme) => { + AddBinding(Keys.CtrlA, (object sender) => { foreach (FileSystemInfo fi in contents) { if (!isDir(fi)) { if (fi is FileInfo file) @@ -126,7 +123,7 @@ public ConsoleFileMultiSelectDialog(string title, }); AddTip($"{Properties.Resources.Ctrl}+D", Properties.Resources.DeselectAll, () => chosenFiles.Count > 0); - AddBinding(Keys.CtrlD, (object sender, ConsoleTheme theme) => { + AddBinding(Keys.CtrlD, (object sender) => { if (chosenFiles.Count > 0) { chosenFiles.Clear(); } @@ -134,12 +131,12 @@ public ConsoleFileMultiSelectDialog(string title, }); AddTip("F9", acceptTip, () => chosenFiles.Count > 0); - AddBinding(Keys.F9, (object sender, ConsoleTheme theme) => false); + AddBinding(Keys.F9, (object sender) => false); } private bool selectRow() { - if (isDir(fileList.Selection)) { + if (fileList.Selection != null && isDir(fileList.Selection)) { if (fileList.Selection is DirectoryInfo di) { curDir = di; @@ -176,17 +173,17 @@ private void pathFieldChanged(ConsoleField sender, string newValue) /// /// Display the dialog and handle its interaction /// - /// The visual theme to use to draw the dialog /// Function to control the dialog, default is normal user interaction /// /// Files user selected /// - public new HashSet Run(ConsoleTheme theme, Action process = null) + public new HashSet Run(Action? process = null) { - base.Run(theme, process); + base.Run(process); return chosenFiles; } + [MemberNotNull(nameof(contents))] private IList getFileList() { contents = new List(); @@ -210,7 +207,7 @@ private static bool isDir(FileSystemInfo fi) /// /// True if they're the same file/directory, false otherwise. /// - private static bool pathEquals(FileSystemInfo a, FileSystemInfo b) + private static bool pathEquals(FileSystemInfo? a, FileSystemInfo? b) { if (a == null || b == null) { return false; @@ -247,7 +244,9 @@ private long totalChosenSize() } private string getRowSymbol(FileSystemInfo fi) - => !isDir(fi) && chosenFiles.Contains(fi as FileInfo) + => !isDir(fi) + && fi is FileInfo file + && chosenFiles.Contains(file) ? chosen : ""; diff --git a/ConsoleUI/Toolkit/ConsoleFrame.cs b/ConsoleUI/Toolkit/ConsoleFrame.cs index 15a9c15c02..c13c9716e8 100644 --- a/ConsoleUI/Toolkit/ConsoleFrame.cs +++ b/ConsoleUI/Toolkit/ConsoleFrame.cs @@ -47,7 +47,7 @@ public override void Draw(ConsoleTheme theme, bool focused) if (topLeftSidePad < 0 || topRightSidePad < 0) { topLeftSidePad = 0; topRightSidePad = 0; - title = title.Substring(0, w - 4); + title = title[..(w - 4)]; } Console.Write(new string(doubleBorder ? Symbols.horizLineDouble : Symbols.horizLine, topLeftSidePad)); Console.Write($" {title} "); diff --git a/ConsoleUI/Toolkit/ConsoleLabel.cs b/ConsoleUI/Toolkit/ConsoleLabel.cs index ebbb9e95ac..f4131f6f12 100644 --- a/ConsoleUI/Toolkit/ConsoleLabel.cs +++ b/ConsoleUI/Toolkit/ConsoleLabel.cs @@ -16,7 +16,12 @@ public class ConsoleLabel : ScreenObject { /// Function returning the text to show in the label /// Function returning the background color for the label /// Function returning the foreground color for the label - public ConsoleLabel(int l, int t, int r, Func lf, Func bgFunc = null, Func fgFunc = null) + public ConsoleLabel(int l, + int t, + int r, + Func lf, + Func? bgFunc = null, + Func? fgFunc = null) : base(l, t, r, t) { labelFunc = lf; @@ -48,8 +53,8 @@ public override void Draw(ConsoleTheme theme, bool focused) public override bool Focusable() { return false; } private readonly Func labelFunc; - private readonly Func getBgColor; - private readonly Func getFgColor; + private readonly Func? getBgColor; + private readonly Func? getFgColor; } } diff --git a/ConsoleUI/Toolkit/ConsoleListBox.cs b/ConsoleUI/Toolkit/ConsoleListBox.cs index c19c628dce..347fb99673 100644 --- a/ConsoleUI/Toolkit/ConsoleListBox.cs +++ b/ConsoleUI/Toolkit/ConsoleListBox.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Collections.Generic; using System.Linq; +using System.Diagnostics.CodeAnalysis; namespace CKAN.ConsoleUI.Toolkit { @@ -30,7 +31,7 @@ public ConsoleListBox(int l, int t, int r, int b, int dfltSortCol, int initialSortCol = 0, ListSortDirection initialSortDir = ListSortDirection.Ascending, - Func filt = null) + Func? filt = null) : base(l, t, r, b) { data = dataList; @@ -47,7 +48,7 @@ public ConsoleListBox(int l, int t, int r, int b, /// /// Fired when the user changes the selection with the arrow keys /// - public event Action SelectionChanged; + public event Action? SelectionChanged; /// /// Set which column to sort by @@ -88,15 +89,15 @@ public string FilterString { /// /// Currently selected row's object /// - public RowT Selection - => selectedRow >= 0 && selectedRow < (sortedFilteredData?.Count ?? 0) + public RowT? Selection + => selectedRow >= 0 && selectedRow < sortedFilteredData.Count ? sortedFilteredData[selectedRow] : default; /// /// Return the number of rows shown in the box /// - public int VisibleRowCount() => sortedFilteredData?.Count ?? 0; + public int VisibleRowCount() => sortedFilteredData.Count; /// /// Draw the list box @@ -311,12 +312,12 @@ public override void PlaceCursor() public ConsolePopupMenu SortMenu() { if (sortMenu == null) { - List opts = new List() { + var opts = new List() { new ConsoleMenuOption( Properties.Resources.Ascending, "", Properties.Resources.AscendingSortTip, true, - (ConsoleTheme theme) => { + () => { SortDirection = ListSortDirection.Ascending; return true; }, @@ -326,7 +327,7 @@ public ConsolePopupMenu SortMenu() Properties.Resources.Descending, "", Properties.Resources.DescendingSortTip, true, - (ConsoleTheme theme) => { + () => { SortDirection = ListSortDirection.Descending; return true; }, @@ -346,7 +347,7 @@ public ConsolePopupMenu SortMenu() ? string.Format(Properties.Resources.ColumnNumberSortTip, i + 1) : string.Format(Properties.Resources.ColumnNameSortTip, columns[i].Header), true, - (ConsoleTheme theme) => { + () => { SortColumnIndex = newIndex; return true; }, @@ -380,10 +381,11 @@ private string FmtHdr(int colIndex, int w) w) : FormatExactWidth(columns[colIndex].Header, w); + [MemberNotNull(nameof(sortedFilteredData))] private void filterAndSort() { // Keep the same row highlighted when the number of rows changes - RowT oldSelect = Selection; + var oldSelect = Selection; sortedFilteredData = string.IsNullOrEmpty(filterStr) || filterCheck == null ? new List(data) @@ -402,9 +404,12 @@ private void filterAndSort() () => dfltCol(a, b) )); } - int newSelRow = sortedFilteredData.IndexOf(oldSelect); - if (newSelRow >= 0) { - selectedRow = newSelRow; + if (oldSelect is RowT r) + { + int newSelRow = sortedFilteredData.IndexOf(r); + if (newSelRow >= 0) { + selectedRow = newSelRow; + } } } @@ -413,7 +418,7 @@ private Comparison getComparer(ConsoleListBoxColumn col, bool ascend ? col.Comparer ?? ((a, b) => col.Renderer(a).Trim().CompareTo(col.Renderer(b).Trim())) : col.Comparer != null - ? (Comparison)((RowT a, RowT b) => col.Comparer(b, a)) + ? ((RowT a, RowT b) => col.Comparer(b, a)) : ((RowT a, RowT b) => col.Renderer(b).Trim().CompareTo(col.Renderer(a).Trim())); // Sometimes type safety can be a minor hindrance; @@ -427,8 +432,8 @@ private int IntOr(Func first, Func second) private List sortedFilteredData; private IList data; private readonly IList> columns; - private readonly Func filterCheck; - private ConsolePopupMenu sortMenu; + private readonly Func? filterCheck; + private ConsolePopupMenu? sortMenu; private readonly int defaultSortColumn = 0; private int sortColIndex; @@ -455,26 +460,35 @@ public class ConsoleListBoxColumn { /// /// Initialize the column /// - public ConsoleListBoxColumn() { } + public ConsoleListBoxColumn(string header, + Func renderer, + Comparison? comparer, + int? width) + { + Header = header; + Renderer = renderer; + Comparer = comparer; + Width = width; + } /// /// Text for the header row /// - public string Header; + public readonly string Header; /// /// Function to translate a row's object into text to display /// - public Func Renderer; + public readonly Func Renderer; /// /// Function to compare two rows for sorting purposes. /// If not defined, the string representation is used. /// - public Comparison Comparer; + public readonly Comparison? Comparer; /// /// Number of screen columns to use for this column. /// If null, take up remaining space left behind by other columns. /// - public int? Width; + public readonly int? Width; } } diff --git a/ConsoleUI/Toolkit/ConsoleMessageDialog.cs b/ConsoleUI/Toolkit/ConsoleMessageDialog.cs index 7f34d14a8e..f50d8d18de 100644 --- a/ConsoleUI/Toolkit/ConsoleMessageDialog.cs +++ b/ConsoleUI/Toolkit/ConsoleMessageDialog.cs @@ -11,13 +11,19 @@ public class ConsoleMessageDialog : ConsoleDialog { /// /// Initialize a dialog /// + /// The visual theme to use to draw the dialog /// Message to show /// List of captions for buttons /// Function to generate the header /// Alignment of the contents /// Pass non-zero to move popup vertically - public ConsoleMessageDialog(string m, List btns, Func hdr = null, TextAlign ta = TextAlign.Center, int vertOffset = 0) - : base() + public ConsoleMessageDialog(ConsoleTheme theme, + string m, + List btns, + Func? hdr = null, + TextAlign ta = TextAlign.Center, + int vertOffset = 0) + : base(theme) { int maxLen = Formatting.MaxLineLength(m); int w = Math.Max(minWidth, Math.Min(maxLen + 6, Console.WindowWidth - 4)); @@ -62,7 +68,7 @@ public ConsoleMessageDialog(string m, List btns, Func hdr = null SetDimensions(l, t, r, b); int btnRow = GetBottom() - 2; - ConsoleTextBox tb = new ConsoleTextBox( + var tb = new ConsoleTextBox( GetLeft() + 2, GetTop() + 2, GetRight() - 2, GetBottom() - 2 - (btns.Count > 0 ? 2 : 0), false, ta, @@ -77,7 +83,7 @@ public ConsoleMessageDialog(string m, List btns, Func hdr = null if (messageLines.Count > boxH) { // Scroll AddTip(Properties.Resources.CursorKeys, Properties.Resources.Scroll); - tb.AddScrollBindings(this); + tb.AddScrollBindings(this, theme); } int btnLeft = (Console.WindowWidth - btnW) / 2; @@ -95,14 +101,13 @@ public ConsoleMessageDialog(string m, List btns, Func hdr = null /// /// Show the dialog and handle its interaction /// - /// The visual theme to use to draw the dialog /// Function to control the dialog, default is normal user interaction /// /// Index of button the user pressed /// - public new int Run(ConsoleTheme theme, Action process = null) + public new int Run(Action? process = null) { - base.Run(theme, process); + base.Run(process); return selectedButton; } diff --git a/ConsoleUI/Toolkit/ConsolePopupMenu.cs b/ConsoleUI/Toolkit/ConsolePopupMenu.cs index effd81888e..cf4caf385e 100644 --- a/ConsoleUI/Toolkit/ConsolePopupMenu.cs +++ b/ConsoleUI/Toolkit/ConsolePopupMenu.cs @@ -13,10 +13,10 @@ public class ConsolePopupMenu { /// Initialize the menu. /// /// List of menu options for the menu - public ConsolePopupMenu(List opts) + public ConsolePopupMenu(List opts) { options = opts; - foreach (ConsoleMenuOption opt in options) { + foreach (var opt in options) { if (opt != null) { int len = opt.Caption.Length + ( string.IsNullOrEmpty(opt.Key) ? 0 : 2 + opt.Key.Length @@ -52,25 +52,27 @@ public bool Run(ConsoleTheme theme, int right, int top) case ConsoleKey.UpArrow: do { selectedOption = (selectedOption + options.Count - 1) % options.Count; - } while (options[selectedOption] == null || !options[selectedOption].Enabled); + } while (!options[selectedOption]?.Enabled ?? true); break; case ConsoleKey.DownArrow: do { selectedOption = (selectedOption + 1) % options.Count; - } while (options[selectedOption] == null || !options[selectedOption].Enabled); + } while (!options[selectedOption]?.Enabled ?? true); break; case ConsoleKey.Enter: - if (options[selectedOption].CloseParent) { - done = true; - } - if (options[selectedOption].OnExec != null) { - val = options[selectedOption].OnExec(theme); + if (options[selectedOption] is ConsoleMenuOption opt) + { + if (opt.CloseParent) { + done = true; + } + if (opt.OnExec != null) { + val = opt.OnExec(); + } + (opt.SubMenu ?? (opt.SubMenuFunc?.Invoke())) + ?.Run(theme, + right - 2, + top + selectedOption + 2); } - (options[selectedOption].SubMenu ?? (options[selectedOption].SubMenuFunc?.Invoke())) - ?.Run( - theme, - right - 2, - top + selectedOption + 2); break; case ConsoleKey.F10: case ConsoleKey.Escape: @@ -95,7 +97,7 @@ private void Draw(ConsoleTheme theme, int right, int top) Console.BackgroundColor = theme.MenuBg; Console.ForegroundColor = theme.MenuFg; - string fullHorizLine = new string(Symbols.horizLine, longestLength + 2); + var fullHorizLine = new string(Symbols.horizLine, longestLength + 2); for (int index = -1, y = top; y < top + h; ++index, ++y) { Console.SetCursorPosition(right - w + 1, y); // Left padding @@ -107,7 +109,7 @@ private void Draw(ConsoleTheme theme, int right, int top) // Draw bottom line Console.Write(Symbols.lowerLeftCorner + fullHorizLine + Symbols.lowerRightCorner); } else { - ConsoleMenuOption opt = options[index]; + var opt = options[index]; if (opt == null) { // Draw separator Console.Write(Symbols.leftTee + fullHorizLine + Symbols.rightTee); @@ -162,9 +164,9 @@ private string AnnotatedCaption(ConsoleMenuOption opt) : $"( ) {opt.Caption}".PadRight(longestLength) : opt.Caption.PadRight(longestLength - opt.Key.Length) + opt.Key; - private readonly List options; - private readonly int longestLength; - private int selectedOption = 0; + private readonly List options; + private readonly int longestLength; + private int selectedOption = 0; private static readonly string submenuIndicator = ">"; } @@ -186,9 +188,15 @@ public class ConsoleMenuOption { /// Submenu to open for this option /// true if this option should be drawn normally and allowed for selection, false to draw it grayed out and not allow selection /// Function to generate submenu to open for this option - public ConsoleMenuOption(string cap, string key, string tt, bool close, - Func exec = null, Func radio = null, ConsolePopupMenu submenu = null, - bool enabled = true, Func submenuFunc = null) + public ConsoleMenuOption(string cap, + string key, + string tt, + bool close, + Func? exec = null, + Func? radio = null, + ConsolePopupMenu? submenu = null, + bool enabled = true, + Func? submenuFunc = null) { Caption = cap; Key = key; @@ -220,19 +228,19 @@ public ConsoleMenuOption(string cap, string key, string tt, bool close, /// /// Function to call if the user chooses this option /// - public readonly Func OnExec; + public readonly Func? OnExec; /// /// If set, this option is a radio button, and this function returns its value /// - public readonly Func RadioActive; + public readonly Func? RadioActive; /// /// Submenu to open for this option /// - public readonly ConsolePopupMenu SubMenu; + public readonly ConsolePopupMenu? SubMenu; /// /// Function to generate submenu to open for this option /// - public readonly Func SubMenuFunc; + public readonly Func? SubMenuFunc; /// /// Function to call to check whether this option is enabled /// diff --git a/ConsoleUI/Toolkit/ConsoleProgressBar.cs b/ConsoleUI/Toolkit/ConsoleProgressBar.cs index ac07f3579b..37e2fc3d02 100644 --- a/ConsoleUI/Toolkit/ConsoleProgressBar.cs +++ b/ConsoleUI/Toolkit/ConsoleProgressBar.cs @@ -49,13 +49,13 @@ public override void Draw(ConsoleTheme theme, bool focused) if (highlightWidth > 0) { Console.BackgroundColor = theme.ProgressBarHighlightBg; Console.ForegroundColor = theme.ProgressBarHighlightFg; - Console.Write(caption.Substring(0, highlightWidth)); + Console.Write(caption[..highlightWidth]); } // Draw the non highlighted part Console.BackgroundColor = theme.ProgressBarBg; Console.ForegroundColor = theme.ProgressBarFg; - Console.Write(caption.Substring(highlightWidth)); + Console.Write(caption[highlightWidth..]); } /// diff --git a/ConsoleUI/Toolkit/ConsoleScreen.cs b/ConsoleUI/Toolkit/ConsoleScreen.cs index c8cdb87ab2..5f4e9c75c3 100644 --- a/ConsoleUI/Toolkit/ConsoleScreen.cs +++ b/ConsoleUI/Toolkit/ConsoleScreen.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace CKAN.ConsoleUI.Toolkit { @@ -12,20 +13,22 @@ public abstract class ConsoleScreen : ScreenContainer, IUser { /// Initialize a screen. /// Sets up the F10 key binding for menus. /// - protected ConsoleScreen() + /// The visual theme to use to draw the dialog + protected ConsoleScreen(ConsoleTheme theme) + : base(theme) { AddTip( "F10", MenuTip(), () => mainMenu != null ); - AddBinding(new ConsoleKeyInfo[] {Keys.F10, Keys.Apps}, (object sender, ConsoleTheme theme) => { + AddBinding(new ConsoleKeyInfo[] {Keys.F10, Keys.Apps}, (object sender) => { bool val = true; if (mainMenu != null) { - DrawSelectedHamburger(theme); + DrawSelectedHamburger(); val = mainMenu.Run(theme, Console.WindowWidth - 1, 1); - DrawBackground(theme); + DrawBackground(); } return val; }); @@ -34,44 +37,37 @@ protected ConsoleScreen() /// /// Launch a screen and then clean up after it so we can continue using this screen. /// - /// The visual theme to use to draw the dialog /// Subscreen to launch /// Function to drive the screen, default is normal interaction - protected void LaunchSubScreen(ConsoleTheme theme, ConsoleScreen cs, Action newProc = null) + protected void LaunchSubScreen(ConsoleScreen cs, Action? newProc = null) { - cs.Run(theme, newProc); - DrawBackground(theme); - Draw(theme); + cs.Run(newProc); + DrawBackground(); + Draw(); } /// /// Function returning text to be shown at the left edge of the top header bar /// protected virtual string LeftHeader() - { - return ""; - } + => ""; /// /// Function returning text to be shown in the center of the top header bar /// protected virtual string CenterHeader() - { - return ""; - } + => ""; /// /// Function returning text to be shown to explain the F10 menu hotkey /// protected virtual string MenuTip() - { - return Properties.Resources.Menu; - } + => Properties.Resources.Menu; /// /// Menu to open for F10 from the hamburger icon of this screen /// - protected ConsolePopupMenu mainMenu = null; + protected ConsolePopupMenu? mainMenu = null; #region IUser @@ -92,25 +88,25 @@ protected virtual string MenuTip() /// public virtual bool RaiseYesNoDialog(string question) { - ConsoleMessageDialog d = new ConsoleMessageDialog( + var d = new ConsoleMessageDialog( + theme, string.Join("", messagePieces) + question, new List() { Properties.Resources.Yes, Properties.Resources.No - } - ); - d.AddBinding(Keys.Y, (object sender, ConsoleTheme theme) => { + }); + d.AddBinding(Keys.Y, (object sender) => { d.PressButton(0); return false; }); - d.AddBinding(Keys.N, (object sender, ConsoleTheme theme) => { + d.AddBinding(Keys.N, (object sender) => { d.PressButton(1); return false; }); messagePieces.Clear(); - bool val = d.Run(userTheme) == 0; - DrawBackground(userTheme); - Draw(userTheme); + bool val = d.Run() == 0; + DrawBackground(); + Draw(); return val; } @@ -126,14 +122,16 @@ public virtual bool RaiseYesNoDialog(string question) /// public int RaiseSelectionDialog(string message, params object[] args) { - ConsoleMessageDialog d = new ConsoleMessageDialog( + var d = new ConsoleMessageDialog( + theme, string.Join("", messagePieces) + message, - new List(Array.ConvertAll(args, p => p.ToString())) - ); + Array.ConvertAll(args, p => p.ToString()) + .OfType() + .ToList()); messagePieces.Clear(); - int val = d.Run(userTheme); - DrawBackground(userTheme); - Draw(userTheme); + int val = d.Run(); + DrawBackground(); + Draw(); return val; } @@ -145,14 +143,14 @@ public int RaiseSelectionDialog(string message, params object[] args) /// Values to be interpolated into the format string public void RaiseError(string message, params object[] args) { - ConsoleMessageDialog d = new ConsoleMessageDialog( + var d = new ConsoleMessageDialog( + theme, string.Join("", messagePieces) + string.Format(message, args), - new List() { Properties.Resources.OK } - ); + new List() { Properties.Resources.OK }); messagePieces.Clear(); - d.Run(userTheme); - DrawBackground(userTheme); - Draw(userTheme); + d.Run(); + DrawBackground(); + Draw(); } /// @@ -164,7 +162,7 @@ public void RaiseError(string message, params object[] args) public void RaiseMessage(string message, params object[] args) { Message(message, args); - Draw(userTheme); + Draw(); } /// @@ -201,7 +199,7 @@ protected virtual void Message(string message, params object[] args) public void RaiseProgress(string message, int percent) { Progress(message, percent); - Draw(userTheme); + Draw(); } /// @@ -216,7 +214,7 @@ public void RaiseProgress(int percent, long bytesPerSecond, long bytesLeft) CkanModule.FmtSize(bytesPerSecond), CkanModule.FmtSize(bytesLeft)); Progress(fullMsg, percent); - Draw(userTheme); + Draw(); } /// @@ -228,7 +226,7 @@ protected virtual void Progress(string message, int percent) { } #endregion IUser - private void DrawSelectedHamburger(ConsoleTheme theme) + private void DrawSelectedHamburger() { Console.SetCursorPosition(Console.WindowWidth - 3, 0); Console.BackgroundColor = theme.MenuSelectedBg; @@ -239,10 +237,11 @@ private void DrawSelectedHamburger(ConsoleTheme theme) /// /// Set the whole screen to dark blue and draw the top header bar /// - protected override void DrawBackground(ConsoleTheme theme) + // [MemberNotNull(nameof(userTheme))] + protected override void DrawBackground() { // Cheat because our IUser handlers need a theme - userTheme = theme; + // userTheme = theme; Console.BackgroundColor = theme.MainBg; Console.Clear(); @@ -271,7 +270,7 @@ private static string LeftCenterRight(string left, string center, string right, { // If the combined string is too long, shorten the center if (center.Length > width - left.Length - right.Length - 4) { - center = center.Substring(0, width - left.Length - right.Length - 4); + center = center[..(width - left.Length - right.Length - 4)]; } // Start with the center centered on the screen int leftSideWidth = (width - center.Length) / 2; @@ -284,7 +283,7 @@ private static string LeftCenterRight(string left, string center, string right, return left.PadRight(leftSideWidth) + center + right.PadLeft(rightSideWidth); } - private ConsoleTheme userTheme; + // private ConsoleTheme userTheme; private static readonly string hamburger = $" {Symbols.hamburger} "; } diff --git a/ConsoleUI/Toolkit/ConsoleTextBox.cs b/ConsoleUI/Toolkit/ConsoleTextBox.cs index edcba9179e..426b4f7bf9 100644 --- a/ConsoleUI/Toolkit/ConsoleTextBox.cs +++ b/ConsoleUI/Toolkit/ConsoleTextBox.cs @@ -23,8 +23,8 @@ public ConsoleTextBox( int l, int t, int r, int b, bool autoScroll = true, TextAlign ta = TextAlign.Left, - Func bgFunc = null, - Func fgFunc = null) + Func? bgFunc = null, + Func? fgFunc = null) : base(l, t, r, b) { scrollToBottom = autoScroll; @@ -175,62 +175,63 @@ public override void Draw(ConsoleTheme theme, bool focused) /// Set up key bindings to scroll the box in a given screen container /// /// Container within which to create the bindings + /// The visual theme to use to draw the dialog /// If true, force a redraw of the text box after scrolling, otherwise rely on the main event loop to do it - public void AddScrollBindings(ScreenContainer cont, bool drawMore = false) + public void AddScrollBindings(ScreenContainer cont, ConsoleTheme theme, bool drawMore = false) { if (drawMore) { - cont.AddBinding(Keys.Home, (object sender, ConsoleTheme theme) => { + cont.AddBinding(Keys.Home, (object sender) => { ScrollToTop(); Draw(theme, false); return true; }); - cont.AddBinding(Keys.End, (object sender, ConsoleTheme theme) => { + cont.AddBinding(Keys.End, (object sender) => { ScrollToBottom(); Draw(theme, false); return true; }); - cont.AddBinding(Keys.PageUp, (object sender, ConsoleTheme theme) => { + cont.AddBinding(Keys.PageUp, (object sender) => { ScrollUp(); Draw(theme, false); return true; }); - cont.AddBinding(Keys.PageDown, (object sender, ConsoleTheme theme) => { + cont.AddBinding(Keys.PageDown, (object sender) => { ScrollDown(); Draw(theme, false); return true; }); - cont.AddBinding(Keys.UpArrow, (object sender, ConsoleTheme theme) => { + cont.AddBinding(Keys.UpArrow, (object sender) => { ScrollUp(1); Draw(theme, false); return true; }); - cont.AddBinding(Keys.DownArrow, (object sender, ConsoleTheme theme) => { + cont.AddBinding(Keys.DownArrow, (object sender) => { ScrollDown(1); Draw(theme, false); return true; }); } else { - cont.AddBinding(Keys.Home, (object sender, ConsoleTheme theme) => { + cont.AddBinding(Keys.Home, (object sender) => { ScrollToTop(); return true; }); - cont.AddBinding(Keys.End, (object sender, ConsoleTheme theme) => { + cont.AddBinding(Keys.End, (object sender) => { ScrollToBottom(); return true; }); - cont.AddBinding(Keys.PageUp, (object sender, ConsoleTheme theme) => { + cont.AddBinding(Keys.PageUp, (object sender) => { ScrollUp(); return true; }); - cont.AddBinding(Keys.PageDown, (object sender, ConsoleTheme theme) => { + cont.AddBinding(Keys.PageDown, (object sender) => { ScrollDown(); return true; }); - cont.AddBinding(Keys.UpArrow, (object sender, ConsoleTheme theme) => { + cont.AddBinding(Keys.UpArrow, (object sender) => { ScrollUp(1); return true; }); - cont.AddBinding(Keys.DownArrow, (object sender, ConsoleTheme theme) => { + cont.AddBinding(Keys.DownArrow, (object sender) => { ScrollDown(1); return true; }); @@ -249,8 +250,8 @@ public void AddScrollBindings(ScreenContainer cont, bool drawMore = false) private readonly TextAlign align; private readonly SynchronizedCollection lines = new SynchronizedCollection(); private readonly SynchronizedCollection displayLines = new SynchronizedCollection(); - private readonly Func getBgColor; - private readonly Func getFgColor; + private readonly Func? getBgColor; + private readonly Func? getFgColor; } /// diff --git a/ConsoleUI/Toolkit/ScreenContainer.cs b/ConsoleUI/Toolkit/ScreenContainer.cs index 802b9e1530..f3305cf6e0 100644 --- a/ConsoleUI/Toolkit/ScreenContainer.cs +++ b/ConsoleUI/Toolkit/ScreenContainer.cs @@ -12,11 +12,12 @@ public abstract class ScreenContainer { /// /// Initialize the container /// - protected ScreenContainer() + protected ScreenContainer(ConsoleTheme theme) { - AddBinding(Keys.CtrlL, (object sender, ConsoleTheme theme) => { + this.theme = theme; + AddBinding(Keys.CtrlL, (object sender) => { // Just redraw everything and keep running - DrawBackground(theme); + DrawBackground(); return true; }); } @@ -24,11 +25,10 @@ protected ScreenContainer() /// /// Draw the contained screen objects and manage their interaction /// - /// The visual theme to use to draw the dialog /// Logic to drive the screen, default is normal user interaction - public virtual void Run(ConsoleTheme theme, Action process = null) + public virtual void Run(Action? process = null) { - DrawBackground(theme); + DrawBackground(); if (process == null) { // This should be a simple default parameter, but C# has trouble @@ -37,11 +37,11 @@ public virtual void Run(ConsoleTheme theme, Action process = null) } else { // Other classes can't call Draw directly, so do it for them once. // Would be nice to make this cleaner somehow. - Draw(theme); + Draw(); } // Run the actual logic for the container - process(theme); + process(); ClearBackground(); } @@ -59,7 +59,7 @@ protected void AddObject(ScreenObject so) /// /// Delegate type for key bindings /// - public delegate bool KeyAction(object sender, ConsoleTheme theme); + public delegate bool KeyAction(object sender); /// /// Bind an action to a key @@ -93,11 +93,9 @@ public void AddBinding(IEnumerable keys, KeyAction a) /// User readable description of the key /// Description of the action /// Function returning true to show the tip or false to hide it - public void AddTip(string key, string descrip, Func displayIf = null) + public void AddTip(string key, string descrip, Func? displayIf = null) { - if (displayIf == null) { - displayIf = () => true; - } + displayIf ??= () => true; tips.Add(new ScreenTip(key, descrip, displayIf)); } @@ -106,7 +104,7 @@ public void AddTip(string key, string descrip, Func displayIf = null) /// Called once at the beginning and then again later if we need to reset the display. /// NOT called every tick, to reduce flickering. /// - protected virtual void DrawBackground(ConsoleTheme theme) { } + protected virtual void DrawBackground() { } /// /// Reset the display, called when closing it. @@ -117,7 +115,7 @@ protected virtual void ClearBackground() { } /// Draw all the contained ScreenObjects. /// Also places the cursor where it should be. /// - protected void Draw(ConsoleTheme theme) + protected void Draw() { lock (screenLock) { Console.CursorVisible = false; @@ -147,19 +145,19 @@ protected void Draw(ConsoleTheme theme) /// then the bindings of the focused ScreenObject. /// Stops when 'done' is true. /// - protected void Interact(ConsoleTheme theme) + protected void Interact() { focusIndex = -1; Blur(null, true); do { - Draw(theme); + Draw(); ConsoleKeyInfo k = Console.ReadKey(true); - if (bindings.ContainsKey(k)) { - done = !bindings[k](this, theme); + if (bindings.TryGetValue(k, out KeyAction? screenBinding)) { + done = !screenBinding(this); } else if (objects.Count > 0) { - if (objects[focusIndex].Bindings.ContainsKey(k)) { - done = !objects[focusIndex].Bindings[k](this, theme); + if (objects[focusIndex].Bindings.TryGetValue(k, out KeyAction? objBinding)) { + done = !objBinding(this); } else { objects[focusIndex].OnKeyPress(k); } @@ -175,7 +173,7 @@ protected void Interact(ConsoleTheme theme) /// /// Currently focused ScreenObject /// - protected ScreenObject Focused() + protected ScreenObject? Focused() => focusIndex >= 0 && focusIndex < objects.Count ? objects[focusIndex] : null; @@ -218,12 +216,12 @@ private void DrawFooter(ConsoleTheme theme) Console.ForegroundColor = theme.FooterKeyFg; Console.Write(tipList[i].Key); Console.ForegroundColor = theme.FooterDescriptionFg; - string remainder = tipList[i].Key == tipList[i].Description.Substring(0, 1) - ? tipList[i].Description.Substring(1) + string remainder = tipList[i].Key == tipList[i].Description[..1] + ? tipList[i].Description[1..] : $" - {tipList[i].Description}"; int maxW = Console.WindowWidth - Console.CursorLeft - 1; if (remainder.Length > maxW && maxW > 0) { - Console.Write(remainder.Substring(0, maxW)); + Console.Write(remainder[..maxW]); } else { Console.Write(remainder); } @@ -235,7 +233,7 @@ private void DrawFooter(ConsoleTheme theme) Console.Write("".PadLeft(Console.WindowWidth - Console.CursorLeft - 1)); } - private void Blur(ScreenObject source, bool forward) + private void Blur(ScreenObject? source, bool forward) { if (objects.Count > 0) { int loops = 0; @@ -251,10 +249,15 @@ private void Blur(ScreenObject source, bool forward) } } + /// + /// The colors used to render the screen or dialog + /// + protected readonly ConsoleTheme theme; + private bool done = false; private readonly List objects = new List(); - private int focusIndex = 0; + private int focusIndex = 0; private readonly Dictionary bindings = new Dictionary(); private readonly List tips = new List(); @@ -274,7 +277,7 @@ public class ScreenTip { /// Description of the keypress /// Description of the bound action /// Function that returns true to display the tip or false to hide it - public ScreenTip(string key, string descrip, Func dispIf = null) + public ScreenTip(string key, string descrip, Func? dispIf = null) { Key = key; Description = descrip; diff --git a/ConsoleUI/Toolkit/ScreenObject.cs b/ConsoleUI/Toolkit/ScreenObject.cs index f13b6bf35c..53067fdb8c 100644 --- a/ConsoleUI/Toolkit/ScreenObject.cs +++ b/ConsoleUI/Toolkit/ScreenObject.cs @@ -35,7 +35,7 @@ protected ScreenObject(int l, int t, int r, int b) public static string PadCenter(string s, int w, char pad = ' ') { if (s.Length > w) { - return s.Substring(0, w); + return s[..w]; } else { int lp = (w - s.Length) / 2; return FormatExactWidth(s, w - lp, pad).PadLeft(w, pad); @@ -53,7 +53,7 @@ public static string PadCenter(string s, int w, char pad = ' ') /// public static string FormatExactWidth(string val, int w, char pad = ' ') { - return val.PadRight(w, pad).Substring(0, w); + return val.PadRight(w, pad)[..w]; } /// @@ -64,7 +64,7 @@ public static string FormatExactWidth(string val, int w, char pad = ' ') /// First 'w' characters of 'val', or whole string if short enough public static string TruncateLength(string val, int w) => val.Length <= w ? val - : val.Substring(0, w); + : val[..w]; /// /// Custom key bindings for this UI element @@ -105,11 +105,9 @@ public void AddBinding(IEnumerable keys, ScreenContainer.KeyActi /// Description of the key /// Description of the action bound to the key /// Function returning true to show the tip, false to hide it - public void AddTip(string key, string descrip, Func displayIf = null) + public void AddTip(string key, string descrip, Func? displayIf = null) { - if (displayIf == null) { - displayIf = () => true; - } + displayIf ??= () => true; Tips.Add(new ScreenTip(key, descrip, displayIf)); } @@ -183,7 +181,7 @@ public virtual void OnKeyPress(ConsoleKeyInfo k) { } /// /// Event to notify container that we'd like to lose focus /// - public event BlurListener OnBlur; + public event BlurListener? OnBlur; /// /// Function to fire event to notify container that we'd like to lose focus /// diff --git a/Core/AutoUpdate/AutoUpdate.cs b/Core/AutoUpdate/AutoUpdate.cs index bcbc44155b..e20ca0987d 100644 --- a/Core/AutoUpdate/AutoUpdate.cs +++ b/Core/AutoUpdate/AutoUpdate.cs @@ -17,7 +17,7 @@ public AutoUpdate() public CkanUpdate GetUpdate(bool devBuild) { - if (updates.TryGetValue(devBuild, out CkanUpdate update)) + if (updates.TryGetValue(devBuild, out CkanUpdate? update)) { return update; } @@ -30,6 +30,9 @@ public CkanUpdate GetUpdate(bool devBuild) private readonly Dictionary updates = new Dictionary(); + // This is null when running tests, seemingly. + private static readonly string exePath = Assembly.GetEntryAssembly()?.Location ?? ""; + /// /// Report whether it's possible to run the auto-updater. /// Checks whether we can overwrite the running ckan.exe. @@ -43,7 +46,7 @@ public CkanUpdate GetUpdate(bool devBuild) /// and then launches the helper allowing us to upgrade. /// /// If set to true launch CKAN after update. - public void StartUpdateProcess(bool launchCKANAfterUpdate, bool devBuild, IUser user = null) + public void StartUpdateProcess(bool launchCKANAfterUpdate, bool devBuild, IUser? user = null) { var pid = Process.GetCurrentProcess().Id; @@ -84,8 +87,8 @@ public static void SetExecutable(string fileName) { UseShellExecute = false }; - Process permsprocess = Process.Start(permsinfo); - permsprocess.WaitForExit(); + var permsprocess = Process.Start(permsinfo); + permsprocess?.WaitForExit(); } } @@ -103,8 +106,5 @@ private static bool CanWrite(string path) return false; } } - - // This is null when running tests, seemingly. - private static readonly string exePath = Assembly.GetEntryAssembly()?.Location ?? ""; } } diff --git a/Core/AutoUpdate/CkanUpdate.cs b/Core/AutoUpdate/CkanUpdate.cs index 8ef171b033..898a11859b 100644 --- a/Core/AutoUpdate/CkanUpdate.cs +++ b/Core/AutoUpdate/CkanUpdate.cs @@ -11,12 +11,8 @@ namespace CKAN /// public abstract class CkanUpdate { - public CkanModuleVersion Version { get; protected set; } - public Uri ReleaseDownload { get; protected set; } - public long ReleaseSize { get; protected set; } - public Uri UpdaterDownload { get; protected set; } - public long UpdaterSize { get; protected set; } - public string ReleaseNotes { get; protected set; } + public CkanModuleVersion? Version { get; protected set; } + public string? ReleaseNotes { get; protected set; } public string updaterFilename = $"{Path.GetTempPath()}{Guid.NewGuid()}.exe"; public string ckanFilename = $"{Path.GetTempPath()}{Guid.NewGuid()}.exe"; diff --git a/Core/AutoUpdate/GithubReleaseCkanUpdate.cs b/Core/AutoUpdate/GithubReleaseCkanUpdate.cs index 11bd037d78..3da2428228 100644 --- a/Core/AutoUpdate/GithubReleaseCkanUpdate.cs +++ b/Core/AutoUpdate/GithubReleaseCkanUpdate.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Collections.Generic; using Autofac; @@ -18,38 +19,47 @@ public class GitHubReleaseCkanUpdate : CkanUpdate /// Initialize the Object /// /// JSON representation of release - public GitHubReleaseCkanUpdate(GitHubReleaseInfo releaseJson = null) + public GitHubReleaseCkanUpdate(GitHubReleaseInfo? releaseJson = null) { if (releaseJson == null) { var coreConfig = ServiceLocator.Container.Resolve(); - var token = coreConfig.TryGetAuthToken(latestCKANReleaseApiUrl.Host, out string t) + var token = coreConfig.TryGetAuthToken(latestCKANReleaseApiUrl.Host, out string? t) ? t : null; - releaseJson = JsonConvert.DeserializeObject( - Net.DownloadText(latestCKANReleaseApiUrl, token)); - if (releaseJson == null) - { - throw new Kraken(Properties.Resources.AutoUpdateNotFetched); - } + releaseJson = Net.DownloadText(latestCKANReleaseApiUrl, token) is string content + ? JsonConvert.DeserializeObject(content) + : null; + } + if (releaseJson is null + || releaseJson.tag_name is null + || releaseJson.name is null + || releaseJson.body is null + || releaseJson.assets is null) + { + throw new Kraken(Properties.Resources.AutoUpdateNotFetched); } Version = new CkanModuleVersion(releaseJson.tag_name.ToString(), releaseJson.name.ToString()); ReleaseNotes = ExtractReleaseNotes(releaseJson.body.ToString()); - foreach (var asset in releaseJson.assets) + + var releaseAsset = releaseJson.assets.First(asset => asset.browser_download_url + ?.ToString() + .EndsWith("ckan.exe") + ?? false); + var updaterAsset = releaseJson.assets.First(asset => asset.browser_download_url + ?.ToString() + .EndsWith("AutoUpdater.exe") + ?? false); + if (releaseAsset.browser_download_url is null + || updaterAsset.browser_download_url is null) { - string url = asset.browser_download_url.ToString(); - if (url.EndsWith("ckan.exe")) - { - ReleaseDownload = asset.browser_download_url; - ReleaseSize = asset.size; - } - else if (url.EndsWith("AutoUpdater.exe")) - { - UpdaterDownload = asset.browser_download_url; - UpdaterSize = asset.size; - } + throw new Kraken(Properties.Resources.AutoUpdateNotFetched); } + ReleaseDownload = releaseAsset.browser_download_url; + ReleaseSize = releaseAsset.size; + UpdaterDownload = updaterAsset.browser_download_url; + UpdaterSize = updaterAsset.size; } public override IList Targets => new[] @@ -60,6 +70,11 @@ public GitHubReleaseCkanUpdate(GitHubReleaseInfo releaseJson = null) ReleaseDownload, ckanFilename, ReleaseSize), }; + private Uri ReleaseDownload { get; set; } + private long ReleaseSize { get; set; } + private Uri UpdaterDownload { get; set; } + private long UpdaterSize { get; set; } + /// /// Extracts release notes from the body of text provided by the github API. /// By default this is everything after the first three dashes on a line by diff --git a/Core/AutoUpdate/GithubReleaseInfo.cs b/Core/AutoUpdate/GithubReleaseInfo.cs index 8216ca892e..4607b2fb7b 100644 --- a/Core/AutoUpdate/GithubReleaseInfo.cs +++ b/Core/AutoUpdate/GithubReleaseInfo.cs @@ -5,15 +5,15 @@ namespace CKAN { public class GitHubReleaseInfo { - public string tag_name; - public string name; - public string body; - public List assets; + public string? tag_name; + public string? name; + public string? body; + public List? assets; public sealed class Asset { - public Uri browser_download_url; - public long size; + public Uri? browser_download_url; + public long size; } } } diff --git a/Core/AutoUpdate/S3BuildCkanUpdate.cs b/Core/AutoUpdate/S3BuildCkanUpdate.cs index 7307790b96..b6f27f4683 100644 --- a/Core/AutoUpdate/S3BuildCkanUpdate.cs +++ b/Core/AutoUpdate/S3BuildCkanUpdate.cs @@ -9,18 +9,15 @@ namespace CKAN { public class S3BuildCkanUpdate : CkanUpdate { - public S3BuildCkanUpdate(S3BuildVersionInfo versionJson = null) + public S3BuildCkanUpdate(S3BuildVersionInfo? versionJson = null) { - if (versionJson == null) + versionJson ??= Net.DownloadText(new Uri(S3BaseUrl, VersionJsonUrlPiece)) is string content + ? JsonConvert.DeserializeObject(content) + : null; + if (versionJson is null || versionJson.version is null) { - versionJson = JsonConvert.DeserializeObject( - Net.DownloadText(new Uri(S3BaseUrl, VersionJsonUrlPiece))); - if (versionJson == null) - { - throw new Kraken(Properties.Resources.AutoUpdateNotFetched); - } + throw new Kraken(Properties.Resources.AutoUpdateNotFetched); } - Version = new CkanModuleVersion(versionJson.version.ToString(), "dev"); ReleaseNotes = versionJson.changelog; UpdaterDownload = new Uri(S3BaseUrl, AutoUpdaterUrlPiece); @@ -29,12 +26,13 @@ public S3BuildCkanUpdate(S3BuildVersionInfo versionJson = null) public override IList Targets => new[] { - new NetAsyncDownloader.DownloadTargetFile( - UpdaterDownload, updaterFilename), - new NetAsyncDownloader.DownloadTargetFile( - ReleaseDownload, ckanFilename), + new NetAsyncDownloader.DownloadTargetFile(UpdaterDownload, updaterFilename), + new NetAsyncDownloader.DownloadTargetFile(ReleaseDownload, ckanFilename), }; + private Uri ReleaseDownload { get; set; } + private Uri UpdaterDownload { get; set; } + private static readonly Uri S3BaseUrl = new Uri("https://ksp-ckan.s3-us-west-2.amazonaws.com/"); private const string VersionJsonUrlPiece = "version.json"; diff --git a/Core/AutoUpdate/S3BuildVersionInfo.cs b/Core/AutoUpdate/S3BuildVersionInfo.cs index 0b5e504f7b..af3b07a30d 100644 --- a/Core/AutoUpdate/S3BuildVersionInfo.cs +++ b/Core/AutoUpdate/S3BuildVersionInfo.cs @@ -4,7 +4,7 @@ namespace CKAN { public class S3BuildVersionInfo { - public ModuleVersion version; - public string changelog; + public ModuleVersion? version; + public string? changelog; } } diff --git a/Core/CKAN-core.csproj b/Core/CKAN-core.csproj index bdac8c276d..40850bbc4e 100644 --- a/Core/CKAN-core.csproj +++ b/Core/CKAN-core.csproj @@ -16,9 +16,12 @@ true Debug;Release;NoGUI false - 7.3 + 9 + enable + true + SYSLIB0050,IDE0090 IDE1006 - netstandard2.0;net48;net7.0 + netstandard2.0;net48;net8.0 PrepareResources;$(CompileDependsOn) @@ -54,6 +57,8 @@ + + diff --git a/Core/CKANPathUtils.cs b/Core/CKANPathUtils.cs index c1cce5eb3f..72e82abc9e 100644 --- a/Core/CKANPathUtils.cs +++ b/Core/CKANPathUtils.cs @@ -25,9 +25,8 @@ public static class CKANPathUtils /// The path to normalize /// The normalized path public static string NormalizePath(string path) - => path == null ? null - : path.Length < 2 ? path.Replace('\\', '/') - : path.Replace('\\', '/').TrimEnd('/'); + => path.Length < 2 ? path.Replace('\\', '/') + : path.Replace('\\', '/').TrimEnd('/'); /// /// Gets the last path element. Ex: /a/b/c returns c @@ -61,11 +60,6 @@ public static string GetLeadingPathElements(string path) /// public static string ToRelative(string path, string root) { - if (path == null || root == null) - { - throw new PathErrorKraken(null, "Null path provided"); - } - // We have to normalise before we check for rootedness, // otherwise backslash separators fail on Linux. @@ -96,11 +90,6 @@ public static string ToRelative(string path, string root) /// public static string ToAbsolute(string path, string root) { - if (path == null || root == null) - { - throw new PathErrorKraken(null, "Null path provided"); - } - path = NormalizePath(path); root = NormalizePath(root); diff --git a/Core/CkanTransaction.cs b/Core/CkanTransaction.cs index f9d4ae504b..12a5b924ee 100644 --- a/Core/CkanTransaction.cs +++ b/Core/CkanTransaction.cs @@ -86,12 +86,12 @@ private static void SetMaxTimeout(TimeSpan timeout) } } - private static void SetField(Type T, string fieldName, object value) + private static void SetField(Type T, string fieldName, object? value) { try { T.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static) - .SetValue(null, value); + ?.SetValue(null, value); } catch { diff --git a/Core/CompatibleGameVersions.cs b/Core/CompatibleGameVersions.cs index ec5c4ea4bb..2fd9c3e3f1 100644 --- a/Core/CompatibleGameVersions.cs +++ b/Core/CompatibleGameVersions.cs @@ -7,7 +7,7 @@ namespace CKAN [JsonConverter(typeof(CompatibleGameVersionsConverter))] public class CompatibleGameVersions { - public string GameVersionWhenWritten { get; set; } + public string? GameVersionWhenWritten { get; set; } public List Versions { get; set; } = new List(); } diff --git a/Core/Configuration/IConfiguration.cs b/Core/Configuration/IConfiguration.cs index 72467536ca..e99946899e 100644 --- a/Core/Configuration/IConfiguration.cs +++ b/Core/Configuration/IConfiguration.cs @@ -1,16 +1,17 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace CKAN.Configuration { public interface IConfiguration { - string AutoStartInstance { get; set; } + string? AutoStartInstance { get; set; } /// /// Get and set the path to the download cache /// - string DownloadCacheDir { get; set; } + string? DownloadCacheDir { get; set; } /// /// Get and set the maximum number of bytes allowed in the cache. @@ -24,7 +25,7 @@ public interface IConfiguration /// int RefreshRate { get; set; } - string Language { get; set; } + string? Language { get; set; } /// /// Get the hosts that have auth tokens stored in the registry @@ -42,14 +43,15 @@ public interface IConfiguration /// /// True if found, false otherwise /// - bool TryGetAuthToken(string host, out string token); + bool TryGetAuthToken(string host, + [NotNullWhen(returnValue: true)] out string? token); /// /// Set an auth token in the registry /// /// Host for which to set the token /// Token to set, or null to delete - void SetAuthToken(string host, string token); + void SetAuthToken(string host, string? token); void SetRegistryToInstances(SortedList instances); IEnumerable> GetInstances(); @@ -63,7 +65,7 @@ public interface IConfiguration /// List of hosts in order of priority when there are multiple URLs to choose from. /// The first null value represents where all other hosts should go. /// - string[] PreferredHosts { get; set; } + string?[] PreferredHosts { get; set; } /// /// true if user wants to use nightly builds from S3, false to use releases from GitHub diff --git a/Core/Configuration/JsonConfiguration.cs b/Core/Configuration/JsonConfiguration.cs index 0ba771f42f..413c48f426 100644 --- a/Core/Configuration/JsonConfiguration.cs +++ b/Core/Configuration/JsonConfiguration.cs @@ -5,9 +5,13 @@ #if NET5_0_OR_GREATER using System.Runtime.Versioning; #endif +using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; +#if !NET8_0_OR_GREATER +using CKAN.Extensions; +#endif using CKAN.Games.KerbalSpaceProgram; namespace CKAN.Configuration @@ -19,16 +23,16 @@ public class JsonConfiguration : IConfiguration [JsonConverter(typeof(ConfigConverter))] private class Config { - public string AutoStartInstance { get; set; } - public string DownloadCacheDir { get; set; } - public long? CacheSizeLimit { get; set; } - public int? RefreshRate { get; set; } - public string Language { get; set; } - public IList GameInstances { get; set; } = new List(); - public IDictionary AuthTokens { get; set; } = new Dictionary(); - public string[] GlobalInstallFilters { get; set; } = Array.Empty(); - public string[] PreferredHosts { get; set; } = Array.Empty(); - public bool? DevBuilds { get; set; } + public string? AutoStartInstance { get; set; } + public string? DownloadCacheDir { get; set; } + public long? CacheSizeLimit { get; set; } + public int? RefreshRate { get; set; } + public string? Language { get; set; } + public IList? GameInstances { get; set; } = new List(); + public IDictionary? AuthTokens { get; set; } = new Dictionary(); + public string[]? GlobalInstallFilters { get; set; } = Array.Empty(); + public string?[]? PreferredHosts { get; set; } = Array.Empty(); + public bool? DevBuilds { get; set; } } public class ConfigConverter : JsonPropertyNamesChangedConverter @@ -42,6 +46,14 @@ protected override Dictionary mapping private class GameInstanceEntry { + [JsonConstructor] + public GameInstanceEntry(string name, string path, string game) + { + Name = name; + Path = path; + Game = game; + } + public string Name { get; set; } public string Path { get; set; } public string Game { get; set; } @@ -52,7 +64,7 @@ private class GameInstanceEntry /// /// Loads configuration from the given file, or the default path if null. /// - public JsonConfiguration(string newConfig = null) + public JsonConfiguration(string? newConfig = null) { configFile = newConfig ?? defaultConfigFile; LoadConfig(); @@ -76,9 +88,9 @@ public JsonConfiguration(string newConfig = null) // believe that their copy of the config file in memory is authoritative, so // changes made by one copy will not be respected by the other. private readonly string configFile = defaultConfigFile; - private Config config = null; + private Config config; - public string DownloadCacheDir + public string? DownloadCacheDir { get => config.DownloadCacheDir ?? DefaultDownloadCacheDir; @@ -117,12 +129,12 @@ public int RefreshRate set { - config.RefreshRate = value <= 0 ? null : (int?)value; + config.RefreshRate = value <= 0 ? null : value; SaveConfig(); } } - public string Language + public string? Language { get => config.Language; @@ -136,7 +148,7 @@ public string Language } } - public string AutoStartInstance + public string? AutoStartInstance { get => config.AutoStartInstance ?? ""; @@ -148,45 +160,57 @@ public string AutoStartInstance } public IEnumerable> GetInstances() - => config.GameInstances.Select(instance => + => config.GameInstances?.Select(instance => new Tuple( instance.Name, instance.Path, - instance.Game)); + instance.Game)) + ?? Enumerable.Empty>(); public void SetRegistryToInstances(SortedList instances) { - config.GameInstances = instances.Select(instance => new GameInstanceEntry - { - Name = instance.Key, - Path = instance.Value.GameDir(), - Game = instance.Value.game.ShortName - }).ToList(); + config.GameInstances = instances.Select(inst => new GameInstanceEntry(inst.Key, + inst.Value.GameDir(), + inst.Value.game.ShortName)) + .ToList(); SaveConfig(); } public IEnumerable GetAuthTokenHosts() - => config.AuthTokens.Keys; + => config.AuthTokens?.Keys + ?? Enumerable.Empty(); - public bool TryGetAuthToken(string host, out string token) - => config.AuthTokens.TryGetValue(host, out token); + public bool TryGetAuthToken(string host, + [NotNullWhen(returnValue: true)] out string? token) + { + if (config.AuthTokens == null) + { + token = null; + return false; + } + return config.AuthTokens.TryGetValue(host, out token); + } - public void SetAuthToken(string host, string token) + public void SetAuthToken(string host, string? token) { - if (string.IsNullOrEmpty(token)) + if (token == null || string.IsNullOrEmpty(token)) { - config.AuthTokens.Remove(host); + config.AuthTokens?.Remove(host); } else { - config.AuthTokens[host] = token; + if (config.AuthTokens is not null) + { + config.AuthTokens[host] = token; + } } SaveConfig(); } public string[] GlobalInstallFilters { - get => config.GlobalInstallFilters; + get => config.GlobalInstallFilters + ?? Array.Empty(); set { @@ -195,9 +219,10 @@ public string[] GlobalInstallFilters } } - public string[] PreferredHosts + public string?[] PreferredHosts { - get => config.PreferredHosts; + get => config.PreferredHosts + ?? Array.Empty(); set { @@ -232,6 +257,7 @@ private void SaveConfig() /// If the configuration file does not exist, this will create it and then /// try to populate it with values in the registry left from the old system. /// + [MemberNotNull(nameof(config))] private void LoadConfig() { try @@ -248,22 +274,16 @@ private void LoadConfig() var gameName = new KerbalSpaceProgram().ShortName; foreach (var e in config.GameInstances) { - if (e.Game == null) - { - e.Game = gameName; - } + e.Game ??= gameName; } } - if (config.AuthTokens == null) - { - config.AuthTokens = new Dictionary(); - } + config.AuthTokens ??= new Dictionary(); } - catch (Exception ex) when (ex is FileNotFoundException || ex is DirectoryNotFoundException) + catch (Exception ex) when (ex is FileNotFoundException or DirectoryNotFoundException) { // This runs if the configuration does not exist // Ensure the directory exists - new FileInfo(configFile).Directory.Create(); + new FileInfo(configFile).Directory?.Create(); // Try to migrate from the real registry if ( @@ -297,22 +317,20 @@ private static Config FromWindowsRegistry(Win32RegistryConfiguration regCfg) => new Config() { GameInstances = regCfg.GetInstances() - .Select(instance => new GameInstanceEntry - { - Name = instance.Item1, - Path = instance.Item2, - Game = instance.Item3 - }) + .Select(inst => new GameInstanceEntry(inst.Item1, + inst.Item2, + inst.Item3)) .ToList(), AutoStartInstance = regCfg.AutoStartInstance, DownloadCacheDir = regCfg.DownloadCacheDir, CacheSizeLimit = regCfg.CacheSizeLimit, RefreshRate = regCfg.RefreshRate, AuthTokens = regCfg.GetAuthTokenHosts() - .ToDictionary(host => host, - host => regCfg.TryGetAuthToken(host, out string token) - ? token - : null), + .Select(host => regCfg.TryGetAuthToken(host, out string? token) + ? new KeyValuePair(host, token) + : (KeyValuePair?)null) + .OfType>() + .ToDictionary(), }; } } diff --git a/Core/Configuration/Win32RegistryConfiguration.cs b/Core/Configuration/Win32RegistryConfiguration.cs index 33d213f30b..e1e2b6e6e1 100644 --- a/Core/Configuration/Win32RegistryConfiguration.cs +++ b/Core/Configuration/Win32RegistryConfiguration.cs @@ -2,10 +2,11 @@ using System.Collections.Generic; using System.Linq; using System.IO; + #if NET5_0_OR_GREATER using System.Runtime.Versioning; #endif -using Microsoft.Win32; +using System.Diagnostics.CodeAnalysis; namespace CKAN.Configuration { @@ -26,12 +27,12 @@ public class Win32RegistryConfiguration : IConfiguration private static readonly string defaultDownloadCacheDir = Path.Combine(CKANPathUtils.AppDataPath, "downloads"); - public string DownloadCacheDir + public string? DownloadCacheDir { get => GetRegistryValue(@"DownloadCacheDir", defaultDownloadCacheDir); set { - if (string.IsNullOrEmpty(value)) + if (value == null || string.IsNullOrEmpty(value)) { DeleteRegistryValue(@"DownloadCacheDir"); } @@ -50,8 +51,8 @@ public long? CacheSizeLimit { get { - string val = GetRegistryValue(@"CacheSizeLimit", null); - return string.IsNullOrEmpty(val) ? null : (long?)Convert.ToInt64(val); + var val = GetRegistryValue(@"CacheSizeLimit", null); + return string.IsNullOrEmpty(val) ? null : Convert.ToInt64(val); } set { @@ -84,7 +85,7 @@ public int RefreshRate private int InstanceCount => GetRegistryValue(@"KSPInstanceCount", 0); - public string AutoStartInstance + public string? AutoStartInstance { get => GetRegistryValue(@"KSPAutoStartInstance", ""); #pragma warning disable IDE0027 @@ -92,12 +93,12 @@ public string AutoStartInstance #pragma warning restore IDE0027 } - public string Language + public string? Language { - get => GetRegistryValue("Language", null); + get => GetRegistryValue("Language", null); set { - if (Utilities.AvailableLanguages.Contains(value)) + if (value != null && Utilities.AvailableLanguages.Contains(value)) { SetRegistryValue("Language", value); } @@ -130,11 +131,10 @@ public void SetRegistryToInstances(SortedList instances) } public IEnumerable> GetInstances() - { - return Enumerable.Range(0, InstanceCount).Select(GetInstance).ToList(); - } + => Enumerable.Range(0, InstanceCount).Select(GetInstance).ToList(); - public bool TryGetAuthToken(string host, out string token) + public bool TryGetAuthToken(string host, + [NotNullWhen(returnValue: true)] out string? token) { try { @@ -152,19 +152,19 @@ public bool TryGetAuthToken(string host, out string token) public IEnumerable GetAuthTokenHosts() { - RegistryKey key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(authTokenKeyNoPrefix); + var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(authTokenKeyNoPrefix); return key?.GetValueNames() ?? Array.Empty(); } - public void SetAuthToken(string host, string token) + public void SetAuthToken(string host, string? token) { ConstructKey(authTokenKeyNoPrefix); if (!string.IsNullOrEmpty(host)) { if (string.IsNullOrEmpty(token)) { - RegistryKey key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(authTokenKeyNoPrefix, true); - key.DeleteValue(host); + var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(authTokenKeyNoPrefix, true); + key?.DeleteValue(host); } else { @@ -176,12 +176,12 @@ public void SetAuthToken(string host, string token) /// /// Not implemented because the Windows registry is deprecated /// - public string[] GlobalInstallFilters { get; set; } + public string[] GlobalInstallFilters { get; set; } = new string[] { }; /// /// Not implemented because the Windows registry is deprecated /// - public string[] PreferredHosts { get; set; } + public string?[] PreferredHosts { get; set; } = new string?[] { }; /// /// Not implemented because the Windows registry is deprecated @@ -190,7 +190,7 @@ public void SetAuthToken(string host, string token) public static bool DoesRegistryConfigurationExist() { - RegistryKey key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(CKAN_KEY_NO_PREFIX); + var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(CKAN_KEY_NO_PREFIX); return key != null; } @@ -207,12 +207,12 @@ private static string StripPrefixKey(string keyname) int firstBackslash = keyname.IndexOf(@"\"); return firstBackslash < 0 ? keyname - : keyname.Substring(1 + firstBackslash); + : keyname[(1 + firstBackslash)..]; } private static void ConstructKey(string whichKey) { - RegistryKey key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(whichKey); + var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(whichKey); if (key == null) { Microsoft.Win32.Registry.CurrentUser.CreateSubKey(whichKey); @@ -235,20 +235,20 @@ private void SetInstanceKeysTo(int instanceIndex, string name, GameInstance ksp) SetRegistryValue(@"KSPInstancePath_" + instanceIndex, ksp.GameDir()); } - private static void SetRegistryValue(string key, T value) + private static void SetRegistryValue(string key, T value) where T: notnull { Microsoft.Win32.Registry.SetValue(CKAN_KEY, key, value); } private static T GetRegistryValue(string key, T defaultValue) - { - return (T)Microsoft.Win32.Registry.GetValue(CKAN_KEY, key, defaultValue); - } + => Microsoft.Win32.Registry.GetValue(CKAN_KEY, key, null) is T v + ? v + : defaultValue; private static void DeleteRegistryValue(string name) { - RegistryKey key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(CKAN_KEY_NO_PREFIX, true); - key.DeleteValue(name, false); + var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(CKAN_KEY_NO_PREFIX, true); + key?.DeleteValue(name, false); } } } diff --git a/Core/Converters/JsonAlwaysEmptyObjectConverter.cs b/Core/Converters/JsonAlwaysEmptyObjectConverter.cs index a979e92f2d..e4672a76ea 100644 --- a/Core/Converters/JsonAlwaysEmptyObjectConverter.cs +++ b/Core/Converters/JsonAlwaysEmptyObjectConverter.cs @@ -11,7 +11,7 @@ namespace CKAN /// public class JsonAlwaysEmptyObjectConverter : JsonConverter { - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { // Read and discard this field's object (without this, loading stops!) _ = JToken.Load(reader); @@ -19,7 +19,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist } public override bool CanWrite => true; - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { serializer.Serialize(writer, new JObject()); } diff --git a/Core/Converters/JsonIgnoreBadUrlConverter.cs b/Core/Converters/JsonIgnoreBadUrlConverter.cs index c68b9393f6..3b15fb1ca7 100644 --- a/Core/Converters/JsonIgnoreBadUrlConverter.cs +++ b/Core/Converters/JsonIgnoreBadUrlConverter.cs @@ -1,5 +1,6 @@ using System; using Newtonsoft.Json; + using log4net; namespace CKAN @@ -12,10 +13,10 @@ public class JsonIgnoreBadUrlConverter : JsonConverter { private static readonly ILog log = LogManager.GetLogger(typeof(JsonIgnoreBadUrlConverter)); - public override object ReadJson( - JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson( + JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { - string value = reader.Value?.ToString(); + var value = reader.Value?.ToString(); if (value == null) { @@ -47,7 +48,7 @@ public override bool CanConvert(Type object_type) /// public override bool CanWrite => false; - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { throw new NotImplementedException(); } diff --git a/Core/Converters/JsonLeakySortedDictionaryConverter.cs b/Core/Converters/JsonLeakySortedDictionaryConverter.cs index a0c017382c..fca195e4ed 100644 --- a/Core/Converters/JsonLeakySortedDictionaryConverter.cs +++ b/Core/Converters/JsonLeakySortedDictionaryConverter.cs @@ -14,18 +14,20 @@ namespace CKAN /// Removes CkanModule objects from AvailableModule.module_version /// if License throws BadMetadataKraken. /// - public class JsonLeakySortedDictionaryConverter : JsonConverter + public class JsonLeakySortedDictionaryConverter : JsonConverter where K: class where V: class { - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { var dict = new SortedDictionary(); foreach (var kvp in JObject.Load(reader)) { try { - dict.Add( - (K)Activator.CreateInstance(typeof(K), kvp.Key), - kvp.Value.ToObject()); + if (Activator.CreateInstance(typeof(K), kvp.Key) is K k + && kvp.Value?.ToObject() is V v) + { + dict.Add(k, v); + } } catch (Exception exc) { @@ -40,7 +42,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist /// public override bool CanWrite => false; - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { throw new NotImplementedException(); } diff --git a/Core/Converters/JsonOldResourceUrlConverter.cs b/Core/Converters/JsonOldResourceUrlConverter.cs index d68ae18ef7..17ad9a65f7 100644 --- a/Core/Converters/JsonOldResourceUrlConverter.cs +++ b/Core/Converters/JsonOldResourceUrlConverter.cs @@ -18,14 +18,14 @@ public override bool CanConvert(Type object_type) return false; } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { JToken token = JToken.Load(reader); // If we've got an object, extract the URL, since that's all we use now. if (token.Type == JTokenType.Object) { - return token["url"].ToObject(); + return token["url"]?.ToObject(); } // Otherwise just return whatever we found, which we hope converts okay. :) @@ -34,7 +34,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist public override bool CanWrite => false; - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { throw new NotImplementedException(); } diff --git a/Core/Converters/JsonParallelDictionaryConverter.cs b/Core/Converters/JsonParallelDictionaryConverter.cs index 09d0c97cd6..996b57bd75 100644 --- a/Core/Converters/JsonParallelDictionaryConverter.cs +++ b/Core/Converters/JsonParallelDictionaryConverter.cs @@ -18,7 +18,7 @@ public class JsonParallelDictionaryConverter : JsonConverter { public override object ReadJson(JsonReader reader, Type objectType, - object existingValue, + object? existingValue, JsonSerializer serializer) => ParseWithProgress(JObject.Load(reader) .Properties() @@ -30,7 +30,7 @@ private object ParseWithProgress(JProperty[] properties, => Partitioner.Create(properties, true) .AsParallel() .WithMergeOptions(ParallelMergeOptions.NotBuffered) - .Select(prop => new KeyValuePair( + .Select(prop => new KeyValuePair( prop.Name, prop.Value.ToObject())) .WithProgress(properties.Length, @@ -38,7 +38,7 @@ private object ParseWithProgress(JProperty[] properties, .ToDictionary(); public override bool CanWrite => false; - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { throw new NotImplementedException(); } diff --git a/Core/Converters/JsonPropertyNamesChangedConverter.cs b/Core/Converters/JsonPropertyNamesChangedConverter.cs index 1c169102d4..fe6ee91939 100644 --- a/Core/Converters/JsonPropertyNamesChangedConverter.cs +++ b/Core/Converters/JsonPropertyNamesChangedConverter.cs @@ -26,7 +26,7 @@ public abstract class JsonPropertyNamesChangedConverter : JsonConverter /// The object writing JSON to disk /// A value to be written for this class /// Generates output objects from tokens - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { throw new NotImplementedException(); } @@ -46,48 +46,44 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s /// Not used /// Generates output objects from tokens /// Class object populated according to the renaming scheme - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { - object instance = Activator.CreateInstance(objectType); + var instance = Activator.CreateInstance(objectType); JObject jo = JObject.Load(reader); var changes = mapping; foreach (JProperty jp in jo.Properties()) { - if (!changes.TryGetValue(jp.Name, out string name)) + if (!changes.TryGetValue(jp.Name, out string? name)) { name = jp.Name; } - PropertyInfo prop = objectType.GetTypeInfo().DeclaredProperties.FirstOrDefault(pi => - pi.CanWrite - && (pi.GetCustomAttribute()?.PropertyName ?? pi.Name) == name); - if (prop != null) + if (objectType.GetTypeInfo().DeclaredProperties.FirstOrDefault(pi => + pi.CanWrite + && (pi.GetCustomAttribute()?.PropertyName ?? pi.Name) == name) is PropertyInfo prop + && GetValue(prop.GetCustomAttribute(), jp.Value, prop.PropertyType, serializer) is object obj) { - prop.SetValue(instance, - GetValue(prop.GetCustomAttribute(), - jp.Value, prop.PropertyType, serializer)); + prop.SetValue(instance, obj); } - else + // No property, maybe there's a field + else if (objectType.GetTypeInfo().DeclaredFields.FirstOrDefault(fi => + (fi.GetCustomAttribute()?.PropertyName ?? fi.Name) == name) is FieldInfo field + && GetValue(field.GetCustomAttribute(), jp.Value, field.FieldType, serializer) is object obj2) { - // No property, maybe there's a field - FieldInfo field = objectType.GetTypeInfo().DeclaredFields.FirstOrDefault(fi => - (fi.GetCustomAttribute()?.PropertyName ?? fi.Name) == name); - field?.SetValue(instance, - GetValue(field.GetCustomAttribute(), - jp.Value, field.FieldType, serializer)); + field.SetValue(instance, obj2); } } return instance; } - private static object GetValue(JsonConverterAttribute attrib, - JToken value, Type outputType, JsonSerializer serializer) - => attrib != null ? ApplyConverter((JsonConverter)Activator.CreateInstance(attrib.ConverterType, - attrib.ConverterParameters), - value, outputType, serializer) - : value.ToObject(outputType, serializer); + private static object? GetValue(JsonConverterAttribute? attrib, + JToken value, Type outputType, JsonSerializer serializer) + => attrib != null + && Activator.CreateInstance(attrib.ConverterType, attrib.ConverterParameters) is JsonConverter conv + ? ApplyConverter(conv, value, outputType, serializer) + : value.ToObject(outputType, serializer); - private static object ApplyConverter(JsonConverter converter, - JToken value, Type outputType, JsonSerializer serializer) + private static object? ApplyConverter(JsonConverter converter, + JToken value, Type outputType, JsonSerializer serializer) => converter.CanRead ? converter.ReadJson(new JTokenReader(value), outputType, null, serializer) : value.ToObject(outputType, serializer); diff --git a/Core/Converters/JsonRelationshipConverter.cs b/Core/Converters/JsonRelationshipConverter.cs index 7d5ca3a553..a96386b52d 100644 --- a/Core/Converters/JsonRelationshipConverter.cs +++ b/Core/Converters/JsonRelationshipConverter.cs @@ -14,7 +14,7 @@ public override bool CanConvert(Type object_type) return false; } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { JToken token = JToken.Load(reader); if (token.Type == JTokenType.Array) @@ -33,11 +33,19 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist Properties.Resources.JsonRelationshipConverterAnyOfCombined, forbiddenPropertyName)); } } - rels.Add(child.ToObject()); + if (child.ToObject() + is AnyOfRelationshipDescriptor rel) + { + rels.Add(rel); + } } else if (child["name"] != null) { - rels.Add(child.ToObject()); + if (child.ToObject() + is ModuleRelationshipDescriptor rel) + { + rels.Add(rel); + } } } @@ -48,7 +56,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist public override bool CanWrite => false; - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { throw new NotImplementedException(); } diff --git a/Core/Converters/JsonSimpleStringConverter.cs b/Core/Converters/JsonSimpleStringConverter.cs index 624baf0708..53f3a7c526 100644 --- a/Core/Converters/JsonSimpleStringConverter.cs +++ b/Core/Converters/JsonSimpleStringConverter.cs @@ -10,12 +10,12 @@ namespace CKAN /// public class JsonSimpleStringConverter : JsonConverter { - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { - writer.WriteValue(value.ToString()); + writer.WriteValue(value?.ToString()); } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { // If we find a null, then that might be okay, so we pass it down to our // activator. Otherwise we convert to string, since that's our job. diff --git a/Core/Converters/JsonSingleOrArrayConverter.cs b/Core/Converters/JsonSingleOrArrayConverter.cs index 7f760ed1ee..4b2f4a5092 100644 --- a/Core/Converters/JsonSingleOrArrayConverter.cs +++ b/Core/Converters/JsonSingleOrArrayConverter.cs @@ -7,12 +7,12 @@ namespace CKAN { /// - /// With thanks to + /// With thanks to /// https://stackoverflow.com/questions/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n /// public class JsonSingleOrArrayConverter : JsonConverter { - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { JToken token = JToken.Load(reader); if (token.Type == JTokenType.Array) @@ -21,12 +21,12 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist } // If the object is null, we'll return null. Otherwise end up with a list of null. - return token.ToObject() == null ? null : new List { token.ToObject() }; + return token.ToObject() == null ? null : new List { token.ToObject() }; } public override bool CanWrite => true; - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { var list = value as List; serializer.Serialize(writer, list?.Count == 1 ? list[0] : value); diff --git a/Core/Converters/JsonToGamesDictionaryConverter.cs b/Core/Converters/JsonToGamesDictionaryConverter.cs index 0b57f9ca6b..f63ec570d1 100644 --- a/Core/Converters/JsonToGamesDictionaryConverter.cs +++ b/Core/Converters/JsonToGamesDictionaryConverter.cs @@ -50,7 +50,7 @@ public class JsonToGamesDictionaryConverter : JsonConverter /// Not used /// Generates output objects from tokens /// Dictionary of type matching the property where this converter was used, containing game-specific keys and values - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { var token = JToken.Load(reader); if (token.Type == JTokenType.Object) @@ -58,16 +58,17 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist return token.ToObject(objectType); } var valueType = objectType.GetGenericArguments()[1]; - var obj = (IDictionary)Activator.CreateInstance(objectType); - if (!IsTokenEmpty(token)) + if (Activator.CreateInstance(objectType) is IDictionary obj + && !IsTokenEmpty(token)) { foreach (var gameName in KnownGames.AllGameShortNames()) { // Make a new copy of the value for each game obj.Add(gameName, token.ToObject(valueType)); } + return obj; } - return obj; + return null; } /// @@ -81,7 +82,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist /// The object writing JSON to disk /// A value to be written for this class /// Generates output objects from tokens - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { throw new NotImplementedException(); } diff --git a/Core/DirectoryLink.cs b/Core/DirectoryLink.cs index 10f4e8dcd2..a87d21207f 100644 --- a/Core/DirectoryLink.cs +++ b/Core/DirectoryLink.cs @@ -52,7 +52,7 @@ private static bool CreateJunction(string link, string target, TxFileManager txM return false; } - public static bool TryGetTarget(string link, out string target) + public static bool TryGetTarget(string link, out string? target) { target = null; var fi = new DirectoryInfo(link); @@ -132,7 +132,7 @@ private static extern bool DeviceIoControl(SafeFileHandle hDevice, uint IoControlCode, ref ReparseDataBuffer InBuffer, int nInBufferSize, - byte[] OutBuffer, + byte[]? OutBuffer, int nOutBufferSize, out int pBytesReturned, IntPtr Overlapped); @@ -140,7 +140,7 @@ private static extern bool DeviceIoControl(SafeFileHandle hDevice, [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] private static extern bool DeviceIoControl(SafeFileHandle hDevice, uint IoControlCode, - byte[] InBuffer, + byte[]? InBuffer, int nInBufferSize, out ReparseDataBuffer OutBuffer, int nOutBufferSize, diff --git a/Core/Exporters/DelimeterSeparatedValueExporter.cs b/Core/Exporters/DelimeterSeparatedValueExporter.cs index c172d7cb80..c6c3d462d7 100644 --- a/Core/Exporters/DelimeterSeparatedValueExporter.cs +++ b/Core/Exporters/DelimeterSeparatedValueExporter.cs @@ -63,7 +63,7 @@ public void Export(IRegistryQuerier registry, Stream stream) QuoteIfNecessary(mod.Module.name), QuoteIfNecessary(mod.Module.@abstract), QuoteIfNecessary(mod.Module.description), - QuoteIfNecessary(string.Join(";", mod.Module.author)), + QuoteIfNecessary(mod.Module.author == null ? "" : string.Join(";", mod.Module.author)), QuoteIfNecessary(mod.Module.kind), WriteUri(mod.Module.download?[0]), mod.Module.download_size, @@ -82,45 +82,47 @@ public void Export(IRegistryQuerier registry, Stream stream) } } - private string WriteUri(Uri uri) + private string WriteUri(Uri? uri) => uri != null ? QuoteIfNecessary(uri.ToString()) : string.Empty; - private string WriteRepository(ResourcesDescriptor resources) + private string WriteRepository(ResourcesDescriptor? resources) => resources != null && resources.repository != null ? QuoteIfNecessary(resources.repository.ToString()) : string.Empty; - private string WriteHomepage(ResourcesDescriptor resources) + private string WriteHomepage(ResourcesDescriptor? resources) => resources != null && resources.homepage != null ? QuoteIfNecessary(resources.homepage.ToString()) : string.Empty; - private string WriteBugtracker(ResourcesDescriptor resources) + private string WriteBugtracker(ResourcesDescriptor? resources) => resources != null && resources.bugtracker != null ? QuoteIfNecessary(resources.bugtracker.ToString()) : string.Empty; - private string WriteDiscussions(ResourcesDescriptor resources) + private string WriteDiscussions(ResourcesDescriptor? resources) => resources != null && resources.discussions != null ? QuoteIfNecessary(resources.discussions.ToString()) : string.Empty; - private string WriteSpaceDock(ResourcesDescriptor resources) + private string WriteSpaceDock(ResourcesDescriptor? resources) => resources != null && resources.spacedock != null ? QuoteIfNecessary(resources.spacedock.ToString()) : string.Empty; - private string WriteCurse(ResourcesDescriptor resources) + private string WriteCurse(ResourcesDescriptor? resources) => resources != null && resources.curse != null ? QuoteIfNecessary(resources.curse.ToString()) : string.Empty; - private string QuoteIfNecessary(string value) - => value != null && value.IndexOf(_delimeter, StringComparison.Ordinal) >= 0 - ? "\"" + value + "\"" - : value; + private string QuoteIfNecessary(string? value) + => value == null + ? "" + : value.IndexOf(_delimeter, StringComparison.Ordinal) >= 0 + ? "\"" + value + "\"" + : value; public enum Delimeter { diff --git a/Core/Extensions/CryptoExtensions.cs b/Core/Extensions/CryptoExtensions.cs index 6c9dd44c1e..ed9aab8246 100644 --- a/Core/Extensions/CryptoExtensions.cs +++ b/Core/Extensions/CryptoExtensions.cs @@ -22,8 +22,8 @@ public static class CryptoExtensions /// The requested hash of the input stream public static byte[] ComputeHash(this HashAlgorithm hashAlgo, Stream stream, - IProgress progress, - CancellationToken cancelToken = default) + IProgress? progress, + CancellationToken? cancelToken = default) { const int bufSize = 1024 * 1024; var buffer = new byte[bufSize]; @@ -31,13 +31,13 @@ public static byte[] ComputeHash(this HashAlgorithm hashAlgo, while (true) { var bytesRead = stream.Read(buffer, 0, bufSize); - cancelToken.ThrowIfCancellationRequested(); + cancelToken?.ThrowIfCancellationRequested(); if (bytesRead < bufSize) { // Done! hashAlgo.TransformFinalBlock(buffer, 0, bytesRead); - progress.Report(100); + progress?.Report(100); break; } else @@ -46,9 +46,9 @@ public static byte[] ComputeHash(this HashAlgorithm hashAlgo, } totalBytesRead += bytesRead; - progress.Report((int)(100 * totalBytesRead / stream.Length)); + progress?.Report((int)(100 * totalBytesRead / stream.Length)); } - return hashAlgo.Hash; + return hashAlgo.Hash ?? Array.Empty(); } } } diff --git a/Core/Extensions/DictionaryExtensions.cs b/Core/Extensions/DictionaryExtensions.cs index ec7b932985..53fcefd036 100644 --- a/Core/Extensions/DictionaryExtensions.cs +++ b/Core/Extensions/DictionaryExtensions.cs @@ -5,17 +5,17 @@ namespace CKAN.Extensions { public static class DictionaryExtensions { - public static bool DictionaryEquals(this IDictionary a, - IDictionary b) + public static bool DictionaryEquals(this IDictionary? a, + IDictionary? b) => a == null ? b == null : b != null && a.Count == b.Count && a.Keys.All(k => b.ContainsKey(k)) && b.Keys.All(k => a.ContainsKey(k) && EqualityComparer.Default.Equals(a[k], b[k])); - public static V GetOrDefault(this Dictionary dict, K key) + public static V? GetOrDefault(this Dictionary dict, K key) where K : notnull { - V val = default; + V? val = default; if (key != null) { dict.TryGetValue(key, out val); diff --git a/Core/Extensions/EnumerableExtensions.cs b/Core/Extensions/EnumerableExtensions.cs index 00ed174070..76dd1da99c 100644 --- a/Core/Extensions/EnumerableExtensions.cs +++ b/Core/Extensions/EnumerableExtensions.cs @@ -57,15 +57,19 @@ public static IEnumerable Append(this IEnumerable source, T next) #endif - public static Dictionary ToDictionary(this IEnumerable> pairs) +#if NETFRAMEWORK || NETSTANDARD || NET5_0 || NET6_0 || NET7_0 + + public static Dictionary ToDictionary(this IEnumerable> pairs) where K: class => pairs.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - public static Dictionary ToDictionary(this ParallelQuery> pairs) + public static Dictionary ToDictionary(this ParallelQuery> pairs) where K: class => pairs.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - public static ConcurrentDictionary ToConcurrentDictionary(this IEnumerable> pairs) +#endif + + public static ConcurrentDictionary ToConcurrentDictionary(this IEnumerable> pairs) where K: class => new ConcurrentDictionary(pairs); public static IEnumerable AsParallelIf(this IEnumerable source, @@ -76,7 +80,7 @@ public static IEnumerable AsParallelIf(this IEnumerable source, // https://stackoverflow.com/a/55591477/2422988 public static ParallelQuery WithProgress(this ParallelQuery source, long totalCount, - IProgress progress) + IProgress? progress) { long count = 0; int prevPercent = -1; @@ -98,7 +102,7 @@ public static IEnumerable Memoize(this IEnumerable source) { if (source == null) { - throw new ArgumentNullException("source"); + throw new ArgumentNullException(nameof(source)); } else if (source is Memoized) { @@ -111,9 +115,8 @@ public static IEnumerable Memoize(this IEnumerable source) } } - public static void RemoveWhere( - this Dictionary source, - Func, bool> predicate) + public static void RemoveWhere(this Dictionary source, + Func, bool> predicate) where K: class { var pairs = source.ToList(); foreach (var kvp in pairs) @@ -166,9 +169,9 @@ public static IEnumerable DistinctBy(this IEnumerable seq, FuncThe first node /// Function to go from one node to the next /// All the nodes in the list as a sequence - public static IEnumerable TraverseNodes(this T start, Func getNext) + public static IEnumerable TraverseNodes(this T start, Func getNext) { - for (T t = start; t != null; t = getNext(t)) + for (T? t = start; t != null; t = getNext(t)) { yield return t; } @@ -256,8 +259,8 @@ public static void Deconstruct(this KeyValuePair kvp, out T1 key /// Pattern to match /// Sequence of Match objects public static IEnumerable WithMatches(this IEnumerable source, Regex pattern) - => source.Select(val => pattern.TryMatch(val, out Match match) ? match : null) - .Where(m => m != null); + => source.Select(val => pattern.TryMatch(val, out Match? match) ? match : null) + .OfType(); } @@ -272,6 +275,9 @@ public Memoized(IEnumerable source) this.source = source; } + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + public IEnumerator GetEnumerator() { lock (gate) @@ -280,32 +286,31 @@ public IEnumerator GetEnumerator() { return cache.GetEnumerator(); } - else if (enumerator == null) + else { - enumerator = source.GetEnumerator(); + enumerator ??= source.GetEnumerator(); } } return GetMemoizingEnumerator(); } - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - private IEnumerator GetMemoizingEnumerator() { - for (int index = 0; TryGetItem(index, out T item); ++index) + for (int index = 0; TryGetItem(index, out T? item); ++index) { yield return item; } } - private bool TryGetItem(int index, out T item) + private bool TryGetItem(int index, + out T item) { lock (gate) { - if (!IsItemInCache(index)) + if (enumerator is not null && !IsItemInCache(index)) { // The iteration may have completed while waiting for the lock + #nullable disable if (isCacheComplete) { item = default; @@ -318,6 +323,7 @@ private bool TryGetItem(int index, out T item) enumerator.Dispose(); return false; } + #nullable enable cache.Add(enumerator.Current); } item = cache[index]; @@ -328,10 +334,10 @@ private bool TryGetItem(int index, out T item) private bool IsItemInCache(int index) => index < cache.Count; - private readonly IEnumerable source; - private IEnumerator enumerator; - private readonly List cache = new List(); - private bool isCacheComplete; - private readonly object gate = new object(); + private readonly IEnumerable source; + private IEnumerator? enumerator; + private readonly List cache = new List(); + private bool isCacheComplete; + private readonly object gate = new object(); } } diff --git a/Core/Extensions/I18nExtensions.cs b/Core/Extensions/I18nExtensions.cs index 78943b94ca..82b14a6a85 100644 --- a/Core/Extensions/I18nExtensions.cs +++ b/Core/Extensions/I18nExtensions.cs @@ -10,10 +10,11 @@ public static class I18nExtensions public static string Localize(this Enum val) => val.GetType() - .GetMember(val.ToString()) - .FirstOrDefault()? - .GetCustomAttribute() - .GetDescription(); + ?.GetMember(val.ToString()) + ?.First() + .GetCustomAttribute() + ?.GetDescription() + ?? ""; } } diff --git a/Core/Extensions/IOExtensions.cs b/Core/Extensions/IOExtensions.cs index b55daffc82..3d7415e277 100644 --- a/Core/Extensions/IOExtensions.cs +++ b/Core/Extensions/IOExtensions.cs @@ -35,7 +35,7 @@ public static void CopyTo(this Stream src, long total = 0; var lastProgressTime = DateTime.Now; // Sometimes a server just freezes and times out, send extra updates if requested - Timer timer = null; + Timer? timer = null; if (idleInterval.HasValue) { timer = new Timer(idleInterval.Value > minProgressInterval diff --git a/Core/Extensions/RegexExtensions.cs b/Core/Extensions/RegexExtensions.cs index 22e6ee58b6..d4f14101b1 100644 --- a/Core/Extensions/RegexExtensions.cs +++ b/Core/Extensions/RegexExtensions.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using System.Diagnostics.CodeAnalysis; namespace CKAN.Extensions { @@ -11,7 +12,8 @@ public static class RegexExtensions /// The string to check /// Object representing the match, if any /// True if the regex matched the value, false otherwise - public static bool TryMatch(this Regex regex, string value, out Match match) + public static bool TryMatch(this Regex regex, string value, + [NotNullWhen(returnValue: true)] out Match? match) { if (value == null) { diff --git a/Core/GameInstance.cs b/Core/GameInstance.cs index 13717b8d16..7d82f20c0f 100644 --- a/Core/GameInstance.cs +++ b/Core/GameInstance.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Text.RegularExpressions; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using ChinhDo.Transactions.FileManager; using log4net; @@ -27,10 +28,10 @@ public class GameInstance : IEquatable private readonly string gameDir; public readonly IGame game; - private GameVersion version; + private GameVersion? version; private List _compatibleVersions = new List(); - public TimeLog playTime; + public TimeLog? playTime; public string Name { get; set; } @@ -38,7 +39,7 @@ public class GameInstance : IEquatable /// Returns a file system safe version of the instance name that can be used within file names. /// public string SanitizedName => string.Join("", Name.Split(Path.GetInvalidFileNameChars())); - public GameVersion GameVersionWhenCompatibleVersionsWereStored { get; private set; } + public GameVersion? GameVersionWhenCompatibleVersionsWereStored { get; private set; } public bool CompatibleVersionsAreFromDifferentGameVersion => _compatibleVersions.Count > 0 && GameVersionWhenCompatibleVersionsWereStored != Version(); @@ -54,11 +55,11 @@ public bool CompatibleVersionsAreFromDifferentGameVersion /// 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) + public GameInstance(IGame game, string gameDir, string name, IUser? user) { this.game = game; Name = name; - User = user; + User = user ?? new NullUser(); // Make sure our path is absolute and has normalised slashes. this.gameDir = CKANPathUtils.NormalizePath(Path.GetFullPath(gameDir)); if (Platform.IsWindows) @@ -96,6 +97,7 @@ public GameInstance(IGame game, string gameDir, string name, IUser user) /// /// Create the CKAN directory and any supporting files. /// + [MemberNotNull(nameof(playTime))] private void SetupCkanDirectories() { log.InfoFormat("Initialising {0}", CkanDir()); @@ -139,7 +141,9 @@ private void SaveCompatibleVersions() JsonConvert.SerializeObject(new CompatibleGameVersions() { GameVersionWhenWritten = Version()?.ToString(), - Versions = _compatibleVersions.Select(v => v.ToString()).ToList() + Versions = _compatibleVersions.Select(v => v.ToString()) + .OfType() + .ToList() }) ); GameVersionWhenCompatibleVersionsWereStored = Version(); @@ -148,15 +152,16 @@ private void SaveCompatibleVersions() private void LoadCompatibleVersions() { string path = CompatibleGameVersionsFile(); - if (File.Exists(path)) + if (File.Exists(path) + && JsonConvert.DeserializeObject(File.ReadAllText(path)) + is CompatibleGameVersions compatibleGameVersions) { - CompatibleGameVersions compatibleGameVersions = JsonConvert.DeserializeObject(File.ReadAllText(path)); - _compatibleVersions = compatibleGameVersions.Versions - .Select(v => GameVersion.Parse(v)).ToList(); + .Select(v => GameVersion.Parse(v)) + .ToList(); // Get version without throwing exceptions for null - GameVersion.TryParse(compatibleGameVersions.GameVersionWhenWritten, out GameVersion mainVer); + GameVersion.TryParse(compatibleGameVersions.GameVersionWhenWritten, out GameVersion? mainVer); GameVersionWhenCompatibleVersionsWereStored = mainVer; } } @@ -167,8 +172,9 @@ private string CompatibleGameVersionsFile() public List GetCompatibleVersions() => new List(_compatibleVersions); - public HashSet GetSuppressedCompatWarningIdentifiers => - SuppressedCompatWarningIdentifiers.LoadFrom(Version(), SuppressedCompatWarningIdentifiersFile).Identifiers; + public HashSet GetSuppressedCompatWarningIdentifiers + => SuppressedCompatWarningIdentifiers.LoadFrom(Version(), SuppressedCompatWarningIdentifiersFile) + .Identifiers; public void AddSuppressedCompatWarningIdentifiers(HashSet idents) { @@ -177,14 +183,15 @@ public void AddSuppressedCompatWarningIdentifiers(HashSet idents) scwi.SaveTo(SuppressedCompatWarningIdentifiersFile); } - private string SuppressedCompatWarningIdentifiersFile => - Path.Combine(CkanDir(), "suppressed_compat_warning_identifiers.json"); + private string SuppressedCompatWarningIdentifiersFile + => Path.Combine(CkanDir(), "suppressed_compat_warning_identifiers.json"); public string[] InstallFilters { - get => File.Exists(InstallFiltersFile) - ? JsonConvert.DeserializeObject(File.ReadAllText(InstallFiltersFile)) - : Array.Empty(); + get => (File.Exists(InstallFiltersFile) + ? JsonConvert.DeserializeObject(File.ReadAllText(InstallFiltersFile)) + : null) + ?? Array.Empty(); #pragma warning disable IDE0027 set @@ -205,7 +212,7 @@ public string[] InstallFilters /// directory as the game, or if the game is in the current directory. /// Otherwise, returns null. /// - public static string PortableDir(IGame game) + public static string? PortableDir(IGame game) { string curDir = Directory.GetCurrentDirectory(); @@ -219,10 +226,11 @@ public static string PortableDir(IGame game) } // Find the directory our executable is stored in. - string exeDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - if (string.IsNullOrEmpty(exeDir)) + var exeDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + if (string.IsNullOrEmpty(exeDir) + && Process.GetCurrentProcess()?.MainModule?.FileName is string s) { - exeDir = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName); + exeDir = Path.GetDirectoryName(s); if (string.IsNullOrEmpty(exeDir)) { log.InfoFormat("Executing assembly path and main module path not found"); @@ -234,7 +242,8 @@ public static string PortableDir(IGame game) log.DebugFormat("Checking if {0} is in my exe dir: {1}", game.ShortName, exeDir); - if (curDir != exeDir && game.GameInFolder(new DirectoryInfo(exeDir))) + if (curDir != exeDir && exeDir != null + && game.GameInFolder(new DirectoryInfo(exeDir))) { log.InfoFormat("{0} found at {1}", game.ShortName, exeDir); return exeDir; @@ -246,9 +255,9 @@ public static string PortableDir(IGame game) /// /// Detects the version of a game in a given directory. /// - private GameVersion DetectVersion(string directory) + private GameVersion? DetectVersion(string directory) { - GameVersion version = game.DetectVersion(new DirectoryInfo(directory)); + var version = game.DetectVersion(new DirectoryInfo(directory)); if (version != null) { log.DebugFormat("Found version {0}", version); @@ -285,12 +294,9 @@ public IOrderedEnumerable InstallHistoryFiles() .Select(f => new FileInfo(f)) .OrderByDescending(fi => fi.CreationTime); - public GameVersion Version() + public GameVersion? Version() { - if (version == null) - { - version = DetectVersion(GameDir()); - } + version ??= DetectVersion(GameDir()); return version; } @@ -332,7 +338,7 @@ public string ToAbsoluteGameDir(string path) /// /// Identifier if found otherwise null /// - public string DllPathToIdentifier(string relative_path) + public string? DllPathToIdentifier(string relative_path) { var paths = Enumerable.Repeat(game.PrimaryModDirectoryRelative, 1) .Concat(game.AlternateModDirectoriesRelative); @@ -342,9 +348,8 @@ public string DllPathToIdentifier(string relative_path) return null; } Match match = dllPattern.Match(relative_path); - return match.Success - ? Identifier.Sanitize(match.Groups["identifier"].Value) - : null; + return match.Success ? Identifier.Sanitize(match.Groups["identifier"].Value) + : null; } /// @@ -373,7 +378,7 @@ public bool HasManagedFiles(Registry registry, string absPath) && Directory.EnumerateFileSystemEntries(absPath, "*", SearchOption.AllDirectories) .Any(f => registry.FileOwner(ToRelativeGameDir(f)) != null)); - public void PlayGame(string command, Action onExit = null) + public void PlayGame(string command, Action? onExit = null) { var split = (command ?? "").Split(' '); if (split.Length == 0) @@ -400,12 +405,12 @@ public void PlayGame(string command, Action onExit = null) p.Exited += (sender, e) => { - playTime.Stop(CkanDir()); + playTime?.Stop(CkanDir()); onExit?.Invoke(); }; p.Start(); - playTime.Start(); + playTime?.Start(); } catch (Exception exception) { @@ -416,10 +421,10 @@ public void PlayGame(string command, Action onExit = null) public override string ToString() => string.Format(Properties.Resources.GameInstanceToString, game.ShortName, gameDir); - public bool Equals(GameInstance other) + public bool Equals(GameInstance? other) => other != null && gameDir.Equals(other.GameDir()); - public override bool Equals(object obj) + public override bool Equals(object? obj) => Equals(obj as GameInstance); public override int GetHashCode() diff --git a/Core/GameInstanceManager.cs b/Core/GameInstanceManager.cs index f8ccd759a2..12a5326dba 100644 --- a/Core/GameInstanceManager.cs +++ b/Core/GameInstanceManager.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Transactions; +using System.Diagnostics.CodeAnalysis; using Autofac; using ChinhDo.Transactions.FileManager; @@ -27,11 +28,11 @@ public class GameInstanceManager : IDisposable /// It is initialized during the startup with a ConsoleUser, /// do not use in functions that could be called by the GUI. /// - public IUser User { get; set; } - public IConfiguration Configuration { get; set; } - public GameInstance CurrentInstance { get; set; } + public IUser User { get; set; } + public IConfiguration Configuration { get; set; } + public GameInstance? CurrentInstance { get; set; } - public NetModuleCache Cache { get; private set; } + public NetModuleCache? Cache { get; private set; } public readonly SteamLibrary SteamLibrary = new SteamLibrary(); @@ -44,14 +45,15 @@ public class GameInstanceManager : IDisposable .Distinct() .ToArray(); - public string AutoStartInstance + public string? AutoStartInstance { - get => HasInstance(Configuration.AutoStartInstance) - ? Configuration.AutoStartInstance - : null; + get => Configuration.AutoStartInstance != null && HasInstance(Configuration.AutoStartInstance) + ? Configuration.AutoStartInstance + : null; + private set { - if (!string.IsNullOrEmpty(value) && !HasInstance(value)) + if (value != null && !string.IsNullOrEmpty(value) && !HasInstance(value)) { throw new InvalidKSPInstanceKraken(value); } @@ -61,7 +63,7 @@ private set public SortedList Instances => new SortedList(instances); - public GameInstanceManager(IUser user, IConfiguration configuration = null) + public GameInstanceManager(IUser user, IConfiguration? configuration = null) { User = user; Configuration = configuration ?? ServiceLocator.Container.Resolve(); @@ -84,14 +86,14 @@ public GameInstanceManager(IUser user, IConfiguration configuration = null) /// /// Returns null if we have multiple instances, but none of them are preferred. /// - public GameInstance GetPreferredInstance() + public GameInstance? GetPreferredInstance() { CurrentInstance = _GetPreferredInstance(); return CurrentInstance; } // Actual worker for GetPreferredInstance() - internal GameInstance _GetPreferredInstance() + internal GameInstance? _GetPreferredInstance() { foreach (IGame game in KnownGames.knownGames) { @@ -99,7 +101,7 @@ internal GameInstance _GetPreferredInstance() // First check if we're part of a portable install // Note that this *does not* register in the config. - string path = GameInstance.PortableDir(game); + string? path = GameInstance.PortableDir(game); if (path != null) { @@ -121,8 +123,9 @@ internal GameInstance _GetPreferredInstance() // Return the autostart, if we can find it. // We check both null and "" as we can't write NULL to the config, so we write an empty string instead // This is necessary so we can indicate that the user wants to reset the current AutoStartInstance without clearing the config! - if (!string.IsNullOrEmpty(AutoStartInstance) - && instances[AutoStartInstance].Valid) + if (AutoStartInstance != null + && !string.IsNullOrEmpty(AutoStartInstance) + && instances[AutoStartInstance].Valid) { return instances[AutoStartInstance]; } @@ -140,7 +143,7 @@ internal GameInstance _GetPreferredInstance() /// /// Returns the resulting game instance if found. /// - public GameInstance FindAndRegisterDefaultInstances() + public GameInstance? FindAndRegisterDefaultInstances() { if (instances.Any()) { @@ -163,12 +166,15 @@ public GameInstance[] FindDefaultInstances() .Select(sg => new { name = sg.Name, dir = sg.GameDir }) .Append(new { - name = string.Format(Properties.Resources.GameInstanceManagerAuto, + name = (string?)string.Format(Properties.Resources.GameInstanceManagerAuto, g.ShortName), dir = g.MacPath(), }) - .Where(obj => obj.dir != null && g.GameInFolder(obj.dir)) - .Select(obj => new GameInstance(g, obj.dir.FullName, obj.name, User))) + .Select(obj => obj.dir != null && g.GameInFolder(obj.dir) + ? new GameInstance(g, obj.dir.FullName, + obj.name ?? g.ShortName, User) + : null) + .OfType()) .Where(inst => inst.Valid) .ToArray(); foreach (var group in found.GroupBy(inst => inst.Name)) @@ -222,7 +228,7 @@ public GameInstance AddInstance(GameInstance instance) /// IUser object for interaction /// The resulting GameInstance object /// Thrown if the instance is not a valid game instance. - public GameInstance AddInstance(string path, string name, IUser user) + public GameInstance? AddInstance(string path, string name, IUser user) { var game = DetermineGame(new DirectoryInfo(path), user); return game == null ? null : AddInstance(new GameInstance(game, path, name, user)); @@ -275,7 +281,7 @@ public void CloneInstance(GameInstance existingInstance, /// 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) + Dictionary? dlcs = null) { TxFileManager fileMgr = new TxFileManager(); using (TransactionScope transaction = CkanTransaction.CreateTransactionScope()) @@ -479,7 +485,7 @@ public void SetCurrentInstanceByPath(string path) } } - public GameInstance InstanceAt(string path) + public GameInstance? InstanceAt(string path) { var matchingGames = KnownGames.knownGames .Where(g => g.GameInFolder(new DirectoryInfo(path))) @@ -554,7 +560,7 @@ private void LoadInstances() private void LoadCacheSettings() { - if (!Directory.Exists(Configuration.DownloadCacheDir)) + if (Configuration.DownloadCacheDir != null && !Directory.Exists(Configuration.DownloadCacheDir)) { try { @@ -564,11 +570,14 @@ private void LoadCacheSettings() { // Can't create the configured directory, try reverting it to the default Configuration.DownloadCacheDir = null; - Directory.CreateDirectory(Configuration.DownloadCacheDir); + if (Configuration.DownloadCacheDir is not null) + { + Directory.CreateDirectory(Configuration.DownloadCacheDir); + } } } - if (!TrySetupCache(Configuration.DownloadCacheDir, out string failReason)) + if (!TrySetupCache(Configuration.DownloadCacheDir, out string? failReason)) { log.ErrorFormat("Cache not found at configured path {0}: {1}", Configuration.DownloadCacheDir, failReason); // Fall back to default path to minimize chance of ending up in an invalid state at startup @@ -584,12 +593,13 @@ private void LoadCacheSettings() /// /// true if successful, false otherwise /// - public bool TrySetupCache(string path, out string failureReason) + public bool TrySetupCache(string? path, + [NotNullWhen(returnValue: false)] out string? failureReason) { - string origPath = Configuration.DownloadCacheDir; + var origPath = Configuration.DownloadCacheDir; try { - if (string.IsNullOrEmpty(path)) + if (path == null || string.IsNullOrEmpty(path)) { Configuration.DownloadCacheDir = ""; Cache = new NetModuleCache(this, Configuration.DownloadCacheDir); @@ -646,7 +656,7 @@ public static bool IsGameInstanceDir(DirectoryInfo path) /// IUser object for interaction /// An instance of the matching game or null if the user cancelled /// Thrown when no games found - public IGame DetermineGame(DirectoryInfo path, IUser user) + public IGame? DetermineGame(DirectoryInfo path, IUser user) { var matchingGames = KnownGames.knownGames.Where(g => g.GameInFolder(path)).ToList(); switch (matchingGames.Count) diff --git a/Core/Games/IGame.cs b/Core/Games/IGame.cs index 93f9c25ded..864c46f6c2 100644 --- a/Core/Games/IGame.cs +++ b/Core/Games/IGame.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json.Linq; @@ -17,8 +18,8 @@ public interface IGame DateTime FirstReleaseDate { get; } // Where are we? - bool GameInFolder(DirectoryInfo where); - DirectoryInfo MacPath(); + bool GameInFolder(DirectoryInfo where); + DirectoryInfo? MacPath(); // What do we contain? string PrimaryModDirectoryRelative { get; } @@ -30,10 +31,10 @@ public interface IGame string[] CreateableDirs { get; } string[] AutoRemovableDirs { get; } bool IsReservedDirectory(GameInstance inst, string path); - bool AllowInstallationIn(string name, out string path); + bool AllowInstallationIn(string name, [NotNullWhen(returnValue: true)] out string? path); void RebuildSubdirectories(string absGameRoot); string[] DefaultCommandLines(SteamLibrary steamLib, DirectoryInfo path); - string[] AdjustCommandLine(string[] args, GameVersion installedVersion); + string[] AdjustCommandLine(string[] args, GameVersion? installedVersion); IDlcDetector[] DlcDetectors { get; } // Which versions exist and which is present? @@ -41,7 +42,7 @@ public interface IGame List KnownVersions { get; } GameVersion[] EmbeddedGameVersions { get; } GameVersion[] ParseBuildsJson(JToken json); - GameVersion DetectVersion(DirectoryInfo where); + GameVersion? DetectVersion(DirectoryInfo where); string CompatibleVersionsFile { get; } string[] InstanceAnchorFiles { get; } diff --git a/Core/Games/KerbalSpaceProgram.cs b/Core/Games/KerbalSpaceProgram.cs index a7651306b0..5ce01a025e 100644 --- a/Core/Games/KerbalSpaceProgram.cs +++ b/Core/Games/KerbalSpaceProgram.cs @@ -3,6 +3,7 @@ using System.IO; using System.Collections.Generic; using System.Reflection; +using System.Diagnostics.CodeAnalysis; using Autofac; using log4net; @@ -31,7 +32,7 @@ public bool GameInFolder(DirectoryInfo where) /// /// "/Applications/Kerbal Space Program" if it exists and we're on a Mac, else null /// - public DirectoryInfo MacPath() + public DirectoryInfo? MacPath() { if (Platform.IsMac) { @@ -106,7 +107,7 @@ public bool IsReservedDirectory(GameInstance inst, string path) || path == ShipsSph(inst) || path == ShipsThumbsSPH(inst) || path == ShipsScript(inst); - public bool AllowInstallationIn(string name, out string path) + public bool AllowInstallationIn(string name, [NotNullWhen(returnValue: true)] out string? path) => allowedFolders.TryGetValue(name, out path); public void RebuildSubdirectories(string absGameRoot) @@ -145,7 +146,7 @@ public string[] DefaultCommandLines(SteamLibrary steamLib, DirectoryInfo path) .Select(url => url.ToString())) .ToArray(); - public string[] AdjustCommandLine(string[] args, GameVersion installedVersion) + public string[] AdjustCommandLine(string[] args, GameVersion? installedVersion) { // -single-instance crashes KSP 1.8 to KSP 1.11 on Linux // https://issuetracker.unity3d.com/issues/linux-segmentation-fault-when-running-a-built-project-with-single-instance-argument @@ -155,7 +156,10 @@ public string[] AdjustCommandLine(string[] args, GameVersion installedVersion) new GameVersion(1, 8), new GameVersion(1, 11) ); - args = filterCmdLineArgs(args, installedVersion, brokenVersionRange, "-single-instance"); + if (installedVersion != null) + { + args = filterCmdLineArgs(args, installedVersion, brokenVersionRange, "-single-instance"); + } } return args; } @@ -172,7 +176,7 @@ public void RefreshVersions() versions = null; } - private List versions; + private List? versions; private readonly object versionMutex = new object(); @@ -184,18 +188,15 @@ public List KnownVersions { lock (versionMutex) { - if (versions == null) - { - // There's a lot of duplicate real versions with different build IDs, - // skip all those extra checks when we use these - versions = ServiceLocator.Container - .Resolve() - .KnownVersions - .Select(v => v.WithoutBuild) - .Distinct() - .OrderBy(v => v) - .ToList(); - } + // There's a lot of duplicate real versions with different build IDs, + // skip all those extra checks when we use these + versions ??= ServiceLocator.Container + .Resolve() + .KnownVersions + .Select(v => v.WithoutBuild) + .Distinct() + .OrderBy(v => v) + .ToList(); } } return versions; @@ -203,28 +204,32 @@ public List KnownVersions } public GameVersion[] EmbeddedGameVersions - => JsonConvert.DeserializeObject( - new StreamReader(Assembly.GetExecutingAssembly() - .GetManifestResourceStream("CKAN.builds-ksp.json")) - .ReadToEnd()) - .Builds - .Select(b => GameVersion.Parse(b.Value)) - .ToArray(); + => (Assembly.GetExecutingAssembly() + .GetManifestResourceStream("CKAN.builds-ksp.json") + is Stream s + ? JsonConvert.DeserializeObject( + new StreamReader(s).ReadToEnd()) + : null) + ?.Builds + ?.Select(b => GameVersion.Parse(b.Value)) + .ToArray() + ?? new GameVersion[] { }; public GameVersion[] ParseBuildsJson(JToken json) => json.ToObject() - .Builds - .Select(b => GameVersion.Parse(b.Value)) - .ToArray(); + ?.Builds + ?.Select(b => GameVersion.Parse(b.Value)) + .ToArray() + ?? new GameVersion[] { }; - public GameVersion DetectVersion(DirectoryInfo where) + public GameVersion? DetectVersion(DirectoryInfo where) => ServiceLocator.Container .ResolveKeyed(GameVersionSource.BuildId) - .TryGetVersion(where.FullName, out GameVersion verFromId) + .TryGetVersion(where.FullName, out GameVersion? verFromId) ? verFromId : ServiceLocator.Container .ResolveKeyed(GameVersionSource.Readme) - .TryGetVersion(where.FullName, out GameVersion verFromReadme) + .TryGetVersion(where.FullName, out GameVersion? verFromReadme) ? verFromReadme : null; diff --git a/Core/Games/KerbalSpaceProgram/DLC/IDlcDetector.cs b/Core/Games/KerbalSpaceProgram/DLC/IDlcDetector.cs index 54e11b1933..074ad8c715 100644 --- a/Core/Games/KerbalSpaceProgram/DLC/IDlcDetector.cs +++ b/Core/Games/KerbalSpaceProgram/DLC/IDlcDetector.cs @@ -1,4 +1,5 @@ using CKAN.Versioning; +using System.Diagnostics.CodeAnalysis; namespace CKAN.DLC { @@ -37,7 +38,9 @@ public interface IDlcDetector /// /// /// - bool IsInstalled(GameInstance inst, out string identifier, out UnmanagedModuleVersion version); + bool IsInstalled(GameInstance inst, + [NotNullWhen(returnValue: true)] out string? identifier, + [NotNullWhen(returnValue: true)] out UnmanagedModuleVersion? version); /// /// Path to the DLC directory relative to GameDir. diff --git a/Core/Games/KerbalSpaceProgram/DLC/StandardDlcDetectorBase.cs b/Core/Games/KerbalSpaceProgram/DLC/StandardDlcDetectorBase.cs index 71c695c1eb..418769b843 100644 --- a/Core/Games/KerbalSpaceProgram/DLC/StandardDlcDetectorBase.cs +++ b/Core/Games/KerbalSpaceProgram/DLC/StandardDlcDetectorBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; +using System.Diagnostics.CodeAnalysis; using CKAN.DLC; using CKAN.Versioning; @@ -32,13 +33,17 @@ public abstract class StandardDlcDetectorBase : IDlcDetector RegexOptions.Compiled | RegexOptions.IgnoreCase ); - protected StandardDlcDetectorBase(IGame game, - string identifierBaseName, - GameVersion releaseGameVersion, - Dictionary canonicalVersions = null) + protected StandardDlcDetectorBase(IGame game, + string identifierBaseName, + GameVersion releaseGameVersion, + Dictionary? canonicalVersions = null) : this(game, identifierBaseName, identifierBaseName, releaseGameVersion, canonicalVersions) { } - protected StandardDlcDetectorBase(IGame game, string identifierBaseName, string directoryBaseName, GameVersion releaseGameVersion, Dictionary canonicalVersions = null) + protected StandardDlcDetectorBase(IGame game, + string identifierBaseName, + string directoryBaseName, + GameVersion releaseGameVersion, + Dictionary? canonicalVersions = null) { if (string.IsNullOrWhiteSpace(identifierBaseName)) { @@ -50,14 +55,16 @@ protected StandardDlcDetectorBase(IGame game, string identifierBaseName, string throw new ArgumentException("Value must be provided.", nameof(directoryBaseName)); } - this.game = game; + this.game = game; IdentifierBaseName = identifierBaseName; - DirectoryBaseName = directoryBaseName; + DirectoryBaseName = directoryBaseName; ReleaseGameVersion = releaseGameVersion; - CanonicalVersions = canonicalVersions ?? new Dictionary(); + CanonicalVersions = canonicalVersions ?? new Dictionary(); } - public virtual bool IsInstalled(GameInstance ksp, out string identifier, out UnmanagedModuleVersion version) + public virtual bool IsInstalled(GameInstance ksp, + [NotNullWhen(returnValue: true)] out string? identifier, + [NotNullWhen(returnValue: true)] out UnmanagedModuleVersion? version) { var directoryPath = Path.Combine(ksp.GameDir(), InstallPath()); var readmeFilePath = Path.Combine(directoryPath, "readme.txt"); @@ -71,7 +78,7 @@ public virtual bool IsInstalled(GameInstance ksp, out string identifier, out Unm .Select(line => VersionPattern.Match(line)) .Where(match => match.Success) .Select(match => match.Groups["version"].Value) - .Select(verStr => CanonicalVersions.TryGetValue(verStr, out string overrideVer) + .Select(verStr => CanonicalVersions.TryGetValue(verStr, out string? overrideVer) ? overrideVer : verStr) // A null string results in UnmanagedModuleVersion with IsUnknownVersion==true diff --git a/Core/Games/KerbalSpaceProgram/GameVersionProviders/IGameVersionProvider.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/IGameVersionProvider.cs index 1e9f4b675f..b9f7e47f6f 100644 --- a/Core/Games/KerbalSpaceProgram/GameVersionProviders/IGameVersionProvider.cs +++ b/Core/Games/KerbalSpaceProgram/GameVersionProviders/IGameVersionProvider.cs @@ -1,9 +1,11 @@ using CKAN.Versioning; +using System.Diagnostics.CodeAnalysis; namespace CKAN.Games.KerbalSpaceProgram.GameVersionProviders { public interface IGameVersionProvider { - bool TryGetVersion(string directory, out GameVersion result); + bool TryGetVersion(string directory, + [NotNullWhen(returnValue: true)] out GameVersion? result); } } diff --git a/Core/Games/KerbalSpaceProgram/GameVersionProviders/IKspBuildMap.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/IKspBuildMap.cs index 3391e5691d..d27b30e079 100644 --- a/Core/Games/KerbalSpaceProgram/GameVersionProviders/IKspBuildMap.cs +++ b/Core/Games/KerbalSpaceProgram/GameVersionProviders/IKspBuildMap.cs @@ -5,7 +5,7 @@ namespace CKAN.Games.KerbalSpaceProgram.GameVersionProviders { public interface IKspBuildMap { - GameVersion this[string buildId] { get; } + GameVersion? this[string buildId] { get; } List KnownVersions { get; } diff --git a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildIdVersionProvider.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildIdVersionProvider.cs index 02a7daee22..a3b3af6997 100644 --- a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildIdVersionProvider.cs +++ b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildIdVersionProvider.cs @@ -1,6 +1,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; +using System.Diagnostics.CodeAnalysis; using log4net; @@ -21,32 +22,35 @@ public KspBuildIdVersionProvider(IKspBuildMap kspBuildMap) "buildID64.txt", "buildID.txt" }; - public bool TryGetVersion(string directory, out GameVersion result) + public bool TryGetVersion(string directory, + [NotNullWhen(returnValue: true)] out GameVersion? result) { var foundVersions = buildIDfilenames .Select(filename => TryGetVersionFromFile(Path.Combine(directory, filename), - out GameVersion v) + out GameVersion? v) ? v : null) - .Where(v => v != null) + .OfType() .Distinct() - .ToList(); + .ToArray(); - if (foundVersions.Count < 1) + if (foundVersions.Length > 0) { - result = default; - return false; - } - if (foundVersions.Count > 1) - { - Log.WarnFormat("Found different KSP versions in {0}: {1}", - string.Join(" and ", buildIDfilenames), - string.Join(", ", foundVersions)); + if (foundVersions.Length > 1) + { + Log.WarnFormat("Found different KSP versions in {0}: {1}", + string.Join(" and ", buildIDfilenames), + string.Join(", ", foundVersions)); + } + result = foundVersions.Max() ?? foundVersions.First(); + return true; } - result = foundVersions.Max(); - return true; + + result = default; + return false; } - private bool TryGetVersionFromFile(string file, out GameVersion result) + private bool TryGetVersionFromFile(string file, + [NotNullWhen(returnValue: true)] out GameVersion? result) { if (File.Exists(file)) { diff --git a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs index d528d59e18..8df77c4134 100644 --- a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs +++ b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs @@ -23,7 +23,7 @@ public sealed class JBuilds { [JsonProperty("builds")] // ReSharper disable once UnusedAutoPropertyAccessor.Local - public Dictionary Builds { get; set; } + public Dictionary? Builds { get; set; } } // ReSharper disable once ClassNeverInstantiated.Global @@ -38,15 +38,16 @@ public sealed class KspBuildMap : IKspBuildMap private static readonly ILog Log = LogManager.GetLogger(typeof(KspBuildMap)); private readonly object _buildMapLock = new object(); - private JBuilds _jBuilds; + private JBuilds? _jBuilds; - public GameVersion this[string buildId] + public GameVersion? this[string buildId] { get { EnsureBuildMap(); - return _jBuilds.Builds.TryGetValue(buildId, out string version) ? GameVersion.Parse(version) : null; + return _jBuilds?.Builds != null && _jBuilds.Builds.TryGetValue(buildId, out string? version) + ? GameVersion.Parse(version) : null; } } @@ -55,9 +56,9 @@ public List KnownVersions get { EnsureBuildMap(); - return _jBuilds.Builds - .Select(b => GameVersion.Parse(b.Value)) - .ToList(); + return _jBuilds?.Builds?.Select(b => GameVersion.Parse(b.Value)) + .ToList() + ?? new List(); } } @@ -138,10 +139,10 @@ private bool TrySetRemoteBuildMap() { Log.Debug("Getting remote build map"); var json = Net.DownloadText(BuildMapUri); - if (TrySetBuildMap(json)) + if (json != null && TrySetBuildMap(json)) { // Save to disk if parse succeeds - new FileInfo(cachedBuildMapPath).Directory.Create(); + new FileInfo(cachedBuildMapPath).Directory?.Create(); File.WriteAllText(cachedBuildMapPath, json); return true; } @@ -159,10 +160,9 @@ private bool TrySetEmbeddedBuildMap() try { Log.Debug("Getting embedded build map"); - var resourceStream = Assembly.GetExecutingAssembly() - .GetManifestResourceStream("CKAN.builds-ksp.json"); - - if (resourceStream != null) + if (Assembly.GetExecutingAssembly() + .GetManifestResourceStream("CKAN.builds-ksp.json") + is Stream resourceStream) { using (var reader = new StreamReader(resourceStream)) { diff --git a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspReadmeVersionProvider.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspReadmeVersionProvider.cs index c5453f483f..f504d99e49 100644 --- a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspReadmeVersionProvider.cs +++ b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspReadmeVersionProvider.cs @@ -1,6 +1,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; +using System.Diagnostics.CodeAnalysis; using CKAN.Versioning; @@ -13,7 +14,8 @@ public sealed class KspReadmeVersionProvider : IGameVersionProvider RegexOptions.IgnoreCase | RegexOptions.Compiled ); - public bool TryGetVersion(string directory, out GameVersion result) + public bool TryGetVersion(string directory, + [NotNullWhen(returnValue: true)] out GameVersion? result) { var readmePath = Path.Combine(directory, "readme.txt"); diff --git a/Core/Games/KerbalSpaceProgram2.cs b/Core/Games/KerbalSpaceProgram2.cs index b5e052dbc6..7d62102047 100644 --- a/Core/Games/KerbalSpaceProgram2.cs +++ b/Core/Games/KerbalSpaceProgram2.cs @@ -4,6 +4,7 @@ using System.IO; using System.Collections.Generic; using System.Reflection; +using System.Diagnostics.CodeAnalysis; using log4net; using Newtonsoft.Json; @@ -30,7 +31,7 @@ public bool GameInFolder(DirectoryInfo where) /// /// "/Applications/Kerbal Space Program" if it exists and we're on a Mac, else null /// - public DirectoryInfo MacPath() + public DirectoryInfo? MacPath() { if (Platform.IsMac) { @@ -86,7 +87,7 @@ public bool IsReservedDirectory(GameInstance inst, string path) => path == inst.GameDir() || path == inst.CkanDir() || path == PrimaryModDirectory(inst); - public bool AllowInstallationIn(string name, out string path) + public bool AllowInstallationIn(string name, [NotNullWhen(returnValue: true)] out string? path) => allowedFolders.TryGetValue(name, out path); public void RebuildSubdirectories(string absGameRoot) @@ -111,7 +112,7 @@ public string[] DefaultCommandLines(SteamLibrary steamLib, DirectoryInfo path) .Select(url => url.ToString())) .ToArray(); - public string[] AdjustCommandLine(string[] args, GameVersion installedVersion) + public string[] AdjustCommandLine(string[] args, GameVersion? installedVersion) => args; public IDlcDetector[] DlcDetectors => Array.Empty(); @@ -121,22 +122,29 @@ public string[] AdjustCommandLine(string[] args, GameVersion installedVersion) private static readonly string cachedBuildMapPath = Path.Combine(CKANPathUtils.AppDataPath, "builds-ksp2.json"); - private List versions = JsonConvert.DeserializeObject>( - File.Exists(cachedBuildMapPath) - ? File.ReadAllText(cachedBuildMapPath) - : new StreamReader(Assembly.GetExecutingAssembly() - .GetManifestResourceStream("CKAN.builds-ksp2.json")) - .ReadToEnd()); + private List versions = + JsonConvert.DeserializeObject>( + File.Exists(cachedBuildMapPath) + ? File.ReadAllText(cachedBuildMapPath) + : Assembly.GetExecutingAssembly() + .GetManifestResourceStream("CKAN.builds-ksp2.json") + is Stream s + ? new StreamReader(s).ReadToEnd() + : "") + ?? new List(); public void RefreshVersions() { try { - var json = Net.DownloadText(BuildMapUri); - versions = JsonConvert.DeserializeObject>(json); - // Save to disk if download and parse succeeds - new FileInfo(cachedBuildMapPath).Directory.Create(); - File.WriteAllText(cachedBuildMapPath, json); + if (Net.DownloadText(BuildMapUri) is string json) + { + versions = JsonConvert.DeserializeObject>(json) ?? versions; + + // Save to disk if download and parse succeeds + new FileInfo(cachedBuildMapPath).Directory?.Create(); + File.WriteAllText(cachedBuildMapPath, json); + } } catch (Exception e) { @@ -148,13 +156,16 @@ public void RefreshVersions() public List KnownVersions => versions; public GameVersion[] EmbeddedGameVersions - => JsonConvert.DeserializeObject( - new StreamReader(Assembly.GetExecutingAssembly() - .GetManifestResourceStream("CKAN.builds-ksp2.json")) - .ReadToEnd()); + => (Assembly.GetExecutingAssembly() + .GetManifestResourceStream("CKAN.builds-ksp2.json") + is Stream s + ? JsonConvert.DeserializeObject(new StreamReader(s).ReadToEnd()) + : null) + ?? new GameVersion[] { }; public GameVersion[] ParseBuildsJson(JToken json) - => json.ToObject(); + => json.ToObject() + ?? new GameVersion[] { }; public GameVersion DetectVersion(DirectoryInfo where) => VersionFromAssembly(Path.Combine(where.FullName, @@ -166,7 +177,7 @@ public GameVersion DetectVersion(DirectoryInfo where) // Fall back to the most recent version ?? KnownVersions.Last(); - private static GameVersion VersionFromAssembly(string assemblyPath) + private static GameVersion? VersionFromAssembly(string assemblyPath) => File.Exists(assemblyPath) && GameVersion.TryParse( AssemblyDefinition.ReadAssembly(assemblyPath) @@ -178,16 +189,16 @@ private static GameVersion VersionFromAssembly(string assemblyPath) .Select(f => (string)f.Constant) .Select(ver => string.Join(".", ver.Split('.').Take(4))) .FirstOrDefault(), - out GameVersion v) + out GameVersion? v) ? v : null; - private static GameVersion VersionFromExecutable(string exePath) + private static GameVersion? VersionFromExecutable(string exePath) => File.Exists(exePath) && GameVersion.TryParse(FileVersionInfo.GetVersionInfo(exePath).ProductVersion // Fake instances have an EXE containing just the version string ?? File.ReadAllText(exePath), - out GameVersion v) + out GameVersion? v) ? v : null; diff --git a/Core/Games/KnownGames.cs b/Core/Games/KnownGames.cs index dc8f1009b6..d2d22e2043 100644 --- a/Core/Games/KnownGames.cs +++ b/Core/Games/KnownGames.cs @@ -17,7 +17,7 @@ public static class KnownGames /// /// The short name to find /// A game object or null if none found - public static IGame GameByShortName(string shortName) + public static IGame? GameByShortName(string? shortName) => knownGames.FirstOrDefault(g => g.ShortName == shortName); /// diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 6bfaee41f7..1fad9768fd 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -28,7 +28,7 @@ public class ModuleInstaller { public IUser User { get; set; } - public event Action onReportModInstalled = null; + public event Action? onReportModInstalled = null; private static readonly ILog log = LogManager.GetLogger(typeof(ModuleInstaller)); @@ -62,7 +62,7 @@ public static string Download(CkanModule module, string filename, NetModuleCache { log.Info("Downloading " + filename); - string tmp_file = Net.Download(module.download + string tmp_file = Net.Download((module.download ?? Enumerable.Empty()) .OrderBy(u => u, new PreferredHostUriComparer( ServiceLocator.Container.Resolve().PreferredHosts)) @@ -78,7 +78,7 @@ public static string Download(CkanModule module, string filename, NetModuleCache /// If no filename is provided, the module's standard name will be used. /// Checks the CKAN cache first. /// - public string CachedOrDownload(CkanModule module, string filename = null) + public string CachedOrDownload(CkanModule module, string? filename = null) => CachedOrDownload(module, Cache, filename); /// @@ -88,14 +88,11 @@ public string CachedOrDownload(CkanModule module, string filename = null) /// If no filename is provided, the module's standard name will be used. /// Chcecks provided cache first. /// - public static string CachedOrDownload(CkanModule module, NetModuleCache cache, string filename = null) + public static string CachedOrDownload(CkanModule module, NetModuleCache cache, string? filename = null) { - if (filename == null) - { - filename = CkanModule.StandardName(module.identifier, module.version); - } + filename ??= CkanModule.StandardName(module.identifier, module.version); - string full_path = cache.GetCachedFilename(module); + var full_path = cache.GetCachedFilename(module); if (full_path == null) { return Download(module, filename, cache); @@ -133,8 +130,8 @@ private void DownloadModules(IEnumerable mods, IDownloader downloade public void InstallList(ICollection modules, RelationshipResolverOptions options, RegistryManager registry_manager, - ref HashSet possibleConfigOnlyDirs, - IDownloader downloader = null, + ref HashSet? possibleConfigOnlyDirs, + IDownloader? downloader = null, bool ConfirmPrompt = true) { // TODO: Break this up into smaller pieces! It's huge! @@ -152,6 +149,7 @@ public void InstallList(ICollection modules, // Make sure we have enough space to install this stuff CKANPathUtils.CheckFreeSpace(new DirectoryInfo(instance.GameDir()), modsToInstall.Select(m => m.install_size) + .OfType() .Sum(), Properties.Resources.NotEnoughSpaceToInstall); @@ -177,10 +175,7 @@ public void InstallList(ICollection modules, if (downloads.Count > 0) { - if (downloader == null) - { - downloader = new NetAsyncModulesDownloader(User, Cache); - } + downloader ??= new NetAsyncModulesDownloader(User, Cache); downloader.DownloadModules(downloads); } @@ -188,13 +183,14 @@ public void InstallList(ICollection modules, // now that the downloads have been stored to the cache CKANPathUtils.CheckFreeSpace(new DirectoryInfo(instance.GameDir()), modsToInstall.Select(m => m.install_size) + .OfType() .Sum(), Properties.Resources.NotEnoughSpaceToInstall); // We're about to install all our mods; so begin our transaction. using (var transaction = CkanTransaction.CreateTransactionScope()) { - CkanModule installing = null; + CkanModule? installing = null; var progress = new ProgressScalePercentsByFileSizes( new Progress(p => User.RaiseProgress( string.Format(Properties.Resources.ModuleInstallerInstallingMod, @@ -211,7 +207,7 @@ public void InstallList(ICollection modules, registry_manager.registry, ref possibleConfigOnlyDirs, progress); - progress.NextFile(); + progress?.NextFile(); } User.RaiseProgress(Properties.Resources.ModuleInstallerUpdatingRegistry, 90); @@ -238,18 +234,18 @@ public void InstallList(ICollection modules, /// /// TODO: The name of this and InstallModule() need to be made more distinctive. /// - private void Install(CkanModule module, - bool autoInstalled, - Registry registry, - ref HashSet possibleConfigOnlyDirs, - IProgress progress, - string filename = null) + private void Install(CkanModule module, + bool autoInstalled, + Registry registry, + ref HashSet? possibleConfigOnlyDirs, + IProgress? progress, + string? filename = null) { CheckKindInstallationKraken(module); var version = registry.InstalledVersion(module.identifier); // TODO: This really should be handled by higher-up code. - if (version != null && !(version is UnmanagedModuleVersion)) + if (version is not null and not UnmanagedModuleVersion) { User.RaiseMessage(Properties.Resources.ModuleInstallerAlreadyInstalled, module.identifier, version); @@ -257,7 +253,7 @@ private void Install(CkanModule module, } // Find ZIP in the cache if we don't already have it. - filename = filename ?? Cache.GetCachedFilename(module); + filename ??= Cache.GetCachedFilename(module); // If we *still* don't have a file, then kraken bitterly. if (filename == null) @@ -312,11 +308,11 @@ private static void CheckKindInstallationKraken(CkanModule module) /// Propagates a CancelledActionKraken if the user decides not to overwite unowned files. /// Propagates a FileExistsKraken if we were going to overwrite a file. /// - private List InstallModule(CkanModule module, - string zip_filename, - Registry registry, - ref HashSet possibleConfigOnlyDirs, - IProgress moduleProgress) + private List InstallModule(CkanModule module, + string zip_filename, + Registry registry, + ref HashSet? possibleConfigOnlyDirs, + IProgress? moduleProgress) { CheckKindInstallationKraken(module); var createdPaths = new List(); @@ -328,7 +324,8 @@ private List InstallModule(CkanModule module, .ToHashSet(); var files = FindInstallableFiles(module, zipfile, instance) .Where(instF => !filters.Any(filt => - instF.destination.Contains(filt)) + instF.destination != null + && instF.destination.Contains(filt)) // Skip the file if it's a ckan file, these should never be copied to GameData && !instF.source.Name.EndsWith( ".ckan", StringComparison.InvariantCultureIgnoreCase)) @@ -337,7 +334,7 @@ private List InstallModule(CkanModule module, try { var dll = registry.DllPath(module.identifier); - if (!string.IsNullOrEmpty(dll)) + if (dll is not null && !string.IsNullOrEmpty(dll)) { // Find where we're installing identifier.optionalversion.dll // (file name might not be an exact match with manually installed) @@ -585,7 +582,7 @@ public static List FindInstallableFiles(CkanModule module, stri /// public IEnumerable GetModuleContentsList(CkanModule module) { - string filename = Cache.GetCachedFilename(module); + var filename = Cache.GetCachedFilename(module); if (filename == null) { @@ -614,11 +611,11 @@ public IEnumerable GetModuleContentsList(CkanModule module) /// Path of file or directory that was created. /// May differ from the input fullPath! /// - internal static string CopyZipEntry(ZipFile zipfile, - ZipEntry entry, - string fullPath, - bool makeDirs, - IProgress progress) + internal static string? CopyZipEntry(ZipFile zipfile, + ZipEntry entry, + string fullPath, + bool makeDirs, + IProgress? progress) { var file_transaction = new TxFileManager(); @@ -632,9 +629,9 @@ internal static string CopyZipEntry(ZipFile zipfile, } // Windows silently trims trailing spaces, get the path it will actually use - fullPath = CKANPathUtils.NormalizePath( - Path.GetDirectoryName( - Path.Combine(fullPath, "DUMMY"))); + fullPath = Path.GetDirectoryName(Path.Combine(fullPath, "DUMMY")) is string p + ? CKANPathUtils.NormalizePath(p) + : fullPath; log.DebugFormat("Making directory '{0}'", fullPath); file_transaction.CreateDirectory(fullPath); @@ -644,11 +641,10 @@ internal static string CopyZipEntry(ZipFile zipfile, log.DebugFormat("Writing file '{0}'", fullPath); // ZIP format does not require directory entries - if (makeDirs) + if (makeDirs && Path.GetDirectoryName(fullPath) is string d) { - string directory = Path.GetDirectoryName(fullPath); - log.DebugFormat("Making parent directory '{0}'", directory); - file_transaction.CreateDirectory(directory); + log.DebugFormat("Making parent directory '{0}'", d); + file_transaction.CreateDirectory(d); } // We don't allow for the overwriting of files. See #208. @@ -704,9 +700,11 @@ internal static string CopyZipEntry(ZipFile zipfile, /// This *DOES* save the registry. /// Preferred over Uninstall. /// - public void UninstallList( - IEnumerable mods, ref HashSet possibleConfigOnlyDirs, - RegistryManager registry_manager, bool ConfirmPrompt = true, List installing = null) + public void UninstallList(IEnumerable mods, + ref HashSet? possibleConfigOnlyDirs, + RegistryManager registry_manager, + bool ConfirmPrompt = true, + List? installing = null) { mods = mods.Memoize(); // Pre-check, have they even asked for things which are installed? @@ -718,6 +716,7 @@ public void UninstallList( var instDlc = mods .Select(ident => registry_manager.registry.InstalledModule(ident)) + .OfType() .FirstOrDefault(m => m.Module.IsDLC); if (instDlc != null) { @@ -751,9 +750,9 @@ public void UninstallList( User.RaiseMessage(Properties.Resources.ModuleInstallerAboutToRemove); User.RaiseMessage(""); - foreach (string mod in goners) + foreach (var module in goners.Select(m => registry_manager.registry.InstalledModule(m)) + .OfType()) { - InstalledModule module = registry_manager.registry.InstalledModule(mod); User.RaiseMessage(" * {0} {1}", module.Module.name, module.Module.version); } @@ -793,7 +792,9 @@ public void UninstallList( /// /// Identifier of module to uninstall /// Directories that the user might want to remove after uninstall - private void Uninstall(string identifier, ref HashSet possibleConfigOnlyDirs, Registry registry) + private void Uninstall(string identifier, + ref HashSet? possibleConfigOnlyDirs, + Registry registry) { var file_transaction = new TxFileManager(); @@ -833,7 +834,10 @@ private void Uninstall(string identifier, ref HashSet possibleConfigOnly // Helps clean up directories when modules are uninstalled out of dependency order // Since we check for directory contents when deleting, this should purge empty // dirs, making less ModuleManager headaches for people. - directoriesToDelete.Add(Path.GetDirectoryName(absPath)); + if (Path.GetDirectoryName(absPath) is string p) + { + directoriesToDelete.Add(p); + } log.DebugFormat("Removing {0}", relPath); file_transaction.Delete(absPath); @@ -952,10 +956,7 @@ private void Uninstall(string identifier, ref HashSet possibleConfigOnly log.DebugFormat("Directory {0} contains only non-registered files, ask user about it later: {1}", directory, string.Join(", ", notRemovable)); - if (possibleConfigOnlyDirs == null) - { - possibleConfigOnlyDirs = new HashSet(Platform.PathComparer); - } + possibleConfigOnlyDirs ??= new HashSet(Platform.PathComparer); possibleConfigOnlyDirs.Add(directory); } } @@ -986,15 +987,15 @@ internal static void GroupFilesByRemovable(string relRoot, // Also skip owned by this module since it's already deregistered && !alreadyRemoving.Contains(f) // Must have a removable dir name somewhere in path AFTER main dir - && f.Substring(relRoot.Length) + && f[relRoot.Length..] .Split('/') .Where(piece => !string.IsNullOrEmpty(piece)) .Any(piece => game.AutoRemovableDirs.Contains(piece))) .ToDictionary(grp => grp.Key, grp => grp.OrderByDescending(f => f.Length) .ToArray()); - removable = contents.TryGetValue(true, out string[] val1) ? val1 : Array.Empty(); - notRemovable = contents.TryGetValue(false, out string[] val2) ? val2 : Array.Empty(); + removable = contents.TryGetValue(true, out string[]? val1) ? val1 : Array.Empty(); + notRemovable = contents.TryGetValue(false, out string[]? val2) ? val2 : Array.Empty(); log.DebugFormat("Got removable: {0}", string.Join(", ", removable)); log.DebugFormat("Got notRemovable: {0}", string.Join(", ", notRemovable)); } @@ -1005,11 +1006,6 @@ internal static void GroupFilesByRemovable(string relRoot, /// The collection of directory path strings to examine public HashSet AddParentDirectories(HashSet directories) { - if (directories == null || directories.Count == 0) - { - return new HashSet(Platform.PathComparer); - } - var gameDir = CKANPathUtils.NormalizePath(instance.GameDir()); return directories .Where(dir => !string.IsNullOrWhiteSpace(dir)) @@ -1069,7 +1065,12 @@ public HashSet AddParentDirectories(HashSet directories) /// Modules to add /// Modules to remove /// true if newly installed modules should be marked auto-installed, false otherwise - private void AddRemove(ref HashSet possibleConfigOnlyDirs, RegistryManager registry_manager, IEnumerable add, IEnumerable remove, bool enforceConsistency, bool newModulesAreAutoInstalled) + private void AddRemove(ref HashSet? possibleConfigOnlyDirs, + RegistryManager registry_manager, + IEnumerable add, + IEnumerable remove, + bool enforceConsistency, + bool newModulesAreAutoInstalled) { // TODO: We should do a consistency check up-front, rather than relying // upon our registry catching inconsistencies at the end. @@ -1078,8 +1079,7 @@ private void AddRemove(ref HashSet possibleConfigOnlyDirs, RegistryManag { remove = remove.Memoize(); add = add.Memoize(); - int totSteps = (remove?.Count() ?? 0) - + (add?.Count() ?? 0); + int totSteps = remove.Count() + add.Count(); int step = 0; foreach (InstalledModule instMod in remove) { @@ -1124,7 +1124,7 @@ private void AddRemove(ref HashSet possibleConfigOnlyDirs, RegistryManag /// public void Upgrade(IEnumerable modules, IDownloader netAsyncDownloader, - ref HashSet possibleConfigOnlyDirs, + ref HashSet? possibleConfigOnlyDirs, RegistryManager registry_manager, bool enforceConsistency = true, bool resolveRelationships = false, @@ -1137,7 +1137,8 @@ public void Upgrade(IEnumerable modules, { var resolver = new RelationshipResolver( modules, - modules.Select(m => registry.InstalledModule(m.identifier)?.Module).Where(m => m != null), + modules.Select(m => registry.InstalledModule(m.identifier)?.Module) + .OfType(), RelationshipResolverOptions.DependsOnlyOpts(), registry, instance.VersionCriteria() @@ -1157,13 +1158,14 @@ public void Upgrade(IEnumerable modules, // Let's discover what we need to do with each module! foreach (CkanModule module in modules) { - InstalledModule installed_mod = registry.InstalledModule(module.identifier); + var installed_mod = registry.InstalledModule(module.identifier); if (installed_mod == null) { - if (!Cache.IsMaybeCachedZip(module)) + if (!Cache.IsMaybeCachedZip(module) + && Cache.GetInProgressFileName(module) is string p) { - var inProgressFile = new FileInfo(Cache.GetInProgressFileName(module)); + var inProgressFile = new FileInfo(p); if (inProgressFile.Exists) { User.RaiseMessage(Properties.Resources.ModuleInstallerUpgradeInstallingResuming, @@ -1203,9 +1205,10 @@ public void Upgrade(IEnumerable modules, } else { - if (!Cache.IsMaybeCachedZip(module)) + if (!Cache.IsMaybeCachedZip(module) + && Cache.GetInProgressFileName(module) is string p) { - var inProgressFile = new FileInfo(Cache.GetInProgressFileName(module)); + var inProgressFile = new FileInfo(p); if (inProgressFile.Exists) { User.RaiseMessage(Properties.Resources.ModuleInstallerUpgradeUpgradingResuming, @@ -1273,7 +1276,12 @@ public void Upgrade(IEnumerable modules, /// /// Thrown if a dependency for a replacing module couldn't be satisfied. /// Thrown if a module that should be replaced is not installed. - public void Replace(IEnumerable replacements, RelationshipResolverOptions options, IDownloader netAsyncDownloader, ref HashSet possibleConfigOnlyDirs, RegistryManager registry_manager, bool enforceConsistency = true) + public void Replace(IEnumerable replacements, + RelationshipResolverOptions options, + IDownloader netAsyncDownloader, + ref HashSet? possibleConfigOnlyDirs, + RegistryManager registry_manager, + bool enforceConsistency = true) { replacements = replacements.Memoize(); log.Debug("Using Replace method"); @@ -1296,11 +1304,11 @@ public void Replace(IEnumerable replacements, RelationshipRes foreach (ModuleReplacement repl in replacements) { string ident = repl.ToReplace.identifier; - InstalledModule installedMod = registry_manager.registry.InstalledModule(ident); + var installedMod = registry_manager.registry.InstalledModule(ident); if (installedMod == null) { - log.DebugFormat("Wait, {0} is not actually installed?", installedMod.identifier); + log.WarnFormat("Wait, {0} is not actually installed?", ident); //Maybe ModuleNotInstalled ? if (registry_manager.registry.IsAutodetected(ident)) { @@ -1320,7 +1328,7 @@ public void Replace(IEnumerable replacements, RelationshipRes log.DebugFormat("Ok, we are removing {0}", repl.ToReplace.identifier); //Check whether our Replacement target is already installed - InstalledModule installed_replacement = registry_manager.registry.InstalledModule(repl.ReplaceWith.identifier); + var installed_replacement = registry_manager.registry.InstalledModule(repl.ReplaceWith.identifier); // If replacement is not installed, we've already added it to modsToInstall above if (installed_replacement != null) @@ -1364,10 +1372,11 @@ public void Replace(IEnumerable replacements, RelationshipRes #endregion - public static IEnumerable PrioritizedHosts(IEnumerable urls) - => urls.OrderBy(u => u, new PreferredHostUriComparer(ServiceLocator.Container.Resolve().PreferredHosts)) - .Select(dl => dl.Host) - .Distinct(); + public static IEnumerable PrioritizedHosts(IEnumerable? urls) + => urls?.OrderBy(u => u, new PreferredHostUriComparer(ServiceLocator.Container.Resolve().PreferredHosts)) + .Select(dl => dl.Host) + .Distinct() + ?? Enumerable.Empty(); #region Recommendations @@ -1398,7 +1407,7 @@ public bool FindRecommendations(HashSet var checkedRecs = resolver.Recommendations(recommenders) .Where(m => resolver.ReasonsFor(m) - .Any(r => (r as SelectionReason.Recommended)?.ProvidesIndex == 0)) + .Any(r => r is SelectionReason.Recommended { ProvidesIndex: 0 })) .ToHashSet(); var conflicting = new RelationshipResolver(toInstall.Concat(checkedRecs), null, RelationshipResolverOptions.ConflictsOpts(), @@ -1412,17 +1421,21 @@ public bool FindRecommendations(HashSet m => new Tuple>( checkedRecs.Contains(m), resolver.ReasonsFor(m) - .Where(r => r is SelectionReason.Recommended rec - && recommenders.Contains(rec.Parent)) - .Select(r => r.Parent.identifier) + .OfType() + .Where(r => recommenders.Contains(r.Parent)) + .Select(r => r.Parent) + .OfType() + .Select(m => m.identifier) .ToList())); suggestions = resolver.Suggestions(recommenders, recommendations.Keys.ToList()) .ToDictionary(m => m, m => resolver.ReasonsFor(m) - .Where(r => r is SelectionReason.Suggested sug - && recommenders.Contains(sug.Parent)) - .Select(r => r.Parent.identifier) + .OfType() + .Where(r => recommenders.Contains(r.Parent)) + .Select(r => r.Parent) + .OfType() + .Select(m => m.identifier) .ToList()); var opts = RelationshipResolverOptions.DependsOnlyOpts(); @@ -1457,7 +1470,7 @@ public bool CanInstall(List toInstall, try { var installed = toInstall.Select(m => registry.InstalledModule(m.identifier)?.Module) - .Where(m => m != null); + .OfType(); var resolver = new RelationshipResolver(toInstall, installed, opts, registry, crit); var resolverModList = resolver.ModList(false).ToList(); @@ -1515,7 +1528,7 @@ private bool TryGetFileHashMatches(HashSet fi foreach (var fi in files.Distinct()) { if (index.TryGetValue(Cache.GetFileHashSha256(fi.FullName, progress), - out List modules) + out List? modules) // The progress bar will jump back and "redo" the same span // for non-matched files, but that's... OK? || index.TryGetValue(Cache.GetFileHashSha1(fi.FullName, progress), @@ -1578,20 +1591,20 @@ public bool ImportFiles(HashSet files, deletable.Count)); // Store once per "primary" URL since each has its own URL hash - var cachedGroups = matched.SelectMany(kvp => kvp.Value.DistinctBy(m => m.download.First()) + var cachedGroups = matched.SelectMany(kvp => kvp.Value.DistinctBy(m => m.download?.First()) .Select(m => (File: kvp.Key, Module: m))) .GroupBy(tuple => Cache.IsMaybeCachedZip(tuple.Module)) .ToDictionary(grp => grp.Key, grp => grp.ToArray()); - if (cachedGroups.TryGetValue(true, out (FileInfo File, CkanModule Module)[] alreadyStored)) + if (cachedGroups.TryGetValue(true, out (FileInfo File, CkanModule Module)[]? alreadyStored)) { // Notify about files that are already cached user.RaiseMessage(" "); user.RaiseMessage(Properties.Resources.ModuleInstallerImportAlreadyCached, string.Join(", ", alreadyStored.Select(tuple => $"{tuple.Module} ({tuple.File.Name})"))); } - if (cachedGroups.TryGetValue(false, out (FileInfo File, CkanModule Module)[] toStore)) + if (cachedGroups.TryGetValue(false, out (FileInfo File, CkanModule Module)[]? toStore)) { // Store any new files user.RaiseMessage(" "); diff --git a/Core/Net/Net.cs b/Core/Net/Net.cs index 8d57d2ea97..9776124865 100644 --- a/Core/Net/Net.cs +++ b/Core/Net/Net.cs @@ -44,7 +44,7 @@ public static class Net /// ETag value of the URL if any, otherwise null, see /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag /// - public static string CurrentETag(Uri url) + public static string? CurrentETag(Uri url) { // HttpClient apparently is worse than what it was supposed to replace #pragma warning disable SYSLIB0014 @@ -54,7 +54,7 @@ public static string CurrentETag(Uri url) try { HttpWebResponse resp = (HttpWebResponse)req.GetResponse(); - string val = resp.Headers["ETag"]?.Replace("\"", ""); + var val = resp.Headers["ETag"]?.Replace("\"", ""); resp.Close(); return val; } @@ -74,27 +74,23 @@ public static string CurrentETag(Uri url) /// Throws a MissingCertificateException *and* prints a message to the /// 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) + public static string Download(Uri url, out string? etag, string? filename = null, IUser? user = null) => Download(url.OriginalString, out etag, filename, user); - public static string Download(Uri url, string filename = null, IUser user = null) + public static string Download(Uri url, string? filename = null, IUser? user = null) => Download(url, out _, filename, user); - public static string Download(string url, string filename = null, IUser user = null) + public static string Download(string url, string? filename = null, IUser? user = null) => Download(url, out _, filename, user); - public static string Download(string url, out string etag, string filename = null, IUser user = null) + public static string Download(string url, out string? etag, string? filename = null, IUser? user = null) { - user = user ?? new NullUser(); + user ??= new NullUser(); user.RaiseMessage(Properties.Resources.NetDownloading, url); var FileTransaction = new TxFileManager(); // Generate a temporary file if none is provided. - if (filename == null) - { - - filename = FileTransaction.GetTempFileName(); - } + filename ??= FileTransaction.GetTempFileName(); log.DebugFormat("Downloading {0} to {1}", url, filename); @@ -105,7 +101,7 @@ public static string Download(string url, out string etag, string filename = nul var agent = new RedirectingTimeoutWebClient(); #pragma warning restore SYSLIB0014 agent.DownloadFile(url, filename); - etag = agent.ResponseHeaders.Get("ETag")?.Replace("\"", ""); + etag = agent.ResponseHeaders?.Get("ETag")?.Replace("\"", ""); } catch (Exception exc) { @@ -159,13 +155,13 @@ public static string Download(string url, out string etag, string filename = nul /// A mime type sent with the "Accept" header /// Timeout for the request in milliseconds, defaulting to 100 000 (=100 seconds) /// The text content returned by the server - public static string DownloadText(Uri url, string authToken = "", string mimeType = null, int timeout = 100000) + public static string? DownloadText(Uri url, string? authToken = "", string? mimeType = null, int timeout = 100000) { log.DebugFormat("About to download {0}", url.OriginalString); // This WebClient child class does some complicated stuff, let's keep using it for now #pragma warning disable SYSLIB0014 - WebClient agent = new RedirectingTimeoutWebClient(timeout, mimeType); + WebClient agent = new RedirectingTimeoutWebClient(timeout, mimeType ?? ""); #pragma warning restore SYSLIB0014 // Check whether to use an auth token for this host @@ -183,7 +179,7 @@ public static string DownloadText(Uri url, string authToken = "", string mimeTyp try { string content = agent.DownloadString(url.OriginalString); - string header = agent.ResponseHeaders.ToString(); + var header = agent.ResponseHeaders?.ToString(); log.DebugFormat("Response from {0}:\r\n\r\n{1}\r\n{2}", url, header, content); @@ -201,14 +197,14 @@ public static string DownloadText(Uri url, string authToken = "", string mimeTyp return null; } - public static Uri ResolveRedirect(Uri url) + public static Uri? ResolveRedirect(Uri url) { const int maxRedirects = 6; for (int redirects = 0; redirects <= maxRedirects; ++redirects) { var rwClient = new RedirectWebClient(); using (rwClient.OpenRead(url)) { } - var location = rwClient.ResponseHeaders["Location"]; + var location = rwClient.ResponseHeaders?["Location"]; if (location == null) { return url; @@ -237,7 +233,7 @@ public static Uri ResolveRedirect(Uri url) /// /// null if the string is not a valid , otherwise its normalized form. /// - public static string NormalizeUri(string uri) + public static string? NormalizeUri(string uri) { // Uri.EscapeUriString has been deprecated because its purpose was ambiguous. // Is it supposed to turn a "&" into part of the content of a form field, diff --git a/Core/Net/NetAsyncDownloader.DownloadPart.cs b/Core/Net/NetAsyncDownloader.DownloadPart.cs index 088a39da24..6c91f166a1 100644 --- a/Core/Net/NetAsyncDownloader.DownloadPart.cs +++ b/Core/Net/NetAsyncDownloader.DownloadPart.cs @@ -19,7 +19,7 @@ private class DownloadPart public long bytesLeft; public long size; public long bytesPerSecond; - public Exception error; + public Exception? error; // Number of target URLs already tried and failed private int triedDownloads; @@ -27,11 +27,11 @@ private class DownloadPart /// /// Percentage, bytes received, total bytes to receive /// - public event Action Progress; - public event Action Done; + public event Action? Progress; + public event Action? Done; private string mimeType => target.mimeType; - private ResumingWebClient agent; + private ResumingWebClient? agent; public DownloadPart(DownloadTarget target) { @@ -46,15 +46,18 @@ public void Download() var url = CurrentUri; ResetAgent(); // Check whether to use an auth token for this host - if (url.IsAbsoluteUri - && ServiceLocator.Container.Resolve().TryGetAuthToken(url.Host, out string token) - && !string.IsNullOrEmpty(token)) + if (agent != null) { - log.InfoFormat("Using auth token for {0}", url.Host); - // Send our auth token to the GitHub API (or whoever else needs one) - agent.Headers.Add("Authorization", $"token {token}"); + if (url.IsAbsoluteUri + && ServiceLocator.Container.Resolve().TryGetAuthToken(url.Host, out string? token) + && !string.IsNullOrEmpty(token)) + { + log.InfoFormat("Using auth token for {0}", url.Host); + // Send our auth token to the GitHub API (or whoever else needs one) + agent.Headers.Add("Authorization", $"token {token}"); + } + target.DownloadWith(agent, url); } - target.DownloadWith(agent, url); } public Uri CurrentUri => target.urls[triedDownloads]; diff --git a/Core/Net/NetAsyncDownloader.DownloadTarget.cs b/Core/Net/NetAsyncDownloader.DownloadTarget.cs index 241f34357a..cce7065f81 100644 --- a/Core/Net/NetAsyncDownloader.DownloadTarget.cs +++ b/Core/Net/NetAsyncDownloader.DownloadTarget.cs @@ -23,6 +23,8 @@ protected DownloadTarget(List urls, public abstract long CalculateSize(); public abstract void DownloadWith(ResumingWebClient wc, Uri url); + + public override string ToString() => string.Join(", ", urls); } public sealed class DownloadTargetFile : DownloadTarget @@ -30,7 +32,7 @@ public sealed class DownloadTargetFile : DownloadTarget public string filename { get; private set; } public DownloadTargetFile(List urls, - string filename = null, + string? filename = null, long size = 0, string mimeType = "") : base(urls, size, mimeType) @@ -38,10 +40,10 @@ public DownloadTargetFile(List urls, this.filename = filename ?? Path.GetTempFileName(); } - public DownloadTargetFile(Uri url, - string filename = null, - long size = 0, - string mimeType = "") + public DownloadTargetFile(Uri url, + string? filename = null, + long size = 0, + string mimeType = "") : this(new List { url }, filename, size, mimeType) { } @@ -56,6 +58,8 @@ public override void DownloadWith(ResumingWebClient wc, Uri url) { wc.DownloadFileAsyncWithResume(url, filename); } + + public override string ToString() => $"{base.ToString()} => {filename}"; } public sealed class DownloadTargetStream : DownloadTarget, IDisposable diff --git a/Core/Net/NetAsyncDownloader.cs b/Core/Net/NetAsyncDownloader.cs index 99c7a5d675..b8cfd69440 100644 --- a/Core/Net/NetAsyncDownloader.cs +++ b/Core/Net/NetAsyncDownloader.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using System.Text.RegularExpressions; using System.Threading; using Autofac; @@ -22,7 +21,7 @@ public partial class NetAsyncDownloader /// /// Raised when data arrives for a download /// - public event Action Progress; + public event Action? Progress; private readonly object dlMutex = new object(); // NOTE: Never remove anything from this, because closures have indexes into it! @@ -41,7 +40,7 @@ public partial class NetAsyncDownloader /// The download that is done /// Exception thrown if failed /// ETag of the URL - public event Action onOneCompleted; + public event Action? onOneCompleted; /// /// Returns a perfectly boring NetAsyncDownloader @@ -52,10 +51,10 @@ public NetAsyncDownloader(IUser user) complete_or_canceled = new ManualResetEvent(false); } - public static string DownloadWithProgress(string url, string filename = null, IUser user = null) + public static string DownloadWithProgress(string url, string? filename = null, IUser? user = null) => DownloadWithProgress(new Uri(url), filename, user); - public static string DownloadWithProgress(Uri url, string filename = null, IUser user = null) + public static string DownloadWithProgress(Uri url, string? filename = null, IUser? user = null) { var targets = new[] { @@ -65,7 +64,7 @@ public static string DownloadWithProgress(Uri url, string filename = null, IUser return targets.First().filename; } - public static void DownloadWithProgress(IList downloadTargets, IUser user = null) + public static void DownloadWithProgress(IList downloadTargets, IUser? user = null) { var downloader = new NetAsyncDownloader(user ?? new NullUser()); downloader.onOneCompleted += (target, error, etag) => @@ -144,43 +143,38 @@ public void DownloadAndWait(IList targets) } // Check to see if we've had any errors. If so, then release the kraken! - var exceptions = new List>(); - for (int i = 0; i < downloads.Count; ++i) + var exceptions = downloads.Select((dl, i) => dl.error is Exception exc + ? new KeyValuePair(i, exc) + : (KeyValuePair?)null) + .OfType>() + .ToList(); + + if (exceptions.Select(kvp => kvp.Value) + .OfType() + // Check if it's a certificate error. If so, report that instead, + // as this is common (and user-fixable) under Linux. + .Any(exc => exc.Status == WebExceptionStatus.SecureChannelFailure)) { - if (downloads[i].error != null) - { - // Check if it's a certificate error. If so, report that instead, - // as this is common (and user-fixable) under Linux. - if (downloads[i].error is WebException) - { - WebException wex = downloads[i].error as WebException; - if (certificatePattern.IsMatch(wex.Message)) - { - throw new MissingCertificateKraken(); - } - #pragma warning disable IDE0011 - else switch ((wex.Response as HttpWebResponse)?.StatusCode) - #pragma warning restore IDE0011 - { - // Handle HTTP 403 used for throttling - case HttpStatusCode.Forbidden: - Uri infoUrl = null; - var throttledUri = downloads[i].target.urls.FirstOrDefault(uri => - uri.IsAbsoluteUri - && Net.ThrottledHosts.TryGetValue(uri.Host, out infoUrl)); - if (throttledUri != null) - { - throw new DownloadThrottledKraken(throttledUri, infoUrl); - } - break; - } - } - // Otherwise just note the error and which download it came from, - // then throw them all at once later. - exceptions.Add(new KeyValuePair( - targets.IndexOf(downloads[i].target), downloads[i].error)); - } + throw new MissingCertificateKraken(); } + + var throttled = exceptions.Select(kvp => kvp.Value is WebException wex + && wex.Response is HttpWebResponse hresp + // Handle HTTP 403 used for throttling + && hresp.StatusCode == HttpStatusCode.Forbidden + && downloads[kvp.Key].CurrentUri is Uri url + && url.IsAbsoluteUri + && Net.ThrottledHosts.TryGetValue(url.Host, out Uri? infoUrl) + && infoUrl is not null + ? new DownloadThrottledKraken(url, infoUrl) + : null) + .OfType() + .FirstOrDefault(); + if (throttled is not null) + { + throw throttled; + } + if (exceptions.Count > 0) { throw new DownloadErrorsKraken(exceptions); @@ -190,11 +184,6 @@ public void DownloadAndWait(IList targets) log.Debug("Done downloading"); } - private static readonly Regex certificatePattern = new Regex( - @"authentication or decryption has failed", - RegexOptions.Compiled - ); - /// /// /// This will also call onCompleted with all null arguments. @@ -375,7 +364,7 @@ private void PopFromQueue(string host) /// This method gets called back by `WebClient` when a download is completed. /// It in turncalls the onCompleted hook when *all* downloads are finished. /// - private void FileDownloadComplete(int index, Exception error, bool canceled, string etag) + private void FileDownloadComplete(int index, Exception? error, bool canceled, string? etag) { var dl = downloads[index]; var doneUri = dl.CurrentUri; @@ -416,11 +405,8 @@ private void FileDownloadComplete(int index, Exception error, bool canceled, str } catch (Exception exc) { - if (dl.error == null) - { - // Capture anything that goes wrong with the post-download process as well - dl.error = exc; - } + // Capture anything that goes wrong with the post-download process as well + dl.error ??= exc; } // Make sure the threads don't trip on one another diff --git a/Core/Net/NetAsyncModulesDownloader.cs b/Core/Net/NetAsyncModulesDownloader.cs index f7b57fbc6e..004709e56e 100644 --- a/Core/Net/NetAsyncModulesDownloader.cs +++ b/Core/Net/NetAsyncModulesDownloader.cs @@ -17,9 +17,9 @@ namespace CKAN /// public class NetAsyncModulesDownloader : IDownloader { - public event Action Progress; - public event Action StoreProgress; - public event Action AllComplete; + public event Action? Progress; + public event Action? StoreProgress; + public event Action? AllComplete; /// /// Returns a perfectly boring NetAsyncModulesDownloader. @@ -44,17 +44,17 @@ public NetAsyncModulesDownloader(IUser user, NetModuleCache cache) internal NetAsyncDownloader.DownloadTargetFile TargetFromModuleGroup( HashSet group, - string[] preferredHosts) + string?[] preferredHosts) => TargetFromModuleGroup(group, group.OrderBy(m => m.identifier).First(), preferredHosts); private NetAsyncDownloader.DownloadTargetFile TargetFromModuleGroup( HashSet group, CkanModule first, - string[] preferredHosts) + string?[] preferredHosts) => new NetAsyncDownloader.DownloadTargetFile( - group.SelectMany(mod => mod.download) + group.SelectMany(mod => mod.download ?? Enumerable.Empty()) .Concat(group.Select(mod => mod.InternetArchiveDownload) - .Where(uri => uri != null) + .OfType() .OrderBy(uri => uri.ToString())) .Distinct() .OrderBy(u => u, @@ -71,7 +71,8 @@ private NetAsyncDownloader.DownloadTargetFile TargetFromModuleGroup( /// public void DownloadModules(IEnumerable modules) { - var activeURLs = this.modules.SelectMany(m => m.download) + var activeURLs = this.modules.SelectMany(m => m.download ?? Enumerable.Empty()) + .OfType() .ToHashSet(); var moduleGroups = CkanModule.GroupByDownloads(modules); // Make sure we have enough space to download and cache @@ -87,7 +88,7 @@ public void DownloadModules(IEnumerable modules) // Start the downloads! downloader.DownloadAndWait(moduleGroups // Skip any group that already has a URL in progress - .Where(grp => grp.All(mod => mod.download.All(dlUri => !activeURLs.Contains(dlUri)))) + .Where(grp => grp.All(mod => mod.download?.All(dlUri => !activeURLs.Contains(dlUri)) ?? false)) // Each group gets one target containing all the URLs .Select(grp => TargetFromModuleGroup(grp, preferredHosts)) .ToArray()); @@ -119,75 +120,78 @@ public void CancelDownload() private const string defaultMimeType = "application/octet-stream"; - private readonly List modules; - private readonly NetAsyncDownloader downloader; - private IUser User => downloader.User; - private readonly NetModuleCache cache; - private CancellationTokenSource cancelTokenSrc; + private readonly List modules; + private readonly NetAsyncDownloader downloader; + private IUser User => downloader.User; + private readonly NetModuleCache cache; + private CancellationTokenSource? cancelTokenSrc; private void ModuleDownloadComplete(NetAsyncDownloader.DownloadTarget target, - Exception error, - string etag) + Exception? error, + string? etag) { - var url = target.urls.First(); - var filename = (target as NetAsyncDownloader.DownloadTargetFile)?.filename; - - log.DebugFormat("Received download completion: {0}, {1}, {2}", - url, filename, error?.Message); - if (error != null) - { - // If there was an error in DOWNLOADING, keep the file so we can retry it later - log.Info(error.Message); - } - else + if (target is NetAsyncDownloader.DownloadTargetFile fileTarget) { - // Cache if this download succeeded - CkanModule module = null; - try + var url = fileTarget.urls.First(); + var filename = fileTarget.filename; + + log.DebugFormat("Received download completion: {0}, {1}, {2}", + url, filename, error?.Message); + if (error != null) { - module = modules.First(m => (m.download?.Any(dlUri => dlUri == url) - ?? false) - || m.InternetArchiveDownload == url); - User.RaiseMessage(Properties.Resources.NetAsyncDownloaderValidating, module); - cache.Store(module, filename, - new Progress(percent => StoreProgress?.Invoke(module, 100 - percent, 100)), - module.StandardName(), - false, - cancelTokenSrc.Token); - File.Delete(filename); + // If there was an error in DOWNLOADING, keep the file so we can retry it later + log.Info(error.Message); } - catch (InvalidModuleFileKraken kraken) + else { - User.RaiseError(kraken.ToString()); - if (module != null) + // Cache if this download succeeded + CkanModule? module = null; + try { - // Finish out the progress bar - StoreProgress?.Invoke(module, 0, 100); + module = modules.First(m => (m.download?.Any(dlUri => dlUri == url) + ?? false) + || m.InternetArchiveDownload == url); + User.RaiseMessage(Properties.Resources.NetAsyncDownloaderValidating, module); + cache.Store(module, filename, + new Progress(percent => StoreProgress?.Invoke(module, 100 - percent, 100)), + module.StandardName(), + false, + cancelTokenSrc?.Token); + File.Delete(filename); } - // If there was an error in STORING, delete the file so we can try it from scratch later - File.Delete(filename); - - // Tell downloader there is a problem with this file - throw; - } - catch (OperationCanceledException exc) - { - log.WarnFormat("Cancellation token threw, validation incomplete: {0}", filename); - User.RaiseMessage(exc.Message); - if (module != null) + catch (InvalidModuleFileKraken kraken) { - // Finish out the progress bar - StoreProgress?.Invoke(module, 0, 100); + User.RaiseError(kraken.ToString()); + if (module != null) + { + // Finish out the progress bar + StoreProgress?.Invoke(module, 0, 100); + } + // If there was an error in STORING, delete the file so we can try it from scratch later + File.Delete(filename); + + // Tell downloader there is a problem with this file + throw; + } + catch (OperationCanceledException exc) + { + log.WarnFormat("Cancellation token threw, validation incomplete: {0}", filename); + User.RaiseMessage(exc.Message); + if (module != null) + { + // Finish out the progress bar + StoreProgress?.Invoke(module, 0, 100); + } + // Don't delete because there might be nothing wrong + } + catch (FileNotFoundException e) + { + log.WarnFormat("cache.Store(): FileNotFoundException: {0}", e.Message); + } + catch (InvalidOperationException) + { + log.WarnFormat("No module found for completed URL: {0}", url); } - // Don't delete because there might be nothing wrong - } - catch (FileNotFoundException e) - { - log.WarnFormat("cache.Store(): FileNotFoundException: {0}", e.Message); - } - catch (InvalidOperationException) - { - log.WarnFormat("No module found for completed URL: {0}", url); } } } diff --git a/Core/Net/NetFileCache.cs b/Core/Net/NetFileCache.cs index 39f2d2e3a6..f7660d8f27 100644 --- a/Core/Net/NetFileCache.cs +++ b/Core/Net/NetFileCache.cs @@ -34,9 +34,9 @@ public class NetFileCache : IDisposable { private readonly FileSystemWatcher watcher; // hash => full file path - private Dictionary cachedFiles; + private Dictionary? cachedFiles; private readonly string cachePath; - private readonly GameInstanceManager manager; + private readonly GameInstanceManager? manager; private static readonly Regex cacheFileRegex = new Regex("^[0-9A-F]{8}-", RegexOptions.Compiled); private static readonly ILog log = LogManager.GetLogger(typeof (NetFileCache)); @@ -44,7 +44,7 @@ public class NetFileCache : IDisposable /// Initialize a cache given a GameInstanceManager /// /// GameInstanceManager object containing the Instances that might have old caches - public NetFileCache(GameInstanceManager mgr, string path) + public NetFileCache(GameInstanceManager? mgr, string path) : this(path) { manager = mgr; @@ -81,9 +81,9 @@ public NetFileCache(string path) // If we spot any changes, we fire our event handler. // NOTE: FileSystemWatcher.Changed fires when you READ info about a file, // do NOT listen for it! - watcher.Created += new FileSystemEventHandler(OnCacheChanged); - watcher.Deleted += new FileSystemEventHandler(OnCacheChanged); - watcher.Renamed += new RenamedEventHandler(OnCacheChanged); + watcher.Created += OnCacheChanged; + watcher.Deleted += OnCacheChanged; + watcher.Renamed += OnCacheChanged; // Enable events! watcher.EnableRaisingEvents = true; @@ -120,7 +120,7 @@ public string GetInProgressFileName(Uri url, string description) => GetInProgressFileName(CreateURLHash(url), description); - public string GetInProgressFileName(List urls, string description) + public string? GetInProgressFileName(List urls, string description) { var filenames = urls?.Select(url => GetInProgressFileName(CreateURLHash(url), description)) .ToArray(); @@ -134,8 +134,15 @@ public string GetInProgressFileName(List urls, string description) /// private void OnCacheChanged(object source, FileSystemEventArgs e) { - log.Debug("File system watcher event fired"); + log.DebugFormat("File system watcher event {0} fired for {1}", + e.ChangeType.ToString(), + e.FullPath); OnCacheChanged(); + if (e.ChangeType == WatcherChangeTypes.Deleted) + { + log.DebugFormat("Purging hashes reactively: {0}", e.FullPath); + PurgeHashes(null, e.FullPath); + } } /// @@ -153,7 +160,7 @@ public void OnCacheChanged() // returns true if a url is already in the cache // returns the filename in the outFilename parameter - public bool IsCached(Uri url, out string outFilename) + public bool IsCached(Uri url, out string? outFilename) { outFilename = GetCachedFilename(url); return outFilename != null; @@ -173,7 +180,7 @@ public bool IsMaybeCachedZip(Uri url, DateTime? remoteTimestamp = null) /// /// The URL to check for in the cache /// Timestamp of the remote file, if known; cached files older than this will be considered invalid - public string GetCachedFilename(Uri url, DateTime? remoteTimestamp = null) + public string? GetCachedFilename(Uri url, DateTime? remoteTimestamp = null) { log.DebugFormat("Checking cache for {0}", url); @@ -190,30 +197,31 @@ public string GetCachedFilename(Uri url, DateTime? remoteTimestamp = null) // *may* get cleared by OnCacheChanged while we're // using it. - Dictionary files = cachedFiles; + var files = cachedFiles; if (files == null) { log.Debug("Rebuilding cache index"); cachedFiles = files = allFiles() - .GroupBy(fi => fi.Name.Substring(0, 8)) - .ToDictionary( - grp => grp.Key, - grp => grp.First().FullName - ); + .GroupBy(fi => fi.Name[..8]) + .ToDictionary(grp => grp.Key, + grp => grp.First().FullName); } // Now that we have a list of files one way or another, // check them to see if we can find the one we're looking // for. - string found = scanDirectory(files, hash, remoteTimestamp); + var found = scanDirectory(files, hash, remoteTimestamp); return string.IsNullOrEmpty(found) ? null : found; } - private string scanDirectory(Dictionary files, string findHash, DateTime? remoteTimestamp = null) + private string? scanDirectory(Dictionary files, + string findHash, + DateTime? remoteTimestamp = null) { - if (files.TryGetValue(findHash, out string file)) + if (files.TryGetValue(findHash, out string? file) + && File.Exists(file)) { log.DebugFormat("Found file {0}", file); // Check local vs remote timestamps; if local is older, then it's invalid. @@ -230,10 +238,7 @@ private string scanDirectory(Dictionary files, string findHash, // Local file too old, delete it log.Debug("Found stale file, deleting it"); File.Delete(file); - File.Delete($"{file}.sha1"); - File.Delete($"{file}.sha256"); - sha1Cache.Remove(file); - sha256Cache.Remove(file); + PurgeHashes(null, file); } } else @@ -292,14 +297,14 @@ public void EnforceSizeLimit(long bytes, Registry registry) if (curBytes > bytes) { // This object will let us determine whether a module is compatible with any of our instances - GameVersionCriteria aggregateCriteria = manager?.Instances.Values + var aggregateCriteria = manager?.Instances.Values .Where(ksp => ksp.Valid) .Select(ksp => ksp.VersionCriteria()) .Aggregate( manager?.CurrentInstance?.VersionCriteria() ?? new GameVersionCriteria(null), - (combinedCrit, nextCrit) => combinedCrit.Union(nextCrit) - ); + (combinedCrit, nextCrit) => combinedCrit.Union(nextCrit)) + ?? new GameVersionCriteria(null); // This object lets us find the modules associated with a cached file var hashMap = registry.GetDownloadUrlHashIndex(); @@ -337,11 +342,11 @@ public void EnforceSizeLimit(long bytes, Registry registry) private int compareFiles(Dictionary> hashMap, FileInfo a, FileInfo b) { // Compatible modules for file A - hashMap.TryGetValue(a.Name.Substring(0, 8), out List modulesA); + hashMap.TryGetValue(a.Name[..8], out List? modulesA); bool compatA = modulesA?.Any() ?? false; // Compatible modules for file B - hashMap.TryGetValue(b.Name.Substring(0, 8), out List modulesB); + hashMap.TryGetValue(b.Name[..8], out List? modulesB); bool compatB = modulesB?.Any() ?? false; if (modulesA == null && modulesB != null) @@ -401,23 +406,25 @@ private List allFiles(bool includeInProgress = false) /// /// This method is filesystem transaction aware. /// - public string Store(Uri url, string path, string description = null, bool move = false) + public string Store(Uri url, + string path, + string? description = null, + bool move = false) { log.DebugFormat("Storing {0}", url); TxFileManager tx_file = new TxFileManager(); - // Make sure we clear our cache entry first. + // Clear our cache entry first Remove(url); string hash = CreateURLHash(url); - description = description ?? Path.GetFileName(path); + description ??= Path.GetFileName(path); Debug.Assert( Regex.IsMatch(description, "^[A-Za-z0-9_.-]*$"), - "description isn't as filesystem safe as we thought... (#1266)" - ); + $"description {description} isn't as filesystem safe as we thought... (#1266)"); string fullName = string.Format("{0}-{1}", hash, Path.GetFileName(description)); string targetPath = Path.Combine(cachePath, fullName); @@ -452,29 +459,32 @@ public string Store(Uri url, string path, string description = null, bool move = /// public bool Remove(Uri url) { - TxFileManager tx_file = new TxFileManager(); - - string file = GetCachedFilename(url); - - if (file != null) + if (GetCachedFilename(url) is string file) { + TxFileManager tx_file = new TxFileManager(); tx_file.Delete(file); // We've changed our cache, so signal that immediately. cachedFiles?.Remove(CreateURLHash(url)); PurgeHashes(tx_file, file); return true; } - return false; } - private void PurgeHashes(TxFileManager tx_file, string file) + private void PurgeHashes(TxFileManager? tx_file, string file) { - tx_file.Delete($"{file}.sha1"); - tx_file.Delete($"{file}.sha256"); + try + { + sha1Cache.Remove(file); + sha256Cache.Remove(file); - sha1Cache.Remove(file); - sha256Cache.Remove(file); + tx_file ??= new TxFileManager(); + tx_file.Delete($"{file}.sha1"); + tx_file.Delete($"{file}.sha256"); + } + catch + { + } } /// @@ -551,13 +561,13 @@ public void MoveFrom(string fromDir) /// /// Returns the 8-byte hash for a given url /// - public static string CreateURLHash(Uri url) + public static string CreateURLHash(Uri? url) { using (SHA1 sha1 = SHA1.Create()) { byte[] hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(url?.ToString() ?? "")); - return BitConverter.ToString(hash).Replace("-", "").Substring(0, 8); + return BitConverter.ToString(hash).Replace("-", "")[..8]; } } @@ -569,7 +579,9 @@ public static string CreateURLHash(Uri url) /// /// SHA1 hash, in all-caps hexadecimal format /// - public string GetFileHashSha1(string filePath, IProgress progress, CancellationToken cancelToken = default) + public string GetFileHashSha1(string filePath, + IProgress? progress, + CancellationToken? cancelToken = default) => GetFileHash(filePath, "sha1", sha1Cache, SHA1.Create, progress, cancelToken); /// @@ -580,7 +592,9 @@ public string GetFileHashSha1(string filePath, IProgress progress, Cancella /// /// SHA256 hash, in all-caps hexadecimal format /// - public string GetFileHashSha256(string filePath, IProgress progress, CancellationToken cancelToken = default) + public string GetFileHashSha256(string filePath, + IProgress? progress, + CancellationToken? cancelToken = default) => GetFileHash(filePath, "sha256", sha256Cache, SHA256.Create, progress, cancelToken); /// @@ -591,15 +605,15 @@ public string GetFileHashSha256(string filePath, IProgress progress, Cancel /// /// Hash, in all-caps hexadecimal format /// - private string GetFileHash(string filePath, - string hashSuffix, + private string GetFileHash(string filePath, + string hashSuffix, Dictionary cache, - Func getHashAlgo, - IProgress progress, - CancellationToken cancelToken) + Func getHashAlgo, + IProgress? progress, + CancellationToken? cancelToken) { string hashFile = $"{filePath}.{hashSuffix}"; - if (cache.TryGetValue(filePath, out string hash)) + if (cache.TryGetValue(filePath, out string? hash)) { return hash; } diff --git a/Core/Net/NetModuleCache.cs b/Core/Net/NetModuleCache.cs index 049385673f..375990015d 100644 --- a/Core/Net/NetModuleCache.cs +++ b/Core/Net/NetModuleCache.cs @@ -42,8 +42,8 @@ public NetModuleCache(string path) cache = new NetFileCache(path); } - public event Action ModStored; - public event Action ModPurged; + public event Action? ModStored; + public event Action? ModPurged; // Simple passthrough wrappers public void Dispose() @@ -62,7 +62,7 @@ public void MoveFrom(string fromDir) public bool IsCached(CkanModule m) => m.download?.Any(dlUri => cache.IsCached(dlUri)) ?? false; - public bool IsCached(CkanModule m, out string outFilename) + public bool IsCached(CkanModule m, out string? outFilename) { if (m.download != null) { @@ -80,7 +80,7 @@ public bool IsCached(CkanModule m, out string outFilename) public bool IsMaybeCachedZip(CkanModule m) => m.download?.Any(dlUri => cache.IsMaybeCachedZip(dlUri, m.release_date)) ?? false; - public string GetCachedFilename(CkanModule m) + public string? GetCachedFilename(CkanModule m) => m.download?.Select(dlUri => cache.GetCachedFilename(dlUri, m.release_date)) .FirstOrDefault(filename => filename != null); public void GetSizeInfo(out int numFiles, out long numBytes, out long bytesFree) @@ -96,13 +96,13 @@ public void CheckFreeSpace(long bytesToStore) cache.CheckFreeSpace(bytesToStore); } - public string GetInProgressFileName(CkanModule m) + public string? GetInProgressFileName(CkanModule m) => m.download == null ? null : cache.GetInProgressFileName(m.download, m.StandardName()); - private static string DescribeUncachedAvailability(CkanModule m, FileInfo fi) - => fi.Exists + private static string DescribeUncachedAvailability(CkanModule m, FileInfo? fi) + => (fi?.Exists ?? false) ? string.Format(Properties.Resources.NetModuleCacheModuleResuming, m.name, m.version, string.Join(", ", ModuleInstaller.PrioritizedHosts(m.download)), @@ -117,7 +117,9 @@ public string DescribeAvailability(CkanModule m) ? string.Format(Properties.Resources.NetModuleCacheMetapackage, m.name, m.version) : IsMaybeCachedZip(m) ? string.Format(Properties.Resources.NetModuleCacheModuleCached, m.name, m.version) - : DescribeUncachedAvailability(m, new FileInfo(GetInProgressFileName(m))); + : DescribeUncachedAvailability(m, + GetInProgressFileName(m) is string s + ? new FileInfo(s) : null); /// /// Calculate the SHA1 hash of a file @@ -127,7 +129,7 @@ public string DescribeAvailability(CkanModule m) /// /// SHA1 hash, in all-caps hexadecimal format /// - public string GetFileHashSha1(string filePath, IProgress progress, CancellationToken cancelToken = default) + public string GetFileHashSha1(string filePath, IProgress progress, CancellationToken? cancelToken = default) => cache.GetFileHashSha1(filePath, progress, cancelToken); /// @@ -138,7 +140,7 @@ public string GetFileHashSha1(string filePath, IProgress progress, Cancella /// /// SHA256 hash, in all-caps hexadecimal format /// - public string GetFileHashSha256(string filePath, IProgress progress, CancellationToken cancelToken = default) + public string GetFileHashSha256(string filePath, IProgress progress, CancellationToken? cancelToken = default) => cache.GetFileHashSha256(filePath, progress, cancelToken); /// @@ -153,13 +155,13 @@ public string GetFileHashSha256(string filePath, IProgress progress, Cancel /// /// Name of the new file in the cache /// - public string Store(CkanModule module, - string path, - IProgress progress, - string description = null, - bool move = false, - CancellationToken cancelToken = default, - bool validate = true) + public string Store(CkanModule module, + string path, + IProgress? progress, + string? description = null, + bool move = false, + CancellationToken? cancelToken = default, + bool validate = true) { if (validate) { @@ -182,7 +184,7 @@ public string Store(CkanModule module, module, path, fi.Length, module.download_size)); } - cancelToken.ThrowIfCancellationRequested(); + cancelToken?.ThrowIfCancellationRequested(); // Check valid CRC if (!ZipValid(path, out string invalidReason, new Progress(percent => @@ -193,7 +195,7 @@ public string Store(CkanModule module, module, path, invalidReason)); } - cancelToken.ThrowIfCancellationRequested(); + cancelToken?.ThrowIfCancellationRequested(); // Some older metadata doesn't have hashes if (module.download_hash != null) @@ -226,10 +228,10 @@ public string Store(CkanModule module, } } - cancelToken.ThrowIfCancellationRequested(); + cancelToken?.ThrowIfCancellationRequested(); } // If no exceptions, then everything is fine - var success = cache.Store(module.download[0], path, description ?? module.StandardName(), move); + var success = cache.Store(module.download?[0]!, path, description ?? module.StandardName(), move); // Make sure completion is signalled so progress bars go away progress?.Report(100); ModStored?.Invoke(module); @@ -245,9 +247,9 @@ public string Store(CkanModule module, /// /// True if valid, false otherwise. See invalidReason param for explanation. /// - public static bool ZipValid(string filename, - out string invalidReason, - IProgress progress) + public static bool ZipValid(string filename, + out string invalidReason, + IProgress? progress) { try { @@ -255,7 +257,7 @@ public static bool ZipValid(string filename, { using (ZipFile zip = new ZipFile(filename)) { - string zipErr = null; + string? zipErr = null; // Limit progress updates to 100 per ZIP file long highestPercent = -1; // Perform CRC and other checks @@ -265,21 +267,24 @@ public static bool ZipValid(string filename, // This delegate is called as TestArchive proceeds through its // steps, both routine and abnormal. // The second parameter is non-null if an error occurred. - if (st != null && !st.EntryValid && !string.IsNullOrEmpty(msg)) + if (st != null) { - // Capture the error string so we can return it - zipErr = string.Format( - Properties.Resources.NetFileCacheZipError, - st.Operation, st.Entry?.Name, msg); - } - else if (st.Entry != null && progress != null) - { - // Report progress - var percent = (int)(100 * st.Entry.ZipFileIndex / zip.Count); - if (percent > highestPercent) + if (!st.EntryValid && !string.IsNullOrEmpty(msg)) + { + // Capture the error string so we can return it + zipErr = string.Format( + Properties.Resources.NetFileCacheZipError, + st.Operation, st.Entry?.Name, msg); + } + else if (st.Entry != null && progress != null) { - progress.Report(percent); - highestPercent = percent; + // Report progress + var percent = (int)(100 * st.Entry.ZipFileIndex / zip.Count); + if (percent > highestPercent) + { + progress.Report(percent); + highestPercent = percent; + } } } })) diff --git a/Core/Net/PreferredHostUriComparer.cs b/Core/Net/PreferredHostUriComparer.cs index 3801b08028..b4decf7182 100644 --- a/Core/Net/PreferredHostUriComparer.cs +++ b/Core/Net/PreferredHostUriComparer.cs @@ -6,24 +6,24 @@ namespace CKAN { public class PreferredHostUriComparer : IComparer { - public PreferredHostUriComparer(IEnumerable hosts) + public PreferredHostUriComparer(IEnumerable hosts) { - this.hosts = (hosts ?? Enumerable.Empty()).ToList(); + this.hosts = (hosts ?? Enumerable.Empty()).ToList(); // null represents the position in the list for all other hosts defaultPriority = this.hosts.IndexOf(null); } - public int Compare(Uri a, Uri b) + public int Compare(Uri? a, Uri? b) => GetPriority(a).CompareTo(GetPriority(b)); - private int GetPriority(Uri u) + private int GetPriority(Uri? u) { - var index = hosts.IndexOf(u.Host); + var index = hosts.IndexOf(u?.Host); return index == -1 ? defaultPriority : index; } - private readonly List hosts; - private readonly int defaultPriority; + private readonly List hosts; + private readonly int defaultPriority; } } diff --git a/Core/Net/RedirectingTimeoutWebClient.cs b/Core/Net/RedirectingTimeoutWebClient.cs index 0f891dbd93..27dea461b0 100644 --- a/Core/Net/RedirectingTimeoutWebClient.cs +++ b/Core/Net/RedirectingTimeoutWebClient.cs @@ -39,7 +39,7 @@ protected override WebRequest GetWebRequest(Uri address) log.InfoFormat("Setting MIME type {0}", mimeType); Headers.Add("Accept", mimeType); } - if (permanentRedirects.TryGetValue(address, out Uri redirUri)) + if (permanentRedirects.TryGetValue(address, out Uri? redirUri)) { // Obey a previously received permanent redirect address = redirUri; @@ -56,17 +56,7 @@ protected override WebRequest GetWebRequest(Uri address) protected override WebResponse GetWebResponse(WebRequest request) { - if (request == null) - { - return null; - } - var response = base.GetWebResponse(request); - if (response == null) - { - return null; - } - if (response is HttpWebResponse hwr) { int statusCode = (int)hwr.StatusCode; @@ -83,7 +73,7 @@ protected override WebResponse GetWebResponse(WebRequest request) Headers.Remove("Authorization"); } // Moved or PermanentRedirect - if (statusCode == 301 || statusCode == 308) + if (statusCode is 301 or 308) { permanentRedirects.Add(request.RequestUri, redirUri); } diff --git a/Core/Net/ResumingWebClient.cs b/Core/Net/ResumingWebClient.cs index 5772873ccf..9263341dca 100644 --- a/Core/Net/ResumingWebClient.cs +++ b/Core/Net/ResumingWebClient.cs @@ -62,7 +62,7 @@ public void DownloadFileAsyncWithResume(Uri url, Stream stream) /// is private instead of protected, so we have to reinvent this wheel. /// (Meanwhile AsyncCompletedEventArgs has none of these problems.) /// - public event Action DownloadProgress; + public event Action? DownloadProgress; /// /// CancelAsync isn't virtual, so we make another function @@ -149,10 +149,10 @@ protected override void OnOpenReadCompleted(OpenReadCompletedEventArgs e) switch (e.UserState) { case string path: - ToFile(netStream, path); + ToFile(netStream, path, cancelTokenSrc.Token); break; case Stream stream: - ToStream(netStream, stream); + ToStream(netStream, stream, cancelTokenSrc.Token); break; } // Make sure caller knows we've finished @@ -176,15 +176,15 @@ protected override void OnOpenReadCompleted(OpenReadCompletedEventArgs e) OnDownloadFileCompleted(new AsyncCompletedEventArgs(e.Error, e.Cancelled, e.UserState)); } - private void ToFile(Stream netStream, string path) + private void ToFile(Stream netStream, string path, CancellationToken token) { using (var outStream = new FileStream(path, FileMode.Append, FileAccess.Write)) { - ToStream(netStream, outStream); + ToStream(netStream, outStream, token); } } - private void ToStream(Stream netStream, Stream outStream) + private void ToStream(Stream netStream, Stream outStream, CancellationToken token) { netStream.CopyTo(outStream, new Progress(bytesDownloaded => { @@ -192,7 +192,7 @@ private void ToStream(Stream netStream, Stream outStream) bytesDownloaded, contentLength); }), TimeSpan.FromSeconds(5), - cancelTokenSrc.Token); + token); } /// @@ -201,7 +201,7 @@ private void ToStream(Stream netStream, Stream outStream) /// private long bytesToSkip = 0; private long contentLength = 0; - private CancellationTokenSource cancelTokenSrc; + private CancellationTokenSource? cancelTokenSrc; private const int timeoutMs = 30 * 1000; private static readonly ILog log = LogManager.GetLogger(typeof(ResumingWebClient)); diff --git a/Core/Platform.cs b/Core/Platform.cs index 5ecb1869d3..db9e2f01ef 100644 --- a/Core/Platform.cs +++ b/Core/Platform.cs @@ -48,6 +48,12 @@ public static class Platform #endif public static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + #if NET6_0_OR_GREATER + [SupportedOSPlatformGuard("windows6.1")] + public static readonly bool IsWindows61 + = IsWindows && OperatingSystem.IsWindowsVersionAtLeast(6, 1); + #endif + /// /// Are we on Mono? /// @@ -99,13 +105,14 @@ public static bool IsAdministrator() new Regex("^\\s*(?\\d+)\\.(?\\d+)\\.(?\\d+)\\s*\\(", RegexOptions.Compiled); - public static readonly Version MonoVersion + public static readonly Version? MonoVersion = versionMatcher.TryMatch((string)Type.GetType("Mono.Runtime") ?.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static) - ?.Invoke(null, null), - out Match match) + ?.Invoke(null, null)!, + out Match? match) + && match is not null ? new Version(int.Parse(match.Groups["major"].Value), int.Parse(match.Groups["minor"].Value), int.Parse(match.Groups["patch"].Value)) diff --git a/Core/Registry/CompatibilitySorter.cs b/Core/Registry/CompatibilitySorter.cs index a6fade8a18..3433cb2ccc 100644 --- a/Core/Registry/CompatibilitySorter.cs +++ b/Core/Registry/CompatibilitySorter.cs @@ -80,10 +80,9 @@ public ICollection LatestCompatible { get { - if (latestCompatible == null) - { - latestCompatible = Compatible.Values.Select(avail => avail.Latest(CompatibleVersions)).ToList(); - } + latestCompatible ??= Compatible.Values.Select(avail => avail.Latest(CompatibleVersions)) + .OfType() + .ToList(); return latestCompatible; } } @@ -97,10 +96,9 @@ public ICollection LatestIncompatible { get { - if (latestIncompatible == null) - { - latestIncompatible = Incompatible.Values.Select(avail => avail.Latest(null)).ToList(); - } + latestIncompatible ??= Incompatible.Values.Select(avail => avail.Latest(null)) + .OfType() + .ToList(); return latestIncompatible; } } @@ -109,8 +107,8 @@ public ICollection LatestIncompatible private readonly HashSet dlls; private readonly IDictionary dlc; - private List latestCompatible; - private List latestIncompatible; + private List? latestCompatible; + private List? latestIncompatible; /// /// Filter the provides mapping by compatibility @@ -261,7 +259,7 @@ private IEnumerable RelationshipIdentifiers(RelationshipDescriptor rel) => rel is ModuleRelationshipDescriptor modRel ? Enumerable.Repeat(modRel.name, 1) : rel is AnyOfRelationshipDescriptor anyRel - ? anyRel.any_of.SelectMany(RelationshipIdentifiers) + ? (anyRel.any_of?.SelectMany(RelationshipIdentifiers) ?? Enumerable.Empty()) : Enumerable.Empty(); private static readonly ILog log = LogManager.GetLogger(typeof(CompatibilitySorter)); diff --git a/Core/Registry/IRegistryQuerier.cs b/Core/Registry/IRegistryQuerier.cs index 5acfcad1fa..db4398b96f 100644 --- a/Core/Registry/IRegistryQuerier.cs +++ b/Core/Registry/IRegistryQuerier.cs @@ -26,9 +26,9 @@ public interface IRegistryQuerier /// /// Returns a simple array of the latest compatible module for each identifier for - /// the specified version of KSP. + /// the specified game version. /// - IEnumerable CompatibleModules(GameVersionCriteria ksp_version); + IEnumerable CompatibleModules(GameVersionCriteria? ksp_version); /// /// Get full JSON metadata string for a mod's available versions @@ -45,17 +45,17 @@ public interface IRegistryQuerier /// If no ksp_version is provided, the latest module for *any* KSP version is returned. /// Throws if asked for a non-existent module. /// - CkanModule LatestAvailable(string identifier, - GameVersionCriteria ksp_version, - RelationshipDescriptor relationship_descriptor = null, - ICollection installed = null, - ICollection toInstall = null); + CkanModule? LatestAvailable(string identifier, + GameVersionCriteria? ksp_version, + RelationshipDescriptor? relationship_descriptor = null, + ICollection? installed = null, + ICollection? toInstall = null); /// /// Returns the max game version that is compatible with the given mod. /// /// Name of mod to check - GameVersion LatestCompatibleGameVersion(List realVersions, string identifier); + GameVersion? LatestCompatibleGameVersion(List realVersions, string identifier); /// /// Returns all available versions of a module. @@ -70,11 +70,11 @@ CkanModule LatestAvailable(string identifier, /// Returns an empty list if nothing is available for our system, which includes if no such module exists. /// If no KSP version is provided, the latest module for *any* KSP version is given. /// - List LatestAvailableWithProvides(string identifier, - GameVersionCriteria ksp_version, - RelationshipDescriptor relationship_descriptor = null, - ICollection installed = null, - ICollection toInstall = null); + List LatestAvailableWithProvides(string identifier, + GameVersionCriteria? ksp_version, + RelationshipDescriptor? relationship_descriptor = null, + ICollection? installed = null, + ICollection? toInstall = null); /// /// Checks the sanity of the registry, to ensure that all dependencies are met, @@ -86,21 +86,21 @@ List LatestAvailableWithProvides(string identifier, /// /// Finds and returns all modules that could not exist without the listed modules installed, including themselves. /// - IEnumerable FindReverseDependencies(List modulesToRemove, - List modulesToInstall = null, - Func satisfiedFilter = null); + IEnumerable FindReverseDependencies(List modulesToRemove, + List? modulesToInstall = null, + Func? satisfiedFilter = null); /// /// Gets the installed version of a mod. Does not check for provided or autodetected mods. /// /// The module or null if not found - CkanModule GetInstalledVersion(string identifier); + CkanModule? GetInstalledVersion(string identifier); /// /// Attempts to find a module with the given identifier and version. /// /// The module if it exists, null otherwise. - CkanModule GetModuleByVersion(string identifier, ModuleVersion version); + CkanModule? GetModuleByVersion(string identifier, ModuleVersion version); /// /// Returns a simple array of all incompatible modules for @@ -120,7 +120,7 @@ IEnumerable FindReverseDependencies(List modulesToRemove, /// Returns the InstalledModule, or null if it is not installed. /// Does *not* look up virtual modules. /// - InstalledModule InstalledModule(string identifier); + InstalledModule? InstalledModule(string identifier); /// /// Returns the installed version of a given mod. @@ -129,7 +129,7 @@ IEnumerable FindReverseDependencies(List modulesToRemove, /// /// If set to false will not check for provided versions. /// The version of the mod or null if not found - ModuleVersion InstalledVersion(string identifier, bool with_provides = true); + ModuleVersion? InstalledVersion(string identifier, bool with_provides = true); /// /// Check whether any versions of this mod are installable (including dependencies) on the given game versions @@ -148,7 +148,7 @@ public static class IRegistryQuerierHelpers /// /// Helper to call /// - public static CkanModule GetModuleByVersion(this IRegistryQuerier querier, string ident, string version) + public static CkanModule? GetModuleByVersion(this IRegistryQuerier querier, string ident, string version) => querier.GetModuleByVersion(ident, new ModuleVersion(version)); /// @@ -171,11 +171,11 @@ public static bool IsAutodetected(this IRegistryQuerier querier, string identifi /// /// Is the mod installed and does it have a newer version compatible with versionCrit /// - public static bool HasUpdate(this IRegistryQuerier querier, - string identifier, - GameInstance instance, - out CkanModule latestMod, - ICollection installed = null) + public static bool HasUpdate(this IRegistryQuerier querier, + string identifier, + GameInstance? instance, + out CkanModule? latestMod, + ICollection? installed = null) { // Check if it's installed (including manually!) var instVer = querier.InstalledVersion(identifier); @@ -187,7 +187,7 @@ public static bool HasUpdate(this IRegistryQuerier querier, // Check if it's available try { - latestMod = querier.LatestAvailable(identifier, instance.VersionCriteria(), null, installed); + latestMod = querier.LatestAvailable(identifier, instance?.VersionCriteria(), null, installed); } catch { @@ -221,7 +221,7 @@ public static bool HasUpdate(this IRegistryQuerier querier, } public static Dictionary> CheckUpgradeable(this IRegistryQuerier querier, - GameInstance instance, + GameInstance? instance, HashSet heldIdents) { // Get the absolute latest versions ignoring restrictions, @@ -230,17 +230,18 @@ public static Dictionary> CheckUpgradeable(this IRegistry .Keys .Select(ident => !heldIdents.Contains(ident) && querier.HasUpdate(ident, instance, - out CkanModule latest) + out CkanModule? latest) + && latest is not null && !latest.IsDLC ? latest : querier.GetInstalledVersion(ident)) - .Where(m => m != null) + .OfType() .ToList(); return querier.CheckUpgradeable(instance, heldIdents, unlimited); } public static Dictionary> CheckUpgradeable(this IRegistryQuerier querier, - GameInstance instance, + GameInstance? instance, HashSet heldIdents, List initial) { @@ -251,7 +252,8 @@ public static Dictionary> CheckUpgradeable(this IRegistry { if (!heldIdents.Contains(ident) && querier.HasUpdate(ident, instance, - out CkanModule latest, initial) + out CkanModule? latest, initial) + && latest is not null && !latest.IsDLC) { upgradeable.Add(latest); @@ -300,7 +302,7 @@ public static string CompatibleGameVersions(this IRegistryQuerier querier, IGame game, string identifier) { - List releases = null; + List? releases = null; try { releases = querier.AvailableByIdentifier(identifier) @@ -318,8 +320,10 @@ public static string CompatibleGameVersions(this IRegistryQuerier querier, if (releases != null && releases.Count > 0) { CkanModule.GetMinMaxVersions(releases, out _, out _, - out GameVersion minKsp, out GameVersion maxKsp); - return GameVersionRange.VersionSpan(game, minKsp, maxKsp); + out GameVersion? minKsp, out GameVersion? maxKsp); + return GameVersionRange.VersionSpan(game, + minKsp ?? GameVersion.Any, + maxKsp ?? GameVersion.Any); } return ""; } @@ -335,12 +339,12 @@ public static string CompatibleGameVersions(this IRegistryQuerier querier, /// public static string CompatibleGameVersions(this CkanModule module, IGame game) { - CkanModule.GetMinMaxVersions( - new CkanModule[] { module }, - out _, out _, - out GameVersion minKsp, out GameVersion maxKsp - ); - return GameVersionRange.VersionSpan(game, minKsp, maxKsp); + CkanModule.GetMinMaxVersions(new CkanModule[] { module }, + out _, out _, + out GameVersion? minKsp, out GameVersion? maxKsp); + return GameVersionRange.VersionSpan(game, + minKsp ?? GameVersion.Any, + maxKsp ?? GameVersion.Any); } /// @@ -350,28 +354,18 @@ public static string CompatibleGameVersions(this CkanModule module, IGame game) /// Given a mod identifier, return a ModuleReplacement containing the relevant replacement /// if compatibility matches. /// - public static ModuleReplacement GetReplacement(this IRegistryQuerier querier, string identifier, GameVersionCriteria version) - { - // We only care about the installed version - CkanModule installedVersion; - try - { - installedVersion = querier.GetInstalledVersion(identifier); - } - catch (ModuleNotFoundKraken) - { - return null; - } - return querier.GetReplacement(installedVersion, version); - } - - public static ModuleReplacement GetReplacement(this IRegistryQuerier querier, CkanModule installedVersion, GameVersionCriteria version) + public static ModuleReplacement? GetReplacement(this IRegistryQuerier querier, + string identifier, + GameVersionCriteria version) + // We only care about the installed version + => querier.GetInstalledVersion(identifier) is CkanModule mod + ? Utilities.DefaultIfThrows(() => querier.GetReplacement(mod, version)) + : null; + + public static ModuleReplacement? GetReplacement(this IRegistryQuerier querier, + CkanModule installedVersion, + GameVersionCriteria version) { - // Mod is not installed, so we don't care about replacements - if (installedVersion == null) - { - return null; - } // No replaced_by relationship if (installedVersion.replaced_by == null) { @@ -379,43 +373,23 @@ public static ModuleReplacement GetReplacement(this IRegistryQuerier querier, Ck } // Get the identifier from the replaced_by relationship, if it exists - ModuleRelationshipDescriptor replacedBy = installedVersion.replaced_by; + var replacedBy = installedVersion.replaced_by; // Now we need to see if there is a compatible version of the replacement try { - ModuleReplacement replacement = new ModuleReplacement - { - ToReplace = installedVersion - }; if (installedVersion.replaced_by.version != null) { - replacement.ReplaceWith = querier.GetModuleByVersion(installedVersion.replaced_by.name, installedVersion.replaced_by.version); - if (replacement.ReplaceWith != null) + if (querier.GetModuleByVersion(installedVersion.replaced_by.name, installedVersion.replaced_by.version) + is CkanModule replacement && replacement.IsCompatible(version)) { - if (replacement.ReplaceWith.IsCompatible(version)) - { - return replacement; - } + return new ModuleReplacement(installedVersion, replacement); } } - else + else if (querier.LatestAvailable(installedVersion.replaced_by.name, version, replacedBy) + is CkanModule replacement && replacement.IsCompatible(version)) { - replacement.ReplaceWith = querier.LatestAvailable(installedVersion.replaced_by.name, version); - if (replacement.ReplaceWith != null) - { - if (installedVersion.replaced_by.min_version != null) - { - if (!replacement.ReplaceWith.version.IsLessThan(replacedBy.min_version)) - { - return replacement; - } - } - else - { - return replacement; - } - } + return new ModuleReplacement(installedVersion, replacement); } return null; } @@ -441,7 +415,7 @@ private static IEnumerable FindRemovableAutoInstalled( List installedModules, HashSet dlls, IDictionary dlc, - GameVersionCriteria crit) + GameVersionCriteria? crit) { log.DebugFormat("Finding removable autoInstalled for: {0}", string.Join(", ", installedModules.Select(im => im.identifier))); @@ -483,7 +457,7 @@ private static IEnumerable FindRemovableAutoInstalled( public static IEnumerable FindRemovableAutoInstalled( this IRegistryQuerier querier, List installedModules, - GameVersionCriteria crit) + GameVersionCriteria? crit) => querier?.FindRemovableAutoInstalled(installedModules, querier.InstalledDlls.ToHashSet(), querier.InstalledDlc, diff --git a/Core/Registry/InstalledModule.cs b/Core/Registry/InstalledModule.cs index 264d65dc81..8833754385 100644 --- a/Core/Registry/InstalledModule.cs +++ b/Core/Registry/InstalledModule.cs @@ -66,7 +66,7 @@ public bool AutoInstalled #region Constructors - public InstalledModule(GameInstance ksp, CkanModule module, IEnumerable relative_files, bool autoInstalled) + public InstalledModule(GameInstance? ksp, CkanModule module, IEnumerable relative_files, bool autoInstalled) { install_time = DateTime.Now; source_module = module; @@ -89,13 +89,6 @@ public InstalledModule(GameInstance ksp, CkanModule module, IEnumerable } } - // If we're being deserialised from our JSON file, we don't need to - // do any special construction. - [JsonConstructor] - private InstalledModule() - { - } - #endregion #region Serialisation Fixes @@ -103,10 +96,7 @@ private InstalledModule() [OnDeserialized] private void DeSerialisationFixes(StreamingContext context) { - if (installed_files == null) - { - installed_files = new Dictionary(); - } + installed_files ??= new Dictionary(); if (Platform.IsWindows) { // We need case insensitive path matching on Windows diff --git a/Core/Registry/Registry.cs b/Core/Registry/Registry.cs index a60b7f1577..89c7595d8e 100644 --- a/Core/Registry/Registry.cs +++ b/Core/Registry/Registry.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Runtime.Serialization; using System.Transactions; +using System.Diagnostics.CodeAnalysis; using Autofac; using Newtonsoft.Json; @@ -33,11 +34,11 @@ public class Registry : IEnlistmentNotification, IRegistryQuerier // name => Repository [JsonProperty("sorted_repositories")] - private SortedDictionary repositories; + private SortedDictionary? repositories; // name => relative path [JsonProperty] - private Dictionary installed_dlls; + private Dictionary? installed_dlls; [JsonProperty] [JsonConverter(typeof(JsonParallelDictionaryConverter))] @@ -54,9 +55,9 @@ public class Registry : IEnlistmentNotification, IRegistryQuerier /// [JsonIgnore] public ReadOnlyDictionary Repositories - => repositories == null - ? null - : new ReadOnlyDictionary(repositories); + => repositories is not null + ? new ReadOnlyDictionary(repositories) + : new ReadOnlyDictionary(new Dictionary()); /// /// Wrapper around assignment to this.repositories that invalidates @@ -78,7 +79,7 @@ public void RepositoriesClear() { EnlistWithTransaction(); InvalidateAvailableModCaches(); - repositories.Clear(); + repositories?.Clear(); } /// @@ -90,7 +91,10 @@ public void RepositoriesAdd(Repository repo) { EnlistWithTransaction(); InvalidateAvailableModCaches(); - repositories.Add(repo.name, repo); + if (repo.name != null) + { + repositories?.Add(repo.name, repo); + } } /// @@ -102,7 +106,7 @@ public void RepositoriesRemove(string name) { EnlistWithTransaction(); InvalidateAvailableModCaches(); - repositories.Remove(name); + repositories?.Remove(name); } /// @@ -115,14 +119,18 @@ [JsonIgnore] public IEnumerable InstalledModules /// Returns the names of installed DLLs. /// [JsonIgnore] public IEnumerable InstalledDlls - => installed_dlls.Keys; + => installed_dlls?.Keys ?? Enumerable.Empty(); /// /// Returns the file path of a DLL. /// null if not found. /// - public string DllPath(string identifier) - => installed_dlls.TryGetValue(identifier, out string path) ? path : null; + public string? DllPath(string identifier) + => installed_dlls == null + ? null + : installed_dlls.TryGetValue(identifier, out string? path) + ? path + : null; /// /// A map between module identifiers and versions for official DLC that are installed. @@ -151,7 +159,7 @@ public IEnumerable IncompatibleInstalled(GameVersionCriteria cr private void DeSerialisationFixes(StreamingContext context) { // Our context is our game instance. - GameInstance ksp = (GameInstance)context.Context; + var ksp = context.Context as GameInstance; // Older registries didn't have the installed_files list, so we create one // if absent. @@ -175,7 +183,7 @@ private void DeSerialisationFixes(StreamingContext context) { string path = CKANPathUtils.NormalizePath(tuple.Key); - if (Path.IsPathRooted(path)) + if (ksp != null && Path.IsPathRooted(path)) { path = ksp.ToRelativeGameDir(path); normalised_installed_files[path] = tuple.Value; @@ -191,9 +199,12 @@ private void DeSerialisationFixes(StreamingContext context) // Now update all our module file manifests. - foreach (InstalledModule module in installed_modules.Values) + if (ksp != null) { - module.Renormalise(ksp); + foreach (InstalledModule module in installed_modules.Values) + { + module.Renormalise(ksp); + } } // Our installed dlls have contained relative paths since forever, @@ -215,7 +226,7 @@ private void DeSerialisationFixes(StreamingContext context) const string old_ident = "001ControlLock"; const string new_ident = "ControlLock"; - if (installed_modules.TryGetValue("001ControlLock", out InstalledModule control_lock_entry)) + if (installed_modules.TryGetValue("001ControlLock", out InstalledModule? control_lock_entry)) { if (ksp == null) { @@ -254,7 +265,10 @@ private void DeSerialisationFixes(StreamingContext context) // custom-added by our user. var oldDefaultRepo = new Uri("https://github.com/KSP-CKAN/CKAN-meta/archive/master.zip"); - if (repositories != null && repositories.TryGetValue(Repository.default_ckan_repo_name, out Repository default_repo) && default_repo.uri == oldDefaultRepo) + if (repositories != null + && repositories.TryGetValue(Repository.default_ckan_repo_name, out Repository? default_repo) + && default_repo.uri == oldDefaultRepo + && ksp != null) { log.InfoFormat("Updating default metadata URL from {0} to {1}", oldDefaultRepo, ksp.game.DefaultRepositoryURL); repositories[Repository.default_ckan_repo_name].uri = ksp.game.DefaultRepositoryURL; @@ -280,6 +294,7 @@ private void DeSerialisationFixes(StreamingContext context) /// Rebuilds our master index of installed_files. /// Called on registry format updates, but safe to be triggered at any time. /// + [MemberNotNull(nameof(installed_files))] public void ReindexInstalled() { // We need case insensitive path matching on Windows @@ -308,13 +323,15 @@ public void Repair() #region Constructors / destructor [JsonConstructor] - private Registry(RepositoryDataManager repoData) + private Registry(RepositoryDataManager? repoData) { if (repoData != null) { repoDataMgr = repoData; repoDataMgr.Updated += RepositoriesUpdated; } + installed_modules = new Dictionary(); + installed_files = new Dictionary(); } ~Registry() @@ -325,7 +342,7 @@ private Registry(RepositoryDataManager repoData) } } - public Registry(RepositoryDataManager repoData, + public Registry(RepositoryDataManager? repoData, Dictionary installed_modules, Dictionary installed_dlls, Dictionary installed_files, @@ -347,7 +364,7 @@ public Registry(RepositoryDataManager repoData, new Dictionary(), new Dictionary(), new SortedDictionary( - repositories.ToDictionary(r => r.name, + repositories.ToDictionary(r => r.name ?? "", r => r))) { } @@ -370,10 +387,10 @@ public static Registry Empty() #region Transaction Handling // Which transaction we're in - private string enlisted_tx; + private string? enlisted_tx; // JSON serialization of self when enlisted with tx - private string transaction_backup; + private string? transaction_backup; // Coordinate access of multiple threads to the tx info private readonly object txMutex = new object(); @@ -425,7 +442,10 @@ public void Rollback(Enlistment enlistment) ObjectCreationHandling = ObjectCreationHandling.Replace }; - JsonConvert.PopulateObject(transaction_backup, this, options); + if (transaction_backup != null) + { + JsonConvert.PopulateObject(transaction_backup, this, options); + } enlisted_tx = null; transaction_backup = null; @@ -485,31 +505,31 @@ private void EnlistWithTransaction() #region Stateful views of data from repo data manager based on which repos we use [JsonIgnore] - private readonly RepositoryDataManager repoDataMgr; + private readonly RepositoryDataManager? repoDataMgr; [JsonIgnore] - private CompatibilitySorter sorter; + private CompatibilitySorter? sorter; [JsonIgnore] - private Dictionary installedProvides = null; + private Dictionary? installedProvides = null; [JsonIgnore] - private Dictionary tags; + private Dictionary? tags; [JsonIgnore] - private HashSet untagged; + private HashSet? untagged; [JsonIgnore] - private Dictionary> downloadHashesIndex; + private Dictionary>? downloadHashesIndex; [JsonIgnore] - private Dictionary> downloadUrlHashIndex; + private Dictionary>? downloadUrlHashIndex; // Index of which mods provide what, format: // providers[provided] = { provider1, provider2, ... } // Built by BuildProvidesIndex, makes LatestAvailableWithProvides much faster. [JsonIgnore] - private Dictionary> providers; + private Dictionary>? providers; private void InvalidateAvailableModCaches() { @@ -536,7 +556,7 @@ private void InvalidateInstalledCaches() private void RepositoriesUpdated(Repository[] which) { - if (Repositories.Values.Any(r => which.Contains(r))) + if (Repositories?.Values.Any(r => which.Contains(r)) ?? false) { // One of our repos changed, old cached data is now junk EnlistWithTransaction(); @@ -563,10 +583,10 @@ public CompatibilitySorter SetCompatibleVersion(GameVersionCriteria versCrit) } sorter = new CompatibilitySorter( versCrit, - repoDataMgr?.GetAllAvailDicts( - repositories.Values.OrderBy(r => r.priority) - // Break ties alphanumerically - .ThenBy(r => r.name)), + repoDataMgr?.GetAllAvailDicts(repositories?.Values.OrderBy(r => r.priority) + // Break ties alphanumerically + .ThenBy(r => r.name)) + ?? Enumerable.Empty>(), providers, installed_modules, InstalledDlls.ToHashSet(), InstalledDlc); } @@ -576,9 +596,13 @@ public CompatibilitySorter SetCompatibleVersion(GameVersionCriteria versCrit) /// /// /// - public IEnumerable CompatibleModules(GameVersionCriteria crit) + public IEnumerable CompatibleModules(GameVersionCriteria? crit) // Set up our compatibility partition - => SetCompatibleVersion(crit).LatestCompatible; + => crit != null ? SetCompatibleVersion(crit).LatestCompatible + : repoDataMgr?.GetAllAvailableModules(Repositories.Values) + .Select(am => am.Latest()) + .OfType() + ?? Enumerable.Empty(); /// /// @@ -614,14 +638,14 @@ private AvailableModule[] getAvail(string identifier) /// /// /// - public CkanModule LatestAvailable(string identifier, - GameVersionCriteria gameVersion, - RelationshipDescriptor relationshipDescriptor = null, - ICollection installed = null, - ICollection toInstall = null) + public CkanModule? LatestAvailable(string identifier, + GameVersionCriteria? gameVersion, + RelationshipDescriptor? relationshipDescriptor = null, + ICollection? installed = null, + ICollection? toInstall = null) => getAvail(identifier)?.Select(am => am.Latest(gameVersion, relationshipDescriptor, installed, toInstall)) - .Where(m => m != null) + .OfType() .OrderByDescending(m => m.version) .FirstOrDefault(); @@ -642,7 +666,7 @@ public IEnumerable AvailableByIdentifier(string identifier) /// or null if it does not exist. /// /// - public CkanModule GetModuleByVersion(string ident, ModuleVersion version) + public CkanModule? GetModuleByVersion(string ident, ModuleVersion version) => Utilities.DefaultIfThrows(() => getAvail(ident)) ?.Select(am => am.ByVersion(version)) .FirstOrDefault(m => m != null); @@ -658,15 +682,15 @@ public string GetAvailableMetadata(string identifier) => repoDataMgr == null ? "" : string.Join("", - repoDataMgr.GetAvailableModules(repositories.Values, identifier) + 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 LatestCompatibleGameVersion(List realVersions, - string identifier) + public GameVersion? LatestCompatibleGameVersion(List realVersions, + string identifier) => Utilities.DefaultIfThrows(() => getAvail(identifier)) ?.Select(am => am.LatestCompatibleGameVersion(realVersions)) .Max(); @@ -674,12 +698,16 @@ public GameVersion LatestCompatibleGameVersion(List realVersions, /// /// Generate the providers index so we can find providing modules quicker /// + [MemberNotNull(nameof(providers))] private void BuildProvidesIndex() { providers = new Dictionary>(); - foreach (AvailableModule am in repoDataMgr.GetAllAvailableModules(repositories?.Values)) + if (repoDataMgr != null) { - BuildProvidesIndexFor(am); + foreach (AvailableModule am in repoDataMgr.GetAllAvailableModules(repositories?.Values)) + { + BuildProvidesIndexFor(am); + } } } @@ -688,15 +716,12 @@ private void BuildProvidesIndex() /// private void BuildProvidesIndexFor(AvailableModule am) { - if (providers == null) - { - providers = new Dictionary>(); - } + providers ??= new Dictionary>(); foreach (CkanModule m in am.AllAvailable()) { foreach (string provided in m.ProvidesList) { - if (providers.TryGetValue(provided, out HashSet provs)) + if (providers.TryGetValue(provided, out HashSet? provs)) { provs.Add(am); } @@ -745,13 +770,17 @@ public HashSet Untagged /// /// Assemble a mapping from tags to modules /// + [MemberNotNull(nameof(tags), nameof(untagged))] private void BuildTagIndex() { tags = new Dictionary(); untagged = new HashSet(); - foreach (AvailableModule am in repoDataMgr.GetAllAvailableModules(repositories.Values)) + if (repoDataMgr != null) { - BuildTagIndexFor(am); + foreach (AvailableModule am in repoDataMgr.GetAllAvailableModules(repositories?.Values)) + { + BuildTagIndexFor(am); + } } } @@ -762,18 +791,18 @@ private void BuildTagIndexFor(AvailableModule am) { if (m.Tags != null) { + tags ??= new Dictionary(); tagged = true; foreach (string tagName in m.Tags) { - if (tags.TryGetValue(tagName, out ModuleTag tag)) + if (tags.TryGetValue(tagName, out ModuleTag? tag)) { tag.Add(m.identifier); } else { - tags.Add(tagName, new ModuleTag() + tags.Add(tagName, new ModuleTag(tagName) { - Name = tagName, ModuleIdentifiers = new HashSet() { m.identifier }, }); } @@ -782,6 +811,7 @@ private void BuildTagIndexFor(AvailableModule am) } if (!tagged) { + untagged ??= new HashSet(); untagged.Add(am.AllAvailable().First().identifier); } } @@ -790,26 +820,27 @@ private void BuildTagIndexFor(AvailableModule am) /// /// public List LatestAvailableWithProvides( - string identifier, - GameVersionCriteria gameVersion, - RelationshipDescriptor relationship_descriptor = null, - ICollection installed = null, - ICollection toInstall = null) + string identifier, + GameVersionCriteria? gameVersion, + RelationshipDescriptor? relationship_descriptor = null, + ICollection? installed = null, + ICollection? toInstall = null) { if (providers == null) { BuildProvidesIndex(); } - if (providers.TryGetValue(identifier, out HashSet provs)) + if (providers.TryGetValue(identifier, out HashSet? provs)) { // For each AvailableModule, we want the latest one matching our constraints return provs .Select(am => am.Latest(gameVersion, relationship_descriptor, installed, toInstall)) + .OfType() .Where(m => m?.ProvidesList?.Contains(identifier) ?? false) // Put the most popular one on top - .OrderByDescending(m => repoDataMgr.GetDownloadCount(Repositories.Values, - m.identifier) + .OrderByDescending(m => repoDataMgr?.GetDownloadCount(Repositories?.Values, + m.identifier) ?? 0) .ToList(); } @@ -850,7 +881,7 @@ public void RegisterModule(CkanModule mod, // We have to flip back to absolute paths to actually test this. foreach (string file in relativeFiles.Where(file => !Directory.Exists(inst.ToAbsoluteGameDir(file)))) { - if (installed_files.TryGetValue(file, out string owner)) + if (installed_files.TryGetValue(file, out string? owner)) { // Woah! Registering an already owned file? Not cool! // (Although if it existed, we should have thrown a kraken well before this.) @@ -879,7 +910,7 @@ public void RegisterModule(CkanModule mod, } // Make sure mod-owned files aren't in the manually installed DLL dict - installed_dlls.RemoveWhere(kvp => relativeFiles.Contains(kvp.Value)); + installed_dlls?.RemoveWhere(kvp => relativeFiles.Contains(kvp.Value)); // Finally register our module proper installed_modules.Add(mod.identifier, @@ -995,7 +1026,7 @@ public Dictionary Installed(bool withProvides = true, boo { var installed = new Dictionary(); - if (withDLLs) + if (withDLLs && installed_dlls != null) { // Index our DLLs, as much as we dislike them. foreach (var dllinfo in installed_dlls) @@ -1026,8 +1057,8 @@ public Dictionary Installed(bool withProvides = true, boo /// /// /// - public InstalledModule InstalledModule(string module) - => installed_modules.TryGetValue(module, out InstalledModule installedModule) + public InstalledModule? InstalledModule(string module) + => installed_modules.TryGetValue(module, out InstalledModule? installedModule) ? installedModule : null; @@ -1040,25 +1071,22 @@ public InstalledModule InstalledModule(string module) /// internal Dictionary ProvidedByInstalled() { - if (installedProvides == null) - { - installedProvides = installed_modules.Values - .Select(im => im.Module) - .Where(m => m.provides != null) - .SelectMany(m => m.provides.Select(p => - new KeyValuePair( - p, new ProvidesModuleVersion( - m.identifier, m.version.ToString())))) - .DistinctBy(kvp => kvp.Key) - .ToDictionary(); - } + installedProvides ??= installed_modules.Values + .Select(im => im.Module) + .SelectMany(m => m.provides?.Select(p => + new KeyValuePair( + p, new ProvidesModuleVersion( + m.identifier, m.version.ToString()))) + ?? Enumerable.Empty>()) + .DistinctBy(kvp => kvp.Key) + .ToDictionary(); return installedProvides; } - private ProvidesModuleVersion ProvidedByInstalled(string provided) + private ProvidesModuleVersion? ProvidedByInstalled(string provided) => installedProvides != null // The dictionary helps if we already have it cached... - ? installedProvides.TryGetValue(provided, out ProvidesModuleVersion version) + ? installedProvides.TryGetValue(provided, out ProvidesModuleVersion? version) ? version : null // ... but otherwise it's not worth the expense to calculate it @@ -1073,14 +1101,15 @@ private ProvidesModuleVersion ProvidedByInstalled(string provided) /// /// /// - public ModuleVersion InstalledVersion(string modIdentifier, bool with_provides = true) + public ModuleVersion? InstalledVersion(string modIdentifier, bool with_provides = true) // If it's genuinely installed, return the details we have. // (Includes DLCs) => installed_modules.TryGetValue(modIdentifier, - out InstalledModule installedModule) + out InstalledModule? installedModule) ? installedModule.Module.version // If it's in our autodetected registry, return that. - : installed_dlls.ContainsKey(modIdentifier) ? (ModuleVersion)new UnmanagedModuleVersion(null) + : installed_dlls != null && installed_dlls.ContainsKey(modIdentifier) + ? new UnmanagedModuleVersion(null) // Finally we have our provided checks. We'll skip these if // withProvides is false. : with_provides ? ProvidedByInstalled(modIdentifier) @@ -1089,16 +1118,16 @@ public ModuleVersion InstalledVersion(string modIdentifier, bool with_provides = /// /// /// - public CkanModule GetInstalledVersion(string mod_identifier) + public CkanModule? GetInstalledVersion(string mod_identifier) => InstalledModule(mod_identifier)?.Module; /// /// Returns the module which owns this file, or null if not known. /// Throws a PathErrorKraken if an absolute path is provided. /// - public InstalledModule FileOwner(string file) + public InstalledModule? FileOwner(string file) => installed_files.TryGetValue(CKANPathUtils.NormalizePath(file), - out string fileOwner) + out string? fileOwner) ? InstalledModule(fileOwner) : null; @@ -1108,7 +1137,7 @@ public InstalledModule FileOwner(string file) public void CheckSanity() { SanityChecker.EnforceConsistency(installed_modules.Select(pair => pair.Value.Module), - installed_dlls.Keys, InstalledDlc); + installed_dlls?.Keys, InstalledDlc); } /// @@ -1122,12 +1151,12 @@ public void CheckSanity() /// Installed DLCs /// List of modules whose dependencies are about to be or already removed. public static IEnumerable FindReverseDependencies( - List modulesToRemove, - List modulesToInstall, - HashSet origInstalled, - HashSet dlls, - IDictionary dlc, - Func satisfiedFilter = null) + List modulesToRemove, + List? modulesToInstall, + HashSet origInstalled, + HashSet? dlls, + IDictionary? dlc, + Func? satisfiedFilter = null) { log.DebugFormat("Finding reverse dependencies of: {0}", string.Join(", ", modulesToRemove)); log.DebugFormat("From installed mods: {0}", string.Join(", ", origInstalled)); @@ -1159,7 +1188,8 @@ public static IEnumerable FindReverseDependencies( log.DebugFormat("Keeping: {0}", string.Join(", ", hypothetical)); // Find what would break with this configuration - var brokenDeps = SanityChecker.FindUnsatisfiedDepends(hypothetical, dlls, dlc); + var brokenDeps = SanityChecker.FindUnsatisfiedDepends(hypothetical, dlls, dlc) + .ToList(); if (satisfiedFilter != null) { brokenDeps.RemoveAll(kvp => satisfiedFilter(kvp.Item2)); @@ -1201,13 +1231,13 @@ public static IEnumerable FindReverseDependencies( /// Return modules which are dependent on the modules passed in or modules in the return list /// public IEnumerable FindReverseDependencies( - List modulesToRemove, - List modulesToInstall = null, - Func satisfiedFilter = null) + List modulesToRemove, + List? modulesToInstall = null, + Func? satisfiedFilter = null) => FindReverseDependencies(modulesToRemove, modulesToInstall, new HashSet(installed_modules.Values.Select(x => x.Module)), - new HashSet(installed_dlls.Keys), + installed_dlls?.Keys.ToHashSet(), InstalledDlc, satisfiedFilter); @@ -1219,24 +1249,28 @@ public IEnumerable FindReverseDependencies( /// dictionary[sha256 or sha1] = {mod1, mod2, mod3}; /// public Dictionary> GetDownloadHashesIndex() - => downloadHashesIndex = downloadHashesIndex - ?? repoDataMgr.GetAllAvailableModules(repositories.Values) - .SelectMany(availMod => availMod.module_version.Values) - .SelectMany(ModWithDownloadHashes) - .GroupBy(tuple => tuple.Item1, - tuple => tuple.Item2) - .ToDictionary(grp => grp.Key, - grp => grp.ToList()); + => downloadHashesIndex ??= + (repoDataMgr?.GetAllAvailableModules(repositories?.Values) + .SelectMany(availMod => availMod.module_version.Values) + ?? Enumerable.Empty()) + .SelectMany(ModWithDownloadHashes) + .GroupBy(tuple => tuple.Item1, + tuple => tuple.Item2) + .ToDictionary(grp => grp.Key, + grp => grp.ToList()); private IEnumerable> ModWithDownloadHashes(CkanModule m) { - if (!string.IsNullOrEmpty(m.download_hash?.sha256)) + if (m.download_hash is DownloadHashesDescriptor descr) { - yield return new Tuple(m.download_hash.sha256, m); - } - if (!string.IsNullOrEmpty(m.download_hash?.sha1)) - { - yield return new Tuple(m.download_hash.sha1, m); + if (descr.sha256 != null && !string.IsNullOrEmpty(descr.sha256)) + { + yield return new Tuple(descr.sha256, m); + } + if (descr.sha1 != null && !string.IsNullOrEmpty(descr.sha1)) + { + yield return new Tuple(descr.sha1, m); + } } } @@ -1248,12 +1282,12 @@ private IEnumerable> ModWithDownloadHashes(CkanModule /// dictionary[urlHash] = {mod1, mod2, mod3}; /// public Dictionary> GetDownloadUrlHashIndex() - => downloadUrlHashIndex = downloadUrlHashIndex - ?? (repoDataMgr?.GetAllAvailableModules(repositories.Values) + => downloadUrlHashIndex ??= + (repoDataMgr?.GetAllAvailableModules(repositories?.Values) ?? Enumerable.Empty()) .SelectMany(am => am.module_version.Values) - .Where(m => m.download != null && m.download.Count > 0) - .SelectMany(m => m.download.Select(url => new Tuple(url, m))) + .SelectMany(m => m.download?.Select(url => new Tuple(url, m)) + ?? Enumerable.Empty>()) .GroupBy(tuple => tuple.Item1, tuple => tuple.Item2) .ToDictionary(grp => NetFileCache.CreateURLHash(grp.Key), @@ -1265,23 +1299,24 @@ public Dictionary> GetDownloadUrlHashIndex() /// /// Host strings without duplicates public IEnumerable GetAllHosts() - => repoDataMgr.GetAllAvailableModules(repositories.Values) - // Pick all latest modules where download is not null - // Merge all the URLs into one sequence - .SelectMany(availMod => (availMod?.Latest()?.download - ?? Enumerable.Empty()) - .Append(availMod?.Latest()?.InternetArchiveDownload)) - // Skip relative URLs because they don't have hosts - .Where(dlUri => dlUri?.IsAbsoluteUri ?? false) - // 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); - + => 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()) + .Append(availMod?.Latest()?.InternetArchiveDownload)) + .OfType() + // 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) + ?? Enumerable.Empty(); // Older clients expect these properties and can handle them being empty ("{}") but not null #pragma warning disable IDE0052 diff --git a/Core/Registry/RegistryManager.cs b/Core/Registry/RegistryManager.cs index 67c4a84732..f6e6a3d7d0 100644 --- a/Core/Registry/RegistryManager.cs +++ b/Core/Registry/RegistryManager.cs @@ -7,13 +7,16 @@ using System.Runtime.Serialization; using System.ComponentModel; using System.Reflection; +using System.Diagnostics.CodeAnalysis; using ChinhDo.Transactions.FileManager; using log4net; using Newtonsoft.Json; using CKAN.Versioning; +#if !NET8_0_OR_GREATER using CKAN.Extensions; +#endif namespace CKAN { @@ -25,8 +28,8 @@ public class RegistryManager : IDisposable private static readonly ILog log = LogManager.GetLogger(typeof(RegistryManager)); private readonly string path; public readonly string lockfilePath; - private FileStream lockfileStream = null; - private StreamWriter lockfileWriter = null; + private FileStream? lockfileStream = null; + private StreamWriter? lockfileWriter = null; private readonly GameInstance gameInstance; @@ -35,12 +38,12 @@ public class RegistryManager : IDisposable /// /// If loading the registry failed, the parsing error text, else null. /// - public string previousCorruptedMessage; + public string? previousCorruptedMessage; /// /// If loading the registry failed, the location to which we moved it, else null. /// - public string previousCorruptedPath; + public string? previousCorruptedPath; private static string InstanceRegistryLockPath(string ckanDirPath) => Path.Combine(ckanDirPath, "registry.locked"); @@ -281,7 +284,7 @@ public static RegistryManager Instance(GameInstance inst, RepositoryDataManager public static void DisposeInstance(GameInstance inst) { - if (registryCache.TryGetValue(inst.CkanDir(), out RegistryManager regMgr)) + if (registryCache.TryGetValue(inst.CkanDir(), out RegistryManager? regMgr)) { regMgr.Dispose(); } @@ -300,6 +303,7 @@ public static void DisposeAll() } } + [MemberNotNull(nameof(registry))] private void Load(RepositoryDataManager repoData) { // Our registry needs to know our game instance when upgrading from older @@ -320,6 +324,7 @@ private void Load(RepositoryDataManager repoData) log.InfoFormat("Loaded CKAN registry at {0}", path); } + [MemberNotNull(nameof(registry))] private void LoadOrCreate(RepositoryDataManager repoData) { try @@ -354,6 +359,7 @@ private void LoadOrCreate(RepositoryDataManager repoData) AscertainDefaultRepo(); } + [MemberNotNull(nameof(registry))] private void Create() { log.InfoFormat("Creating new CKAN registry at {0}", path); @@ -365,7 +371,7 @@ private void Create() private void AscertainDefaultRepo() { - if (registry.Repositories == null || registry.Repositories.Count == 0) + if (registry.Repositories.Count == 0) { log.InfoFormat("Fabricating repository: {0}", gameInstance.game.DefaultRepositoryURL); var name = $"{gameInstance.game.ShortName}-{Repository.default_ckan_repo_name}"; @@ -405,7 +411,7 @@ public void Save(bool enforce_consistency = true) registry.CheckSanity(); } - string directoryPath = Path.GetDirectoryName(path); + var directoryPath = Path.GetDirectoryName(path); if (directoryPath == null) { @@ -490,9 +496,9 @@ public CkanModule GenerateModpack(bool recommends = false, bool with_versions = ksp_version_min = minAndMax.Lower.AsInclusiveLower().WithoutBuild, ksp_version_max = minAndMax.Upper.AsInclusiveUpper().WithoutBuild, download_content_type = typeof(CkanModule).GetTypeInfo() - .GetDeclaredField("download_content_type") - .GetCustomAttribute() - .Value.ToString(), + ?.GetDeclaredField("download_content_type") + ?.GetCustomAttribute() + ?.Value?.ToString(), release_date = DateTime.Now, }; @@ -568,7 +574,7 @@ public bool ScanUnmanagedFiles() .Where(file => file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase)) .Select(absPath => gameInstance.ToRelativeGameDir(absPath)) .Where(relPath => !gameInstance.game.StockFolders.Any(f => relPath.StartsWith($"{f}/"))) - .GroupBy(relPath => gameInstance.DllPathToIdentifier(relPath)) + .GroupBy(relPath => gameInstance.DllPathToIdentifier(relPath) ?? "") .ToDictionary(grp => grp.Key, grp => grp.First()); log.DebugFormat("Registering DLLs: {0}", string.Join(", ", dlls.Values)); @@ -605,9 +611,10 @@ private static IEnumerable> TestDlcScan(stri private IEnumerable> WellKnownDlcScan() => gameInstance.game.DlcDetectors - .Select(d => d.IsInstalled(gameInstance, out string identifier, out UnmanagedModuleVersion version) + .Select(d => d.IsInstalled(gameInstance, out string? identifier, out UnmanagedModuleVersion? version) + && identifier is not null && version is not null ? new KeyValuePair(identifier, version) - : new KeyValuePair(null, null)) - .Where(pair => pair.Key != null); + : (KeyValuePair?)null) + .OfType>(); } } diff --git a/Core/Registry/Tags/ModuleTag.cs b/Core/Registry/Tags/ModuleTag.cs index 85e2a9e75a..730fa070d0 100644 --- a/Core/Registry/Tags/ModuleTag.cs +++ b/Core/Registry/Tags/ModuleTag.cs @@ -4,6 +4,11 @@ namespace CKAN { public class ModuleTag { + public ModuleTag(string name) + { + Name = name; + } + public string Name; public HashSet ModuleIdentifiers = new HashSet(); diff --git a/Core/Registry/Tags/ModuleTagList.cs b/Core/Registry/Tags/ModuleTagList.cs index dac8fc6e7f..1e8e39b263 100644 --- a/Core/Registry/Tags/ModuleTagList.cs +++ b/Core/Registry/Tags/ModuleTagList.cs @@ -12,7 +12,7 @@ public class ModuleTagList public static readonly string DefaultPath = Path.Combine(CKANPathUtils.AppDataPath, "tags.json"); - public static ModuleTagList Load(string path) + public static ModuleTagList? Load(string path) { try { @@ -24,6 +24,8 @@ public static ModuleTagList Load(string path) } } + public static readonly ModuleTagList ModuleTags = Load(DefaultPath) ?? new ModuleTagList(); + public bool Save(string path) { try diff --git a/Core/Relationships/RelationshipResolver.cs b/Core/Relationships/RelationshipResolver.cs index 5712baa9d4..3bcc6bfa41 100644 --- a/Core/Relationships/RelationshipResolver.cs +++ b/Core/Relationships/RelationshipResolver.cs @@ -9,7 +9,7 @@ namespace CKAN { - using ModPair = KeyValuePair; + using ModPair = KeyValuePair; /// /// Resolves relationships between mods. Primarily used to satisfy missing dependencies and to check for conflicts on proposed installs. @@ -28,10 +28,10 @@ public class RelationshipResolver /// CKAN registry object for current game instance /// The current KSP version criteria to consider public RelationshipResolver(IEnumerable modulesToInstall, - IEnumerable modulesToRemove, + IEnumerable? modulesToRemove, RelationshipResolverOptions options, IRegistryQuerier registry, - GameVersionCriteria versionCrit) + GameVersionCriteria? versionCrit) : this(options, registry, versionCrit) { if (modulesToRemove != null) @@ -52,7 +52,7 @@ public RelationshipResolver(IEnumerable modulesToInstall, /// The current KSP version criteria to consider private RelationshipResolver(RelationshipResolverOptions options, IRegistryQuerier registry, - GameVersionCriteria versionCrit) + GameVersionCriteria? versionCrit) { this.options = options; this.registry = registry; @@ -127,8 +127,8 @@ private void AddModulesToInstall(CkanModule[] modules) } catch (BadRelationshipsKraken k) when (options.without_enforce_consistency) { - conflicts.AddRange(k.Conflicts.Select(kvp => new ModPair(kvp.Item1, kvp.Item3)) - .Where(kvp => !conflicts.Contains(kvp)) + conflicts.AddRange(k.Conflicts.Select(tuple => new ModPair(tuple.Item1, tuple.Item3)) + .Where(pair => !conflicts.Contains(pair)) .ToArray()); } } @@ -143,7 +143,7 @@ private void RemoveModsFromInstalledList(IEnumerable mods) foreach (var module in mods) { installed_modules.Remove(module); - conflicts.RemoveAll(kvp => kvp.Key.Equals(module) || kvp.Value.Equals(module)); + conflicts.RemoveAll(kvp => module.Equals(kvp.Key) || module.Equals(kvp.Value)); } } @@ -151,9 +151,9 @@ private void RemoveModsFromInstalledList(IEnumerable mods) /// Resolves all relationships for a module. /// May recurse to ResolveStanza, which may add additional modules to be installed. /// - private void Resolve(CkanModule module, - RelationshipResolverOptions options, - IEnumerable old_stanza = null) + private void Resolve(CkanModule module, + RelationshipResolverOptions options, + IEnumerable? old_stanza = null) { if (alreadyResolved.Contains(module)) { @@ -210,11 +210,11 @@ private void Resolve(CkanModule module, /// /// See RelationshipResolverOptions for further adjustments that can be made. /// - private void ResolveStanza(List stanza, - SelectionReason reason, - RelationshipResolverOptions options, - bool soft_resolve = false, - IEnumerable old_stanza = null) + private void ResolveStanza(List? stanza, + SelectionReason reason, + RelationshipResolverOptions options, + bool soft_resolve = false, + IEnumerable? old_stanza = null) { if (stanza == null) { @@ -236,7 +236,7 @@ private void ResolveStanza(List stanza, // If we already have this dependency covered, // resolve its relationships if we haven't already. - if (descriptor.MatchesAny(modlist.Values, null, null, out CkanModule installingCandidate)) + if (descriptor.MatchesAny(modlist.Values, null, null, out CkanModule? installingCandidate)) { if (installingCandidate != null) { @@ -252,11 +252,14 @@ private void ResolveStanza(List stanza, else if (descriptor.ContainsAny(modlist.Keys)) { // Two installing mods depend on different versions of this dependency - CkanModule module = modlist.Values.FirstOrDefault(m => descriptor.ContainsAny(new string[] { m.identifier })); + var module = modlist.Values.FirstOrDefault(m => descriptor.ContainsAny(new string[] { m.identifier })); if (options.proceed_with_inconsistencies) { - conflicts.Add(new ModPair(module, reason.Parent)); - conflicts.Add(new ModPair(reason.Parent, module)); + if (module != null && reason is SelectionReason.RelationshipReason rel) + { + conflicts.Add(new ModPair(module, rel.Parent)); + conflicts.Add(new ModPair(rel.Parent, module)); + } continue; } else @@ -277,11 +280,14 @@ private void ResolveStanza(List stanza, else if (descriptor.ContainsAny(installed_modules.Select(im => im.identifier))) { // We need a different version of the mod than is already installed - CkanModule module = installed_modules.FirstOrDefault(m => descriptor.ContainsAny(new string[] { m.identifier })); + var module = installed_modules.FirstOrDefault(m => descriptor.ContainsAny(new string[] { m.identifier })); if (options.proceed_with_inconsistencies) { - conflicts.Add(new ModPair(module, reason.Parent)); - conflicts.Add(new ModPair(reason.Parent, module)); + if (module != null && reason is SelectionReason.RelationshipReason rel) + { + conflicts.Add(new ModPair(module, rel.Parent)); + conflicts.Add(new ModPair(rel.Parent, module)); + } continue; } else @@ -298,7 +304,8 @@ private void ResolveStanza(List stanza, .LatestAvailableWithProvides(registry, versionCrit, installed_modules, modlist.Values) .Where(mod => !modlist.ContainsKey(mod.identifier) && descriptor1.WithinBounds(mod) - && MightBeInstallable(mod, reason.Parent, installed_modules)) + && reason is SelectionReason.RelationshipReason rel + && MightBeInstallable(mod, rel.Parent, installed_modules)) .ToList(); if (!candidates.Any()) { @@ -314,10 +321,10 @@ private void ResolveStanza(List stanza, if (!candidates.Any()) { - if (!soft_resolve) + if (!soft_resolve && reason is SelectionReason.RelationshipReason rel) { log.InfoFormat("Dependency on {0} found but it is not listed in the index, or not available for your game version.", descriptor.ToString()); - throw new DependencyNotSatisfiedKraken(reason.Parent, descriptor.ToString()); + throw new DependencyNotSatisfiedKraken(rel.Parent, descriptor.ToString() ?? ""); } log.InfoFormat("{0} is recommended/suggested but it is not listed in the index, or not available for your game version.", descriptor.ToString()); continue; @@ -327,7 +334,7 @@ private void ResolveStanza(List stanza, // Oh no, too many to pick from! if (options.without_toomanyprovides_kraken) { - if (options.get_recommenders && !(reason is SelectionReason.Depends)) + if (options.get_recommenders && reason is not SelectionReason.Depends) { for (int i = 0; i < candidates.Count; ++i) { @@ -346,18 +353,18 @@ private void ResolveStanza(List stanza, List provide = candidates .Where(cand => old_stanza.Any(rel => rel.WithinBounds(cand))) .ToList(); - if (provide.Count != 1) + if (provide.Count != 1 && reason is SelectionReason.RelationshipReason rel) { // We still have either nothing, or too many to pick from // Just throw the TMP now - throw new TooManyModsProvideKraken(reason.Parent, descriptor.ToString(), + throw new TooManyModsProvideKraken(rel.Parent, descriptor.ToString() ?? "", candidates, descriptor.choice_help_text); } candidates[0] = provide.First(); } - else + else if (reason is SelectionReason.RelationshipReason rel) { - throw new TooManyModsProvideKraken(reason.Parent, descriptor.ToString(), + throw new TooManyModsProvideKraken(rel.Parent, descriptor.ToString() ?? "", candidates, descriptor.choice_help_text); } } @@ -391,16 +398,16 @@ private void ResolveStanza(List stanza, throw new InconsistentKraken(string.Format( Properties.Resources.RelationshipResolverConflictsWith, conflictingModDescription(conflicting_mod, null), - conflictingModDescription(candidate, reason.Parent))); + conflictingModDescription(candidate, (reason as SelectionReason.RelationshipReason)?.Parent))); } } } - private string conflictingModDescription(CkanModule mod, CkanModule parent) + private string conflictingModDescription(CkanModule? mod, CkanModule? parent) => mod == null ? Properties.Resources.RelationshipResolverAnUnmanaged : parent == null && ReasonsFor(mod).Any(r => r is SelectionReason.UserRequested - || r is SelectionReason.Installed) + or SelectionReason.Installed) // No parenthetical needed if it's user requested ? mod.ToString() // Explain why we're looking at this mod @@ -426,9 +433,9 @@ private void Add(CkanModule module, SelectionReason reason) log.DebugFormat("Adding {0} {1}", module.identifier, module.version); - if (modlist.TryGetValue(module.identifier, out CkanModule possibleDup)) + if (modlist.TryGetValue(module.identifier, out CkanModule? possibleDup)) { - if (possibleDup.identifier == module.identifier) + if (possibleDup?.identifier == module.identifier) { // We should never add the same module twice! log.ErrorFormat("Assertion failed: Adding {0} twice in relationship resolution", module.identifier); @@ -462,9 +469,9 @@ private void Add(CkanModule module, SelectionReason reason) } } - private bool MightBeInstallable(CkanModule module, - CkanModule stanzaSource = null, - ICollection installed = null) + private bool MightBeInstallable(CkanModule module, + CkanModule? stanzaSource = null, + ICollection? installed = null) => MightBeInstallable(module, stanzaSource, installed ?? new List(), new List()); @@ -479,7 +486,7 @@ private bool MightBeInstallable(CkanModule module, /// For internal use /// Whether its dependencies are compatible with the current game version private bool MightBeInstallable(CkanModule module, - CkanModule stanzaSource, + CkanModule? stanzaSource, ICollection installed, List parentCompat) { @@ -535,11 +542,6 @@ public IEnumerable ModList(bool parallel = true) private int totalDependers(CkanModule module) => allDependers(module).Count(); - private static bool AnyRelationship(SelectionReason r) - => r is SelectionReason.Depends - || r is SelectionReason.Recommended - || r is SelectionReason.Suggested; - private static IEnumerable BreadthFirstSearch(IEnumerable startingGroup, Func, IEnumerable> getNextGroup) { @@ -558,12 +560,12 @@ private static IEnumerable BreadthFirstSearch(IEnumerable } } - private IEnumerable allDependers(CkanModule module, - Func followReason = null) + private IEnumerable allDependers(CkanModule module) => BreadthFirstSearch(Enumerable.Repeat(module, 1), (searching, found) => - ReasonsFor(searching).Where(followReason ?? AnyRelationship) + ReasonsFor(searching).OfType() .Select(r => r.Parent) + .OfType() .Except(found)); public IEnumerable Dependencies() @@ -571,14 +573,14 @@ public IEnumerable Dependencies() (searching, found) => modlist.Values .Except(found) - .Where(m => ReasonsFor(m).Any(r => r is SelectionReason.Depends - && r.Parent == searching))); + .Where(m => ReasonsFor(m).Any(r => r is SelectionReason.Depends dep + && dep.Parent == searching))); public IEnumerable Recommendations(HashSet dependencies) => modlist.Values.Except(dependencies) .Where(m => ValidRecSugReasons(dependencies, - ReasonsFor(m).Where(r => r is SelectionReason.Recommended) - .ToList())) + ReasonsFor(m).OfType() + .ToArray())) .OrderByDescending(totalDependers); public IEnumerable Suggestions(HashSet dependencies, @@ -586,14 +588,16 @@ public IEnumerable Suggestions(HashSet dependencies, => modlist.Values.Except(dependencies) .Except(recommendations) .Where(m => ValidRecSugReasons(dependencies, - ReasonsFor(m).Where(r => r is SelectionReason.Suggested) - .ToList())) + ReasonsFor(m).OfType() + .ToArray())) .OrderByDescending(totalDependers); private bool ValidRecSugReasons(HashSet dependencies, - List recSugReasons) - => recSugReasons.Any(r => dependencies.Contains(r.Parent)) - && !suppressedRecommenders.Any(rel => recSugReasons.Any(r => rel.WithinBounds(r.Parent))); + SelectionReason.RelationshipReason[] recSugReasons) + => recSugReasons.OfType() + .Any(r => dependencies.Contains(r.Parent)) + && !suppressedRecommenders.Any(rel => recSugReasons.Any(r => r.Parent != null + && rel.WithinBounds(r.Parent))); public ParallelQuery>> Supporters( HashSet supported, @@ -607,9 +611,10 @@ public ParallelQuery>> Supporters( // Find each module that "supports" something we're installing .Select(mod => new KeyValuePair>( mod, - mod.supports + (mod.supports ?? Enumerable.Empty()) .Where(rel => rel.MatchesAny(supported, null, null)) .Select(rel => (rel as ModuleRelationshipDescriptor)?.name) + .OfType() .Where(name => !string.IsNullOrEmpty(name)) .ToHashSet())) .Where(kvp => kvp.Value.Count > 0); @@ -648,7 +653,7 @@ public IEnumerable ConflictDescriptions public bool IsConsistent => !conflicts.Any(); public List ReasonsFor(CkanModule mod) - => reasons.TryGetValue(mod, out List r) + => reasons.TryGetValue(mod, out List? r) ? r : new List(); @@ -665,12 +670,13 @@ public IEnumerable UserReasonsFor(CkanModule mod) /// true if auto-installed, false otherwise /// public bool IsAutoInstalled(CkanModule mod) - => ReasonsFor(mod).All(reason => reason is SelectionReason.Depends - && !reason.Parent.IsMetapackage); + => ReasonsFor(mod).All(reason => reason is SelectionReason.Depends dep + && dep.Parent != null + && !dep.Parent.IsMetapackage); private void AddReason(CkanModule module, SelectionReason reason) { - if (reasons.TryGetValue(module, out List modReasons)) + if (reasons.TryGetValue(module, out List? modReasons)) { modReasons.Add(reason); } @@ -697,7 +703,7 @@ private void AddReason(CkanModule module, SelectionReason reason) private readonly HashSet suppressedRecommenders = new HashSet(); private readonly IRegistryQuerier registry; - private readonly GameVersionCriteria versionCrit; + private readonly GameVersionCriteria? versionCrit; private readonly RelationshipResolverOptions options; /// @@ -848,63 +854,26 @@ public RelationshipResolverOptions WithoutSuggestions() public abstract class SelectionReason : IEquatable { // Currently assumed to exist for any relationship other than UserRequested or Installed - public virtual CkanModule Parent { get; protected set; } - public virtual string DescribeWith(IEnumerable others) => ToString(); + public virtual string DescribeWith(IEnumerable others) + => ToString() ?? ""; - public override bool Equals(object obj) + public override bool Equals(object? obj) => Equals(obj as SelectionReason); - public bool Equals(SelectionReason rsn) - { - // Parent throws in some derived classes - try - { - return GetType() == rsn?.GetType() - && Parent == rsn?.Parent; - } - catch - { - // If thrown, then the type check passed and Parent threw - return true; - } - } + public bool Equals(SelectionReason? rsn) + => GetType() == rsn?.GetType(); public override int GetHashCode() - { - var type = GetType(); - // Parent throws in some derived classes - try - { - #if NET5_0_OR_GREATER - return HashCode.Combine(type, Parent); - #else - unchecked - { - return (type, Parent).GetHashCode(); - } - #endif - } - catch - { - // If thrown, then we're type-only - return type.GetHashCode(); - } - } + => GetType().GetHashCode(); public class Installed : SelectionReason { - public override CkanModule Parent - => throw new Exception("Should never be called on Installed"); - public override string ToString() => Properties.Resources.RelationshipResolverInstalledReason; } public class UserRequested : SelectionReason { - public override CkanModule Parent - => throw new Exception("Should never be called on UserRequested"); - public override string ToString() => Properties.Resources.RelationshipResolverUserReason; } @@ -921,76 +890,90 @@ public override string ToString() => Properties.Resources.RelationshipResolverNoLongerUsedReason; } - public class Replacement : SelectionReason + public abstract class RelationshipReason : SelectionReason, IEquatable { - public Replacement(CkanModule module) + public RelationshipReason(CkanModule parent) { - if (module == null) + Parent = parent; + } + + public CkanModule Parent; + + public bool Equals(RelationshipReason? rsn) + => GetType() == rsn?.GetType() + && Parent == rsn?.Parent; + + public override int GetHashCode() + { + var type = GetType(); + #if NET5_0_OR_GREATER + return HashCode.Combine(type, Parent); + #else + unchecked { - #pragma warning disable IDE0016 - throw new ArgumentNullException(); - #pragma warning restore IDE0016 + return (type, Parent).GetHashCode(); } - Parent = module; + #endif + } + } + + public class Replacement : RelationshipReason + { + public Replacement(CkanModule module) + : base(module) + { } public override string ToString() - => string.Format(Properties.Resources.RelationshipResolverReplacementReason, Parent.name); + => string.Format(Properties.Resources.RelationshipResolverReplacementReason, + Parent.name); public override string DescribeWith(IEnumerable others) => string.Format(Properties.Resources.RelationshipResolverReplacementReason, - string.Join(", ", Enumerable.Repeat(this, 1).Concat(others).Select(r => r.Parent.name))); + string.Join(", ", + Enumerable.Repeat(this, 1) + .Concat(others) + .OfType() + .Select(r => r.Parent.name))); } - public sealed class Suggested : SelectionReason + public sealed class Suggested : RelationshipReason { public Suggested(CkanModule module) + : base(module) { - if (module == null) - { - #pragma warning disable IDE0016 - throw new ArgumentNullException(); - #pragma warning restore IDE0016 - } - Parent = module; } public override string ToString() - => string.Format(Properties.Resources.RelationshipResolverSuggestedReason, Parent.name); + => string.Format(Properties.Resources.RelationshipResolverSuggestedReason, + Parent.name); } - public sealed class Depends : SelectionReason + public sealed class Depends : RelationshipReason { public Depends(CkanModule module) + : base(module) { - if (module == null) - { - #pragma warning disable IDE0016 - throw new ArgumentNullException(); - #pragma warning restore IDE0016 - } - Parent = module; } public override string ToString() - => string.Format(Properties.Resources.RelationshipResolverDependsReason, Parent.name); + => string.Format(Properties.Resources.RelationshipResolverDependsReason, + Parent.name); public override string DescribeWith(IEnumerable others) => string.Format(Properties.Resources.RelationshipResolverDependsReason, - string.Join(", ", Enumerable.Repeat(this, 1).Concat(others).Select(r => r.Parent.name))); + string.Join(", ", + Enumerable.Repeat(this, 1) + .Concat(others) + .OfType() + .Select(r => r.Parent.name))); } - public sealed class Recommended : SelectionReason + public sealed class Recommended : RelationshipReason { public Recommended(CkanModule module, int providesIndex) + : base(module) { - if (module == null) - { - #pragma warning disable IDE0016 - throw new ArgumentNullException(); - #pragma warning restore IDE0016 - } - Parent = module; ProvidesIndex = providesIndex; } @@ -1000,7 +983,8 @@ public Recommended WithIndex(int providesIndex) => new Recommended(Parent, providesIndex); public override string ToString() - => string.Format(Properties.Resources.RelationshipResolverRecommendedReason, Parent.name); + => string.Format(Properties.Resources.RelationshipResolverRecommendedReason, + Parent.name); } } } diff --git a/Core/Relationships/SanityChecker.cs b/Core/Relationships/SanityChecker.cs index 0fb88eefe3..089d99518f 100644 --- a/Core/Relationships/SanityChecker.cs +++ b/Core/Relationships/SanityChecker.cs @@ -9,8 +9,8 @@ namespace CKAN { - using modRelPair = Tuple; - using modRelList = List>; + using modRelPair = Tuple; + using modRelList = List>; /// /// Sanity checks on what mods we have installed, or may install. @@ -22,12 +22,13 @@ public static class SanityChecker /// Throws a BadRelationshipsKraken describing the problems otherwise. /// Does nothing if the modules can happily co-exist. /// - public static void EnforceConsistency(IEnumerable modules, - IEnumerable dlls = null, - IDictionary dlc = null) + public static void EnforceConsistency(IEnumerable modules, + IEnumerable? dlls = null, + IDictionary? dlc = null) { if (!CheckConsistency(modules, dlls, dlc, - out modRelList unmetDepends, out modRelList conflicts)) + out List> unmetDepends, + out modRelList conflicts)) { throw new BadRelationshipsKraken(unmetDepends, conflicts); } @@ -37,21 +38,21 @@ public static void EnforceConsistency(IEnumerable modules /// Returns true if the mods supplied can co-exist. This checks depends/pre-depends/conflicts only. /// This is only used by tests! /// - public static bool IsConsistent(IEnumerable modules, - IEnumerable dlls = null, - IDictionary dlc = null) + public static bool IsConsistent(IEnumerable modules, + IEnumerable? dlls = null, + IDictionary? dlc = null) => CheckConsistency(modules, dlls, dlc, out var _, out var _); - private static bool CheckConsistency(IEnumerable modules, - IEnumerable dlls, - IDictionary dlc, - out modRelList UnmetDepends, - out modRelList Conflicts) + private static bool CheckConsistency(IEnumerable modules, + IEnumerable? dlls, + IDictionary? dlc, + out List> UnmetDepends, + out modRelList Conflicts) { - var modList = modules?.ToList(); + var modList = modules.ToList(); var dllSet = dlls?.ToHashSet(); - UnmetDepends = FindUnsatisfiedDepends(modList, dllSet, dlc); + UnmetDepends = FindUnsatisfiedDepends(modList, dllSet, dlc).ToList(); Conflicts = FindConflicting(modList, dllSet, dlc); return !UnmetDepends.Any() && !Conflicts.Any(); } @@ -66,14 +67,17 @@ private static bool CheckConsistency(IEnumerable modules, /// List of dependencies that aren't satisfied represented as pairs. /// Each Key is the depending module, and each Value is the relationship. /// - public static modRelList FindUnsatisfiedDepends(ICollection modules, - HashSet dlls, - IDictionary dlc) + public static IEnumerable> FindUnsatisfiedDepends( + ICollection modules, + HashSet? dlls, + IDictionary? dlc) => (modules?.Where(m => m.depends != null) - .SelectMany(m => m.depends.Select(dep => new modRelPair(m, dep, null))) + .SelectMany(m => (m.depends ?? Enumerable.Empty()) + .Select(dep => + new Tuple(m, dep))) .Where(kvp => !kvp.Item2.MatchesAny(modules, dlls, dlc)) - ?? Enumerable.Empty()) - .ToList(); + ?? Enumerable.Empty>()); /// /// Find conflicts among the given modules and DLLs. @@ -85,25 +89,25 @@ public static modRelList FindUnsatisfiedDepends(ICollection /// List of conflicts represented as pairs. /// Each Key is the depending module, and each Value is the relationship. /// - private static modRelList FindConflicting(List modules, - HashSet dlls, - IDictionary dlc) - => (modules?.Where(m => m.conflicts != null) - .SelectMany(m => FindConflictingWith( - m, - modules.Where(other => other.identifier != m.identifier) - .ToList(), - dlls, dlc)) - ?? Enumerable.Empty()) - .ToList(); + private static modRelList FindConflicting(List modules, + HashSet? dlls, + IDictionary? dlc) + => modules.Where(m => m.conflicts != null) + .SelectMany(m => FindConflictingWith( + m, + modules.Where(other => other.identifier != m.identifier) + .ToList(), + dlls, dlc)) + .ToList(); - private static IEnumerable FindConflictingWith(CkanModule module, - List otherMods, - HashSet dlls, - IDictionary dlc) - => module.conflicts.Select(rel => rel.MatchesAny(otherMods, dlls, dlc, out CkanModule other) - ? new modRelPair(module, rel, other) - : null) - .Where(pair => pair != null); + private static IEnumerable FindConflictingWith(CkanModule module, + List otherMods, + HashSet? dlls, + IDictionary? dlc) + => module.conflicts?.Select(rel => rel.MatchesAny(otherMods, dlls, dlc, out CkanModule? other) + ? new modRelPair(module, rel, other) + : null) + .OfType() + ?? Enumerable.Empty(); } } diff --git a/Core/Repositories/AvailableModule.cs b/Core/Repositories/AvailableModule.cs index 47dd524e7f..139fdd4dfe 100644 --- a/Core/Repositories/AvailableModule.cs +++ b/Core/Repositories/AvailableModule.cs @@ -26,12 +26,8 @@ public class AvailableModule [JsonIgnore] private string identifier; - [JsonConstructor] - private AvailableModule() - { - } - /// The module to keep track of + [JsonConstructor] public AvailableModule(string identifier) { this.identifier = identifier; @@ -49,7 +45,8 @@ public AvailableModule(string identifier, IEnumerable modules) [OnDeserialized] internal void DeserialisationFixes(StreamingContext context) { - identifier = module_version.Values.LastOrDefault()?.identifier; + identifier = module_version.Values.Select(m => m.identifier) + .Last(); Debug.Assert(module_version.Values.All(m => identifier.Equals(m.identifier))); } @@ -103,11 +100,11 @@ private void Add(CkanModule module) /// Modules that are already installed /// Modules that are planned to be installed /// - public CkanModule Latest( - GameVersionCriteria ksp_version = null, - RelationshipDescriptor relationship = null, - ICollection installed = null, - ICollection toInstall = null) + public CkanModule? Latest( + GameVersionCriteria? ksp_version = null, + RelationshipDescriptor? relationship = null, + ICollection? installed = null, + ICollection? toInstall = null) { IEnumerable modules = module_version.Values.Reverse(); if (relationship != null) @@ -150,7 +147,7 @@ private static bool DependsAndConflictsOK(CkanModule module, ICollection realVersions) { // Can't get later than Any, so no need for more complex logic return realVersions?.LastOrDefault() + // This is needed for when we have no real versions loaded, such as tests ?? module_version.Values.Select(m => m.LatestCompatibleGameVersion()) - .Max(); + .Max() + ?? module_version.Values.Last().LatestCompatibleGameVersion(); } // Find the range with the highest upper bound var bestRange = ranges.Distinct() @@ -213,15 +212,17 @@ public GameVersion LatestCompatibleGameVersion(List realVersions) ? r : best); return realVersions?.LastOrDefault(v => bestRange.Contains(v)) + // This is needed for when we have no real versions loaded, such as tests ?? module_version.Values.Select(m => m.LatestCompatibleGameVersion()) - .Max(); + .Max() + ?? module_version.Values.Last().LatestCompatibleGameVersion(); } /// /// Returns the module with the specified version, or null if that does not exist. /// - public CkanModule ByVersion(ModuleVersion v) - => module_version.TryGetValue(v, out CkanModule module) ? module : null; + public CkanModule? ByVersion(ModuleVersion v) + => module_version.TryGetValue(v, out CkanModule? module) ? module : null; /// /// Some code may expect this to be sorted in descending order diff --git a/Core/Repositories/ProgressScalePercentsByFileSize.cs b/Core/Repositories/ProgressScalePercentsByFileSize.cs index e726190482..d91d6d56d5 100644 --- a/Core/Repositories/ProgressScalePercentsByFileSize.cs +++ b/Core/Repositories/ProgressScalePercentsByFileSize.cs @@ -15,7 +15,7 @@ public class ProgressScalePercentsByFileSizes : IProgress /// /// The upstream progress object expecting percentages /// Sequence of sizes of files in our group - public ProgressScalePercentsByFileSizes(IProgress percentProgress, + public ProgressScalePercentsByFileSizes(IProgress? percentProgress, IEnumerable sizes) { this.percentProgress = percentProgress; @@ -59,12 +59,12 @@ public void NextFile() } } - private readonly IProgress percentProgress; - private readonly long[] sizes; - private readonly long totalSize; - private long doneSize = 0; - private int currentIndex = 0; - private int basePercent = 0; - private int lastPercent = -1; + private readonly IProgress? percentProgress; + private readonly long[] sizes; + private readonly 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 index 854b461779..083d678842 100644 --- a/Core/Repositories/ReadProgressStream.cs +++ b/Core/Repositories/ReadProgressStream.cs @@ -7,7 +7,7 @@ namespace CKAN { public class ReadProgressStream : ContainerStream { - public ReadProgressStream(Stream stream, IProgress progress) + public ReadProgressStream(Stream stream, IProgress? progress) : base(stream) { if (!stream.CanRead) @@ -32,8 +32,8 @@ public override int Read(byte[] buffer, int offset, int count) return amountRead; } - private readonly IProgress progress; - private long lastProgress = 0; + private readonly IProgress? progress; + private long lastProgress = 0; } public abstract class ContainerStream : Stream @@ -43,7 +43,7 @@ protected ContainerStream(Stream stream) if (stream == null) { #pragma warning disable IDE0016 - throw new ArgumentNullException("stream"); + throw new ArgumentNullException(nameof(stream)); #pragma warning restore IDE0016 } inner = stream; diff --git a/Core/Repositories/Repository.cs b/Core/Repositories/Repository.cs index 514acdeef7..47f93dd179 100644 --- a/Core/Repositories/Repository.cs +++ b/Core/Repositories/Repository.cs @@ -23,11 +23,9 @@ public class Repository : IEquatable public bool x_mirror; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string x_comment; - - public Repository() - { } + public string? x_comment; + [JsonConstructor] public Repository(string name, Uri uri) { this.name = name; @@ -44,10 +42,10 @@ public Repository(string name, string uri, int priority) this.priority = priority; } - public override bool Equals(object other) + public override bool Equals(object? other) => Equals(other as Repository); - public bool Equals(Repository other) + public bool Equals(Repository? other) => other != null && uri == other.uri; public override int GetHashCode() diff --git a/Core/Repositories/RepositoryData.cs b/Core/Repositories/RepositoryData.cs index 62e007f9b6..55872da6f6 100644 --- a/Core/Repositories/RepositoryData.cs +++ b/Core/Repositories/RepositoryData.cs @@ -20,16 +20,16 @@ namespace CKAN { - using ArchiveEntry = Tuple, - GameVersion[], - Repository[], + using ArchiveEntry = Tuple?, + GameVersion[]?, + Repository[]?, long>; using ArchiveList = Tuple, - SortedDictionary, - GameVersion[], - Repository[], + SortedDictionary?, + GameVersion[]?, + Repository[]?, bool>; /// @@ -42,27 +42,27 @@ public class RepositoryData /// [JsonProperty("available_modules", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonParallelDictionaryConverter))] - public readonly Dictionary AvailableModules; + public readonly Dictionary? AvailableModules; /// /// The download counts from this repository's download_counts.json /// [JsonProperty("download_counts", NullValueHandling = NullValueHandling.Ignore)] - public readonly SortedDictionary DownloadCounts; + 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; + 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; + public readonly Repository[]? Repositories; /// /// true if any module we found requires a newer client version, false otherwise @@ -88,12 +88,13 @@ private RepositoryData(Dictionary modules, /// 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, - bool unsupportedSpec) - : this(modules?.GroupBy(m => m.identifier) + public RepositoryData(IEnumerable? modules, + SortedDictionary? counts, + IEnumerable? versions, + IEnumerable? repos, + bool unsupportedSpec) + : this((modules ?? Enumerable.Empty()) + .GroupBy(m => m.identifier) .ToDictionary(grp => grp.Key, grp => new AvailableModule(grp.Key, grp)), counts ?? new SortedDictionary(), @@ -134,7 +135,7 @@ public void SaveTo(string path) /// Filename of the JSON file to load /// Progress notifier to receive updates of percent completion of this file /// A repo data object or null if loading fails - public static RepositoryData FromJson(string path, IProgress progress) + public static RepositoryData? FromJson(string path, IProgress? progress) { try { @@ -216,16 +217,16 @@ private static RepositoryData FromTarGzStream(Stream inputStream, IGame game, IP using (var gzipStream = new GZipInputStream(progressStream)) using (var tarStream = new TarInputStream(gzipStream, Encoding.UTF8)) { - (List modules, - SortedDictionary counts, - GameVersion[] versions, - Repository[] repos, - bool unsupSpec) = AggregateArchiveEntries(archiveEntriesFromTar(tarStream, game)); + (List modules, + SortedDictionary? counts, + GameVersion[]? versions, + Repository[]? repos, + bool unsupSpec) = AggregateArchiveEntries(archiveEntriesFromTar(tarStream, game)); return new RepositoryData(modules, counts, versions, repos, unsupSpec); } } - private static ParallelQuery archiveEntriesFromTar(TarInputStream tarStream, IGame game) + private static ParallelQuery archiveEntriesFromTar(TarInputStream tarStream, IGame game) => Partitioner.Create(getTarEntries(tarStream)) .AsParallel() .Select(tuple => getArchiveEntry(tuple.Item1.Name, @@ -233,19 +234,19 @@ private static ParallelQuery archiveEntriesFromTar(TarInputStream game, tarStream.Position)); - private static IEnumerable> getTarEntries(TarInputStream tarStream) + private static IEnumerable> getTarEntries(TarInputStream tarStream) { TarEntry entry; while ((entry = tarStream.GetNextEntry()) != null) { if (!entry.Name.EndsWith(".frozen")) { - yield return new Tuple(entry, tarStreamString(tarStream, entry)); + yield return new Tuple(entry, tarStreamString(tarStream, entry)); } } } - private static string tarStreamString(TarInputStream stream, TarEntry entry) + private static string? tarStreamString(TarInputStream stream, TarEntry entry) { // Read each file into a buffer. int buffer_size; @@ -282,17 +283,17 @@ private static RepositoryData FromZipStream(Stream inputStream, IGame game, IPro using (var progressStream = new ReadProgressStream(inputStream, progress)) using (var zipfile = new ZipFile(progressStream)) { - (List modules, - SortedDictionary counts, - GameVersion[] versions, - Repository[] repos, - bool unsupSpec) = AggregateArchiveEntries(archiveEntriesFromZip(zipfile, game)); + (List modules, + SortedDictionary? counts, + GameVersion[]? versions, + Repository[]? repos, + bool unsupSpec) = AggregateArchiveEntries(archiveEntriesFromZip(zipfile, game)); zipfile.Close(); return new RepositoryData(modules, counts, versions, repos, unsupSpec); } } - private static ParallelQuery archiveEntriesFromZip(ZipFile zipfile, IGame game) + private static ParallelQuery archiveEntriesFromZip(ZipFile zipfile, IGame game) => zipfile.Cast() .ToArray() .AsParallel() @@ -302,7 +303,7 @@ private static ParallelQuery archiveEntriesFromZip(ZipFile zipfile game, entry.Offset)); - private static ArchiveList AggregateArchiveEntries(ParallelQuery entries) + private static ArchiveList AggregateArchiveEntries(ParallelQuery entries) => entries.Aggregate(new ArchiveList(new List(), null, null, null, false), (subtotal, item) => item == null @@ -326,38 +327,39 @@ private static ArchiveList AggregateArchiveEntries(ParallelQuery e total.Item5 || subtotal.Item5), total => total); - private static ArchiveEntry getArchiveEntry(string filename, - Func getContents, - IGame game, - long position) + private static ArchiveEntry? getArchiveEntry(string filename, + Func getContents, + IGame game, + long position) => filename.EndsWith(".ckan") - ? new ArchiveEntry(ProcessRegistryMetadataFromJSON(getContents(), filename), + ? new ArchiveEntry(ProcessRegistryMetadataFromJSON(getContents() ?? "", filename), null, null, null, position) : filename.EndsWith("download_counts.json") ? new ArchiveEntry(null, - JsonConvert.DeserializeObject>(getContents()), + JsonConvert.DeserializeObject>(getContents() ?? ""), null, null, position) : filename.EndsWith("builds.json") ? new ArchiveEntry(null, null, - game.ParseBuildsJson(JToken.Parse(getContents())), + game.ParseBuildsJson(JToken.Parse(getContents() ?? "")), null, position) : filename.EndsWith("repositories.json") ? new ArchiveEntry(null, null, null, - JObject.Parse(getContents())["repositories"] - .ToObject(), + JObject.Parse(getContents() ?? "") + ?["repositories"] + ?.ToObject(), position) : null; - private static CkanModule ProcessRegistryMetadataFromJSON(string metadata, string filename) + private static CkanModule? ProcessRegistryMetadataFromJSON(string metadata, string filename) { try { @@ -369,7 +371,7 @@ private static CkanModule ProcessRegistryMetadataFromJSON(string metadata, strin } return module; } - catch (Exception exception) + catch (Exception? exception) { // Alas, we can get exceptions which *wrap* our exceptions, // because json.net seems to enjoy wrapping rather than propagating. @@ -380,7 +382,7 @@ private static CkanModule ProcessRegistryMetadataFromJSON(string metadata, strin while (exception != null) { - if (exception is UnsupportedKraken || exception is BadMetadataKraken) + if (exception is UnsupportedKraken or BadMetadataKraken) { // Either of these can be caused by data meant for future // clients, so they're not really warnings, they're just diff --git a/Core/Repositories/RepositoryDataManager.cs b/Core/Repositories/RepositoryDataManager.cs index a698ba0daf..dc33aeb7f7 100644 --- a/Core/Repositories/RepositoryDataManager.cs +++ b/Core/Repositories/RepositoryDataManager.cs @@ -22,7 +22,7 @@ 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) + public RepositoryDataManager(string? path = null) { reposDir = path ?? defaultReposDir; Directory.CreateDirectory(reposDir); @@ -38,13 +38,13 @@ public RepositoryDataManager(string path = null) /// The repositories we want to use /// The identifier to look up /// Sequence of available modules, if any - public IEnumerable GetAvailableModules(IEnumerable repos, - string identifier) + public IEnumerable GetAvailableModules(IEnumerable? repos, + string identifier) => GetRepoDatas(repos) - .Where(data => data.AvailableModules != null) - .Select(data => data.AvailableModules.TryGetValue(identifier, out AvailableModule am) + .Select(data => data.AvailableModules != null + && data.AvailableModules.TryGetValue(identifier, out AvailableModule? am) ? am : null) - .Where(am => am != null); + .OfType(); /// /// Return the cached available module dictionaries for a given set of repositories. @@ -53,10 +53,10 @@ public IEnumerable GetAvailableModules(IEnumerable /// /// The repositories we want to use /// Sequence of available module dictionaries - public IEnumerable> GetAllAvailDicts(IEnumerable repos) + public IEnumerable> GetAllAvailDicts(IEnumerable? repos) => GetRepoDatas(repos).Select(data => data.AvailableModules) - .Where(availMods => availMods != null - && availMods.Count > 0); + .OfType>() + .Where(availMods => availMods.Count > 0); /// /// Return the cached AvailableModule objects from the given repositories. @@ -64,7 +64,7 @@ public IEnumerable> GetAllAvailDicts(IEnumer /// /// Sequence of repositories to get modules from /// Sequence of available modules - public IEnumerable GetAllAvailableModules(IEnumerable repos) + public IEnumerable GetAllAvailableModules(IEnumerable? repos) => GetAllAvailDicts(repos).SelectMany(d => d.Values); /// @@ -73,11 +73,13 @@ public IEnumerable GetAllAvailableModules(IEnumerableThe 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) - .FirstOrDefault(count => count != null); + public int? GetDownloadCount(IEnumerable? repos, string identifier) + => GetRepoDatas(repos).Select(data => data.DownloadCounts) + .OfType>() + .Select(counts => counts.TryGetValue(identifier, out int count) + ? (int?)count : null) + .OfType() + .FirstOrDefault(); #endregion @@ -88,7 +90,7 @@ public IEnumerable GetAllAvailableModules(IEnumerable /// Repositories for which to load data /// Progress object for reporting percentage complete - public void Prepopulate(List repos, IProgress percentProgress) + public void Prepopulate(List repos, IProgress? percentProgress) { // Look up the sizes of repos that have uncached files var reposAndSizes = repos.Where(r => r.uri != null && !repositoriesData.ContainsKey(r)) @@ -174,7 +176,10 @@ public UpdateResult Update(Repository[] repos, try { // Download metadata - var targets = toUpdate.Select(r => new NetAsyncDownloader.DownloadTargetStream(r.uri)) + var targets = toUpdate.Select(r => r.uri == null + ? null + : new NetAsyncDownloader.DownloadTargetStream(r.uri)) + .OfType() .ToArray(); downloader.DownloadAndWait(targets); @@ -250,7 +255,7 @@ public UpdateResult Update(Repository[] repos, /// Fired when repository data changes so registries can invalidate their /// caches of available module data /// - public event Action Updated; + public event Action? Updated; #region ETags @@ -274,7 +279,9 @@ private void saveETags() file_transaction.WriteAllText(etagsPath, JsonConvert.SerializeObject(etags, Formatting.Indented)); } - private void setETag(NetAsyncDownloader.DownloadTarget target, Exception error, string etag) + private void setETag(NetAsyncDownloader.DownloadTarget target, + Exception? error, + string? etag) { var url = target.urls.First(); if (etag != null) @@ -288,8 +295,10 @@ private void setETag(NetAsyncDownloader.DownloadTarget target, Exception error, } private bool repoDataStale(Repository r) - // No ETag on file - => !etags.TryGetValue(r.uri, out string etag) + // URL missing + => r.uri == null + // No ETag on file + ||!etags.TryGetValue(r.uri, out string? etag) // No data on disk || !File.Exists(GetRepoDataPath(r)) // Current ETag doesn't match @@ -297,12 +306,12 @@ private bool repoDataStale(Repository r) #endregion - private RepositoryData GetRepoData(Repository repo) - => repositoriesData.TryGetValue(repo, out RepositoryData data) + private RepositoryData? GetRepoData(Repository repo) + => repositoriesData.TryGetValue(repo, out RepositoryData? data) ? data : LoadRepoData(repo, null); - private RepositoryData LoadRepoData(Repository repo, IProgress progress) + private RepositoryData? LoadRepoData(Repository repo, IProgress? progress) { var path = GetRepoDataPath(repo); log.DebugFormat("Looking for data in {0}", path); @@ -315,18 +324,18 @@ private RepositoryData LoadRepoData(Repository repo, IProgress progress) return data; } - private IEnumerable GetRepoDatas(IEnumerable repos) + private IEnumerable GetRepoDatas(IEnumerable? repos) => repos?.OrderBy(repo => repo.priority) .ThenBy(repo => repo.name) .Select(repo => GetRepoData(repo)) - .Where(data => data != null) + .OfType() ?? Enumerable.Empty(); private DateTime? RepoUpdateTimestamp(Repository repo) => FileTimestamp(GetRepoDataPath(repo)); private static DateTime? FileTimestamp(string path) - => File.Exists(path) ? (DateTime?)File.GetLastWriteTime(path) + => File.Exists(path) ? File.GetLastWriteTime(path) : null; private string etagsPath => Path.Combine(reposDir, "etags.json"); diff --git a/Core/Repositories/RepositoryList.cs b/Core/Repositories/RepositoryList.cs index c8a2f2a2d0..217cb0698f 100644 --- a/Core/Repositories/RepositoryList.cs +++ b/Core/Repositories/RepositoryList.cs @@ -8,12 +8,12 @@ public struct RepositoryList { public Repository[] repositories; - public static RepositoryList DefaultRepositories(IGame game) + public static RepositoryList? DefaultRepositories(IGame game) { try { return JsonConvert.DeserializeObject( - Net.DownloadText(game.RepositoryListURL)); + Net.DownloadText(game.RepositoryListURL) ?? ""); } catch { diff --git a/Core/ServiceLocator.cs b/Core/ServiceLocator.cs index 1561cb80b3..9edc9e4b7b 100644 --- a/Core/ServiceLocator.cs +++ b/Core/ServiceLocator.cs @@ -11,17 +11,13 @@ namespace CKAN /// public static class ServiceLocator { - private static IContainer _container; + private static IContainer? _container; public static IContainer Container { // NB: Totally not thread-safe. get { - if (_container == null) - { - Init(); - } - + _container ??= Init(); return _container; } @@ -33,7 +29,7 @@ public static IContainer Container #pragma warning restore IDE0027 } - private static void Init() + private static IContainer Init() { var builder = new ContainerBuilder(); @@ -61,7 +57,7 @@ private static void Init() builder.RegisterType() .SingleInstance(); - _container = builder.Build(); + return builder.Build(); } } } diff --git a/Core/SingleAssemblyResourceManager.cs b/Core/SingleAssemblyResourceManager.cs index 30db7db008..0b58c6bd64 100644 --- a/Core/SingleAssemblyResourceManager.cs +++ b/Core/SingleAssemblyResourceManager.cs @@ -1,4 +1,3 @@ -using System.IO; using System.Globalization; using System.Resources; using System.Reflection; @@ -14,16 +13,14 @@ public SingleAssemblyResourceManager(string basename, Assembly assembly) : base( { } - protected override ResourceSet InternalGetResourceSet(CultureInfo culture, - bool createIfNotExists, bool tryParents) + protected override ResourceSet? InternalGetResourceSet(CultureInfo culture, + bool createIfNotExists, + bool tryParents) { - if (!myResourceSets.TryGetValue(culture, out ResourceSet rs) && createIfNotExists) + if (!myResourceSets.TryGetValue(culture, out ResourceSet? rs) && createIfNotExists && MainAssembly != null) { // Lazy-load default language (without caring about duplicate assignment in race conditions, no harm done) - if (neutralResourcesCulture == null) - { - neutralResourcesCulture = GetNeutralResourcesLanguage(MainAssembly); - } + neutralResourcesCulture ??= GetNeutralResourcesLanguage(MainAssembly); // If we're asking for the default language, then ask for the // invariant (non-specific) resources. @@ -33,7 +30,7 @@ protected override ResourceSet InternalGetResourceSet(CultureInfo culture, } string resourceFileName = GetResourceFileName(culture); - Stream store = MainAssembly.GetManifestResourceStream(resourceFileName); + var store = MainAssembly.GetManifestResourceStream(resourceFileName); // If we found the appropriate resources in the local assembly if (store != null) @@ -50,7 +47,7 @@ protected override ResourceSet InternalGetResourceSet(CultureInfo culture, return rs; } - private CultureInfo neutralResourcesCulture; + private CultureInfo? neutralResourcesCulture; private readonly Dictionary myResourceSets = new Dictionary(); } } diff --git a/Core/SteamLibrary.cs b/Core/SteamLibrary.cs index 11cc0757f9..284352f82d 100644 --- a/Core/SteamLibrary.cs +++ b/Core/SteamLibrary.cs @@ -27,9 +27,11 @@ public SteamLibrary() File.OpenRead(Path.Combine(libraryPath, "config", "libraryfolders.vdf"))) - .Select(lf => appRelPaths.Select(p => Path.Combine(lf.Path, p)) - .FirstOrDefault(Directory.Exists)) - .Where(p => p != null) + .Select(lf => lf.Path is string libPath + ? appRelPaths.Select(p => Path.Combine(libPath, p)) + .FirstOrDefault(Directory.Exists) + : null) + .OfType() .ToArray(); var steamGames = appPaths.SelectMany(p => LibraryPathGames(txtParser, p)); var binParser = KVSerializer.Create(KVSerializationFormat.KeyValues1Binary); @@ -47,7 +49,7 @@ public SteamLibrary() } public IEnumerable GameAppURLs(DirectoryInfo gameDir) - => Games.Where(g => gameDir.FullName.Equals(g.GameDir.FullName, Platform.PathComparison)) + => Games.Where(g => gameDir.FullName.Equals(g.GameDir?.FullName, Platform.PathComparison)) .Select(g => g.LaunchUrl); public readonly GameBase[] Games; @@ -65,11 +67,14 @@ private static IEnumerable ShortcutsFileGames(KVSerializer vdfParser, private const string registryKey = @"HKEY_CURRENT_USER\Software\Valve\Steam"; private const string registryValue = @"SteamPath"; - private static string[] SteamPaths - => Platform.IsWindows ? new string[] + private static string?[] SteamPaths + => Platform.IsWindows + // First check the registry + && Microsoft.Win32.Registry.GetValue(registryKey, registryValue, "") is string val + && !string.IsNullOrEmpty(val) + ? new string[] { - // First check the registry - (string)Microsoft.Win32.Registry.GetValue(registryKey, registryValue, null), + val, } : Platform.IsUnix ? new string[] { @@ -92,31 +97,34 @@ private static string[] SteamPaths public class LibraryFolder { - [KVProperty("path")] public string Path { get; set; } + [KVProperty("path")] public string? Path { get; set; } } public abstract class GameBase { - public abstract string Name { get; set; } + public abstract string? Name { get; set; } - [KVIgnore] public DirectoryInfo GameDir { get; set; } - [KVIgnore] public abstract Uri LaunchUrl { get; } + [KVIgnore] public DirectoryInfo? GameDir { get; set; } + [KVIgnore] public abstract Uri LaunchUrl { get; } public abstract GameBase NormalizeDir(string appPath); } public class SteamGame : GameBase { - [KVProperty("appid")] public ulong AppId { get; set; } - [KVProperty("name")] public override string Name { get; set; } - [KVProperty("installdir")] public string InstallDir { get; set; } + [KVProperty("appid")] public ulong AppId { get; set; } + [KVProperty("name")] public override string? Name { get; set; } + [KVProperty("installdir")] public string? InstallDir { get; set; } [KVIgnore] public override Uri LaunchUrl => new Uri($"steam://rungameid/{AppId}"); public override GameBase NormalizeDir(string commonPath) { - GameDir = new DirectoryInfo(CKANPathUtils.NormalizePath(Path.Combine(commonPath, InstallDir))); + if (InstallDir != null) + { + GameDir = new DirectoryInfo(CKANPathUtils.NormalizePath(Path.Combine(commonPath, InstallDir))); + } return this; } } @@ -124,11 +132,11 @@ public override GameBase NormalizeDir(string commonPath) public class NonSteamGame : GameBase { [KVProperty("appid")] - public int AppId { get; set; } + public int AppId { get; set; } [KVProperty("AppName")] - public override string Name { get; set; } - public string Exe { get; set; } - public string StartDir { get; set; } + public override string? Name { get; set; } + public string? Exe { get; set; } + public string? StartDir { get; set; } [KVIgnore] private ulong UrlId => (unchecked((ulong)AppId) << 32) | 0x02000000; @@ -138,7 +146,8 @@ public class NonSteamGame : GameBase public override GameBase NormalizeDir(string appPath) { - GameDir = new DirectoryInfo(CKANPathUtils.NormalizePath(StartDir.Trim('"'))); + GameDir = StartDir == null ? null + : new DirectoryInfo(CKANPathUtils.NormalizePath(StartDir.Trim('"'))); return this; } } diff --git a/Core/SuppressedCompatWarningIdentifiers.cs b/Core/SuppressedCompatWarningIdentifiers.cs index 1de097fb91..c153d7f152 100644 --- a/Core/SuppressedCompatWarningIdentifiers.cs +++ b/Core/SuppressedCompatWarningIdentifiers.cs @@ -10,16 +10,16 @@ namespace CKAN { public class SuppressedCompatWarningIdentifiers { - public GameVersion GameVersionWhenWritten; + public GameVersion? GameVersionWhenWritten; public HashSet Identifiers = new HashSet(); - public static SuppressedCompatWarningIdentifiers LoadFrom(GameVersion gameVer, string filename) + public static SuppressedCompatWarningIdentifiers LoadFrom(GameVersion? gameVer, string filename) { try { var saved = JsonConvert.DeserializeObject(File.ReadAllText(filename)); // Reset warnings if e.g. Steam auto-updates the game - if (saved.GameVersionWhenWritten == gameVer) + if (saved != null && saved.GameVersionWhenWritten == gameVer) { return saved; } diff --git a/Core/Types/CkanModule.cs b/Core/Types/CkanModule.cs index c0430516b9..34787880a8 100644 --- a/Core/Types/CkanModule.cs +++ b/Core/Types/CkanModule.cs @@ -6,7 +6,7 @@ using System.Runtime.Serialization; using System.Text; using System.Text.RegularExpressions; -using System.Reflection; +using System.Diagnostics.CodeAnalysis; using Autofac; using log4net; @@ -14,36 +14,13 @@ using CKAN.Versioning; using CKAN.Games; + #if NETSTANDARD2_0 using CKAN.Extensions; #endif namespace CKAN { - public class ModuleReplacement - { - public CkanModule ToReplace; - public CkanModule ReplaceWith; - } - - public class DownloadHashesDescriptor - { - [JsonProperty("sha1")] - public string sha1; - - [JsonProperty("sha256")] - public string sha256; - } - - public class NameComparer : IEqualityComparer - { - public bool Equals(CkanModule x, CkanModule y) - => x.identifier.Equals(y.identifier); - - public int GetHashCode(CkanModule obj) - => obj.identifier.GetHashCode(); - } - /// /// Describes a CKAN module (ie, what's in the CKAN.schema file). /// @@ -58,45 +35,6 @@ public class CkanModule : IEquatable private static readonly ILog log = LogManager.GetLogger(typeof (CkanModule)); - private static readonly Dictionary required_fields = - new Dictionary() - { - { - "package", new string[] - { - "spec_version", - "name", - "abstract", - "identifier", - "download", - "license", - "version" - } - }, - { - "metapackage", new string[] - { - "spec_version", - "name", - "abstract", - "identifier", - "license", - "version" - } - }, - { - "dlc", new string[] - { - "spec_version", - "name", - "abstract", - "identifier", - "license", - "version" - } - }, - }; - // identifier, license, and version are always required, so we know // what we've got. @@ -104,45 +42,45 @@ public class CkanModule : IEquatable public string @abstract; [JsonProperty("description", Order = 6, NullValueHandling = NullValueHandling.Ignore)] - public string description; + public string? description; // Package type: in spec v1.6 can be either "package" or "metapackage" // In spec v1.28, "dlc" [JsonProperty("kind", Order = 31, NullValueHandling = NullValueHandling.Ignore)] - public string kind; + public string? kind; [JsonProperty("author", Order = 7, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonSingleOrArrayConverter))] public List author; [JsonProperty("comment", Order = 2, NullValueHandling = NullValueHandling.Ignore)] - public string comment; + public string? comment; [JsonProperty("conflicts", Order = 23, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonRelationshipConverter))] - public List conflicts; + public List? conflicts; [JsonProperty("depends", Order = 19, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonRelationshipConverter))] - public List depends; + public List? depends; [JsonProperty("replaced_by", NullValueHandling = NullValueHandling.Ignore)] - public ModuleRelationshipDescriptor replaced_by; + public ModuleRelationshipDescriptor? replaced_by; [JsonProperty("download", Order = 25, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonSingleOrArrayConverter))] - public List download; + public List? download; [JsonProperty("download_size", Order = 26, DefaultValueHandling = DefaultValueHandling.Ignore)] [DefaultValue(0)] public long download_size; [JsonProperty("download_hash", Order = 27, NullValueHandling = NullValueHandling.Ignore)] - public DownloadHashesDescriptor download_hash; + public DownloadHashesDescriptor? download_hash; [JsonProperty("download_content_type", Order = 28, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] [DefaultValue("application/zip")] - public string download_content_type; + public string? download_content_type; [JsonProperty("install_size", Order = 29, DefaultValueHandling = DefaultValueHandling.Ignore)] [DefaultValue(0)] @@ -152,17 +90,17 @@ public class CkanModule : IEquatable public string identifier; [JsonProperty("ksp_version", Order = 9, NullValueHandling = NullValueHandling.Ignore)] - public GameVersion ksp_version; + public GameVersion? ksp_version; [JsonProperty("ksp_version_max", Order = 11, NullValueHandling = NullValueHandling.Ignore)] - public GameVersion ksp_version_max; + public GameVersion? ksp_version_max; [JsonProperty("ksp_version_min", Order = 10, NullValueHandling = NullValueHandling.Ignore)] - public GameVersion ksp_version_min; + public GameVersion? ksp_version_min; [JsonProperty("ksp_version_strict", Order = 12, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] [DefaultValue(false)] - public bool ksp_version_strict = false; + public bool? ksp_version_strict = false; [JsonProperty("license", Order = 13)] [JsonConverter(typeof(JsonSingleOrArrayConverter))] @@ -172,34 +110,34 @@ public class CkanModule : IEquatable public string name; [JsonProperty("provides", Order = 18, NullValueHandling = NullValueHandling.Ignore)] - public List provides; + public List? provides; [JsonProperty("recommends", Order = 20, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonRelationshipConverter))] - public List recommends; + public List? recommends; [JsonProperty("release_status", Order = 14, NullValueHandling = NullValueHandling.Ignore)] - public ReleaseStatus release_status; + public ReleaseStatus? release_status; [JsonProperty("resources", Order = 15, NullValueHandling = NullValueHandling.Ignore)] - public ResourcesDescriptor resources; + public ResourcesDescriptor? resources; [JsonProperty("suggests", Order = 21, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonRelationshipConverter))] - public List suggests; + public List? suggests; [JsonProperty("version", Order = 8, Required = Required.Always)] public ModuleVersion version; [JsonProperty("supports", Order = 22, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonRelationshipConverter))] - public List supports; + public List? supports; [JsonProperty("install", Order = 24, NullValueHandling = NullValueHandling.Ignore)] - public ModuleInstallDescriptor[] install; + public ModuleInstallDescriptor[]? install; [JsonProperty("localizations", Order = 17, NullValueHandling = NullValueHandling.Ignore)] - public string[] localizations; + public string[]? localizations; // Used to see if we're compatible with a given game/KSP version or not. private readonly IGameComparator _comparator; @@ -226,6 +164,7 @@ public ModuleVersion spec_version return specVersion; } #pragma warning disable IDE0027 + [MemberNotNull(nameof(specVersion))] set { specVersion = value ?? new ModuleVersion("1"); @@ -234,7 +173,7 @@ public ModuleVersion spec_version } [JsonProperty("tags", Order = 16, NullValueHandling = NullValueHandling.Ignore)] - public HashSet Tags; + public HashSet? Tags; [JsonProperty("release_date", Order = 30, NullValueHandling = NullValueHandling.Ignore)] public DateTime? release_date; @@ -276,23 +215,6 @@ public List ProvidesList #region Constructors - /// - /// To be used by test cases and cascaded json deserialisation only, - /// and even then I'm not sure this is a great idea. - /// - [JsonConstructor] - internal CkanModule() - { - // We don't have this passed in, so we'll ask the service locator - // directly. Yuck. - _comparator = ServiceLocator.Container.Resolve(); - download_content_type = typeof(CkanModule).GetTypeInfo() - .GetDeclaredField("download_content_type") - .GetCustomAttribute() - .Value - .ToString(); - } - /// /// Initialize a CkanModule /// @@ -307,19 +229,22 @@ internal CkanModule() /// Where to download this module /// package, metapackage, or dlc /// Object used for checking compatibility of this module + [JsonConstructor] public CkanModule( - ModuleVersion spec_version, - string identifier, - string name, - string @abstract, - string description, - List author, - List license, - ModuleVersion version, - Uri download, - string kind = null, - IGameComparator comparator = null - ) + ModuleVersion spec_version, + string identifier, + string name, + string @abstract, + string? description, + [JsonConverter(typeof(JsonSingleOrArrayConverter))] + List author, + [JsonConverter(typeof(JsonSingleOrArrayConverter))] + List license, + ModuleVersion version, + [JsonConverter(typeof(JsonSingleOrArrayConverter))] + List? download, + string? kind = null, + IGameComparator? comparator = null) { this.spec_version = spec_version; this.identifier = identifier; @@ -329,7 +254,7 @@ public CkanModule( this.author = author; this.license = license; this.version = version; - this.download = new List { download }; + this.download = download; this.kind = kind; _comparator = comparator ?? ServiceLocator.Container.Resolve(); CheckHealth(); @@ -339,7 +264,7 @@ public CkanModule( /// /// Inflates a CKAN object from a JSON string. /// - public CkanModule(string json, IGameComparator comparator = null) + public CkanModule(string json, IGameComparator? comparator = null) { try { @@ -363,58 +288,94 @@ public CkanModule(string json, IGameComparator comparator = null) /// /// Throw an exception if there's anything wrong with this module /// + [MemberNotNull(nameof(specVersion), nameof(identifier), nameof(name), + nameof(@abstract), nameof(author), nameof(license), + nameof(version))] private void CheckHealth() { + // Check everything in the spec is defined. + if (spec_version == null) + { + throw new BadMetadataKraken(null, + string.Format(Properties.Resources.CkanModuleMissingRequired, + identifier, "spec_version")); + } if (!IsSpecSupported()) { throw new UnsupportedKraken(string.Format( Properties.Resources.CkanModuleUnsupportedSpec, this, spec_version)); } - - // Check everything in the spec is defined. - foreach (string field in required_fields[kind ?? "package"]) + if (identifier == null) + { + throw new BadMetadataKraken(null, + string.Format(Properties.Resources.CkanModuleMissingRequired, + identifier, "identifier")); + } + if (name == null) + { + throw new BadMetadataKraken(null, + string.Format(Properties.Resources.CkanModuleMissingRequired, + identifier, "name")); + } + if (@abstract == null) + { + throw new BadMetadataKraken(null, + string.Format(Properties.Resources.CkanModuleMissingRequired, + identifier, "abstract")); + } + if (license == null) + { + throw new BadMetadataKraken(null, + string.Format(Properties.Resources.CkanModuleMissingRequired, + identifier, "license")); + } + if (version == null) { - object value = null; - if (GetType().GetField(field) != null) + throw new BadMetadataKraken(null, + string.Format(Properties.Resources.CkanModuleMissingRequired, + identifier, "version")); + } + if (author == null) + { + if (spec_version < v1p28) { - value = typeof(CkanModule).GetField(field).GetValue(this); + // Some very old modules in the test data lack authors + author = new List { "" }; } else { - // uh, maybe it is not a field, but a property? - value = typeof(CkanModule).GetProperty(field).GetValue(this, null); - } - - if (value == null) - { - throw new BadMetadataKraken(null, string.Format( - Properties.Resources.CkanModuleMissingRequired, identifier, field)); + throw new BadMetadataKraken(null, + string.Format(Properties.Resources.CkanModuleMissingRequired, + identifier, "author")); } } + if (kind is null or "package" && download == null) + { + throw new BadMetadataKraken(null, + string.Format(Properties.Resources.CkanModuleMissingRequired, + identifier, "download")); + } } + private static readonly ModuleVersion v1p28 = new ModuleVersion("v1.28"); + /// /// Calculate the mod properties used for searching via Regex. /// + [MemberNotNull(nameof(SearchableIdentifier)), + MemberNotNull(nameof(SearchableName)), + MemberNotNull(nameof(SearchableAbstract)), + MemberNotNull(nameof(SearchableDescription)), + MemberNotNull(nameof(SearchableAuthors))] private void CalculateSearchables() { SearchableIdentifier = identifier == null ? string.Empty : nonAlphaNums.Replace(identifier, ""); SearchableName = name == null ? string.Empty : nonAlphaNums.Replace(name, ""); SearchableAbstract = @abstract == null ? string.Empty : nonAlphaNums.Replace(@abstract, ""); SearchableDescription = description == null ? string.Empty : nonAlphaNums.Replace(description, ""); - SearchableAuthors = new List(); - - if (author == null) - { - SearchableAuthors.Add(string.Empty); - } - else - { - foreach (string auth in author) - { - SearchableAuthors.Add(nonAlphaNums.Replace(auth, "")); - } - } + SearchableAuthors = author?.Select(auth => nonAlphaNums.Replace(auth, "")) + .ToList() + ?? new List { string.Empty }; } public string serialise() @@ -433,9 +394,9 @@ private void DeSerialisationFixes(StreamingContext like_i_could_care) throw new InvalidModuleAttributesException(Properties.Resources.CkanModuleKspVersionMixed, this); } - license = license ?? new List { License.UnknownLicense }; - @abstract = @abstract ?? string.Empty; - name = name ?? string.Empty; + license ??= new List { License.UnknownLicense }; + @abstract ??= string.Empty; + name ??= string.Empty; CalculateSearchables(); } @@ -449,9 +410,10 @@ private void DeSerialisationFixes(StreamingContext like_i_could_care) /// The current KSP version criteria to consider /// A CkanModule /// Thrown if no matching module could be found - public static CkanModule FromIDandVersion(IRegistryQuerier registry, string mod, GameVersionCriteria ksp_version) + public static CkanModule? FromIDandVersion(IRegistryQuerier registry, + string mod, + GameVersionCriteria? ksp_version) { - CkanModule module; Match match = idAndVersionMatcher.Match(mod); @@ -460,7 +422,7 @@ public static CkanModule FromIDandVersion(IRegistryQuerier registry, string mod, string ident = match.Groups["mod"].Value; string version = match.Groups["version"].Value; - module = registry.GetModuleByVersion(ident, version); + var module = registry.GetModuleByVersion(ident, version); if (module == null || (ksp_version != null && !module.IsCompatible(ksp_version))) @@ -578,23 +540,23 @@ private GameVersion LatestCompatibleRealGameVersion(GameVersionRange range, public bool IsDLC => kind == "dlc"; - protected bool Equals(CkanModule other) - => string.Equals(identifier, other.identifier) && version.Equals(other.version); + protected bool Equals(CkanModule? other) + => string.Equals(identifier, other?.identifier) && version.Equals(other?.version); - public override bool Equals(object obj) - => !(obj is null) + public override bool Equals(object? obj) + => obj is not null && (ReferenceEquals(this, obj) || (obj.GetType() == GetType() && Equals((CkanModule)obj))); public bool MetadataEquals(CkanModule other) { if ((install == null) != (other.install == null) - || (install != null + || (install != null && other.install != null && install.Length != other.install.Length)) { return false; } - else if (install != null) + else if (install != null && other.install != null) { for (int i = 0; i < install.Length; i++) { @@ -644,7 +606,7 @@ public bool MetadataEquals(CkanModule other) return true; } - private static bool RelationshipsAreEquivalent(List a, List b) + private static bool RelationshipsAreEquivalent(List? a, List? b) { if (a == b) { @@ -686,7 +648,7 @@ private static bool RelationshipsAreEquivalent(List a, L public override int GetHashCode() => (identifier, version).GetHashCode(); - bool IEquatable.Equals(CkanModule other) + bool IEquatable.Equals(CkanModule? other) => Equals(other); /// @@ -698,8 +660,17 @@ internal static bool IsSpecSupported(ModuleVersion spec_version) /// /// Returns true if we support the CKAN spec used by this module. /// + [MemberNotNull(nameof(specVersion), nameof(spec_version))] private bool IsSpecSupported() - => IsSpecSupported(spec_version); + { + if (specVersion == null || spec_version == null) + { + throw new BadMetadataKraken(null, + string.Format(Properties.Resources.CkanModuleMissingRequired, + identifier, "specVersion")); + } + return IsSpecSupported(spec_version); + } /// /// Returns a standardised name for this module, in the form @@ -723,28 +694,15 @@ public override string ToString() => string.Format("{0} {1}", identifier, version); public string DescribeInstallStanzas(IGame game) - { - List descriptions = new List(); - if (install != null) - { - foreach (ModuleInstallDescriptor mid in install) - { - descriptions.Add(mid.DescribeMatch()); - } - } - else - { - descriptions.Add(ModuleInstallDescriptor.DefaultInstallStanza(game, identifier).DescribeMatch()); - } - return string.Join(", ", descriptions); - } + => install == null ? ModuleInstallDescriptor.DefaultInstallStanza(game, identifier).DescribeMatch() + : string.Join(", ", install.Select(mid => mid.DescribeMatch())); /// /// Return an archive.org URL for this download, or null if it's not there. /// The filenames look a lot like the filenames in Net.Cache, but don't be fooled! /// Here it's the first 8 characters of the SHA1 of the DOWNLOADED FILE, not the URL! /// - public Uri InternetArchiveDownload + public Uri? InternetArchiveDownload => !license.Any(l => l.Redistributable) ? null : InternetArchiveURL( @@ -759,12 +717,12 @@ public Uri InternetArchiveDownload private static string Truncate(string s, int len) => s.Length <= len ? s - : s.Substring(0, len); + : s[..len]; - private static Uri InternetArchiveURL(string bucket, string sha1) + private static Uri? InternetArchiveURL(string bucket, string? sha1) => string.IsNullOrEmpty(sha1) ? null - : new Uri($"https://archive.org/download/{bucket}/{sha1.Substring(0, 8)}-{bucket}.zip"); + : new Uri($"https://archive.org/download/{bucket}/{sha1?[..8]}-{bucket}.zip"); // InternetArchive says: // Bucket names should be valid archive identifiers; @@ -820,11 +778,14 @@ private static HashSet OneDownloadGroupingPass(HashSet u var origin = searching.First(); searching.Remove(origin); var neighbors = origin.download - .SelectMany(dlUri => unsearched.Where(other => other.download.Contains(dlUri))) - .ToHashSet(); - unsearched.ExceptWith(neighbors); - searching.AddRange(neighbors); - found.UnionWith(neighbors); + ?.SelectMany(dlUri => unsearched.Where(other => other.download != null && other.download.Contains(dlUri))) + .ToHashSet(); + if (neighbors is not null) + { + unsearched.ExceptWith(neighbors); + searching.AddRange(neighbors); + found.UnionWith(neighbors); + } } return found; } @@ -838,24 +799,25 @@ private static HashSet OneDownloadGroupingPass(HashSet u /// Return parameter for the highest mod version /// Return parameter for the lowest game version /// Return parameter for the highest game version - public static void GetMinMaxVersions(IEnumerable modVersions, - out ModuleVersion minMod, out ModuleVersion maxMod, - out GameVersion minGame, out GameVersion maxGame) + public static void GetMinMaxVersions( + IEnumerable modVersions, + out ModuleVersion? minMod, out ModuleVersion? maxMod, + out GameVersion? minGame, out GameVersion? maxGame) { minMod = maxMod = null; minGame = maxGame = null; - foreach (CkanModule rel in modVersions.Where(v => v != null)) + foreach (var mod in modVersions.OfType()) { - if (minMod == null || minMod > rel.version) + if (minMod == null || minMod > mod.version) { - minMod = rel.version; + minMod = mod.version; } - if (maxMod == null || maxMod < rel.version) + if (maxMod == null || maxMod < mod.version) { - maxMod = rel.version; + maxMod = mod.version; } - GameVersion relMin = rel.EarliestCompatibleGameVersion(); - GameVersion relMax = rel.LatestCompatibleGameVersion(); + GameVersion relMin = mod.EarliestCompatibleGameVersion(); + GameVersion relMax = mod.LatestCompatibleGameVersion(); if (minGame == null || (!minGame.IsAny && (minGame > relMin || relMin.IsAny))) { minGame = relMin; @@ -868,28 +830,4 @@ public static void GetMinMaxVersions(IEnumerable modVersions, } } - public class InvalidModuleAttributesException : Exception - { - private readonly CkanModule module; - private readonly string why; - - public InvalidModuleAttributesException(string why, CkanModule module = null) - : base(why) - { - this.why = why; - this.module = module; - } - - public override string ToString() - { - string modname = "unknown"; - - if (module != null) - { - modname = module.identifier; - } - - return string.Format("[InvalidModuleAttributesException] {0} in {1}", why, modname); - } - } } diff --git a/Core/Types/DownloadHashesDescriptor.cs b/Core/Types/DownloadHashesDescriptor.cs new file mode 100644 index 0000000000..7578f3f01d --- /dev/null +++ b/Core/Types/DownloadHashesDescriptor.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace CKAN +{ + public class DownloadHashesDescriptor + { + [JsonProperty("sha1")] + public string? sha1; + + [JsonProperty("sha256")] + public string? sha256; + } +} diff --git a/Core/Types/FileType.cs b/Core/Types/FileType.cs index 53814738ef..a019709c86 100644 --- a/Core/Types/FileType.cs +++ b/Core/Types/FileType.cs @@ -7,6 +7,6 @@ public enum FileType Tar, TarGz, Unknown, - Zip + Zip, } } diff --git a/Core/Types/GameComparator/IGameComparator.cs b/Core/Types/GameComparator/IGameComparator.cs index 0ef949d0f6..b875dabd66 100644 --- a/Core/Types/GameComparator/IGameComparator.cs +++ b/Core/Types/GameComparator/IGameComparator.cs @@ -14,4 +14,3 @@ public interface IGameComparator bool Compatible(GameVersionCriteria gameVersion, CkanModule module); } } - diff --git a/Core/Types/Kraken.cs b/Core/Types/Kraken.cs index e5bdddf0c8..85820c0a01 100644 --- a/Core/Types/Kraken.cs +++ b/Core/Types/Kraken.cs @@ -8,14 +8,14 @@ namespace CKAN { - using modRelList = List>; + using modRelList = List>; /// /// Our application exceptions are called Krakens. /// public class Kraken : Exception { - public Kraken(string reason = null, Exception innerException = null) + public Kraken(string? reason = null, Exception? innerException = null) : base(reason, innerException) { } @@ -23,9 +23,9 @@ public Kraken(string reason = null, Exception innerException = null) public class FileNotFoundKraken : Kraken { - public readonly string file; + public readonly string? file; - public FileNotFoundKraken(string file, string reason = null, Exception innerException = null) + public FileNotFoundKraken(string? file, string? reason = null, Exception? innerException = null) : base(reason, innerException) { this.file = file; @@ -36,7 +36,7 @@ public class DirectoryNotFoundKraken : Kraken { public readonly string directory; - public DirectoryNotFoundKraken(string directory, string reason = null, Exception innerException = null) + public DirectoryNotFoundKraken(string directory, string? reason = null, Exception? innerException = null) : base(reason, innerException) { this.directory = directory; @@ -67,7 +67,7 @@ public NotEnoughSpaceKraken(string description, DirectoryInfo destination, long /// public class BadInstallLocationKraken : Kraken { - public BadInstallLocationKraken(string reason = null, Exception innerException = null) + public BadInstallLocationKraken(string? reason = null, Exception? innerException = null) : base(reason, innerException) { } @@ -75,10 +75,10 @@ public BadInstallLocationKraken(string reason = null, Exception innerException = public class ModuleNotFoundKraken : Kraken { - public readonly string module; - public readonly string version; + public readonly string module; + public readonly string? version; - public ModuleNotFoundKraken(string module, string version, string reason, Exception innerException = null) + public ModuleNotFoundKraken(string module, string? version, string reason, Exception? innerException = null) : base(reason ?? string.Format(Properties.Resources.KrakenDependencyNotSatisfied, module, version), innerException) @@ -87,7 +87,7 @@ public ModuleNotFoundKraken(string module, string version, string reason, Except this.version = version; } - public ModuleNotFoundKraken(string module, string version = null) + public ModuleNotFoundKraken(string module, string? version = null) : this(module, version, string.Format(Properties.Resources.KrakenDependencyModuleNotFound, module, version ?? "")) { } @@ -113,9 +113,9 @@ public class DependencyNotSatisfiedKraken : ModuleNotFoundKraken /// Originating exception parameter for base class public DependencyNotSatisfiedKraken(CkanModule parentModule, string module, - string version = null, - string reason = null, - Exception innerException = null) + string? version = null, + string? reason = null, + Exception? innerException = null) : base(module, version, reason ?? string.Format( Properties.Resources.KrakenParentDependencyNotSatisfied, @@ -132,7 +132,7 @@ public class NotKSPDirKraken : Kraken { public readonly string path; - public NotKSPDirKraken(string path, string reason = null, Exception innerException = null) + public NotKSPDirKraken(string path, string? reason = null, Exception? innerException = null) : base(reason, innerException) { this.path = path; @@ -141,7 +141,7 @@ public NotKSPDirKraken(string path, string reason = null, Exception innerExcepti public class TransactionalKraken : Kraken { - public TransactionalKraken(string reason = null, Exception innerException = null) + public TransactionalKraken(string? reason = null, Exception? innerException = null) : base(reason, innerException) { } @@ -153,9 +153,9 @@ public TransactionalKraken(string reason = null, Exception innerException = null /// public class BadMetadataKraken : Kraken { - public CkanModule module; + public CkanModule? module; - public BadMetadataKraken(CkanModule module, string reason = null, Exception innerException = null) + public BadMetadataKraken(CkanModule? module, string? reason = null, Exception? innerException = null) : base(reason, innerException) { this.module = module; @@ -169,7 +169,7 @@ public class RegistryVersionNotSupportedKraken : Kraken { public readonly int requestVersion; - public RegistryVersionNotSupportedKraken(int v, string reason = null, Exception innerException = null) + public RegistryVersionNotSupportedKraken(int v, string? reason = null, Exception? innerException = null) : base(reason, innerException) { requestVersion = v; @@ -178,16 +178,16 @@ public RegistryVersionNotSupportedKraken(int v, string reason = null, Exception public class TooManyModsProvideKraken : Kraken { - public readonly CkanModule requester; + public readonly CkanModule requester; public readonly List modules; - public readonly string requested; - public readonly string choice_help_text; + public readonly string requested; + public readonly string? choice_help_text; public TooManyModsProvideKraken(CkanModule requester, string requested, List modules, - string choice_help_text = null, - Exception innerException = null) + string? choice_help_text = null, + Exception? innerException = null) : base(choice_help_text ?? string.Format(Properties.Resources.KrakenProvidedByMoreThanOne, requested, requester.name), innerException) @@ -205,7 +205,7 @@ public TooManyModsProvideKraken(CkanModule requester, /// public class InconsistentKraken : Kraken { - public InconsistentKraken(ICollection inconsistencies, Exception innerException = null) + public InconsistentKraken(ICollection inconsistencies, Exception? innerException = null) : base(string.Join(Environment.NewLine, new string[] { Properties.Resources.KrakenInconsistenciesHeader } .Concat(inconsistencies.Select(msg => $"* {msg}"))), @@ -214,7 +214,7 @@ public InconsistentKraken(ICollection inconsistencies, Exception innerEx this.inconsistencies = inconsistencies; } - public InconsistentKraken(string inconsistency, Exception innerException = null) + public InconsistentKraken(string inconsistency, Exception? innerException = null) : this(new List { inconsistency }, innerException) { } @@ -247,7 +247,7 @@ public FailedToDeleteFilesKraken(string identifier, List undeletableFile public class BadRelationshipsKraken : InconsistentKraken { public BadRelationshipsKraken( - modRelList depends, + List> depends, modRelList conflicts ) : base( (depends?.Select(dep => string.Format(Properties.Resources.KrakenMissingDependency, dep.Item1, dep.Item2)) @@ -258,11 +258,11 @@ modRelList conflicts ).ToArray() ) { - Depends = depends ?? new modRelList(); + Depends = depends ?? new List>(); Conflicts = conflicts ?? new modRelList(); } - public readonly modRelList Depends; + public readonly List> Depends; public readonly modRelList Conflicts; } @@ -276,10 +276,10 @@ public class FileExistsKraken : Kraken // These aren't set at construction time, but exist so that we can decorate the // kraken as appropriate. - public CkanModule installingModule; - public InstalledModule owningModule; + public CkanModule? installingModule; + public InstalledModule? owningModule; - public FileExistsKraken(string filename, string reason = null, Exception innerException = null) + public FileExistsKraken(string filename, string? reason = null, Exception? innerException = null) : base(reason, innerException) { this.filename = filename; @@ -363,7 +363,7 @@ public override string ToString() return builder.ToString(); } - private StringBuilder builder = null; + private StringBuilder? builder = null; } /// @@ -372,7 +372,7 @@ public override string ToString() /// public class CancelledActionKraken : Kraken { - public CancelledActionKraken(string reason = null, Exception innerException = null) + public CancelledActionKraken(string? reason = null, Exception? innerException = null) : base(reason, innerException) { } @@ -384,7 +384,7 @@ public CancelledActionKraken(string reason = null, Exception innerException = nu /// public class UnsupportedKraken : Kraken { - public UnsupportedKraken(string reason, Exception innerException = null) + public UnsupportedKraken(string reason, Exception? innerException = null) : base(reason, innerException) { } @@ -396,9 +396,9 @@ public UnsupportedKraken(string reason, Exception innerException = null) /// public class PathErrorKraken : Kraken { - public readonly string path; + public readonly string? path; - public PathErrorKraken(string path, string reason = null, Exception innerException = null) + public PathErrorKraken(string? path, string? reason = null, Exception? innerException = null) : base(reason, innerException) { this.path = path; @@ -420,7 +420,7 @@ public class ModNotInstalledKraken : Kraken // here? Is there a way we can check if that was set, and then access it directly from // our base class? - public ModNotInstalledKraken(string mod, string reason = null, Exception innerException = null) + public ModNotInstalledKraken(string mod, string? reason = null, Exception? innerException = null) : base(reason, innerException) { this.mod = mod; @@ -432,7 +432,7 @@ public ModNotInstalledKraken(string mod, string reason = null, Exception innerEx /// public class BadCommandKraken : Kraken { - public BadCommandKraken(string reason = null, Exception innerException = null) + public BadCommandKraken(string? reason = null, Exception? innerException = null) : base(reason, innerException) { } @@ -440,7 +440,7 @@ public BadCommandKraken(string reason = null, Exception innerException = null) public class MissingCertificateKraken : Kraken { - public MissingCertificateKraken(string reason = null, Exception innerException = null) + public MissingCertificateKraken(string? reason = null, Exception? innerException = null) : base(reason, innerException) { } @@ -474,7 +474,7 @@ public class RegistryInUseKraken : Kraken { public readonly string lockfilePath; - public RegistryInUseKraken(string path, string reason = null, Exception inner_exception = null) + public RegistryInUseKraken(string path, string? reason = null, Exception? inner_exception = null) :base(reason, inner_exception) { lockfilePath = path; @@ -511,7 +511,7 @@ public class InvalidModuleFileKraken : Kraken /// Module to check against path /// Path to the file to check against module /// Human-readable description of the problem - public InvalidModuleFileKraken(CkanModule module, string path, string reason = null) + public InvalidModuleFileKraken(CkanModule module, string path, string? reason = null) : base(reason) { this.module = module; @@ -524,7 +524,7 @@ public InvalidModuleFileKraken(CkanModule module, string path, string reason = n /// public class BadGameVersionKraken : Kraken { - public BadGameVersionKraken(string reason = null, Exception inner_exception = null) + public BadGameVersionKraken(string? reason = null, Exception? inner_exception = null) : base(reason, inner_exception) { } @@ -538,7 +538,7 @@ public class WrongGameVersionKraken : Kraken { public readonly GameVersion version; - public WrongGameVersionKraken(GameVersion version, string reason = null, Exception inner_exception = null) + public WrongGameVersionKraken(GameVersion version, string? reason = null, Exception? inner_exception = null) : base(reason, inner_exception) { this.version = version; @@ -552,7 +552,7 @@ public class InstanceNameTakenKraken : Kraken { public readonly string instName; - public InstanceNameTakenKraken(string name, string reason = null) + public InstanceNameTakenKraken(string name, string? reason = null) : base(reason) { instName = name; @@ -566,7 +566,7 @@ public class ModuleIsDLCKraken : Kraken /// public readonly CkanModule module; - public ModuleIsDLCKraken(CkanModule module, string reason = null) + public ModuleIsDLCKraken(CkanModule module, string? reason = null) : base(reason) { this.module = module; @@ -581,7 +581,7 @@ public class DllLocationMismatchKraken : Kraken { public readonly string path; - public DllLocationMismatchKraken(string path, string reason = null) + public DllLocationMismatchKraken(string path, string? reason = null) : base(reason) { this.path = path; @@ -590,7 +590,7 @@ public DllLocationMismatchKraken(string path, string reason = null) public class KSPManagerKraken : Kraken { - public KSPManagerKraken(string reason = null, Exception innerException = null) + public KSPManagerKraken(string? reason = null, Exception? innerException = null) : base(reason, innerException) { } @@ -600,11 +600,35 @@ public class InvalidKSPInstanceKraken : Kraken { public readonly string instance; - public InvalidKSPInstanceKraken(string instance, string reason = null, Exception innerException = null) + public InvalidKSPInstanceKraken(string instance, string? reason = null, Exception? innerException = null) : base(reason, innerException) { this.instance = instance; } } + public class InvalidModuleAttributesException : Exception + { + private readonly CkanModule? module; + private readonly string why; + + public InvalidModuleAttributesException(string why, CkanModule? module = null) + : base(why) + { + this.why = why; + this.module = module; + } + + public override string ToString() + { + string modname = "unknown"; + + if (module != null) + { + modname = module.identifier; + } + + return string.Format("[InvalidModuleAttributesException] {0} in {1}", why, modname); + } + } } diff --git a/Core/Types/License.cs b/Core/Types/License.cs index 3eb5a6a169..f7ba0c985d 100644 --- a/Core/Types/License.cs +++ b/Core/Types/License.cs @@ -16,15 +16,19 @@ namespace CKAN public class License { public static readonly HashSet valid_licenses = - CKANSchema.schema.Definitions["license"] - .Enumeration - .Select(obj => obj.ToString()) + (CKANSchema.schema?.Definitions["license"] + .Enumeration + .Select(obj => obj.ToString()) + .OfType() + ?? Enumerable.Empty()) .ToHashSet(); private static readonly HashSet redistributable_licenses = - CKANSchema.schema.Definitions["redistributable_license"] - .Enumeration - .Select(obj => obj.ToString()) + (CKANSchema.schema?.Definitions["redistributable_license"] + .Enumeration + .Select(obj => obj.ToString()) + .OfType() + ?? Enumerable.Empty()) .ToHashSet(); // Make sure this is the last static field so the others will be ready for the instance constructor! diff --git a/Core/Types/ModuleInstallDescriptor.cs b/Core/Types/ModuleInstallDescriptor.cs index 780454032f..c43a25aa27 100644 --- a/Core/Types/ModuleInstallDescriptor.cs +++ b/Core/Types/ModuleInstallDescriptor.cs @@ -26,43 +26,43 @@ public class ModuleInstallDescriptor : ICloneable, IEquatable))] - public List filter; + public List? filter; [JsonProperty("filter_regexp", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonSingleOrArrayConverter))] - public List filter_regexp; + public List? filter_regexp; [JsonProperty("include_only", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonSingleOrArrayConverter))] - public List include_only; + public List? include_only; [JsonProperty("include_only_regexp", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonSingleOrArrayConverter))] - public List include_only_regexp; + public List? include_only_regexp; [JsonIgnore] - private Regex inst_pattern = null; + private Regex? inst_pattern = null; private static readonly Regex trailingSlashPattern = new Regex("/$", RegexOptions.Compiled); @@ -111,21 +111,18 @@ internal void DeSerialisationFixes(StreamingContext like_i_could_care) private ModuleInstallDescriptor() { install_to = typeof(ModuleInstallDescriptor).GetTypeInfo() - .GetDeclaredField("install_to") - .GetCustomAttribute() - .Value - .ToString(); + ?.GetDeclaredField("install_to") + ?.GetCustomAttribute() + ?.Value + ?.ToString(); } /// /// Returns a deep clone of our object. Implements ICloneable. /// public object Clone() - { // Deep clone our object by running it through a serialisation cycle. - string json = JsonConvert.SerializeObject(this, Formatting.None); - return JsonConvert.DeserializeObject(json); - } + => JsonConvert.DeserializeObject(JsonConvert.SerializeObject(this, Formatting.None))!; /// /// Compare two install stanzas @@ -134,7 +131,7 @@ public object Clone() /// /// True if they're equivalent, false if they're different. /// - public override bool Equals(object other) + public override bool Equals(object? other) { return Equals(other as ModuleInstallDescriptor); } @@ -147,7 +144,7 @@ public override bool Equals(object other) /// True if they're equivalent, false if they're different. /// IEquatable<> uses this for more efficient comparisons. /// - public bool Equals(ModuleInstallDescriptor otherStanza) + public bool Equals(ModuleInstallDescriptor? otherStanza) { if (otherStanza == null) { @@ -155,12 +152,12 @@ public bool Equals(ModuleInstallDescriptor otherStanza) return false; } - if (CKANPathUtils.NormalizePath(file) != CKANPathUtils.NormalizePath(otherStanza.file)) + if (CKANPathUtils.NormalizePath(file ?? "") != CKANPathUtils.NormalizePath(otherStanza.file ?? "")) { return false; } - if (CKANPathUtils.NormalizePath(find) != CKANPathUtils.NormalizePath(otherStanza.find)) + if (CKANPathUtils.NormalizePath(find ?? "") != CKANPathUtils.NormalizePath(otherStanza.find ?? "")) { return false; } @@ -170,7 +167,7 @@ public bool Equals(ModuleInstallDescriptor otherStanza) return false; } - if (CKANPathUtils.NormalizePath(install_to) != CKANPathUtils.NormalizePath(otherStanza.install_to)) + if (CKANPathUtils.NormalizePath(install_to ?? "") != CKANPathUtils.NormalizePath(otherStanza.install_to ?? "")) { return false; } @@ -185,7 +182,7 @@ public bool Equals(ModuleInstallDescriptor otherStanza) return false; } - if (filter != null + if (filter != null && otherStanza.filter != null && !filter.SequenceEqual(otherStanza.filter)) { return false; @@ -196,7 +193,7 @@ public bool Equals(ModuleInstallDescriptor otherStanza) return false; } - if (filter_regexp != null + if (filter_regexp != null && otherStanza.filter_regexp != null && !filter_regexp.SequenceEqual(otherStanza.filter_regexp)) { return false; @@ -212,7 +209,7 @@ public bool Equals(ModuleInstallDescriptor otherStanza) return false; } - if (include_only != null + if (include_only != null && otherStanza.include_only != null && !include_only.SequenceEqual(otherStanza.include_only)) { return false; @@ -223,7 +220,7 @@ public bool Equals(ModuleInstallDescriptor otherStanza) return false; } - if (include_only_regexp != null + if (include_only_regexp != null && otherStanza.include_only_regexp != null && !include_only_regexp.SequenceEqual(otherStanza.include_only_regexp)) { return false; @@ -270,7 +267,7 @@ public static ModuleInstallDescriptor DefaultInstallStanza(IGame game, string id #endregion - private void EnsurePattern() + private Regex EnsurePattern() { if (inst_pattern == null) { @@ -296,6 +293,7 @@ private void EnsurePattern() throw new Kraken(Properties.Resources.ModuleInstallDescriptorRequireFileFind); } } + return inst_pattern; } /// @@ -306,12 +304,12 @@ private void EnsurePattern() /// private bool IsWanted(string path, int? matchWhere) { - EnsurePattern(); + var pat = EnsurePattern(); // Make sure our path always uses slashes we expect. string normalised_path = path.Replace('\\', '/'); - var match = inst_pattern.Match(normalised_path); + var match = pat.Match(normalised_path); if (!match.Success) { // Doesn't match our install pattern, ignore it @@ -362,13 +360,13 @@ private bool IsWanted(string path, int? matchWhere) /// Throws a BadMetadataKraken if the stanza resulted in no files being returned. /// /// Thrown when the installation path is not valid according to the spec. - public List FindInstallableFiles(ZipFile zipfile, GameInstance ksp) + public List FindInstallableFiles(ZipFile zipfile, GameInstance? ksp) { - string installDir; + string? installDir; var files = new List(); // Normalize the path before doing everything else - string install_to = CKANPathUtils.NormalizePath(this.install_to); + string? install_to = CKANPathUtils.NormalizePath(this.install_to ?? ""); // The installation path cannot contain updirs if (install_to.Contains("/../") || install_to.EndsWith("/..")) @@ -385,8 +383,8 @@ public List FindInstallableFiles(ZipFile zipfile, GameInstance || install_to.StartsWith($"{ksp.game.PrimaryModDirectoryRelative}/")) { // The installation path can be either "GameData" or a sub-directory of "GameData" - string subDir = install_to.Substring(ksp.game.PrimaryModDirectoryRelative.Length); // remove "GameData" - subDir = subDir.StartsWith("/") ? subDir.Substring(1) : subDir; // remove a "/" at the beginning, if present + string subDir = install_to[ksp.game.PrimaryModDirectoryRelative.Length..]; // remove "GameData" + subDir = subDir.StartsWith("/") ? subDir[1..] : subDir; // remove a "/" at the beginning, if present // Add the extracted subdirectory to the path of KSP's GameData installDir = CKANPathUtils.NormalizePath(ksp.game.PrimaryModDirectory(ksp) + "/" + subDir); @@ -400,7 +398,7 @@ public List FindInstallableFiles(ZipFile zipfile, GameInstance break; default: - if (ksp.game.AllowInstallationIn(install_to, out string path)) + if (ksp.game.AllowInstallationIn(install_to, out string? path)) { installDir = ksp.ToAbsoluteGameDir(path); } @@ -413,12 +411,12 @@ public List FindInstallableFiles(ZipFile zipfile, GameInstance } } - EnsurePattern(); + var pat = EnsurePattern(); // `find` is supposed to match the "topmost" folder. Find it. var shortestMatch = find == null ? null : zipfile.Cast() - .Select(entry => inst_pattern.Match(entry.Name.Replace('\\', '/'))) + .Select(entry => pat.Match(entry.Name.Replace('\\', '/'))) .Where(match => match.Success) .DefaultIfEmpty() .Min(match => match?.Index); @@ -439,24 +437,21 @@ public List FindInstallableFiles(ZipFile zipfile, GameInstance } // Prepare our file info. - InstallableFile file_info = new InstallableFile + var file_info = new InstallableFile { - source = entry, - makedir = false, - destination = null + source = entry, + makedir = false, + destination = "", }; // If we have a place to install it, fill that in... - if (installDir != null) + if (installDir != null && ksp != null) { // Get the full name of the file. // Update our file info with the install location - file_info.destination = TransformOutputName( - ksp.game, entryName, installDir, @as); - file_info.makedir = AllowDirectoryCreation( - ksp.game, - ksp?.ToRelativeGameDir(file_info.destination) - ?? file_info.destination); + file_info.destination = TransformOutputName(ksp.game, entryName, installDir, @as); + file_info.makedir = AllowDirectoryCreation(ksp.game, + ksp.ToRelativeGameDir(file_info.destination)); } files.Add(file_info); @@ -474,10 +469,7 @@ public List FindInstallableFiles(ZipFile zipfile, GameInstance } private bool AllowDirectoryCreation(IGame game, string relativePath) - { - return game.CreateableDirs.Any(dir => - relativePath == dir || relativePath.StartsWith($"{dir}/")); - } + => game.CreateableDirs.Any(dir => relativePath == dir || relativePath.StartsWith($"{dir}/")); /// /// Transforms the name of the output. This will strip the leading directories from the stanza file from @@ -488,11 +480,10 @@ private bool AllowDirectoryCreation(IGame game, string relativePath) /// The name of the file to transform /// The installation dir where the file should end up with /// The output name - internal string TransformOutputName(IGame game, string outputName, string installDir, string @as) + internal string TransformOutputName(IGame game, string outputName, string installDir, string? @as) { - string leadingPathToRemove = Path - .GetDirectoryName(ShortestMatchingPrefix(outputName)) - .Replace('\\', '/'); + var leadingPathToRemove = Path.GetDirectoryName(ShortestMatchingPrefix(outputName)) + ?.Replace('\\', '/'); if (!string.IsNullOrEmpty(leadingPathToRemove)) { @@ -512,7 +503,7 @@ internal string TransformOutputName(IGame game, string outputName, string instal // Now outputName looks like PATH/what/ever/file.ext, where // PATH is the part that matched `file` or `find` or `find_regexp` - if (!string.IsNullOrWhiteSpace(@as)) + if (@as != null && !string.IsNullOrWhiteSpace(@as)) { if (@as.Contains("/") || @as.Contains("\\")) { @@ -530,7 +521,7 @@ internal string TransformOutputName(IGame game, string outputName, string instal // If we try to install a folder with the same name as // one of the reserved directories, strip it off. // Delete reservedPrefix and one forward slash - outputName = outputName.Substring(reservedPrefix.Length + 1); + outputName = outputName[(reservedPrefix.Length + 1)..]; } } @@ -547,14 +538,14 @@ internal string TransformOutputName(IGame game, string outputName, string instal private string ShortestMatchingPrefix(string fullPath) { - EnsurePattern(); + var pat = EnsurePattern(); string shortest = fullPath; - for (string path = trailingSlashPattern.Replace(fullPath.Replace('\\', '/'), ""); - !string.IsNullOrEmpty(path); - path = Path.GetDirectoryName(path).Replace('\\', '/')) + for (var path = trailingSlashPattern.Replace(fullPath.Replace('\\', '/'), ""); + path != null && !string.IsNullOrEmpty(path); + path = Path.GetDirectoryName(path)?.Replace('\\', '/')) { - if (inst_pattern.IsMatch(path)) + if (pat.IsMatch(path)) { shortest = path; } @@ -574,7 +565,7 @@ private static string ReplaceFirstPiece(string text, string delimiter, string re // No delimiter, replace whole string return replacement; } - return replacement + text.Substring(pos); + return replacement + text[pos..]; } public string DescribeMatch() diff --git a/Core/Types/ModuleReplacement.cs b/Core/Types/ModuleReplacement.cs new file mode 100644 index 0000000000..0435b0ee00 --- /dev/null +++ b/Core/Types/ModuleReplacement.cs @@ -0,0 +1,14 @@ +namespace CKAN +{ + public class ModuleReplacement + { + public ModuleReplacement(CkanModule toReplace, CkanModule replaceWith) + { + ToReplace = toReplace; + ReplaceWith = replaceWith; + } + + public readonly CkanModule ToReplace; + public readonly CkanModule ReplaceWith; + } +} diff --git a/Core/Types/NameComparer.cs b/Core/Types/NameComparer.cs new file mode 100644 index 0000000000..54e32519e8 --- /dev/null +++ b/Core/Types/NameComparer.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace CKAN +{ + public class NameComparer : IEqualityComparer + { + public bool Equals(CkanModule? x, CkanModule? y) + => x?.identifier.Equals(y?.identifier) + ?? y == null; + + public int GetHashCode(CkanModule obj) + => obj.identifier.GetHashCode(); + } +} diff --git a/Core/Types/RelationshipDescriptor.cs b/Core/Types/RelationshipDescriptor.cs index be41fd14cf..cbe83e54c9 100644 --- a/Core/Types/RelationshipDescriptor.cs +++ b/Core/Types/RelationshipDescriptor.cs @@ -11,34 +11,36 @@ namespace CKAN { public abstract class RelationshipDescriptor : IEquatable { - public bool MatchesAny(ICollection modules, - HashSet dlls, - IDictionary dlc) - => MatchesAny(modules, dlls, dlc, out CkanModule _); + public bool MatchesAny(ICollection modules, + HashSet? dlls, + IDictionary? dlc) + => MatchesAny(modules, dlls, dlc, out CkanModule? _); - public abstract bool MatchesAny(ICollection modules, - HashSet dlls, - IDictionary dlc, - out CkanModule matched); + public abstract bool MatchesAny(ICollection modules, + HashSet? dlls, + IDictionary? dlc, + out CkanModule? matched); public abstract bool WithinBounds(CkanModule otherModule); - public abstract List LatestAvailableWithProvides( - IRegistryQuerier registry, GameVersionCriteria crit, ICollection installed = null, - ICollection toInstall = null); + public abstract List LatestAvailableWithProvides(IRegistryQuerier registry, + GameVersionCriteria? crit, + ICollection? installed = null, + ICollection? toInstall = null); - public abstract CkanModule ExactMatch( - IRegistryQuerier registry, GameVersionCriteria crit, ICollection installed = null, - ICollection toInstall = null); + public abstract CkanModule? ExactMatch(IRegistryQuerier registry, + GameVersionCriteria? crit, + ICollection? installed = null, + ICollection? toInstall = null); - public abstract bool Equals(RelationshipDescriptor other); + public abstract bool Equals(RelationshipDescriptor? other); public abstract bool ContainsAny(IEnumerable identifiers); public abstract bool StartsWith(string prefix); [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string choice_help_text; + public string? choice_help_text; /// /// If true, then don't show recommendations and suggestions of this module or its dependencies. @@ -55,16 +57,16 @@ public abstract CkanModule ExactMatch( public class ModuleRelationshipDescriptor : RelationshipDescriptor { [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public ModuleVersion max_version; + public ModuleVersion? max_version; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public ModuleVersion min_version; + public ModuleVersion? min_version; // The identifier to match - public string name; + public string name = ""; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public ModuleVersion version; + public ModuleVersion? version; public override bool WithinBounds(CkanModule otherModule) // See if the real thing is there @@ -81,10 +83,11 @@ public override bool WithinBounds(CkanModule otherModule) /// /// /// True if other_version is within the bounds - public bool WithinBounds(ModuleVersion other) + public bool WithinBounds(ModuleVersion? other) // UnmanagedModuleVersions with unknown versions always satisfy the bound - => (other is UnmanagedModuleVersion unmanagedModuleVersion - && unmanagedModuleVersion.IsUnknownVersion) + => other == null + || (other is UnmanagedModuleVersion unmanagedModuleVersion + && unmanagedModuleVersion.IsUnknownVersion) || (version?.Equals(other) ?? ((min_version == null || min_version <= other) && (max_version == null || max_version >= other))); @@ -100,10 +103,10 @@ public bool WithinBounds(ModuleVersion other) /// /// true if any of the modules match this descriptor, false otherwise. /// - public override bool MatchesAny(ICollection modules, - HashSet dlls, - IDictionary dlc, - out CkanModule matched) + public override bool MatchesAny(ICollection modules, + HashSet? dlls, + IDictionary? dlc, + out CkanModule? matched) { // DLLs are considered to match any version if (dlls != null && dlls.Contains(name)) @@ -119,26 +122,26 @@ public override bool MatchesAny(ICollection modules, return true; } - return dlc != null && dlc.TryGetValue(name, out ModuleVersion dlcVer) + return dlc != null && dlc.TryGetValue(name, out ModuleVersion? dlcVer) && WithinBounds(dlcVer); } - public override List LatestAvailableWithProvides(IRegistryQuerier registry, - GameVersionCriteria crit, - ICollection installed = null, - ICollection toInstall = null) + public override List LatestAvailableWithProvides(IRegistryQuerier registry, + GameVersionCriteria? crit, + ICollection? installed = null, + ICollection? toInstall = null) => registry.LatestAvailableWithProvides(name, crit, this, installed, toInstall); - public override CkanModule ExactMatch(IRegistryQuerier registry, - GameVersionCriteria crit, - ICollection installed = null, - ICollection toInstall = null) + public override CkanModule? ExactMatch(IRegistryQuerier registry, + GameVersionCriteria? crit, + ICollection? installed = null, + ICollection? toInstall = null) => Utilities.DefaultIfThrows(() => registry.LatestAvailable(name, crit, this, installed, toInstall)); - public override bool Equals(RelationshipDescriptor other) + public override bool Equals(RelationshipDescriptor? other) => Equals(other as ModuleRelationshipDescriptor); - protected bool Equals(ModuleRelationshipDescriptor other) + protected bool Equals(ModuleRelationshipDescriptor? other) => other != null && name == other.name && version == other.version @@ -177,7 +180,7 @@ public class AnyOfRelationshipDescriptor : RelationshipDescriptor { [JsonProperty("any_of", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonRelationshipConverter))] - public List any_of; + public List? any_of; public static readonly List ForbiddenPropertyNames = new List() { @@ -190,36 +193,42 @@ public class AnyOfRelationshipDescriptor : RelationshipDescriptor public override bool WithinBounds(CkanModule otherModule) => any_of?.Any(r => r.WithinBounds(otherModule)) ?? false; - public override bool MatchesAny(ICollection modules, - HashSet dlls, - IDictionary dlc, - out CkanModule matched) + public override bool MatchesAny(ICollection modules, + HashSet? dlls, + IDictionary? dlc, + out CkanModule? matched) { matched = any_of?.AsParallel() - .Select(rel => rel.MatchesAny(modules, dlls, dlc, out CkanModule whatMached) + .Select(rel => rel.MatchesAny(modules, dlls, dlc, out CkanModule? whatMached) ? whatMached : null) .FirstOrDefault(m => m != null); return matched != null; } - public override List LatestAvailableWithProvides( - IRegistryQuerier registry, GameVersionCriteria crit, ICollection installed = null, - ICollection toInstall = null) - => any_of?.SelectMany(r => r.LatestAvailableWithProvides(registry, crit, installed, toInstall)).Distinct().ToList(); + public override List LatestAvailableWithProvides(IRegistryQuerier registry, + GameVersionCriteria? crit, + ICollection? installed = null, + ICollection? toInstall = null) + => (any_of?.SelectMany(r => r.LatestAvailableWithProvides(registry, crit, installed, toInstall)) + .Distinct() + ?? Enumerable.Empty()) + .ToList(); // Exact match is not possible for any_of - public override CkanModule ExactMatch( - IRegistryQuerier registry, GameVersionCriteria crit, ICollection installed = null, - ICollection toInstall = null) + public override CkanModule? ExactMatch(IRegistryQuerier registry, + GameVersionCriteria? crit, + ICollection? installed = null, + ICollection? toInstall = null) => null; - public override bool Equals(RelationshipDescriptor other) + public override bool Equals(RelationshipDescriptor? other) => Equals(other as AnyOfRelationshipDescriptor); - protected bool Equals(AnyOfRelationshipDescriptor other) + protected bool Equals(AnyOfRelationshipDescriptor? other) => other != null - && (any_of?.SequenceEqual(other.any_of) ?? other.any_of == null); + && (any_of?.SequenceEqual(other.any_of ?? Enumerable.Empty()) + ?? other.any_of == null); public override bool ContainsAny(IEnumerable identifiers) => any_of?.Any(r => r.ContainsAny(identifiers)) ?? false; @@ -230,6 +239,7 @@ public override bool StartsWith(string prefix) public override string ToString() => any_of?.Select(r => r.ToString()) .Aggregate((a, b) => - string.Format(Properties.Resources.RelationshipDescriptorAnyOfJoiner, a, b)); + string.Format(Properties.Resources.RelationshipDescriptorAnyOfJoiner, a, b)) + ?? ""; } } diff --git a/Core/Types/ReleaseStatus.cs b/Core/Types/ReleaseStatus.cs index 4d89ee8107..37a3d0abcd 100644 --- a/Core/Types/ReleaseStatus.cs +++ b/Core/Types/ReleaseStatus.cs @@ -21,7 +21,7 @@ public class ReleaseStatus /// Throws a BadMetadataKraken if passed a non-compliant string. /// /// Status. - public ReleaseStatus(string status) + public ReleaseStatus(string? status) { switch (status) { diff --git a/Core/Types/ResourcesDescriptor.cs b/Core/Types/ResourcesDescriptor.cs index 2fa01a95e5..2373acedfb 100644 --- a/Core/Types/ResourcesDescriptor.cs +++ b/Core/Types/ResourcesDescriptor.cs @@ -8,58 +8,58 @@ public class ResourcesDescriptor { [JsonProperty("homepage", Order = 1, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonIgnoreBadUrlConverter))] - public Uri homepage; + public Uri? homepage; [JsonProperty("spacedock", Order = 2, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonOldResourceUrlConverter))] - public Uri spacedock; + public Uri? spacedock; [JsonProperty("curse", Order = 3, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonOldResourceUrlConverter))] - public Uri curse; + public Uri? curse; [JsonProperty("repository", Order = 4, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonIgnoreBadUrlConverter))] - public Uri repository; + public Uri? repository; [JsonProperty("bugtracker", Order = 5, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonIgnoreBadUrlConverter))] - public Uri bugtracker; + public Uri? bugtracker; [JsonProperty("discussions", Order = 6, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonIgnoreBadUrlConverter))] - public Uri discussions; + public Uri? discussions; [JsonProperty("ci", Order = 7, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonIgnoreBadUrlConverter))] - public Uri ci; + public Uri? ci; [JsonProperty("license", Order = 8, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonIgnoreBadUrlConverter))] - public Uri license; + public Uri? license; [JsonProperty("manual", Order = 9, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonIgnoreBadUrlConverter))] - public Uri manual; + public Uri? manual; [JsonProperty("metanetkan", Order = 10, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonOldResourceUrlConverter))] - public Uri metanetkan; + public Uri? metanetkan; [JsonProperty("remote-avc", Order = 11, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonOldResourceUrlConverter))] - public Uri remoteAvc; + public Uri? remoteAvc; [JsonProperty("remote-swinfo", Order = 12, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonOldResourceUrlConverter))] - public Uri remoteSWInfo; + public Uri? remoteSWInfo; [JsonProperty("store", Order = 13, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonOldResourceUrlConverter))] - public Uri store; + public Uri? store; [JsonProperty("steamstore", Order = 14, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonOldResourceUrlConverter))] - public Uri steamstore; + public Uri? steamstore; } } diff --git a/Core/Types/Schema.cs b/Core/Types/Schema.cs index 2bedf575e3..7fca585262 100644 --- a/Core/Types/Schema.cs +++ b/Core/Types/Schema.cs @@ -13,15 +13,15 @@ public static class CKANSchema /// /// Parsed representation of our embedded CKAN.schema resource /// - public static readonly JsonSchema schema = - JsonSchema.FromJsonAsync( - // ♫ Ooh-ooh StreamReader, I believe you can get me all the lines ♫ - // ♫ Ooh-ooh StreamReader, I believe we can reach the end of file ♫ - new StreamReader( - Assembly.GetExecutingAssembly() - .GetManifestResourceStream(embeddedSchema) - ).ReadToEnd() - ).Result; + public static readonly JsonSchema? schema = + Assembly.GetExecutingAssembly() + .GetManifestResourceStream(embeddedSchema) + is Stream s + ? JsonSchema.FromJsonAsync( + // ♫ Ooh-ooh StreamReader, I believe you can get me all the lines ♫ + // ♫ Ooh-ooh StreamReader, I believe we can reach the end of file ♫ + new StreamReader(s).ReadToEnd()).Result + : null; private const string embeddedSchema = "CKAN.Core.CKAN.schema"; } diff --git a/Core/Types/TimeLog.cs b/Core/Types/TimeLog.cs index fbb032e6ae..f9b710329d 100644 --- a/Core/Types/TimeLog.cs +++ b/Core/Types/TimeLog.cs @@ -15,7 +15,7 @@ public class TimeLog public static string GetPath(string dir) => Path.Combine(dir, filename); - public static TimeLog Load(string path) + public static TimeLog? Load(string path) { try { diff --git a/Core/Utilities.cs b/Core/Utilities.cs index 923a5a4125..2d48cf8463 100644 --- a/Core/Utilities.cs +++ b/Core/Utilities.cs @@ -28,7 +28,7 @@ public static class Utilities "nl-NL", }; - public static T DefaultIfThrows(Func func) + public static T? DefaultIfThrows(Func func) where T : class { try { @@ -104,7 +104,8 @@ private static void CopyDirectory(string sourceDirPath, { var temppath = Path.Combine(destDirPath, subdir.Name); // If already a sym link, replicate it in the new location - if (DirectoryLink.TryGetTarget(subdir.FullName, out string existingLinkTarget)) + if (DirectoryLink.TryGetTarget(subdir.FullName, out string? existingLinkTarget) + && existingLinkTarget is not null) { DirectoryLink.Create(existingLinkTarget, temppath, file_transaction); } @@ -178,6 +179,23 @@ public static bool ProcessStartURL(string url) return false; } + public static void OpenFileBrowser(string location) + { + // We need the folder of the file + // Otherwise the OS would try to open the file in its default application + if (DirPath(location) is string path) + { + ProcessStartURL(path); + } + } + + private static string? DirPath(string path) + => Directory.Exists(path) ? path + : File.Exists(path) && Path.GetDirectoryName(path) is string parent + && Directory.Exists(parent) + ? parent + : null; + private static readonly ILog log = LogManager.GetLogger(typeof(Utilities)); } } diff --git a/Core/Versioning/GameVersion.cs b/Core/Versioning/GameVersion.cs index c6225c70f7..51dcf030cb 100644 --- a/Core/Versioning/GameVersion.cs +++ b/Core/Versioning/GameVersion.cs @@ -3,6 +3,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; @@ -29,7 +30,7 @@ public sealed partial class GameVersion private readonly int _patch; private readonly int _build; - private readonly string _string; + private readonly string? _string; /// /// Gets the value of the major component of the version number for the current @@ -102,7 +103,8 @@ public sealed partial class GameVersion /// /// True if null or Any, false otherwise /// - public static bool IsNullOrAny(GameVersion v) => v == null || v.IsAny; + + public static bool IsNullOrAny([NotNullWhen(false)] GameVersion? v) => v == null || v.IsAny; /// /// Initialize a new instance of the class with all components unspecified. @@ -252,10 +254,10 @@ public GameVersion(int major, int minor, int patch, int build) /// the returned string is "1.3.4.2". /// /// - /// If the current is totally undefined the return value will null. + /// If the current is totally undefined the return value will be null. /// /// - public override string ToString() => _string; + public override string? ToString() => _string; /// /// Strip off the build number if it's defined @@ -321,14 +323,14 @@ public GameVersionRange ToVersionRange() /// A object that is equivalent to the version number specified in the /// parameter. /// - public static GameVersion Parse(string input) + public static GameVersion Parse(string? input) { if (input is null) { - throw new ArgumentNullException("input"); + throw new ArgumentNullException(nameof(input)); } - if (TryParse(input, out GameVersion result)) + if (TryParse(input, out GameVersion? result) && result is not null) { return result; } @@ -352,7 +354,8 @@ public static GameVersion Parse(string input) /// /// true if the parameter was converted successfully; otherwise, false. /// - public static bool TryParse(string input, out GameVersion result) + public static bool TryParse(string? input, + [NotNullWhen(returnValue: true)] out GameVersion? result) { result = null; @@ -388,7 +391,7 @@ public static bool TryParse(string input, out GameVersion result) return false; } - if (major < 0 || major == int.MaxValue) + if (major is < 0 or int.MaxValue) { major = Undefined; } @@ -401,7 +404,7 @@ public static bool TryParse(string input, out GameVersion result) return false; } - if (minor < 0 || minor == int.MaxValue) + if (minor is < 0 or int.MaxValue) { minor = Undefined; } @@ -414,7 +417,7 @@ public static bool TryParse(string input, out GameVersion result) return false; } - if (patch < 0 || patch == int.MaxValue) + if (patch is < 0 or int.MaxValue) { patch = Undefined; } @@ -427,7 +430,7 @@ public static bool TryParse(string input, out GameVersion result) return false; } - if (build < 0 || build == int.MaxValue) + if (build is < 0 or int.MaxValue) { build = Undefined; } @@ -490,13 +493,12 @@ public bool InBuildMap(IGame game) /// /// A complete GameVersion object /// A IUser instance, to raise the corresponding dialog. - public GameVersion RaiseVersionSelectionDialog(IGame game, IUser user) + public GameVersion RaiseVersionSelectionDialog(IGame game, IUser? user) { if (IsFullyDefined && InBuildMap(game)) { // The specified version is complete and known :hooray:. Return this instance. return this; - } else if (!IsMajorDefined || !IsMinorDefined) { @@ -553,9 +555,9 @@ public GameVersion RaiseVersionSelectionDialog(IGame game, IUser user) // Lucky, there's only one possible version. Happens f.e. if there's only one build per patch (especially the case for newer versions). return possibleVersions.ElementAt(0); } - else if (user.Headless) + else if (user == null || user.Headless) { - return possibleVersions.LastOrDefault(); + return possibleVersions.Last(); } else { @@ -568,7 +570,6 @@ public GameVersion RaiseVersionSelectionDialog(IGame game, IUser user) { throw new CancelledActionKraken(); } - } } } @@ -588,8 +589,8 @@ public sealed partial class GameVersion : IEquatable /// true if every component of the current matches the corresponding component /// of the parameter; otherwise, false. /// - public bool Equals(GameVersion obj) - => !(obj is null) + public bool Equals(GameVersion? obj) + => obj is not null && (ReferenceEquals(obj, this) || (_major == obj._major && _minor == obj._minor @@ -608,8 +609,8 @@ public bool Equals(GameVersion obj) /// objects and every component of the current object /// matches the corresponding component of ; otherwise, false. /// - public override bool Equals(object obj) - => !(obj is null) + public override bool Equals(object? obj) + => obj is not null && (ReferenceEquals(obj, this) || (obj is GameVersion gv && Equals(gv))); @@ -630,7 +631,7 @@ public override int GetHashCode() /// The first object. /// The second object. /// true if equals ; otherwise, false. - public static bool operator ==(GameVersion v1, GameVersion v2) + public static bool operator ==(GameVersion? v1, GameVersion? v2) => Equals(v1, v2); /// @@ -641,7 +642,7 @@ public override int GetHashCode() /// /// true if does not equal ; otherwise, false. /// - public static bool operator !=(GameVersion v1, GameVersion v2) + public static bool operator !=(GameVersion? v1, GameVersion? v2) => !Equals(v1, v2); } @@ -681,11 +682,11 @@ public sealed partial class GameVersion : IComparable, IComparable /// /// /// - public int CompareTo(object obj) + public int CompareTo(object? obj) { if (obj is null) { - throw new ArgumentNullException("obj"); + throw new ArgumentNullException(nameof(obj)); } var objGameVersion = obj as GameVersion; @@ -734,11 +735,11 @@ public int CompareTo(object obj) /// /// /// - public int CompareTo(GameVersion other) + public int CompareTo(GameVersion? other) { if (other is null) { - throw new ArgumentNullException("other"); + throw new ArgumentNullException(nameof(other)); } if (Equals(this, other)) @@ -779,19 +780,7 @@ public int CompareTo(GameVersion other) /// true if is less than ; otherwise, flase. /// public static bool operator <(GameVersion left, GameVersion right) - { - if (left is null) - { - throw new ArgumentNullException("left"); - } - - if (right is null) - { - throw new ArgumentNullException("right"); - } - - return left.CompareTo(right) < 0; - } + => left.CompareTo(right) < 0; /// /// Determines whether the first specified object is greater than the second @@ -803,19 +792,7 @@ public int CompareTo(GameVersion other) /// true if is greater than ; otherwise, flase. /// public static bool operator >(GameVersion left, GameVersion right) - { - if (left is null) - { - throw new ArgumentNullException("left"); - } - - if (right is null) - { - throw new ArgumentNullException("right"); - } - - return left.CompareTo(right) > 0; - } + => left.CompareTo(right) > 0; /// /// Determines whether the first specified object is less than or equal to the second @@ -827,19 +804,7 @@ public int CompareTo(GameVersion other) /// true if is less than or equal to ; otherwise, flase. /// public static bool operator <=(GameVersion left, GameVersion right) - { - if (left is null) - { - throw new ArgumentNullException("left"); - } - - if (right is null) - { - throw new ArgumentNullException("right"); - } - - return left.CompareTo(right) <= 0; - } + => left.CompareTo(right) <= 0; /// /// Determines whether the first specified object is greater than or equal to the @@ -851,67 +816,12 @@ public int CompareTo(GameVersion other) /// true if is greater than or equal to ; otherwise, flase. /// public static bool operator >=(GameVersion left, GameVersion right) - { - if (left is null) - { - throw new ArgumentNullException("left"); - } - - if (right is null) - { - throw new ArgumentNullException("right"); - } - - return left.CompareTo(right) >= 0; - } - } - - public sealed partial class GameVersion - { - public static GameVersion Min(params GameVersion[] versions) - { - if (versions == null) - { - throw new ArgumentNullException("versions"); - } - - if (!versions.Any()) - { - throw new ArgumentException("Value cannot be empty.", "versions"); - } - - if (versions.Any(i => i == null)) - { - throw new ArgumentException("Value cannot contain null.", "versions"); - } - - return versions.Min(); - } - - public static GameVersion Max(params GameVersion[] versions) - { - if (versions == null) - { - throw new ArgumentNullException("versions"); - } - - if (!versions.Any()) - { - throw new ArgumentException("Value cannot be empty.", "versions"); - } - - if (versions.Any(i => i == null)) - { - throw new ArgumentException("Value cannot contain null.", "versions"); - } - - return versions.Max(); - } + => left.CompareTo(right) >= 0; } public sealed partial class GameVersion { - private static string DeriveString(int major, int minor, int patch, int build) + private static string? DeriveString(int major, int minor, int patch, int build) { var sb = new StringBuilder(); @@ -946,12 +856,12 @@ private static string DeriveString(int major, int minor, int patch, int build) public sealed class GameVersionJsonConverter : JsonConverter { - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { - writer.WriteValue(value.ToString()); + writer.WriteValue(value?.ToString()); } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { var value = reader.Value?.ToString(); @@ -959,9 +869,8 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist { case null: return null; - default: - GameVersion result; + default: // For a little while, AVC files which didn't specify a full three-part // version number could result in versions like `1.1.`, which cause our // code to fail. Here we strip any trailing dot from the version number, @@ -969,13 +878,13 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist value = Regex.Replace(value, @"\.$", ""); - if (GameVersion.TryParse(value, out result)) + if (GameVersion.TryParse(value, out GameVersion? result)) { return result; } else { - throw new JsonException(string.Format("Could not parse KSP version: {0}", value)); + throw new JsonException(string.Format("Could not parse game version: {0}", value)); } } } diff --git a/Core/Versioning/GameVersionBound.cs b/Core/Versioning/GameVersionBound.cs index df540b7767..94964ee2eb 100644 --- a/Core/Versioning/GameVersionBound.cs +++ b/Core/Versioning/GameVersionBound.cs @@ -17,11 +17,6 @@ public GameVersionBound() public GameVersionBound(GameVersion value, bool inclusive) { - if (value is null) - { - throw new ArgumentNullException("value"); - } - if (!value.IsAny && !value.IsFullyDefined) { throw new ArgumentException("Version must be either fully undefined or fully defined.", "value"); @@ -70,7 +65,7 @@ public GameVersion AsInclusiveUpper() public sealed partial class GameVersionBound : IEquatable { - public bool Equals(GameVersionBound other) + public bool Equals(GameVersionBound? other) { if (other is null) { @@ -85,7 +80,7 @@ public bool Equals(GameVersionBound other) return Equals(Value, other.Value) && Inclusive == other.Inclusive; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is null) { @@ -103,24 +98,24 @@ public override bool Equals(object obj) public override int GetHashCode() => (Value, Inclusive).GetHashCode(); - public static bool operator ==(GameVersionBound left, GameVersionBound right) => Equals(left, right); - public static bool operator !=(GameVersionBound left, GameVersionBound right) => !Equals(left, right); + public static bool operator ==(GameVersionBound? left, GameVersionBound? right) => Equals(left, right); + public static bool operator !=(GameVersionBound? left, GameVersionBound? right) => !Equals(left, right); } public sealed partial class GameVersionBound { /// - /// Returns the lowest of a set of objects. Analagous to - /// but does not produce a stable sort because in the event of a + /// Returns the lowest of a set of objects. + /// Does not produce a stable sort because in the event of a /// tie inclusive bounds are treated as both lower and higher than equivalent exclusive bounds. /// /// The set of objects to compare. /// The lowest value in . - public static GameVersionBound Lowest(params GameVersionBound[] versionBounds) + public static GameVersionBound Lowest(params GameVersionBound?[] versionBounds) { if (versionBounds == null) { - throw new ArgumentNullException("versionBounds"); + throw new ArgumentNullException(nameof(versionBounds)); } if (!versionBounds.Any()) @@ -128,30 +123,30 @@ public static GameVersionBound Lowest(params GameVersionBound[] versionBounds) throw new ArgumentException("Value cannot be empty.", "versionBounds"); } - if (versionBounds.Any(i => i == null)) + if (versionBounds.Contains(null)) { throw new ArgumentException("Value cannot contain null.", "versionBounds"); } - return versionBounds - .OrderBy(i => i == Unbounded) - .ThenBy(i => i.Value) - .ThenBy(i => i.Inclusive) - .First(); + return versionBounds.OfType() + .OrderBy(i => i == Unbounded) + .ThenBy(i => i.Value) + .ThenBy(i => i.Inclusive) + .First(); } /// - /// Returns the highest of a set of objects. Analagous to - /// but does not produce a stable sort because in the event of a + /// Returns the highest of a set of objects. + /// Does not produce a stable sort because in the event of a /// tie inclusive bounds are treated as both lower and higher than equivalent exclusive bounds. /// /// The set of objects to compare. /// The highest value in . - public static GameVersionBound Highest(params GameVersionBound[] versionBounds) + public static GameVersionBound Highest(params GameVersionBound?[] versionBounds) { if (versionBounds == null) { - throw new ArgumentNullException("versionBounds"); + throw new ArgumentNullException(nameof(versionBounds)); } if (!versionBounds.Any()) @@ -159,16 +154,16 @@ public static GameVersionBound Highest(params GameVersionBound[] versionBounds) throw new ArgumentException("Value cannot be empty.", "versionBounds"); } - if (versionBounds.Any(i => i == null)) + if (versionBounds.Contains(null)) { throw new ArgumentException("Value cannot contain null.", "versionBounds"); } - return versionBounds - .OrderBy(i => i == Unbounded) - .ThenByDescending(i => i.Value) - .ThenBy(i => i.Inclusive) - .First(); + return versionBounds.OfType() + .OrderBy(i => i == Unbounded) + .ThenByDescending(i => i.Value) + .ThenBy(i => i.Inclusive) + .First(); } } } diff --git a/Core/Versioning/GameVersionCriteria.cs b/Core/Versioning/GameVersionCriteria.cs index e7a6435e2b..10783100ce 100644 --- a/Core/Versioning/GameVersionCriteria.cs +++ b/Core/Versioning/GameVersionCriteria.cs @@ -10,7 +10,7 @@ public class GameVersionCriteria : IEquatable { private readonly List _versions = new List(); - public GameVersionCriteria(GameVersion v) + public GameVersionCriteria(GameVersion? v) { if (v != null) { @@ -18,7 +18,7 @@ public GameVersionCriteria(GameVersion v) } } - public GameVersionCriteria(GameVersion v, List compatibleVersions) + public GameVersionCriteria(GameVersion? v, List compatibleVersions) { if (v != null) { @@ -41,11 +41,11 @@ public GameVersionCriteria(GameVersion v, List compatibleVersions) public GameVersionCriteria Union(GameVersionCriteria other) => new GameVersionCriteria(null, _versions.Union(other.Versions).ToList()); - public override bool Equals(object obj) + public override bool Equals(object? obj) => Equals(obj as GameVersionCriteria); // From IEquatable - public bool Equals(GameVersionCriteria other) + public bool Equals(GameVersionCriteria? other) => other != null && !_versions.Except(other._versions).Any() && !other._versions.Except(_versions).Any(); diff --git a/Core/Versioning/GameVersionRange.cs b/Core/Versioning/GameVersionRange.cs index 648d2a3c9e..e463c60072 100644 --- a/Core/Versioning/GameVersionRange.cs +++ b/Core/Versioning/GameVersionRange.cs @@ -14,7 +14,7 @@ public sealed partial class GameVersionRange public GameVersionBound Lower { get; private set; } public GameVersionBound Upper { get; private set; } - public GameVersionRange(GameVersionBound lower, GameVersionBound upper) + public GameVersionRange(GameVersionBound? lower, GameVersionBound? upper) { Lower = lower ?? GameVersionBound.Unbounded; Upper = upper ?? GameVersionBound.Unbounded; @@ -23,21 +23,16 @@ public GameVersionRange(GameVersionBound lower, GameVersionBound upper) } public GameVersionRange(GameVersion lower, GameVersion upper) - : this(lower?.ToVersionRange().Lower, upper?.ToVersionRange().Upper) { } + : this(lower.ToVersionRange().Lower, upper.ToVersionRange().Upper) { } public override string ToString() => _string; - public GameVersionRange IntersectWith(GameVersionRange other) + public GameVersionRange? IntersectWith(GameVersionRange other) { - if (other is null) - { - throw new ArgumentNullException("other"); - } - var highestLow = GameVersionBound.Highest(Lower, other.Lower); var lowestHigh = GameVersionBound.Lowest(Upper, other.Upper); - - return IsEmpty(highestLow, lowestHigh) ? null : new GameVersionRange(highestLow, lowestHigh); + return IsEmpty(highestLow, lowestHigh) ? null + : new GameVersionRange(highestLow, lowestHigh); } // Same logic as above but without "new" @@ -47,11 +42,6 @@ private bool Intersects(GameVersionRange other) public bool IsSupersetOf(GameVersionRange other) { - if (other is null) - { - throw new ArgumentNullException("other"); - } - var lowerIsOkay = Lower.Value.IsAny || (Lower.Value < other.Lower.Value) || (Lower.Value == other.Lower.Value && (Lower.Inclusive || !other.Lower.Inclusive)); @@ -98,7 +88,7 @@ private static string DeriveString(GameVersionRange versionRange) return sb.ToString(); } - private static string SameVersionString(GameVersion v) + private static string? SameVersionString(GameVersion? v) => v == null ? "???" : v.IsAny ? Properties.Resources.CkanModuleAllVersions : v.ToString(); @@ -129,7 +119,7 @@ public string ToSummaryString(IGame game) public sealed partial class GameVersionRange : IEquatable { - public bool Equals(GameVersionRange other) + public bool Equals(GameVersionRange? other) { if (other is null) { @@ -144,7 +134,7 @@ public bool Equals(GameVersionRange other) return Equals(Lower, other.Lower) && Equals(Upper, other.Upper); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is null) { @@ -162,7 +152,7 @@ public override bool Equals(object obj) public override int GetHashCode() => (Lower, Upper).GetHashCode(); - public static bool operator ==(GameVersionRange left, GameVersionRange right) => Equals(left, right); - public static bool operator !=(GameVersionRange left, GameVersionRange right) => !Equals(left, right); + public static bool operator ==(GameVersionRange? left, GameVersionRange? right) => Equals(left, right); + public static bool operator !=(GameVersionRange? left, GameVersionRange? right) => !Equals(left, right); } } diff --git a/Core/Versioning/ModuleVersion.cs b/Core/Versioning/ModuleVersion.cs index dde88bbfa2..13a3cfb4c7 100644 --- a/Core/Versioning/ModuleVersion.cs +++ b/Core/Versioning/ModuleVersion.cs @@ -98,11 +98,11 @@ public string ToString(bool hideEpoch, bool hideV) public partial class ModuleVersion : IEquatable { - public override bool Equals(object obj) + public override bool Equals(object? obj) => ReferenceEquals(this, obj) || (obj is ModuleVersion version && Equals(version)); - public bool Equals(ModuleVersion other) + public bool Equals(ModuleVersion? other) => ReferenceEquals(this, other) || CompareTo(other) == 0; @@ -130,7 +130,7 @@ public override int GetHashCode() /// /// /// - public static bool operator ==(ModuleVersion left, ModuleVersion right) + public static bool operator ==(ModuleVersion? left, ModuleVersion? right) => Equals(left, right); /// @@ -154,7 +154,7 @@ public override int GetHashCode() /// /// /// - public static bool operator !=(ModuleVersion left, ModuleVersion right) + public static bool operator !=(ModuleVersion? left, ModuleVersion? right) => !Equals(left, right); } @@ -192,7 +192,7 @@ public partial class ModuleVersion : IComparable /// /// /// - public int CompareTo(ModuleVersion other) + public int CompareTo(ModuleVersion? other) { Comparison stringComp(string v1, string v2) { @@ -212,8 +212,8 @@ Comparison stringComp(string v1, string v2) { if (char.IsNumber(v1[i])) { - comparison.FirstRemainder = v1.Substring(i); - str1 = v1.Substring(0, i); + comparison.FirstRemainder = v1[i..]; + str1 = v1[..i]; break; } } @@ -222,8 +222,8 @@ Comparison stringComp(string v1, string v2) { if (char.IsNumber(v2[i])) { - comparison.SecondRemainder = v2.Substring(i); - str2 = v2.Substring(0, i); + comparison.SecondRemainder = v2[i..]; + str2 = v2[..i]; break; } } @@ -272,7 +272,7 @@ Comparison numComp(string v1, string v2) { if (!char.IsNumber(v1[i])) { - comparison.FirstRemainder = v1.Substring(i); + comparison.FirstRemainder = v1[i..]; break; } @@ -284,7 +284,7 @@ Comparison numComp(string v1, string v2) { if (!char.IsNumber(v2[i])) { - comparison.SecondRemainder = v2.Substring(i); + comparison.SecondRemainder = v2[i..]; break; } @@ -292,12 +292,12 @@ Comparison numComp(string v1, string v2) } - if (!int.TryParse(v1.Substring(0, minimumLength1), out var integer1)) + if (!int.TryParse(v1[..minimumLength1], out var integer1)) { integer1 = 0; } - if (!int.TryParse(v2.Substring(0, minimumLength2), out var integer2)) + if (!int.TryParse(v2[..minimumLength2], out var integer2)) { integer2 = 0; } @@ -437,10 +437,8 @@ public bool IsEqualTo(ModuleVersion other) /// /// /// - public bool IsLessThan(ModuleVersion other) - { - return CompareTo(other) < 0; - } + public bool IsLessThan(ModuleVersion? other) + => CompareTo(other) < 0; /// /// Compares the current object to a specified object @@ -469,9 +467,7 @@ public bool IsLessThan(ModuleVersion other) /// /// public bool IsGreaterThan(ModuleVersion other) - { - return CompareTo(other) > 0; - } + => CompareTo(other) > 0; /// /// Compares two objects to determine if the first is less than the second. diff --git a/Core/Versioning/UnmanagedModuleVersion.cs b/Core/Versioning/UnmanagedModuleVersion.cs index f71cc77980..d792eb0368 100644 --- a/Core/Versioning/UnmanagedModuleVersion.cs +++ b/Core/Versioning/UnmanagedModuleVersion.cs @@ -10,7 +10,7 @@ public sealed class UnmanagedModuleVersion : ModuleVersion public bool IsUnknownVersion { get; } // HACK: Hardcoding a value of "0" for autodetected DLLs preserves previous behavior. - public UnmanagedModuleVersion(string version) : base(version ?? "0") + public UnmanagedModuleVersion(string? version) : base(version ?? "0") { IsUnknownVersion = version == null; _string = version == null diff --git a/GUI/CKAN-GUI.csproj b/GUI/CKAN-GUI.csproj index ff39ae5f99..aed179e0ad 100644 --- a/GUI/CKAN-GUI.csproj +++ b/GUI/CKAN-GUI.csproj @@ -17,15 +17,18 @@ true Debug;Release false - 7.3 - net48;net7.0-windows + 9 + enable + true + CS0618 + IDE1006,NU1701 + net48;net8.0-windows $(TargetFramework.Replace("-windows", "")) - true + true true 512 prompt 4 - IDE1006 PrepareResources;$(CompileDependsOn) @@ -33,6 +36,8 @@ + + @@ -44,7 +49,7 @@ - + diff --git a/GUI/Controls/Changeset.cs b/GUI/Controls/Changeset.cs index d8ed71d250..d7b7e01cd9 100644 --- a/GUI/Controls/Changeset.cs +++ b/GUI/Controls/Changeset.cs @@ -10,6 +10,7 @@ #endif using CKAN.Extensions; +using CKAN.Games; namespace CKAN.GUI { @@ -25,12 +26,12 @@ public Changeset() ChangesGrid.GetType() .GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic) - .SetValue(ChangesGrid, true, null); + ?.SetValue(ChangesGrid, true, null); } - public void LoadChangeset(List changes, - List AlertLabels, - Dictionary conflicts) + public void LoadChangeset(List changes, + List AlertLabels, + Dictionary? conflicts) { changeset = changes; ConfirmChangesButton.Enabled = conflicts == null || !conflicts.Any(); @@ -42,13 +43,13 @@ public void LoadChangeset(List changes, ?? new List()); } - public CkanModule SelectedItem => SelectedRow?.Change.Mod; + public CkanModule? SelectedItem => SelectedRow?.Change.Mod; - public event Action OnSelectedItemsChanged; - public event Action OnRemoveItem; + public event Action? OnSelectedItemsChanged; + public event Action? OnRemoveItem; - public event Action> OnConfirmChanges; - public event Action OnCancelChanges; + public event Action?>? OnConfirmChanges; + public event Action? OnCancelChanges; private void ChangesGrid_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e) { @@ -80,28 +81,28 @@ private void ChangesGrid_CellClick(object sender, DataGridViewCellEventArgs e) && row.ConfirmUncheck()) { (ChangesGrid.DataSource as BindingList)?.Remove(row); - changeset.Remove(row.Change); + changeset?.Remove(row.Change); OnRemoveItem?.Invoke(row.Change); } } - private void ConfirmChangesButton_Click(object sender, EventArgs e) + private void ConfirmChangesButton_Click(object? sender, EventArgs? e) { OnConfirmChanges?.Invoke(changeset); } - private void CancelChangesButton_Click(object sender, EventArgs e) + private void CancelChangesButton_Click(object? sender, EventArgs? e) { changeset = null; OnCancelChanges?.Invoke(true); } - private void BackButton_Click(object sender, EventArgs e) + private void BackButton_Click(object? sender, EventArgs? e) { OnCancelChanges?.Invoke(false); } - private void ChangesGrid_SelectionChanged(object sender, EventArgs e) + private void ChangesGrid_SelectionChanged(object? sender, EventArgs? e) { if (ChangesGrid.SelectedRows.Count > 0 && !Visible) { @@ -122,13 +123,13 @@ private void ChangesGrid_SelectionChanged(object sender, EventArgs e) GUIModChangeType.Replace, }; - private ChangesetRow SelectedRow + private ChangesetRow? SelectedRow => ChangesGrid.SelectedRows .OfType() .FirstOrDefault() ?.DataBoundItem as ChangesetRow; - private List changeset; + private List? changeset; } #if NET5_0_OR_GREATER @@ -136,14 +137,15 @@ private ChangesetRow SelectedRow #endif public class ChangesetRow { - public ChangesetRow(ModChange change, - List alertLabels, - Dictionary conflicts) + public ChangesetRow(ModChange change, + List alertLabels, + Dictionary? conflicts) { - Change = change; - WarningLabel = alertLabels?.FirstOrDefault(l => - l.ContainsModule(Main.Instance.CurrentInstance.game, - Change.Mod.identifier)); + Change = change; + if (Main.Instance?.CurrentInstance?.game is IGame game) + { + WarningLabel = alertLabels?.FirstOrDefault(l => l.ContainsModule(game, Change.Mod.identifier)); + } conflicts?.TryGetValue(Change.Mod, out Conflict); Reasons = Conflict != null ? string.Format("{0} ({1})", Conflict, Change.Description) @@ -154,26 +156,28 @@ public ChangesetRow(ModChange change, : Change.Description; } - public readonly ModChange Change; - public readonly ModuleLabel WarningLabel = null; - public readonly string Conflict = null; + public readonly ModChange Change; + public readonly ModuleLabel? WarningLabel = null; + public readonly string? Conflict = null; - public string Mod => Change.NameAndStatus; + public string Mod => Change.NameAndStatus ?? ""; public string ChangeType => Change.ChangeType.Localize(); public string Reasons { get; private set; } - public Bitmap DeleteImage => Change.IsRemovable ? EmbeddedImages.textClear + public Bitmap DeleteImage => Change.IsRemovable ? EmbeddedImages.textClear ?? EmptyBitmap : EmptyBitmap; public bool ConfirmUncheck() => Change.IsAutoRemoval - ? Main.Instance.YesNoDialog( + ? Main.Instance?.YesNoDialog( string.Format(Properties.Resources.ChangesetConfirmRemoveAutoRemoval, Change.Mod), Properties.Resources.ChangesetConfirmRemoveAutoRemovalYes, Properties.Resources.ChangesetConfirmRemoveAutoRemovalNo) - : Main.Instance.YesNoDialog( + ?? true + : Main.Instance?.YesNoDialog( string.Format(Properties.Resources.ChangesetConfirmRemoveUserRequested, ChangeType, Change.Mod), Properties.Resources.ChangesetConfirmRemoveUserRequestedYes, - Properties.Resources.ChangesetConfirmRemoveUserRequestedNo); + Properties.Resources.ChangesetConfirmRemoveUserRequestedNo) + ?? true; private static readonly Bitmap EmptyBitmap = new Bitmap(1, 1); } diff --git a/GUI/Controls/ChooseProvidedMods.cs b/GUI/Controls/ChooseProvidedMods.cs index 8355f8c2c2..c269a6a19c 100644 --- a/GUI/Controls/ChooseProvidedMods.cs +++ b/GUI/Controls/ChooseProvidedMods.cs @@ -55,17 +55,17 @@ protected override void OnResize(EventArgs e) } [ForbidGUICalls] - public CkanModule Wait() + public CkanModule? Wait() { - task = new TaskCompletionSource(); + task = new TaskCompletionSource(); return task.Task.Result; } public ListView.SelectedListViewItemCollection SelectedItems => ChooseProvidedModsListView.SelectedItems; - public event Action OnSelectedItemsChanged; + public event Action? OnSelectedItemsChanged; - private void ChooseProvidedModsListView_SelectedIndexChanged(object sender, EventArgs e) + private void ChooseProvidedModsListView_SelectedIndexChanged(object? sender, EventArgs? e) { OnSelectedItemsChanged?.Invoke(ChooseProvidedModsListView.SelectedItems); } @@ -85,18 +85,20 @@ private void ChooseProvidedModsListView_ItemChecked(object sender, ItemCheckedEv } } - private void ChooseProvidedModsCancelButton_Click(object sender, EventArgs e) + private void ChooseProvidedModsCancelButton_Click(object? sender, EventArgs? e) { - task.SetResult(null); + task?.SetResult(null); } - private void ChooseProvidedModsContinueButton_Click(object sender, EventArgs e) + private void ChooseProvidedModsContinueButton_Click(object? sender, EventArgs? e) { - task.SetResult(ChooseProvidedModsListView.CheckedItems.Cast() - .Select(item => item?.Tag as CkanModule) - .FirstOrDefault()); + task?.SetResult( + ChooseProvidedModsListView.CheckedItems + .OfType() + .Select(item => item?.Tag as CkanModule) + .FirstOrDefault()); } - private TaskCompletionSource task; + private TaskCompletionSource? task; } } diff --git a/GUI/Controls/ChooseRecommendedMods.cs b/GUI/Controls/ChooseRecommendedMods.cs index 51f8770de2..cdc609690d 100644 --- a/GUI/Controls/ChooseRecommendedMods.cs +++ b/GUI/Controls/ChooseRecommendedMods.cs @@ -32,7 +32,7 @@ public void LoadRecommendations(IRegistryQuerier registry, NetModuleCache cache, IGame game, List labels, - GUIConfiguration config, + GUIConfiguration? config, Dictionary>> recommendations, Dictionary> suggestions, Dictionary> supporters) @@ -44,7 +44,7 @@ public void LoadRecommendations(IRegistryQuerier registry, this.config = config; Util.Invoke(this, () => { - AlwaysUncheckAllButton.Checked = config.SuppressRecommendations; + AlwaysUncheckAllButton.Checked = config?.SuppressRecommendations ?? false; RecommendedModsListView.BeginUpdate(); RecommendedModsListView.ItemChecked -= RecommendedModsListView_ItemChecked; RecommendedModsListView.Items.AddRange( @@ -64,23 +64,23 @@ public void LoadRecommendations(IRegistryQuerier registry, } [ForbidGUICalls] - public HashSet Wait() + public HashSet? Wait() { if (Platform.IsMono) { // Workaround: make sure the ListView headers are drawn Util.Invoke(this, () => RecommendedModsListView.EndUpdate()); } - task = new TaskCompletionSource>(); + task = new TaskCompletionSource?>(); return task.Task.Result; } public ListView.SelectedListViewItemCollection SelectedItems => RecommendedModsListView.SelectedItems; - public event Action OnSelectedItemsChanged; + public event Action? OnSelectedItemsChanged; - public event Action OnConflictFound; + public event Action? OnConflictFound; protected override void OnResize(EventArgs e) { @@ -88,7 +88,7 @@ protected override void OnResize(EventArgs e) RecommendedModsListView_ColumnWidthChanged(null, null); } - private void RecommendedModsListView_ColumnWidthChanged(object sender, ColumnWidthChangedEventArgs args) + private void RecommendedModsListView_ColumnWidthChanged(object? sender, ColumnWidthChangedEventArgs? args) { if (args?.ColumnIndex != DescriptionHeader.Index) { @@ -108,17 +108,17 @@ private void RecommendedModsListView_ColumnWidthChanged(object sender, ColumnWid } } - private void RecommendedModsListView_SelectedIndexChanged(object sender, EventArgs e) + private void RecommendedModsListView_SelectedIndexChanged(object? sender, EventArgs? e) { OnSelectedItemsChanged?.Invoke(RecommendedModsListView.SelectedItems); } - private void RecommendedModsListView_ItemChecked(object sender, ItemCheckedEventArgs e) + private void RecommendedModsListView_ItemChecked(object? sender, ItemCheckedEventArgs? e) { - var module = e.Item.Tag as CkanModule; + var module = e?.Item.Tag as CkanModule; if (module?.IsDLC ?? false) { - if (e.Item.Checked) + if (e != null && e.Item.Checked) { e.Item.Checked = false; } @@ -132,42 +132,46 @@ private void RecommendedModsListView_ItemChecked(object sender, ItemCheckedEvent private void MarkConflicts() { - try + if (registry != null && versionCrit != null) { - var resolver = new RelationshipResolver( - RecommendedModsListView.CheckedItems - .Cast() - .Select(item => item.Tag as CkanModule) - .Concat(toInstall) - .Distinct(), - toUninstall, - RelationshipResolverOptions.ConflictsOpts(), registry, versionCrit); - var conflicts = resolver.ConflictList; - foreach (var item in RecommendedModsListView.Items.Cast() - // Apparently ListView handes AddRange by: - // 1. Expanding the Items list to the new size by filling it with nulls - // 2. One by one, replace each null with a real item and call _ItemChecked - // ... so the Items list can contain null!! - .Where(it => it != null)) + try { - item.BackColor = conflicts.ContainsKey(item.Tag as CkanModule) - ? Color.LightCoral - : Color.Empty; + var resolver = new RelationshipResolver( + RecommendedModsListView.CheckedItems + .OfType() + .Select(item => item.Tag as CkanModule) + .OfType() + .Concat(toInstall) + .Distinct(), + toUninstall, + RelationshipResolverOptions.ConflictsOpts(), registry, versionCrit); + var conflicts = resolver.ConflictList; + foreach (var item in RecommendedModsListView.Items.Cast() + // Apparently ListView handes AddRange by: + // 1. Expanding the Items list to the new size by filling it with nulls + // 2. One by one, replace each null with a real item and call _ItemChecked + // ... so the Items list can contain null!! + .OfType()) + { + item.BackColor = item.Tag is CkanModule m && conflicts.ContainsKey(m) + ? Color.LightCoral + : Color.Empty; + } + RecommendedModsContinueButton.Enabled = !conflicts.Any(); + OnConflictFound?.Invoke(string.Join("; ", resolver.ConflictDescriptions)); } - RecommendedModsContinueButton.Enabled = !conflicts.Any(); - OnConflictFound?.Invoke(string.Join("; ", resolver.ConflictDescriptions)); - } - catch (DependencyNotSatisfiedKraken k) - { - var row = RecommendedModsListView.Items - .Cast() - .FirstOrDefault(it => (it?.Tag as CkanModule) == k.parent); - if (row != null) + catch (DependencyNotSatisfiedKraken k) { - row.BackColor = Color.LightCoral; + var row = RecommendedModsListView.Items + .Cast() + .FirstOrDefault(it => (it?.Tag as CkanModule) == k.parent); + if (row != null) + { + row.BackColor = Color.LightCoral; + } + RecommendedModsContinueButton.Enabled = false; + OnConflictFound?.Invoke(k.Message); } - RecommendedModsContinueButton.Enabled = false; - OnConflictFound?.Invoke(k.Message); } } @@ -182,7 +186,7 @@ private IEnumerable getRecSugRows( kvp.Key, string.Join(", ", kvp.Value.Item2), RecommendationsGroup, - !config.SuppressRecommendations + (!config?.SuppressRecommendations ?? true) && kvp.Value.Item1 && !uncheckLabels.Any(mlbl => mlbl.ContainsModule(game, kvp.Key.identifier)))) @@ -217,14 +221,14 @@ private ListViewItem getRecSugItem(NetModuleCache cache, Group = group }; - private void UncheckAllButton_Click(object sender, EventArgs e) + private void UncheckAllButton_Click(object? sender, EventArgs? e) { CheckUncheckRows(RecommendedModsListView.Items, false); } - private void AlwaysUncheckAllButton_CheckedChanged(object sender, EventArgs e) + private void AlwaysUncheckAllButton_CheckedChanged(object? sender, EventArgs? e) { - if (config.SuppressRecommendations != AlwaysUncheckAllButton.Checked) + if (config != null && config.SuppressRecommendations != AlwaysUncheckAllButton.Checked) { config.SuppressRecommendations = AlwaysUncheckAllButton.Checked; config.Save(); @@ -235,12 +239,12 @@ private void AlwaysUncheckAllButton_CheckedChanged(object sender, EventArgs e) } } - private void CheckAllButton_Click(object sender, EventArgs e) + private void CheckAllButton_Click(object? sender, EventArgs? e) { CheckUncheckRows(RecommendedModsListView.Items, true); } - private void CheckRecommendationsButton_Click(object sender, EventArgs e) + private void CheckRecommendationsButton_Click(object? sender, EventArgs? e) { CheckUncheckRows(RecommendationsGroup.Items, true); } @@ -273,29 +277,30 @@ private void EnableDisableButtons() .Any(lvi => !lvi.Checked); } - private void RecommendedModsCancelButton_Click(object sender, EventArgs e) + private void RecommendedModsCancelButton_Click(object? sender, EventArgs? e) { task?.SetResult(null); RecommendedModsListView.Items.Clear(); RecommendedModsListView.ItemChecked -= RecommendedModsListView_ItemChecked; } - private void RecommendedModsContinueButton_Click(object sender, EventArgs e) + private void RecommendedModsContinueButton_Click(object? sender, EventArgs? e) { task?.SetResult(RecommendedModsListView.CheckedItems - .Cast() + .OfType() .Select(item => item.Tag as CkanModule) + .OfType() .ToHashSet()); RecommendedModsListView.Items.Clear(); RecommendedModsListView.ItemChecked -= RecommendedModsListView_ItemChecked; } - private IRegistryQuerier registry; - private List toInstall; - private HashSet toUninstall; - private GameVersionCriteria versionCrit; - private GUIConfiguration config; + private IRegistryQuerier? registry; + private List toInstall = new List(); + private HashSet toUninstall = new HashSet(); + private GameVersionCriteria? versionCrit; + private GUIConfiguration? config; - private TaskCompletionSource> task; + private TaskCompletionSource?>? task; } } diff --git a/GUI/Controls/DeleteDirectories.cs b/GUI/Controls/DeleteDirectories.cs index da447d008d..43e4bb7abe 100644 --- a/GUI/Controls/DeleteDirectories.cs +++ b/GUI/Controls/DeleteDirectories.cs @@ -7,6 +7,7 @@ #if NET5_0_OR_GREATER using System.Runtime.Versioning; #endif +using System.Diagnostics.CodeAnalysis; using CKAN.GUI.Attributes; @@ -59,7 +60,7 @@ public void LoadDirs(GameInstance ksp, HashSet possibleConfigOnlyDirs) /// true if user chose to delete, false otherwise /// [ForbidGUICalls] - public bool Wait(out HashSet toDelete) + public bool Wait([NotNullWhen(true)] out HashSet? toDelete) { if (Platform.IsMono) { @@ -76,10 +77,11 @@ public bool Wait(out HashSet toDelete) // This will block until one of the buttons calls SetResult if (task.Task.Result) { - toDelete = DirectoriesListView.CheckedItems.Cast() - .Select(lvi => lvi.Tag as string) - .Where(s => !string.IsNullOrEmpty(s)) - .ToHashSet(); + toDelete = DirectoriesListView.CheckedItems + .OfType() + .Select(lvi => lvi.Tag as string) + .OfType() + .ToHashSet(); return true; } else @@ -103,16 +105,17 @@ protected override void OnHelpRequested(HelpEventArgs evt) evt.Handled = Util.TryOpenWebPage(HelpURLs.DeleteDirectories); } - private void DirectoriesListView_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e) + private void DirectoriesListView_ItemSelectionChanged(object? sender, ListViewItemSelectionChangedEventArgs? e) { ContentsListView.Items.Clear(); ContentsListView.Items.AddRange( DirectoriesListView.SelectedItems.Cast() - .SelectMany(lvi => Directory.EnumerateFileSystemEntries( - lvi.Tag as string, - "*", - SearchOption.AllDirectories) - .Select(f => new ListViewItem(Platform.FormatPath(CKANPathUtils.ToRelative(f, lvi.Tag as string))))) + .SelectMany(lvi => lvi.Tag is string s + ? Directory.EnumerateFileSystemEntries(s, + "*", + SearchOption.AllDirectories) + .Select(f => new ListViewItem(Platform.FormatPath(CKANPathUtils.ToRelative(f, s)))) + : Enumerable.Empty()) .ToArray()); if (DirectoriesListView.SelectedItems.Count == 0) { @@ -125,25 +128,28 @@ private void DirectoriesListView_ItemSelectionChanged(object sender, ListViewIte OpenDirectoryButton.Enabled = DirectoriesListView.SelectedItems.Count > 0; } - private void OpenDirectoryButton_Click(object sender, EventArgs e) + private void OpenDirectoryButton_Click(object? sender, EventArgs? e) { - foreach (ListViewItem lvi in DirectoriesListView.SelectedItems) + foreach (var url in DirectoriesListView.SelectedItems + .OfType() + .Select(lvi => lvi.Tag) + .OfType()) { - Utilities.ProcessStartURL(lvi.Tag as string); + Utilities.ProcessStartURL(url); } } - private void DeleteButton_Click(object sender, EventArgs e) + private void DeleteButton_Click(object? sender, EventArgs? e) { task?.SetResult(true); } - private void KeepAllButton_Click(object sender, EventArgs e) + private void KeepAllButton_Click(object? sender, EventArgs? e) { task?.SetResult(false); } - private TaskCompletionSource task = null; - private GameInstance instance = null; + private TaskCompletionSource? task = null; + private GameInstance? instance = null; } } diff --git a/GUI/Controls/DropdownMenuButton.cs b/GUI/Controls/DropdownMenuButton.cs index 9cacd77782..314ea39fd8 100644 --- a/GUI/Controls/DropdownMenuButton.cs +++ b/GUI/Controls/DropdownMenuButton.cs @@ -26,7 +26,7 @@ public class DropdownMenuButton : Button Browsable(true), DesignerSerializationVisibility(DesignerSerializationVisibility.Visible) ] - public ContextMenuStrip Menu { get; set; } + public ContextMenuStrip? Menu { get; set; } /// /// Draw the triangle on the button diff --git a/GUI/Controls/EditModSearch.cs b/GUI/Controls/EditModSearch.cs index a59d36f063..ed8efa97cc 100644 --- a/GUI/Controls/EditModSearch.cs +++ b/GUI/Controls/EditModSearch.cs @@ -61,16 +61,16 @@ public void ExpandCollapse() /// /// Event fired when a search needs to be executed. /// - public event Action ApplySearch; + public event Action? ApplySearch; /// /// Event fired when user wants to switch focus away from this control. /// - public event Action SurrenderFocus; + public event Action? SurrenderFocus; - public event Action ShowError; + public event Action? ShowError; - public ModSearch Search + public ModSearch? Search { get => currentSearch; set @@ -113,10 +113,10 @@ protected override void OnResize(EventArgs e) FormGeometryChanged(); } - private bool suppressSearch = false; - private ModSearch currentSearch = null; + private bool suppressSearch = false; + private ModSearch? currentSearch = null; - private void ImmediateHandler(object sender, EventArgs e) + private void ImmediateHandler(object? sender, EventArgs? e) { try { @@ -136,9 +136,14 @@ private void ImmediateHandler(object sender, EventArgs e) } break; } - // Sync the search boxes immediately - currentSearch = ModSearch.Parse(FilterCombinedTextBox.Text, - Main.Instance.ManageMods.mainModList.ModuleLabels.LabelsFor(Main.Instance.CurrentInstance.Name).ToList()); + if (Main.Instance?.CurrentInstance != null) + { + // Sync the search boxes immediately + currentSearch = ModSearch.Parse( + FilterCombinedTextBox.Text, + ModuleLabelList.ModuleLabels.LabelsFor(Main.Instance.CurrentInstance.Name) + .ToList()); + } SearchToEditor(); } catch (Kraken k) @@ -147,7 +152,7 @@ private void ImmediateHandler(object sender, EventArgs e) } } - private bool SkipDelayIf(object sender, EventArgs e) + private bool SkipDelayIf(object? sender, EventArgs? e) { if (e == null) { @@ -176,9 +181,9 @@ private bool SkipDelayIf(object sender, EventArgs e) || (currentSearch?.Name.Length ?? 0) > 4; } - private bool AbortIf(object sender, EventArgs e) => suppressSearch; + private bool AbortIf(object? sender, EventArgs? e) => suppressSearch; - private void DelayedHandler(object sender, EventArgs e) + private void DelayedHandler(object? sender, EventArgs? e) { ApplySearch?.Invoke(this, currentSearch); } @@ -191,7 +196,7 @@ private void DelayedHandler(object sender, EventArgs e) /// private readonly EventHandler handler; - private void ExpandButton_CheckedChanged(object sender, EventArgs e) + private void ExpandButton_CheckedChanged(object? sender, EventArgs? e) { ExpandButton.Text = ExpandButton.Checked ? "▴" : "▾"; DoLayout(ExpandButton.Checked); @@ -226,7 +231,7 @@ private void SearchToEditor() suppressSearch = false; } - private void SearchDetails_ApplySearch(object sender, EventArgs e) + private void SearchDetails_ApplySearch(object? sender, EventArgs e) { if (suppressSearch) { @@ -250,7 +255,7 @@ protected override void OnLeave(EventArgs e) ExpandButton.Checked = false; } - private void FilterTextBox_Enter(object sender, EventArgs e) + private void FilterTextBox_Enter(object? sender, EventArgs? e) { (sender as TextBox)?.SelectAll(); } diff --git a/GUI/Controls/EditModSearchDetails.cs b/GUI/Controls/EditModSearchDetails.cs index a7618d4569..42a8e4cc6b 100644 --- a/GUI/Controls/EditModSearchDetails.cs +++ b/GUI/Controls/EditModSearchDetails.cs @@ -32,12 +32,12 @@ public EditModSearchDetails() /// Event fired when a search needs to be executed. /// Upstream source event is passed along. /// - public event EventHandler ApplySearch; + public event Action? ApplySearch; /// /// Event fired when user wants to switch focus away from this control. /// - public event Action SurrenderFocus; + public event Action? SurrenderFocus; public void SetFocus() { @@ -46,12 +46,10 @@ public void SetFocus() public ModSearch CurrentSearch() => CurrentSearch( - Main.Instance.ManageMods.mainModList - .ModuleLabels - .LabelsFor(Main.Instance.CurrentInstance.Name) - .ToList()); + ModuleLabelList.ModuleLabels.LabelsFor(Main.Instance?.CurrentInstance?.Name ?? "") + .ToList()); - private ModSearch CurrentSearch(List knownLabels) + private ModSearch CurrentSearch(List? knownLabels) => new ModSearch( FilterByNameTextBox.Text, FilterByAuthorTextBox.Text.Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries).ToList(), @@ -65,8 +63,8 @@ private ModSearch CurrentSearch(List knownLabels) FilterBySupportsTextBox.Text.Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries).ToList(), FilterByTagsTextBox.Text.Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries).ToList(), FilterByLabelsTextBox.Text.Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries) - .Select(ln => knownLabels.FirstOrDefault(lb => lb.Name == ln)) - .Where(lb => lb != null) + .Select(ln => knownLabels?.FirstOrDefault(lb => lb.Name == ln)) + .OfType() .ToList(), CompatibleToggle.Value, InstalledToggle.Value, @@ -75,7 +73,7 @@ private ModSearch CurrentSearch(List knownLabels) UpgradeableToggle.Value, ReplaceableToggle.Value); - public void PopulateSearch(ModSearch search) + public void PopulateSearch(ModSearch? search) { FilterByNameTextBox.Text = search?.Name ?? ""; @@ -150,12 +148,12 @@ protected override void OnLeave(EventArgs e) SurrenderFocus?.Invoke(); } - private void FilterTextBox_TextChanged(object sender, EventArgs e) + private void FilterTextBox_TextChanged(object? sender, EventArgs e) { ApplySearch?.Invoke(sender, e); } - private void FilterTextBox_KeyDown(object sender, KeyEventArgs e) + private void FilterTextBox_KeyDown(object? sender, KeyEventArgs e) { // Switch focus from filters to mod list on enter, down, or pgdn switch (e.KeyCode) @@ -178,7 +176,7 @@ private void FilterTextBox_KeyDown(object sender, KeyEventArgs e) private void TriStateChanged(bool? val) { - ApplySearch?.Invoke(null, null); + ApplySearch?.Invoke(null, new EventArgs()); } protected override bool ProcessCmdKey(ref Message msg, Keys keyData) diff --git a/GUI/Controls/EditModSearches.cs b/GUI/Controls/EditModSearches.cs index 174a04c4bd..9c70959855 100644 --- a/GUI/Controls/EditModSearches.cs +++ b/GUI/Controls/EditModSearches.cs @@ -24,9 +24,9 @@ public EditModSearches() ActiveControl = AddSearch(); } - public event Action SurrenderFocus; - public event Action> ApplySearches; - public event Action ShowError; + public event Action? SurrenderFocus; + public event Action?>? ApplySearches; + public event Action? ShowError; public void Clear() { @@ -62,7 +62,7 @@ public void SetSearches(List searches) { while (editors.Count > searches.Count && editors.Count > 1) { - RemoveSearch(editors[editors.Count - 1]); + RemoveSearch(editors[^1]); } if (searches.Count < 1) { @@ -95,7 +95,7 @@ public void MergeSearches(List searches) Apply(); } - private void AddSearchButton_Click(object sender, EventArgs e) + private void AddSearchButton_Click(object? sender, EventArgs? e) { AddSearch().Focus(); AddSearchButton.Enabled = false; @@ -128,7 +128,7 @@ private EditModSearch AddSearch() ResumeLayout(false); PerformLayout(); - AddSearchButton.Top = editors[editors.Count - 1].Top; + AddSearchButton.Top = editors[^1].Top; return ctl; } @@ -151,13 +151,13 @@ private void RemoveSearch(EditModSearch which) // Make sure the top label is always visible editors[0].ShowLabel = true; - AddSearchButton.Top = editors[editors.Count - 1].Top; + AddSearchButton.Top = editors[^1].Top; Height = editors.Sum(ems => ems.Height); } } - private void EditModSearch_ApplySearch(EditModSearch source, ModSearch what) + private void EditModSearch_ApplySearch(EditModSearch source, ModSearch? what) { if (what == null && editors.Count > 1) { @@ -169,7 +169,6 @@ private void EditModSearch_ApplySearch(EditModSearch source, ModSearch what) private void Apply() { var searches = editors.Select(ems => ems.Search) - .Where(s => s != null) .ToList(); ApplySearches?.Invoke(searches.Count == 0 ? null : searches); AddSearchButton.Enabled = editors.Count == searches.Count; diff --git a/GUI/Controls/EditModpack.cs b/GUI/Controls/EditModpack.cs index f7be67ae93..7a0a674d50 100644 --- a/GUI/Controls/EditModpack.cs +++ b/GUI/Controls/EditModpack.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -7,6 +6,7 @@ #if NET5_0_OR_GREATER using System.Runtime.Versioning; #endif +using System.Diagnostics.CodeAnalysis; using Autofac; @@ -53,7 +53,8 @@ public void LoadModule(CkanModule module, IRegistryQuerier registry) AbstractTextBox.Text = module.@abstract; AuthorTextBox.Text = string.Join(", ", module.author); VersionTextBox.Text = module.version.ToString(); - var options = new string[] { "" }.Concat(Main.Instance.CurrentInstance.game.KnownVersions + var options = new string[] { "" }.Concat( + Main.Instance?.CurrentInstance?.game.KnownVersions .SelectMany(v => new GameVersion[] { new GameVersion(v.Major, v.Minor, v.Patch), new GameVersion(v.Major, v.Minor) @@ -61,18 +62,18 @@ public void LoadModule(CkanModule module, IRegistryQuerier registry) .Distinct() .OrderByDescending(v => v) .Select(v => v.ToString()) - ); + ?? Enumerable.Empty()); GameVersionMinComboBox.DataSource = options.ToArray(); GameVersionMinComboBox.Text = (module.ksp_version_min ?? module.ksp_version)?.ToString(); GameVersionMaxComboBox.DataSource = options.ToArray(); GameVersionMaxComboBox.Text = (module.ksp_version_max ?? module.ksp_version)?.ToString(); LicenseComboBox.DataSource = License.valid_licenses.OrderBy(l => l).ToArray(); LicenseComboBox.Text = module.license?.FirstOrDefault()?.ToString(); - LoadRelationships(registry); + LoadRelationships(module, registry); }); } - public event Action OnSelectedItemsChanged; + public event Action? OnSelectedItemsChanged; [ForbidGUICalls] public bool Wait(IUser user) @@ -87,20 +88,11 @@ public bool Wait(IUser user) return task.Task.Result; } - private void LoadRelationships(IRegistryQuerier registry) + private void LoadRelationships(CkanModule module, IRegistryQuerier registry) { - if (module.depends == null) - { - module.depends = new List(); - } - if (module.recommends == null) - { - module.recommends = new List(); - } - if (module.suggests == null) - { - module.suggests = new List(); - } + module.depends ??= new List(); + module.recommends ??= new List(); + module.suggests ??= new List(); ignored.Clear(); // Find installed modules that aren't in the module's relationships @@ -141,29 +133,32 @@ protected override void OnHelpRequested(HelpEventArgs evt) evt.Handled = Util.TryOpenWebPage(HelpURLs.ModPacks); } - private void AddGroup(List relationships, ListViewGroup group, IRegistryQuerier registry) + private void AddGroup(List relationships, + ListViewGroup group, + IRegistryQuerier registry) { if (relationships != null) { - RelationshipsListView.Items.AddRange(relationships - .OrderBy(r => (r as ModuleRelationshipDescriptor)?.name) - .Select(r => new ListViewItem(new string[] - { - (r as ModuleRelationshipDescriptor)?.name, - (r as ModuleRelationshipDescriptor)?.version?.ToString(), - registry.InstalledModules.First( - im => im.identifier == (r as ModuleRelationshipDescriptor)?.name - )?.Module.@abstract - }) - { - Tag = r, - Group = group, - }) - .ToArray()); + RelationshipsListView.Items.AddRange( + relationships.OfType() + .OrderBy(r => r.name) + .Select(r => new ListViewItem(new string[] + { + r.name, + r.version?.ToString() ?? "", + registry.InstalledModules.FirstOrDefault(im => im.identifier == r.name)?.Module.@abstract ?? "" + }) + { + Tag = r, + Group = group, + }) + .OfType() + .ToArray()); } } - private bool TryFieldsToModule(out string error, out Control badField) + private bool TryFieldsToModule([NotNullWhen(false)] out string? error, + [NotNullWhen(false)] out Control? badField) { if (!Identifier.ValidIdentifierPattern.IsMatch(IdentifierTextBox.Text)) { @@ -193,25 +188,28 @@ private bool TryFieldsToModule(out string error, out Control badField) error = null; badField = null; - module.identifier = IdentifierTextBox.Text; - module.name = NameTextBox.Text; - module.@abstract = AbstractTextBox.Text; - module.author = AuthorTextBox.Text - .Split(',').Select(a => a.Trim()).ToList(); - module.version = new ModuleVersion(VersionTextBox.Text); - module.license = new List() { new License(LicenseComboBox.Text) }; - module.ksp_version_min = string.IsNullOrEmpty(GameVersionMinComboBox.Text) - ? null - : GameVersion.Parse(GameVersionMinComboBox.Text); - module.ksp_version_max = string.IsNullOrEmpty(GameVersionMaxComboBox.Text) - ? null - : GameVersion.Parse(GameVersionMaxComboBox.Text); + if (module != null) + { + module.identifier = IdentifierTextBox.Text; + module.name = NameTextBox.Text; + module.@abstract = AbstractTextBox.Text; + module.author = AuthorTextBox.Text + .Split(',').Select(a => a.Trim()).ToList(); + module.version = new ModuleVersion(VersionTextBox.Text); + module.license = new List() { new License(LicenseComboBox.Text) }; + module.ksp_version_min = string.IsNullOrEmpty(GameVersionMinComboBox.Text) + ? null + : GameVersion.Parse(GameVersionMinComboBox.Text); + module.ksp_version_max = string.IsNullOrEmpty(GameVersionMaxComboBox.Text) + ? null + : GameVersion.Parse(GameVersionMaxComboBox.Text); + } return true; } - private void RelationshipsListView_KeyDown(object sender, KeyEventArgs e) + private void RelationshipsListView_KeyDown(object? sender, KeyEventArgs? e) { - switch (e.KeyCode) + switch (e?.KeyCode) { // Select all on ctrl-A case Keys.A: @@ -234,11 +232,12 @@ private void RelationshipsListView_KeyDown(object sender, KeyEventArgs e) } } - private void RelationshipsListView_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e) + private void RelationshipsListView_ItemSelectionChanged(object? sender, ListViewItemSelectionChangedEventArgs? e) { OnSelectedItemsChanged?.Invoke(RelationshipsListView.SelectedItems); var kinds = RelationshipsListView.SelectedItems.Cast() .Select(lvi => lvi.Group) + .OfType() .Distinct() .ToList(); if (kinds.Count == 1) @@ -274,72 +273,84 @@ private void RelationshipsListView_ItemSelectionChanged(object sender, ListViewI } } - private void DependsRadioButton_CheckedChanged(object sender, EventArgs e) + private void DependsRadioButton_CheckedChanged(object? sender, EventArgs? e) { - MoveItemsTo(RelationshipsListView.SelectedItems.Cast(), DependsGroup, module.depends); - RelationshipsListView.Focus(); + if (module?.depends != null) + { + MoveItemsTo(RelationshipsListView.SelectedItems.OfType(), DependsGroup, module.depends); + RelationshipsListView.Focus(); + } } - private void RecommendsRadioButton_CheckedChanged(object sender, EventArgs e) + private void RecommendsRadioButton_CheckedChanged(object? sender, EventArgs? e) { - MoveItemsTo(RelationshipsListView.SelectedItems.Cast(), RecommendationsGroup, module.recommends); - RelationshipsListView.Focus(); + if (module?.recommends != null) + { + MoveItemsTo(RelationshipsListView.SelectedItems.OfType(), RecommendationsGroup, module.recommends); + RelationshipsListView.Focus(); + } } - private void SuggestsRadioButton_CheckedChanged(object sender, EventArgs e) + private void SuggestsRadioButton_CheckedChanged(object? sender, EventArgs? e) { - MoveItemsTo(RelationshipsListView.SelectedItems.Cast(), SuggestionsGroup, module.suggests); - RelationshipsListView.Focus(); + if (module?.suggests != null) + { + MoveItemsTo(RelationshipsListView.SelectedItems.OfType(), SuggestionsGroup, module.suggests); + RelationshipsListView.Focus(); + } } - private void IgnoreRadioButton_CheckedChanged(object sender, EventArgs e) + private void IgnoreRadioButton_CheckedChanged(object? sender, EventArgs? e) { - MoveItemsTo(RelationshipsListView.SelectedItems.Cast(), IgnoredGroup, ignored); + MoveItemsTo(RelationshipsListView.SelectedItems.OfType(), IgnoredGroup, ignored); RelationshipsListView.Focus(); } private void MoveItemsTo(IEnumerable items, ListViewGroup group, List relationships) { - foreach (ListViewItem lvi in items.Where(lvi => lvi.Group != group)) + foreach (ListViewItem lvi in items) { - // UI - var rel = lvi.Tag as RelationshipDescriptor; - var fromRel = GroupToRelationships[lvi.Group]; - fromRel.Remove(rel); - relationships.Add(rel); - // Model - lvi.Group = group; + if (lvi.Tag is RelationshipDescriptor rel + && lvi.Group is ListViewGroup grp) + { + // UI + var fromRel = GroupToRelationships[grp]; + fromRel.Remove(rel); + relationships.Add(rel); + // Model + lvi.Group = group; + } } } - private void CancelExportButton_Click(object sender, EventArgs e) + private void CancelExportButton_Click(object? sender, EventArgs? e) { task?.SetResult(false); } - private void ExportModpackButton_Click(object sender, EventArgs e) + private void ExportModpackButton_Click(object? sender, EventArgs? e) { - if (!TryFieldsToModule(out string error, out Control badField)) + if (!TryFieldsToModule(out string? error, out Control? badField)) { badField.Focus(); - user.RaiseError(error); + user?.RaiseError(error); } - else if (TrySavePrompt(modpackExportOptions, out string filename)) + else if (module != null && TrySavePrompt(modpackExportOptions, out string? filename)) { - if (module.depends.Count == 0) + if (module.depends?.Count == 0) { module.depends = null; } - if (module.recommends.Count == 0) + if (module.recommends?.Count == 0) { module.recommends = null; } - if (module.suggests.Count == 0) + if (module.suggests?.Count == 0) { module.suggests = null; } CkanModule.ToFile(ApplyCheckboxes(module), filename); - OpenFileBrowser(filename); + Utilities.OpenFileBrowser(filename); task?.SetResult(true); } } @@ -360,7 +371,7 @@ private CkanModule ApplyVersionsCheckbox(CkanModule input) // in case the user changes the checkbox after cancelling out of the // save popup. So we create a new CkanModule instead. var newMod = CkanModule.FromJson(CkanModule.ToJson(input)); - foreach (var rels in new List>() + foreach (var rels in new List?>() { newMod.depends, newMod.recommends, @@ -390,29 +401,19 @@ private CkanModule ApplyIncludeOptRelsCheckbox(CkanModule input) else { var newMod = CkanModule.FromJson(CkanModule.ToJson(input)); - foreach (var rel in newMod.depends) + if (newMod.depends != null) { - rel.suppress_recommendations = true; + foreach (var rel in newMod.depends) + { + rel.suppress_recommendations = true; + } } return newMod; } } - private void OpenFileBrowser(string location) - { - if (File.Exists(location)) - { - // We need the folder of the file - // Otherwise the OS would try to open the file in its default application - location = Path.GetDirectoryName(location); - } - if (Directory.Exists(location)) - { - Utilities.ProcessStartURL(location); - } - } - - private bool TrySavePrompt(List exportOptions, out string filename) + private bool TrySavePrompt(List exportOptions, + [NotNullWhen(true)] out string? filename) { var dlg = new SaveFileDialog() { @@ -436,9 +437,9 @@ private bool TrySavePrompt(List exportOptions, out string filename new ExportOption(ExportFileType.Ckan, Properties.Resources.MainModPack, "ckan"), }; - private CkanModule module; - private IUser user; - private TaskCompletionSource task; + private CkanModule? module; + private IUser? user; + private TaskCompletionSource? task; private readonly List ignored = new List(); private readonly Dictionary> GroupToRelationships = new Dictionary>(); diff --git a/GUI/Controls/HintTextBox.cs b/GUI/Controls/HintTextBox.cs index 125d218056..887c5856d1 100644 --- a/GUI/Controls/HintTextBox.cs +++ b/GUI/Controls/HintTextBox.cs @@ -1,6 +1,9 @@ using System; using System.Drawing; using System.Windows.Forms; +#if NET5_0_OR_GREATER +using System.Runtime.Versioning; +#endif namespace CKAN.GUI { @@ -8,6 +11,9 @@ namespace CKAN.GUI /// A textbox which shows a "clear text" icon on the right side /// whenever data is present. /// + #if NET5_0_OR_GREATER + [SupportedOSPlatform("windows")] + #endif public partial class HintTextBox : TextBox { @@ -25,7 +31,7 @@ public HintTextBox() /// /// The control sending the event /// The event arguments - private void HintClearIcon_Click(object sender, EventArgs e) + private void HintClearIcon_Click(object? sender, EventArgs? e) { Text = ""; } @@ -35,7 +41,7 @@ private void HintClearIcon_Click(object sender, EventArgs e) /// /// The control sending the event /// The event arguments - private void HintTextBox_TextChanged(object sender, EventArgs e) + private void HintTextBox_TextChanged(object? sender, EventArgs? e) { ClearIcon.Visible = (TextLength > 0) && !ReadOnly; } @@ -45,7 +51,7 @@ private void HintTextBox_TextChanged(object sender, EventArgs e) /// /// The control sending the event /// The event arguments - private void HintTextBox_SizeChanged(object sender, EventArgs e) + private void HintTextBox_SizeChanged(object? sender, EventArgs? e) { if (ClearIcon.Image != null) { diff --git a/GUI/Controls/InstallationHistory.cs b/GUI/Controls/InstallationHistory.cs index f73ea3fc8d..e89c05b15a 100644 --- a/GUI/Controls/InstallationHistory.cs +++ b/GUI/Controls/InstallationHistory.cs @@ -56,17 +56,17 @@ public void LoadHistory(GameInstance inst, GUIConfiguration config, RepositoryDa /// /// Invoked when the user selects a module /// - public event Action OnSelectedModuleChanged; + public event Action? OnSelectedModuleChanged; /// /// Invoked when the user clicks the Install toolbar button /// - public event Action Install; + public event Action? Install; /// /// Invoked when the user clicks OK /// - public event Action Done; + public event Action? Done; /// /// Open the user guide when the user presses F1 @@ -76,7 +76,7 @@ protected override void OnHelpRequested(HelpEventArgs evt) evt.Handled = Util.TryOpenWebPage(HelpURLs.InstallationHistory); } - private void HistoryListView_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e) + private void HistoryListView_ItemSelectionChanged(object? sender, ListViewItemSelectionChangedEventArgs? e) { UseWaitCursor = true; Task.Factory.StartNew(() => @@ -84,15 +84,17 @@ private void HistoryListView_ItemSelectionChanged(object sender, ListViewItemSel try { var path = HistoryListView.SelectedItems - .Cast() + .OfType() .Select(lvi => lvi.Tag as FileInfo) + .OfType() .First(); var modRows = CkanModule.FromFile(path.FullName) .depends - .OfType() - .Select(ItemFromRelationship) - .Where(row => row != null) - .ToArray(); + ?.OfType() + .Select(ItemFromRelationship) + .OfType() + .ToArray() + ?? Array.Empty(); Util.Invoke(this, () => { ModsListView.BeginUpdate(); @@ -140,8 +142,12 @@ private void HistoryListView_ItemSelectionChanged(object sender, ListViewItemSel }); } - private ListViewItem ItemFromRelationship(ModuleRelationshipDescriptor rel) + private ListViewItem? ItemFromRelationship(ModuleRelationshipDescriptor rel) { + if (registry == null || config == null || rel.version == null) + { + return null; + } var mod = registry.GetModuleByVersion(rel.name, rel.version) ?? SaneLatestAvail(rel.name); return mod == null @@ -177,17 +183,17 @@ private ListViewItem ItemFromRelationship(ModuleRelationshipDescriptor rel) } // Registry.LatestAvailable without exceptions - private CkanModule SaneLatestAvail(string identifier) + private CkanModule? SaneLatestAvail(string identifier) { try { - return registry.LatestAvailable(identifier, inst.VersionCriteria()); + return registry?.LatestAvailable(identifier, inst?.VersionCriteria()); } catch { try { - return registry.LatestAvailable(identifier, null); + return registry?.LatestAvailable(identifier, null); } catch { @@ -196,7 +202,7 @@ private CkanModule SaneLatestAvail(string identifier) } } - private void ModsListView_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e) + private void ModsListView_ItemSelectionChanged(object? sender, ListViewItemSelectionChangedEventArgs? e) { var mod = ModsListView.SelectedItems .Cast() @@ -208,7 +214,7 @@ private void ModsListView_ItemSelectionChanged(object sender, ListViewItemSelect } } - private void InstallButton_Click(object sender, EventArgs e) + private void InstallButton_Click(object? sender, EventArgs? e) { Install?.Invoke(ModsListView.Items .Cast() @@ -218,13 +224,13 @@ private void InstallButton_Click(object sender, EventArgs e) .ToArray()); } - private void OKButton_Click(object sender, EventArgs e) + private void OKButton_Click(object? sender, EventArgs? e) { Done?.Invoke(); } - private GameInstance inst; - private Registry registry; - private GUIConfiguration config; + private GameInstance? inst; + private Registry? registry; + private GUIConfiguration? config; } } diff --git a/GUI/Controls/LeftRightRowPanel.cs b/GUI/Controls/LeftRightRowPanel.cs index 297f24086c..11cc969ba6 100644 --- a/GUI/Controls/LeftRightRowPanel.cs +++ b/GUI/Controls/LeftRightRowPanel.cs @@ -1,5 +1,8 @@ using System.Windows.Forms; using System.Drawing; +#if NET5_0_OR_GREATER +using System.Runtime.Versioning; +#endif namespace CKAN.GUI { @@ -8,6 +11,9 @@ namespace CKAN.GUI /// one on the right side and one on the left. /// Intended to allow autosizing of Buttons. /// + #if NET5_0_OR_GREATER + [SupportedOSPlatform("windows")] + #endif public class LeftRightRowPanel : TableLayoutPanel { /// diff --git a/GUI/Controls/ManageMods.cs b/GUI/Controls/ManageMods.cs index 38e6b3b4ca..7b6481ba85 100644 --- a/GUI/Controls/ManageMods.cs +++ b/GUI/Controls/ManageMods.cs @@ -12,7 +12,6 @@ using log4net; using CKAN.Extensions; -using CKAN.Versioning; using CKAN.GUI.Attributes; namespace CKAN.GUI @@ -69,28 +68,33 @@ public ManageMods() private static readonly ILog log = LogManager.GetLogger(typeof(ManageMods)); private readonly RepositoryDataManager repoData; private DateTime lastSearchTime; - private string lastSearchKey; + private string? lastSearchKey; private readonly NavigationHistory navHistory; private static readonly Font uninstallingFont = new Font(SystemFonts.DefaultFont, FontStyle.Strikeout); - private List currentChangeSet; - private Dictionary conflicts; + private List? currentChangeSet; + private Dictionary? conflicts; private bool freezeChangeSet = false; - public event Action RaiseMessage; - public event Action RaiseError; - public event Action SetStatusBar; - public event Action ClearStatusBar; - public event Action LaunchGame; - public event Action EditCommandLines; + public event Action? RaiseMessage; + public event Action? RaiseError; + public event Action? SetStatusBar; + public event Action? ClearStatusBar; + public event Action? LaunchGame; + public event Action? EditCommandLines; public readonly ModList mainModList; private List SortColumns { get { + if (guiConfig == null) + { + return new List(); + } // Make sure we don't return any column the GUI doesn't know about. - var unknownCols = guiConfig.SortColumns.Where(col => !ModGrid.Columns.Contains(col)).ToList(); + var unknownCols = guiConfig.SortColumns.Where(col => !ModGrid.Columns.Contains(col)) + .ToList(); foreach (var unknownCol in unknownCols) { int index = guiConfig.SortColumns.IndexOf(unknownCol); @@ -101,26 +105,26 @@ private List SortColumns } } - private GUIConfiguration guiConfig => Main.Instance.configuration; - private GameInstance currentInstance => Main.Instance.CurrentInstance; - private GameInstanceManager manager => Main.Instance.Manager; - private IUser user => Main.Instance.currentUser; + private GUIConfiguration? guiConfig => Main.Instance?.configuration; + private GameInstance? currentInstance => Main.Instance?.CurrentInstance; + private GameInstanceManager? manager => Main.Instance?.Manager; + private IUser? user => Main.Instance?.currentUser; - private List descending => guiConfig.MultiSortDescending; + private List descending => guiConfig?.MultiSortDescending ?? new List(); - public event Action OnSelectedModuleChanged; - public event Action, Dictionary> OnChangeSetChanged; - public event Action OnRegistryChanged; + public event Action? OnSelectedModuleChanged; + public event Action?, Dictionary?>? OnChangeSetChanged; + public event Action? OnRegistryChanged; - public event Action, Dictionary> StartChangeSet; - public event Action> LabelsAfterUpdate; + public event Action?, Dictionary?>? StartChangeSet; + public event Action>? LabelsAfterUpdate; private void EditModSearches_ShowError(string error) { RaiseError?.Invoke(error); } - private List ChangeSet + private List? ChangeSet { get => currentChangeSet; [ForbidGUICalls] @@ -175,7 +179,7 @@ private IEnumerable changeIdentifiersOfType(GUIModChangeType changeType) .Where(ch => ch?.ChangeType == changeType) .Select(ch => ch.Mod.identifier); - private Dictionary Conflicts + private Dictionary? Conflicts { get => conflicts; [ForbidGUICalls] @@ -190,7 +194,7 @@ private Dictionary Conflicts } } - private void ConflictsUpdated(Dictionary prevConflicts) + private void ConflictsUpdated(Dictionary? prevConflicts) { if (Conflicts == null) { @@ -198,29 +202,32 @@ private void ConflictsUpdated(Dictionary prevConflicts) ClearStatusBar?.Invoke(); } - var registry = RegistryManager.Instance(currentInstance, repoData).registry; - if (prevConflicts != null) + if (currentInstance != null) { - // Mark old conflicts as non-conflicted - // (rows that are _still_ conflicted will be marked as such in the next loop) - foreach (GUIMod guiMod in prevConflicts.Keys) + var registry = RegistryManager.Instance(currentInstance, repoData).registry; + if (prevConflicts != null) { - SetUnsetRowConflicted(guiMod, false, null, currentInstance, registry); + // Mark old conflicts as non-conflicted + // (rows that are _still_ conflicted will be marked as such in the next loop) + foreach (GUIMod guiMod in prevConflicts.Keys) + { + SetUnsetRowConflicted(guiMod, false, null, currentInstance, registry); + } } - } - if (Conflicts != null) - { - // Mark current conflicts as conflicted - foreach ((GUIMod guiMod, string conflict_text) in Conflicts) + if (Conflicts != null) { - SetUnsetRowConflicted(guiMod, true, conflict_text, currentInstance, registry); + // Mark current conflicts as conflicted + foreach ((GUIMod guiMod, string conflict_text) in Conflicts) + { + SetUnsetRowConflicted(guiMod, true, conflict_text, currentInstance, registry); + } } } } private void SetUnsetRowConflicted(GUIMod guiMod, bool conflicted, - string tooltip, + string? tooltip, GameInstance inst, Registry registry) { @@ -239,59 +246,64 @@ private void SetUnsetRowConflicted(GUIMod guiMod, } } - private void RefreshToolButton_Click(object sender, EventArgs e) + private void RefreshToolButton_Click(object? sender, EventArgs? e) { // If user is holding Shift or Ctrl, force a full update - Main.Instance.UpdateRepo(ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift)); + Main.Instance?.UpdateRepo(ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift)); } #region Filter dropdown - private void FilterToolButton_DropDown_Opening(object sender, CancelEventArgs e) + private void FilterToolButton_DropDown_Opening(object? sender, CancelEventArgs? e) { // The menu items' dropdowns can't be accessed if they're empty FilterTagsToolButton_DropDown_Opening(null, null); FilterLabelsToolButton_DropDown_Opening(null, null); } - private void FilterTagsToolButton_DropDown_Opening(object sender, CancelEventArgs e) - { - var registry = RegistryManager.Instance(currentInstance, repoData).registry; - FilterTagsToolButton.DropDownItems.Clear(); - foreach (var kvp in registry.Tags.OrderBy(kvp => kvp.Key)) - { - FilterTagsToolButton.DropDownItems.Add(new ToolStripMenuItem( - $"{kvp.Key} ({kvp.Value.ModuleIdentifiers.Count})", - null, tagFilterButton_Click - ) - { - Tag = kvp.Value, - ToolTipText = Properties.Resources.FilterLinkToolTip, - }); - } - FilterTagsToolButton.DropDownItems.Add(untaggedFilterToolStripSeparator); - FilterTagsToolButton.DropDownItems.Add(new ToolStripMenuItem( - string.Format(Properties.Resources.MainLabelsUntagged, registry.Untagged.Count), - null, tagFilterButton_Click - ) - { - Tag = null - }); - } - - private void FilterLabelsToolButton_DropDown_Opening(object sender, CancelEventArgs e) + private void FilterTagsToolButton_DropDown_Opening(object? sender, CancelEventArgs? e) { - FilterLabelsToolButton.DropDownItems.Clear(); - foreach (ModuleLabel mlbl in mainModList.ModuleLabels.LabelsFor(currentInstance.Name)) + if (currentInstance != null) { - FilterLabelsToolButton.DropDownItems.Add(new ToolStripMenuItem( - $"{mlbl.Name} ({mlbl.ModuleCount(currentInstance.game)})", - null, customFilterButton_Click - ) + var registry = RegistryManager.Instance(currentInstance, repoData).registry; + FilterTagsToolButton.DropDownItems.Clear(); + FilterTagsToolButton.DropDownItems.AddRange( + registry.Tags.OrderBy(kvp => kvp.Key) + .Select(kvp => new ToolStripMenuItem( + $"{kvp.Key} ({kvp.Value.ModuleIdentifiers.Count})", + null, tagFilterButton_Click) + { + Tag = kvp.Value, + ToolTipText = Properties.Resources.FilterLinkToolTip, + }) + .OfType() + .Append(untaggedFilterToolStripSeparator) + .Append(new ToolStripMenuItem( + string.Format(Properties.Resources.MainLabelsUntagged, + registry.Untagged.Count), + null, tagFilterButton_Click) + { + Tag = null + }) + .ToArray()); + } + } + + private void FilterLabelsToolButton_DropDown_Opening(object? sender, CancelEventArgs? e) + { + if (currentInstance != null) + { + FilterLabelsToolButton.DropDownItems.Clear(); + foreach (ModuleLabel mlbl in ModuleLabelList.ModuleLabels.LabelsFor(currentInstance.Name)) { - Tag = mlbl, - ToolTipText = Properties.Resources.FilterLinkToolTip, - }); + FilterLabelsToolButton.DropDownItems.Add(new ToolStripMenuItem( + $"{mlbl.Name} ({mlbl.ModuleCount(currentInstance.game)})", + null, customFilterButton_Click) + { + Tag = mlbl, + ToolTipText = Properties.Resources.FilterLinkToolTip, + }); + } } } @@ -299,141 +311,146 @@ private void FilterLabelsToolButton_DropDown_Opening(object sender, CancelEventA #region Filter right click menu - private void LabelsContextMenuStrip_Opening(object sender, CancelEventArgs e) + private void LabelsContextMenuStrip_Opening(object? sender, CancelEventArgs? e) { - LabelsContextMenuStrip.Items.Clear(); - - var module = SelectedModule; - foreach (ModuleLabel mlbl in mainModList.ModuleLabels.LabelsFor(currentInstance.Name)) + if (e != null && currentInstance != null && SelectedModule is GUIMod module) { - LabelsContextMenuStrip.Items.Add( - new ToolStripMenuItem(mlbl.Name, null, labelMenuItem_Click) - { - BackColor = mlbl.Color, - Checked = mlbl.ContainsModule(currentInstance.game, module.Identifier), - CheckOnClick = true, - Tag = mlbl, - } - ); + LabelsContextMenuStrip.Items.Clear(); + foreach (ModuleLabel mlbl in ModuleLabelList.ModuleLabels.LabelsFor(currentInstance.Name)) + { + LabelsContextMenuStrip.Items.Add( + new ToolStripMenuItem(mlbl.Name, null, labelMenuItem_Click) + { + BackColor = mlbl.Color ?? Color.Transparent, + Checked = mlbl.ContainsModule(currentInstance.game, module.Identifier), + CheckOnClick = true, + Tag = mlbl, + }); + } + LabelsContextMenuStrip.Items.Add(labelToolStripSeparator); + LabelsContextMenuStrip.Items.Add(editLabelsToolStripMenuItem); + e.Cancel = false; } - LabelsContextMenuStrip.Items.Add(labelToolStripSeparator); - LabelsContextMenuStrip.Items.Add(editLabelsToolStripMenuItem); - e.Cancel = false; } - private void labelMenuItem_Click(object sender, EventArgs e) + private void labelMenuItem_Click(object? sender, EventArgs? e) { - var item = sender as ToolStripMenuItem; - var mlbl = item.Tag as ModuleLabel; - var module = SelectedModule; - if (item.Checked) - { - mlbl.Add(currentInstance.game, module.Identifier); - } - else - { - mlbl.Remove(currentInstance.game, module.Identifier); - } - var registry = RegistryManager.Instance(currentInstance, repoData).registry; - mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, - currentInstance.Name, currentInstance.game, registry); - mainModList.ModuleLabels.Save(ModuleLabelList.DefaultPath); - UpdateHiddenTagsAndLabels(); - if (mlbl.HoldVersion) + if (user != null && manager != null && currentInstance != null && SelectedModule != null) { - UpdateCol.Visible = UpdateAllToolButton.Enabled = - mainModList.ResetHasUpdate(currentInstance, registry, ChangeSet, ModGrid.Rows); + var item = sender as ToolStripMenuItem; + var mlbl = item?.Tag as ModuleLabel; + if (item?.Checked ?? false) + { + mlbl?.Add(currentInstance.game, SelectedModule.Identifier); + } + else + { + mlbl?.Remove(currentInstance.game, SelectedModule.Identifier); + } + var registry = RegistryManager.Instance(currentInstance, repoData).registry; + mainModList.ReapplyLabels(SelectedModule, Conflicts?.ContainsKey(SelectedModule) ?? false, + currentInstance.Name, currentInstance.game, registry); + ModuleLabelList.ModuleLabels.Save(ModuleLabelList.DefaultPath); + UpdateHiddenTagsAndLabels(); + if (mlbl?.HoldVersion ?? false) + { + UpdateCol.Visible = UpdateAllToolButton.Enabled = + mainModList.ResetHasUpdate(currentInstance, registry, ChangeSet, ModGrid.Rows); + } } } - private void editLabelsToolStripMenuItem_Click(object sender, EventArgs e) + private void editLabelsToolStripMenuItem_Click(object? sender, EventArgs? e) { - var eld = new EditLabelsDialog(user, manager, mainModList.ModuleLabels); - eld.ShowDialog(this); - eld.Dispose(); - mainModList.ModuleLabels.Save(ModuleLabelList.DefaultPath); - var registry = RegistryManager.Instance(currentInstance, repoData).registry; - foreach (var module in mainModList.Modules) + if (user != null && manager != null && currentInstance != null) { - mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, - currentInstance.Name, currentInstance.game, registry); + var eld = new EditLabelsDialog(user, manager, ModuleLabelList.ModuleLabels); + eld.ShowDialog(this); + eld.Dispose(); + ModuleLabelList.ModuleLabels.Save(ModuleLabelList.DefaultPath); + var registry = RegistryManager.Instance(currentInstance, repoData).registry; + foreach (var module in mainModList.Modules) + { + mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, + currentInstance.Name, currentInstance.game, registry); + } + UpdateHiddenTagsAndLabels(); + UpdateCol.Visible = UpdateAllToolButton.Enabled = + mainModList.ResetHasUpdate(currentInstance, registry, ChangeSet, ModGrid.Rows); } - UpdateHiddenTagsAndLabels(); - UpdateCol.Visible = UpdateAllToolButton.Enabled = - mainModList.ResetHasUpdate(currentInstance, registry, ChangeSet, ModGrid.Rows); } #endregion - private void tagFilterButton_Click(object sender, EventArgs e) + private void tagFilterButton_Click(object? sender, EventArgs? e) { var clicked = sender as ToolStripMenuItem; var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); - Filter(ModList.FilterToSavedSearch(GUIModFilter.Tag, clicked.Tag as ModuleTag, null), merge); + Filter(ModList.FilterToSavedSearch(GUIModFilter.Tag, clicked?.Tag as ModuleTag, null), merge); } - private void customFilterButton_Click(object sender, EventArgs e) + private void customFilterButton_Click(object? sender, EventArgs? e) { var clicked = sender as ToolStripMenuItem; var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); - Filter(ModList.FilterToSavedSearch(GUIModFilter.CustomLabel, null, clicked.Tag as ModuleLabel), merge); + Filter(ModList.FilterToSavedSearch(GUIModFilter.CustomLabel, null, clicked?.Tag as ModuleLabel), merge); } - private void FilterCompatibleButton_Click(object sender, EventArgs e) + private void FilterCompatibleButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); Filter(ModList.FilterToSavedSearch(GUIModFilter.Compatible), merge); } - private void FilterInstalledButton_Click(object sender, EventArgs e) + private void FilterInstalledButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); Filter(ModList.FilterToSavedSearch(GUIModFilter.Installed), merge); } - private void FilterInstalledUpdateButton_Click(object sender, EventArgs e) + private void FilterInstalledUpdateButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); Filter(ModList.FilterToSavedSearch(GUIModFilter.InstalledUpdateAvailable), merge); } - private void FilterReplaceableButton_Click(object sender, EventArgs e) + private void FilterReplaceableButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); Filter(ModList.FilterToSavedSearch(GUIModFilter.Replaceable), merge); } - private void FilterCachedButton_Click(object sender, EventArgs e) + private void FilterCachedButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); Filter(ModList.FilterToSavedSearch(GUIModFilter.Cached), merge); } - private void FilterUncachedButton_Click(object sender, EventArgs e) + private void FilterUncachedButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); Filter(ModList.FilterToSavedSearch(GUIModFilter.Uncached), merge); } - private void FilterNewButton_Click(object sender, EventArgs e) + private void FilterNewButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); Filter(ModList.FilterToSavedSearch(GUIModFilter.NewInRepository), merge); } - private void FilterNotInstalledButton_Click(object sender, EventArgs e) + private void FilterNotInstalledButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); Filter(ModList.FilterToSavedSearch(GUIModFilter.NotInstalled), merge); } - private void FilterIncompatibleButton_Click(object sender, EventArgs e) + private void FilterIncompatibleButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); Filter(ModList.FilterToSavedSearch(GUIModFilter.Incompatible), merge); } - private void FilterAllButton_Click(object sender, EventArgs e) + private void FilterAllButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); Filter(ModList.FilterToSavedSearch(GUIModFilter.All), merge); @@ -446,22 +463,28 @@ private void FilterAllButton_Click(object sender, EventArgs e) /// If true, merge with current searches, else replace public void Filter(SavedSearch search, bool merge) { - var searches = search.Values.Select(s => ModSearch.Parse(s, - mainModList.ModuleLabels.LabelsFor(currentInstance.Name).ToList() - )).ToList(); - - Util.Invoke(ModGrid, () => + if (currentInstance != null) { - if (merge) - { - EditModSearches.MergeSearches(searches); - } - else + var lbls = ModuleLabelList.ModuleLabels.LabelsFor(currentInstance.Name) + .ToList(); + var searches = search.Values + .Select(s => ModSearch.Parse(s, lbls)) + .OfType() + .ToList(); + + Util.Invoke(ModGrid, () => { - EditModSearches.SetSearches(searches); - } - ShowHideColumns(searches); - }); + if (merge) + { + EditModSearches.MergeSearches(searches); + } + else + { + EditModSearches.SetSearches(searches); + } + ShowHideColumns(searches); + }); + } } public void SetSearches(List searches) @@ -483,7 +506,7 @@ private void ShowHideColumns(List searches) if (col.Name != "Installed" && col.Name != "UpdateCol" && col.Name != "ReplaceCol" && !installedColumnNames.Contains(col.Name)) { - col.Visible = !guiConfig.HiddenColumnNames.Contains(col.Name); + col.Visible = !guiConfig?.HiddenColumnNames.Contains(col.Name) ?? true; } } @@ -500,15 +523,18 @@ private void ShowHideColumns(List searches) private void setInstalledColumnsVisible(bool visible) { - var hiddenColumnNames = guiConfig.HiddenColumnNames; - foreach (var colName in installedColumnNames.Where(nm => ModGrid.Columns.Contains(nm))) + if (guiConfig != null) { - ModGrid.Columns[colName].Visible = visible && !hiddenColumnNames.Contains(colName); + var hiddenColumnNames = guiConfig.HiddenColumnNames; + foreach (var colName in installedColumnNames.Where(nm => ModGrid.Columns.Contains(nm))) + { + ModGrid.Columns[colName].Visible = visible && !hiddenColumnNames.Contains(colName); + } } } private static bool SearchesExcludeInstalled(List searches) - => searches?.All(s => s != null && s.Installed == false) ?? false; + => searches.Count > 0 && searches.All(s => s?.Installed == false); public void MarkAllUpdates() { @@ -521,7 +547,7 @@ public void MarkAllUpdates() { if (gmod?.HasUpdate ?? false) { - if (!Main.Instance.LabelsHeld(gmod.Identifier)) + if (!Main.Instance?.LabelsHeld(gmod.Identifier) ?? false) { gmod.SelectedMod = gmod.LatestCompatibleMod; } @@ -529,7 +555,7 @@ public void MarkAllUpdates() } // only sort by Update column if checkbox in settings checked - if (guiConfig.AutoSortByUpdate) + if (guiConfig?.AutoSortByUpdate ?? false) { // Retain their current sort as secondaries AddSort(UpdateCol, true); @@ -543,63 +569,67 @@ public void MarkAllUpdates() }); } - private void UpdateAllToolButton_Click(object sender, EventArgs e) + private void UpdateAllToolButton_Click(object? sender, EventArgs? e) { MarkAllUpdates(); } - private void ApplyToolButton_Click(object sender, EventArgs e) + private void ApplyToolButton_Click(object? sender, EventArgs? e) { StartChangeSet?.Invoke(currentChangeSet, Conflicts); } - private void LaunchGameToolStripMenuItem_MouseHover(object sender, EventArgs e) + private void LaunchGameToolStripMenuItem_MouseHover(object? sender, EventArgs? e) { - var cmdLines = guiConfig.CommandLines; - LaunchGameToolStripMenuItem.DropDownItems.Clear(); - LaunchGameToolStripMenuItem.DropDownItems.AddRange( - cmdLines.Select(cmdLine => (ToolStripItem) - new ToolStripMenuItem(cmdLine, null, - LaunchGameToolStripMenuItem_Click) - { - Tag = cmdLine, - ShortcutKeyDisplayString = CmdLineHelp(cmdLine), - }) - .Append(CommandLinesToolStripSeparator) - .Append(EditCommandLinesToolStripMenuItem) - .ToArray()); - LaunchGameToolStripMenuItem.ShowDropDown(); + if (guiConfig != null) + { + var cmdLines = guiConfig.CommandLines; + LaunchGameToolStripMenuItem.DropDownItems.Clear(); + LaunchGameToolStripMenuItem.DropDownItems.AddRange( + cmdLines.Select(cmdLine => (ToolStripItem) + new ToolStripMenuItem(cmdLine, null, + LaunchGameToolStripMenuItem_Click) + { + Tag = cmdLine, + ShortcutKeyDisplayString = CmdLineHelp(cmdLine), + }) + .Append(CommandLinesToolStripSeparator) + .Append(EditCommandLinesToolStripMenuItem) + .ToArray()); + LaunchGameToolStripMenuItem.ShowDropDown(); + } } private string CmdLineHelp(string cmdLine) - => manager.SteamLibrary.Games.Length > 0 + => manager?.SteamLibrary.Games.Length > 0 ? cmdLine.StartsWith("steam://", StringComparison.InvariantCultureIgnoreCase) ? Properties.Resources.ManageModsSteamPlayTimeYesTooltip : Properties.Resources.ManageModsSteamPlayTimeNoTooltip - : ""; + : "" + ?? ""; - private void LaunchGameToolStripMenuItem_Click(object sender, EventArgs e) + private void LaunchGameToolStripMenuItem_Click(object? sender, EventArgs? e) { var menuItem = sender as ToolStripMenuItem; LaunchGame?.Invoke(menuItem?.Tag as string); } - private void EditCommandLinesToolStripMenuItem_Click(object sender, EventArgs e) + private void EditCommandLinesToolStripMenuItem_Click(object? sender, EventArgs? e) { EditCommandLines?.Invoke(); } - private void NavBackwardToolButton_Click(object sender, EventArgs e) + private void NavBackwardToolButton_Click(object? sender, EventArgs? e) { NavGoBackward(); } - private void NavForwardToolButton_Click(object sender, EventArgs e) + private void NavForwardToolButton_Click(object? sender, EventArgs? e) { NavGoForward(); } - private void ModGrid_SelectionChanged(object sender, EventArgs e) + private void ModGrid_SelectionChanged(object? sender, EventArgs? e) { // Skip if already disposed (i.e. after the form has been closed). // Needed for TransparentTextBoxes @@ -620,10 +650,10 @@ private void ModGrid_SelectionChanged(object sender, EventArgs e) /// Called when there's a click on the ModGrid header row. /// Handles sorting and the header right click context menu. /// - private void ModGrid_HeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e) + private void ModGrid_HeaderMouseClick(object? sender, DataGridViewCellMouseEventArgs? e) { // Left click -> sort by new column / change sorting direction. - if (e.Button == MouseButtons.Left) + if (e?.Button == MouseButtons.Left) { if (ModifierKeys.HasFlag(Keys.Shift)) { @@ -636,7 +666,7 @@ private void ModGrid_HeaderMouseClick(object sender, DataGridViewCellMouseEventA UpdateFilters(); } // Right click -> Bring up context menu to change visibility of columns. - else if (e.Button == MouseButtons.Right) + else if (e?.Button == MouseButtons.Right) { ShowHeaderContextMenu(); } @@ -658,17 +688,19 @@ private void ShowHeaderContextMenu(bool columns = true, { // Add columns ModListHeaderContextMenuStrip.Items.AddRange( - ModGrid.Columns.Cast() - .Where(col => col.Name != "Installed" && col.Name != "UpdateCol" && col.Name != "ReplaceCol") - .Select(col => new ToolStripMenuItem() - { - Name = col.Name, - Text = col.HeaderText, - Checked = col.Visible, - Tag = col - }) - .ToArray() - ); + ModGrid.Columns + .OfType() + .Where(col => col.Name is not "Installed" + and not "UpdateCol" + and not "ReplaceCol") + .Select(col => new ToolStripMenuItem() + { + Name = col.Name, + Text = col.HeaderText, + Checked = col.Visible, + Tag = col + }) + .ToArray()); } if (columns && tags) @@ -677,7 +709,7 @@ private void ShowHeaderContextMenu(bool columns = true, ModListHeaderContextMenuStrip.Items.Add(new ToolStripSeparator()); } - if (tags) + if (tags && currentInstance != null) { // Add tags var registry = RegistryManager.Instance(currentInstance, repoData).registry; @@ -687,7 +719,7 @@ private void ShowHeaderContextMenu(bool columns = true, { Name = kvp.Key, Text = kvp.Key, - Checked = !mainModList.ModuleTags.HiddenTags.Contains(kvp.Key), + Checked = !ModuleTagList.ModuleTags.HiddenTags.Contains(kvp.Key), Tag = kvp.Value, }) .ToArray() @@ -701,15 +733,15 @@ private void ShowHeaderContextMenu(bool columns = true, /// /// Called if a ToolStripButton of the header context menu is pressed. /// - private void ModListHeaderContextMenuStrip_ItemClicked(object sender, ToolStripItemClickedEventArgs e) + private void ModListHeaderContextMenuStrip_ItemClicked(object? sender, ToolStripItemClickedEventArgs? e) { // ClickedItem is of type ToolStripItem, we need ToolStripButton. - ToolStripMenuItem clickedItem = e.ClickedItem as ToolStripMenuItem; + var clickedItem = e?.ClickedItem as ToolStripMenuItem; if (clickedItem?.Tag is DataGridViewColumn col) { col.Visible = !clickedItem.Checked; - guiConfig.SetColumnVisibility(col.Name, !clickedItem.Checked); + guiConfig?.SetColumnVisibility(col.Name, !clickedItem.Checked); if (col.Index == 0) { InstallAllCheckbox.Visible = col.Visible; @@ -719,13 +751,13 @@ private void ModListHeaderContextMenuStrip_ItemClicked(object sender, ToolStripI { if (!clickedItem.Checked) { - mainModList.ModuleTags.HiddenTags.Remove(tag.Name); + ModuleTagList.ModuleTags.HiddenTags.Remove(tag.Name); } else { - mainModList.ModuleTags.HiddenTags.Add(tag.Name); + ModuleTagList.ModuleTags.HiddenTags.Add(tag.Name); } - mainModList.ModuleTags.Save(ModuleTagList.DefaultPath); + ModuleTagList.ModuleTags.Save(ModuleTagList.DefaultPath); UpdateFilters(); UpdateHiddenTagsAndLabels(); } @@ -735,9 +767,9 @@ private void ModListHeaderContextMenuStrip_ItemClicked(object sender, ToolStripI /// Called on key down when the mod list is focused. /// Makes the Home/End keys go to the top/bottom of the list respectively. /// - private void ModGrid_KeyDown(object sender, KeyEventArgs e) + private void ModGrid_KeyDown(object? sender, KeyEventArgs? e) { - switch (e.KeyCode) + switch (e?.KeyCode) { case Keys.Home: // First row. @@ -753,7 +785,7 @@ private void ModGrid_KeyDown(object sender, KeyEventArgs e) // Last row. if (ModGrid.Rows.Count > 0) //Handles for empty filters { - ModGrid.CurrentCell = ModGrid.Rows[ModGrid.Rows.Count - 1].Cells[SelectableColumnIndex()]; + ModGrid.CurrentCell = ModGrid.Rows[^1].Cells[SelectableColumnIndex()]; } e.Handled = true; @@ -793,48 +825,51 @@ private void ModGrid_KeyDown(object sender, KeyEventArgs e) /// being pressed repeatedly, it cycles through mods names beginning with that key. /// If space is pressed, the checkbox at the current row is toggled. /// - private void ModGrid_KeyPress(object sender, KeyPressEventArgs e) + private void ModGrid_KeyPress(object? sender, KeyPressEventArgs? e) { - // Don't search for spaces or newlines - if (e.KeyChar == (char)Keys.Space || e.KeyChar == (char)Keys.Enter) + if (e != null) { - return; - } + // Don't search for spaces or newlines + if (e.KeyChar is ((char)Keys.Space) or ((char)Keys.Enter)) + { + return; + } - var key = e.KeyChar.ToString(); - // Determine time passed since last key press. - TimeSpan interval = DateTime.Now - lastSearchTime; - if (interval.TotalSeconds < 1) - { - // Last keypress was < 1 sec ago, so combine the last and current keys. - key = lastSearchKey + key; - } + var key = e.KeyChar.ToString(); + // Determine time passed since last key press. + TimeSpan interval = DateTime.Now - lastSearchTime; + if (interval.TotalSeconds < 1) + { + // Last keypress was < 1 sec ago, so combine the last and current keys. + key = lastSearchKey + key; + } - // Remember the current time and key. - lastSearchTime = DateTime.Now; - lastSearchKey = key; + // Remember the current time and key. + lastSearchTime = DateTime.Now; + lastSearchKey = key; - if (key.Distinct().Count() == 1) - { - // Treat repeating and single keypresses the same. - key = key.Substring(0, 1); - } + if (key.Distinct().Count() == 1) + { + // Treat repeating and single keypresses the same. + key = key[..1]; + } - FocusMod(key, false); - e.Handled = true; + FocusMod(key, false); + e.Handled = true; + } } /// /// I'm pretty sure this is what gets called when the user clicks on a ticky in the mod list. /// - private void ModGrid_CellContentClick(object sender, DataGridViewCellEventArgs e) + private void ModGrid_CellContentClick(object? sender, DataGridViewCellEventArgs? e) { ModGrid.CommitEdit(DataGridViewDataErrorContexts.Commit); } - private void ModGrid_CellMouseDoubleClick(object sender, DataGridViewCellMouseEventArgs e) + private void ModGrid_CellMouseDoubleClick(object? sender, DataGridViewCellMouseEventArgs? e) { - if (e.Button != MouseButtons.Left) + if (e?.Button != MouseButtons.Left) { return; } @@ -845,7 +880,7 @@ private void ModGrid_CellMouseDoubleClick(object sender, DataGridViewCellMouseEv } DataGridViewRow row = ModGrid.Rows[e.RowIndex]; - if (!(row.Cells[0] is DataGridViewCheckBoxCell)) + if (row.Cells[0] is not DataGridViewCheckBoxCell) { return; } @@ -855,16 +890,16 @@ private void ModGrid_CellMouseDoubleClick(object sender, DataGridViewCellMouseEv ModGrid.CommitEdit(DataGridViewDataErrorContexts.Commit); } - private void ModGrid_CellValueChanged(object sender, DataGridViewCellEventArgs e) + private void ModGrid_CellValueChanged(object? sender, DataGridViewCellEventArgs? e) { - if (e.RowIndex >= 0) + if (currentInstance != null && e?.RowIndex >= 0) { - var row = ModGrid?.Rows?[e.RowIndex]; + var row = ModGrid.Rows?[e.RowIndex]; switch (row?.Cells[e.ColumnIndex]) { case DataGridViewLinkCell linkCell: // Launch URLs if found in grid - string cmd = linkCell.Value.ToString(); + var cmd = linkCell.Value.ToString(); if (!string.IsNullOrEmpty(cmd)) { Utilities.ProcessStartURL(cmd); @@ -915,13 +950,14 @@ private void ModGrid_CellValueChanged(object sender, DataGridViewCellEventArgs e } } - private void guiModule_PropertyChanged(object sender, PropertyChangedEventArgs e) + private void guiModule_PropertyChanged(object? sender, PropertyChangedEventArgs? e) { - if (sender is GUIMod gmod + if (currentInstance != null + && sender is GUIMod gmod && mainModList.full_list_of_mod_rows.TryGetValue(gmod.Identifier, - out DataGridViewRow row)) + out DataGridViewRow? row)) { - switch (e.PropertyName) + switch (e?.PropertyName) { case "SelectedMod": Util.Invoke(this, () => @@ -963,11 +999,12 @@ private void guiModule_PropertyChanged(object sender, PropertyChangedEventArgs e public void RemoveChangesetItem(ModChange change) { - if (currentChangeSet != null + if (currentInstance != null + && currentChangeSet != null && currentChangeSet.Contains(change) && change.IsRemovable && mainModList.full_list_of_mod_rows.TryGetValue(change.Mod.identifier, - out DataGridViewRow row) + out DataGridViewRow? row) && row.Tag is GUIMod guiMod) { if (change.IsAutoRemoval) @@ -999,7 +1036,7 @@ public void RemoveChangesetItem(ModChange change) } } - private void ModGrid_GotFocus(object sender, EventArgs e) + private void ModGrid_GotFocus(object? sender, EventArgs? e) { Util.Invoke(this, () => { @@ -1009,7 +1046,7 @@ private void ModGrid_GotFocus(object sender, EventArgs e) }); } - private void ModGrid_LostFocus(object sender, EventArgs e) + private void ModGrid_LostFocus(object? sender, EventArgs? e) { Util.Invoke(this, () => { @@ -1019,7 +1056,7 @@ private void ModGrid_LostFocus(object sender, EventArgs e) }); } - private void InstallAllCheckbox_CheckChanged(object sender, EventArgs e) + private void InstallAllCheckbox_CheckChanged(object? sender, EventArgs? e) { WithFrozenChangeset(() => { @@ -1061,17 +1098,21 @@ public void ClearChangeSet() updateCell.Value = false; } } - // Marking a mod as AutoInstalled can immediately queue it for removal if there is no dependent mod. - // Reset the state of the AutoInstalled checkbox for these by deducing it from the changeset. - foreach (DataGridViewRow row in mainModList.full_list_of_mod_rows.Values) + if (ChangeSet != null) { - GUIMod mod = row.Tag as GUIMod; - if (mod.InstalledMod != null - && ChangeSet.Contains(new ModChange(mod.InstalledMod?.Module, - GUIModChangeType.Remove, - new SelectionReason.NoLongerUsed()))) + // Marking a mod as AutoInstalled can immediately queue it for removal if there is no dependent mod. + // Reset the state of the AutoInstalled checkbox for these by deducing it from the changeset. + var noLongerUsed = ChangeSet.Where(ch => ch.ChangeType == GUIModChangeType.Remove + && ch.Reasons.Any(r => r is SelectionReason.NoLongerUsed)) + .Select(ch => ch.Mod) + .ToArray(); + foreach (var mod in noLongerUsed) { - mod.SetAutoInstallChecked(row, AutoInstalled, false); + if (mainModList.full_list_of_mod_rows.TryGetValue(mod.identifier, out DataGridViewRow? row) + && row.Tag is GUIMod gmod) + { + gmod.SetAutoInstallChecked(row, AutoInstalled, false); + } } } }); @@ -1096,8 +1137,11 @@ private void WithFrozenChangeset(Action action) // Don't let anything ever prevent us from unfreezing the changeset freezeChangeSet = false; ModGrid.Refresh(); - UpdateChangeSetAndConflicts(currentInstance, - RegistryManager.Instance(currentInstance, repoData).registry); + if (currentInstance != null) + { + UpdateChangeSetAndConflicts(currentInstance, + RegistryManager.Instance(currentInstance, repoData).registry); + } } } } @@ -1111,35 +1155,34 @@ private void WithFrozenChangeset(Action action) /// Index of the column to use. /// private int SelectableColumnIndex() - { // First try the currently active cell's column - return ModGrid.CurrentCell?.ColumnIndex + => ModGrid.CurrentCell?.ColumnIndex // If there's no currently active cell, use the first visible non-checkbox column ?? ModGrid.Columns.Cast() .FirstOrDefault(c => c is DataGridViewTextBoxColumn && c.Visible)?.Index // Otherwise use the Installed checkbox column since it can't be hidden ?? Installed.Index; - } public void FocusMod(string key, bool exactMatch, bool showAsFirst = false) { DataGridViewRow current_row = ModGrid.CurrentRow; int currentIndex = current_row?.Index ?? 0; - DataGridViewRow first_match = null; + DataGridViewRow? first_match = null; var does_name_begin_with_key = new Func(row => { - GUIMod mod = row.Tag as GUIMod; + var mod = row.Tag as GUIMod; bool row_match; if (exactMatch) { - row_match = mod.Name == key || mod.Identifier == key; + row_match = mod?.Name == key || mod?.Identifier == key; } else { - row_match = mod.Name.StartsWith(key, StringComparison.OrdinalIgnoreCase) || - mod.Abbrevation.StartsWith(key, StringComparison.OrdinalIgnoreCase) || - mod.Identifier.StartsWith(key, StringComparison.OrdinalIgnoreCase); + row_match = mod != null + && (mod.Name.StartsWith(key, StringComparison.OrdinalIgnoreCase) + || mod.Abbrevation.StartsWith(key, StringComparison.OrdinalIgnoreCase) + || mod.Identifier.StartsWith(key, StringComparison.OrdinalIgnoreCase)); } if (row_match && first_match == null) @@ -1158,9 +1201,10 @@ public void FocusMod(string key, bool exactMatch, bool showAsFirst = false) }); ModGrid.ClearSelection(); - DataGridViewRow match = ModGrid.Rows.Cast() - .Where(row => row.Visible) - .FirstOrDefault(does_name_begin_with_key); + var match = ModGrid.Rows + .OfType() + .Where(row => row.Visible) + .FirstOrDefault(does_name_begin_with_key); if (match == null && first_match != null) { // If there were no matches after the first match, cycle over to the beginning. @@ -1183,7 +1227,7 @@ public void FocusMod(string key, bool exactMatch, bool showAsFirst = false) } } - private void ModGrid_MouseDown(object sender, MouseEventArgs e) + private void ModGrid_MouseDown(object? sender, MouseEventArgs e) { var rowIndex = ModGrid.HitTest(e.X, e.Y).RowIndex; @@ -1215,15 +1259,15 @@ private bool ShowModContextMenu() return false; } - private void ModGrid_Resize(object sender, EventArgs e) + private void ModGrid_Resize(object? sender, EventArgs? e) { InstallAllCheckbox.Top = ModGrid.Top - InstallAllCheckbox.Height; } - private void reinstallToolStripMenuItem_Click(object sender, EventArgs e) + private void reinstallToolStripMenuItem_Click(object? sender, EventArgs? e) { var module = SelectedModule?.ToModule(); - if (module != null) + if (module != null && currentInstance != null) { IRegistryQuerier registry = RegistryManager.Instance(currentInstance, repoData).registry; StartChangeSet?.Invoke(new List() @@ -1244,25 +1288,25 @@ public Dictionary AllGUIMods() => mainModList.Modules.ToDictionary(guiMod => guiMod.Identifier, guiMod => guiMod); - private void purgeContentsToolStripMenuItem_Click(object sender, EventArgs e) + private void purgeContentsToolStripMenuItem_Click(object? sender, EventArgs? e) { // Purge other versions as well since the user is likely to want that // and has no other way to achieve it var selected = SelectedModule; - if (selected != null) + if (selected != null && currentInstance != null) { IRegistryQuerier registry = RegistryManager.Instance(currentInstance, repoData).registry; var allAvail = registry.AvailableByIdentifier(selected.Identifier); foreach (CkanModule mod in allAvail) { - manager.Cache.Purge(mod); + manager?.Cache?.Purge(mod); } } } - private void downloadContentsToolStripMenuItem_Click(object sender, EventArgs e) + private void downloadContentsToolStripMenuItem_Click(object? sender, EventArgs? e) { - Main.Instance.StartDownload(SelectedModule); + Main.Instance?.StartDownload(SelectedModule); } private void EditModSearches_ApplySearches(List searches) @@ -1288,7 +1332,7 @@ public void UpdateFilters() private void _UpdateFilters() { - if (ModGrid == null || mainModList?.full_list_of_mod_rows == null) + if (ModGrid == null || mainModList?.full_list_of_mod_rows == null || currentInstance == null) { return; } @@ -1301,10 +1345,10 @@ private void _UpdateFilters() mainModList.full_list_of_mod_rows.Values.CopyTo(rows, 0); // Try to remember the current scroll position and selected mod var scroll_col = Math.Max(0, ModGrid.FirstDisplayedScrollingColumnIndex); - GUIMod selected_mod = null; + GUIMod? selected_mod = null; if (ModGrid.CurrentRow != null) { - selected_mod = (GUIMod)ModGrid.CurrentRow.Tag; + selected_mod = ModGrid.CurrentRow.Tag as GUIMod; } var registry = RegistryManager.Instance(currentInstance, repoData).registry; @@ -1312,8 +1356,8 @@ private void _UpdateFilters() var instName = currentInstance.Name; var instGame = currentInstance.game; rows.AsParallel().ForAll(row => - row.Visible = mainModList.IsVisible((GUIMod)row.Tag, - instName, instGame, registry)); + row.Visible = row.Tag is GUIMod gmod + && mainModList.IsVisible(gmod, instName, instGame, registry)); ApplyHeaderGlyphs(); ModGrid.Rows.AddRange(Sort(rows.Where(row => row.Visible)).ToArray()); @@ -1321,7 +1365,7 @@ private void _UpdateFilters() if (selected_mod != null) { var selected_row = ModGrid.Rows.Cast() - .FirstOrDefault(row => selected_mod.Identifier.Equals(((GUIMod)row.Tag).Identifier)); + .FirstOrDefault(row => selected_mod.Identifier.Equals((row.Tag as GUIMod)?.Identifier)); if (selected_row != null) { ModGrid.CurrentCell = selected_row.Cells[scroll_col]; @@ -1330,14 +1374,22 @@ private void _UpdateFilters() } [ForbidGUICalls] - public void Update(object sender, DoWorkEventArgs e) + public void Update(object? sender, DoWorkEventArgs? e) { - e.Result = _UpdateModsList(e.Argument as Dictionary); + if (e != null) + { + e.Result = _UpdateModsList(e.Argument as Dictionary); + } } [ForbidGUICalls] - private bool _UpdateModsList(Dictionary old_modules = null) + private bool _UpdateModsList(Dictionary? old_modules = null) { + if (currentInstance == null || guiConfig == null) + { + return false; + } + log.Info("Updating the mod list"); var regMgr = RegistryManager.Instance(currentInstance, repoData); @@ -1345,7 +1397,7 @@ private bool _UpdateModsList(Dictionary old_modules = null) repoData.Prepopulate( registry.Repositories.Values.ToList(), - new Progress(p => user.RaiseProgress( + new Progress(p => user?.RaiseProgress( Properties.Resources.LoadingCachedRepoData, p))); if (!regMgr.registry.HasAnyAvailable()) @@ -1417,7 +1469,8 @@ private bool _UpdateModsList(Dictionary old_modules = null) RaiseMessage?.Invoke(Properties.Resources.MainModListUpdatingFilters); - var has_unheld_updates = mainModList.Modules.Any(mod => mod.HasUpdate && !Main.Instance.LabelsHeld(mod.Identifier)); + var has_unheld_updates = mainModList.Modules.Any(mod => mod.HasUpdate + && (!Main.Instance?.LabelsHeld(mod.Identifier) ?? true)); Util.Invoke(menuStrip2, () => { FilterCompatibleButton.Text = string.Format(Properties.Resources.MainModListCompatible, @@ -1469,7 +1522,13 @@ private bool _UpdateModsList(Dictionary old_modules = null) } else if (timeSinceUpdate < RepositoryDataManager.TimeTillVeryStale) { - RefreshToolButton.Image = EmbeddedImages.refreshStale; + // Gradually turn the dot from yellow to red as the user ignores it longer and longer + RefreshToolButton.Image = Util.LerpBitmaps( + EmbeddedImages.refreshStale, + EmbeddedImages.refreshVeryStale, + (float)((timeSinceUpdate - RepositoryDataManager.TimeTillStale).TotalSeconds + / (RepositoryDataManager.TimeTillVeryStale + - RepositoryDataManager.TimeTillStale).TotalSeconds)); RefreshToolButton.ToolTipText = string.Format(Properties.Resources.ManageModsRefreshStaleToolTip, Math.Round(timeSinceUpdate.TotalDays)); } @@ -1486,7 +1545,7 @@ private bool _UpdateModsList(Dictionary old_modules = null) return true; } - private void ModGrid_CurrentCellDirtyStateChanged(object sender, EventArgs e) + private void ModGrid_CurrentCellDirtyStateChanged(object? sender, EventArgs? e) { ModGrid_CellContentClick(sender, null); } @@ -1507,9 +1566,9 @@ private void SetSort(DataGridViewColumn col) private void AddSort(DataGridViewColumn col, bool atStart = false) { - if (SortColumns.Count > 0 && SortColumns[SortColumns.Count - 1] == col.Name) + if (SortColumns.Count > 0 && SortColumns[^1] == col.Name) { - descending[descending.Count - 1] = !descending[descending.Count - 1]; + descending[^1] = !descending[^1]; } else { @@ -1581,10 +1640,10 @@ private int CompareRows(DataGridViewRow a, DataGridViewRow b) /// private int CompareColumn(DataGridViewRow a, DataGridViewRow b, DataGridViewColumn col) { - GUIMod gmodA = a.Tag as GUIMod; - GUIMod gmodB = b.Tag as GUIMod; - CkanModule modA = gmodA.ToModule(); - CkanModule modB = gmodB.ToModule(); + var gmodA = a.Tag as GUIMod; + var gmodB = b.Tag as GUIMod; + var modA = gmodA?.ToModule(); + var modB = gmodB?.ToModule(); var cellA = a.Cells[col.Index]; var cellB = b.Cells[col.Index]; if (col is DataGridViewCheckBoxColumn) @@ -1606,7 +1665,7 @@ private int CompareColumn(DataGridViewRow a, DataGridViewRow b, DataGridViewColu : ((string)cellA.Value).CompareTo((string)cellB.Value); } } - else + else if (gmodA != null && gmodB != null && modA != null && modB != null) { switch (col.Name) { @@ -1626,6 +1685,7 @@ private int CompareColumn(DataGridViewRow a, DataGridViewRow b, DataGridViewColu return valA.CompareTo(valB); } } + return 0; } private static int CompareToNullable(T? a, T? b) where T : struct, IComparable @@ -1647,8 +1707,8 @@ private static int CompareToNullable(T? a, T? b) where T : struct, IComparabl /// private int GameCompatComparison(DataGridViewRow a, DataGridViewRow b) { - GameVersion verA = ((GUIMod)a.Tag)?.GameCompatibilityVersion; - GameVersion verB = ((GUIMod)b.Tag)?.GameCompatibilityVersion; + var verA = (a.Tag as GUIMod)?.GameCompatibilityVersion; + var verB = (b.Tag as GUIMod)?.GameCompatibilityVersion; if (verA == null) { return verB == null ? 0 : -1; @@ -1701,7 +1761,7 @@ public void ResetFilterAndSelectModOnList(CkanModule module) FocusMod(module.identifier, true); } - public GUIMod SelectedModule => + public GUIMod? SelectedModule => ModGrid.SelectedRows.Count == 0 ? null : ModGrid.SelectedRows[0]?.Tag as GUIMod; @@ -1721,32 +1781,35 @@ public void ParentMoved() [ForbidGUICalls] private void UpdateHiddenTagsAndLabels() { - var registry = RegistryManager.Instance(currentInstance, repoData).registry; - var tags = mainModList.ModuleTags.HiddenTags - .Intersect(registry.Tags.Keys) - .OrderByDescending(tagName => tagName) - .Select(tagName => registry.Tags[tagName]) - .ToList(); - var labels = mainModList.ModuleLabels.LabelsFor(currentInstance.Name) - .Where(l => l.Hide && l.ModuleCount(currentInstance.game) > 0) - .ToList(); - hiddenTagsLabelsLinkList.UpdateTagsAndLabels(tags, labels); - Util.Invoke(hiddenTagsLabelsLinkList, () => - { - hiddenTagsLabelsLinkList.Visible = tags.Count > 0 || labels.Count > 0; - if (tags.Count > 0 || labels.Count > 0) - { - hiddenTagsLabelsLinkList.Controls.Add(new Label() + if (currentInstance != null) + { + var registry = RegistryManager.Instance(currentInstance, repoData).registry; + var tags = ModuleTagList.ModuleTags.HiddenTags + .Intersect(registry.Tags.Keys) + .OrderByDescending(tagName => tagName) + .Select(tagName => registry.Tags[tagName]) + .ToList(); + var labels = ModuleLabelList.ModuleLabels.LabelsFor(currentInstance.Name) + .Where(l => l.Hide && l.ModuleCount(currentInstance.game) > 0) + .ToList(); + hiddenTagsLabelsLinkList.UpdateTagsAndLabels(tags, labels); + Util.Invoke(hiddenTagsLabelsLinkList, () => + { + hiddenTagsLabelsLinkList.Visible = tags.Count > 0 || labels.Count > 0; + if (tags.Count > 0 || labels.Count > 0) { - Text = tags.Count == 0 ? Properties.Resources.ManageModsHiddenLabels - : labels.Count == 0 ? Properties.Resources.ManageModsHiddenTags - : Properties.Resources.ManageModsHiddenLabelsAndTags, - AutoSize = true, - Padding = new Padding(0), - Margin = new Padding(0, 2, 0, 2), - }); - } - }); + hiddenTagsLabelsLinkList.Controls.Add(new Label() + { + Text = tags.Count == 0 ? Properties.Resources.ManageModsHiddenLabels + : labels.Count == 0 ? Properties.Resources.ManageModsHiddenTags + : Properties.Resources.ManageModsHiddenLabelsAndTags, + AutoSize = true, + Padding = new Padding(0), + Margin = new Padding(0, 2, 0, 2), + }); + } + }); + } } private void hiddenTagsLabelsLinkList_TagClicked(ModuleTag tag, bool merge) @@ -1775,12 +1838,6 @@ private void NavInit() } } - private void NavUpdateUI() - { - NavBackwardToolButton.Enabled = navHistory.CanNavigateBackward; - NavForwardToolButton.Enabled = navHistory.CanNavigateForward; - } - private void NavSelectMod(GUIMod module) { navHistory.AddToHistory(module); @@ -1788,24 +1845,24 @@ private void NavSelectMod(GUIMod module) public void NavGoBackward() { - if (navHistory.CanNavigateBackward) + if (navHistory.TryGoBackward(out GUIMod? newCurrentItem)) { - NavGoToMod(navHistory.NavigateBackward()); + NavGoToMod(newCurrentItem); } } public void NavGoForward() { - if (navHistory.CanNavigateForward) + if (navHistory.TryGoForward(out GUIMod? newCurrentItem)) { - NavGoToMod(navHistory.NavigateForward()); + NavGoToMod(newCurrentItem); } } private void NavGoToMod(GUIMod module) { - // Focussing on a mod also causes navigation, but we don't want - // this to affect the history. so we switch to read-only mode. + // Focusing on a mod also causes navigation, but we don't want + // this to affect the history, so we switch to read-only mode. navHistory.IsReadOnly = true; FocusMod(module.Name, true); navHistory.IsReadOnly = false; @@ -1813,7 +1870,8 @@ private void NavGoToMod(GUIMod module) private void NavOnHistoryChange() { - NavUpdateUI(); + NavBackwardToolButton.Enabled = navHistory.CanNavigateBackward; + NavForwardToolButton.Enabled = navHistory.CanNavigateForward; } #endregion @@ -1851,9 +1909,9 @@ public bool AllowClose() string confDescrip = Conflicts .Select(kvp => kvp.Value) .Aggregate((a, b) => $"{a}, {b}"); - if (!Main.Instance.YesNoDialog(string.Format(Properties.Resources.MainQuitWithConflicts, confDescrip), + if (!Main.Instance?.YesNoDialog(string.Format(Properties.Resources.MainQuitWithConflicts, confDescrip), Properties.Resources.MainQuit, - Properties.Resources.MainGoBack)) + Properties.Resources.MainGoBack) ?? false) { return false; } @@ -1866,9 +1924,9 @@ public bool AllowClose() .Select(grp => $"{grp.Key}: " + grp.Aggregate((a, b) => $"{a}, {b}")) .Aggregate((a, b) => $"{a}\r\n{b}"); - if (!Main.Instance.YesNoDialog(string.Format(Properties.Resources.MainQuitWithUnappliedChanges, changeDescrip), + if (!Main.Instance?.YesNoDialog(string.Format(Properties.Resources.MainQuitWithUnappliedChanges, changeDescrip), Properties.Resources.MainQuit, - Properties.Resources.MainGoBack)) + Properties.Resources.MainGoBack) ?? false) { return false; } @@ -1884,11 +1942,13 @@ public void InstanceUpdated() } public HashSet ComputeUserChangeSet() - => mainModList.ComputeUserChangeSet( - RegistryManager.Instance(currentInstance, repoData).registry, - currentInstance.VersionCriteria(), - currentInstance, - UpdateCol, ReplaceCol); + => currentInstance == null + ? new HashSet() + : mainModList.ComputeUserChangeSet( + RegistryManager.Instance(currentInstance, repoData).registry, + currentInstance.VersionCriteria(), + currentInstance, + UpdateCol, ReplaceCol); [ForbidGUICalls] public void UpdateChangeSetAndConflicts(GameInstance inst, IRegistryQuerier registry) @@ -1899,8 +1959,8 @@ public void UpdateChangeSetAndConflicts(GameInstance inst, IRegistryQuerier regi return; } - List full_change_set = null; - Dictionary new_conflicts = null; + List? full_change_set = null; + Dictionary? new_conflicts = null; var gameVersion = inst.VersionCriteria(); var user_change_set = mainModList.ComputeUserChangeSet(registry, gameVersion, inst, UpdateCol, ReplaceCol); @@ -1920,7 +1980,8 @@ public void UpdateChangeSetAndConflicts(GameInstance inst, IRegistryQuerier regi full_change_set = tuple.Item1.ToList(); new_conflicts = tuple.Item2.ToDictionary( item => new GUIMod(item.Key, repoData, registry, gameVersion, null, - guiConfig.HideEpochs, guiConfig.HideV), + guiConfig?.HideEpochs ?? false, + guiConfig?.HideV ?? false), item => item.Value); if (new_conflicts.Count > 0) { diff --git a/GUI/Controls/ModInfo.cs b/GUI/Controls/ModInfo.cs index b382393a66..fb5c77bc07 100644 --- a/GUI/Controls/ModInfo.cs +++ b/GUI/Controls/ModInfo.cs @@ -24,22 +24,21 @@ public ModInfo() Relationships.ModuleDoubleClicked += mod => ModuleDoubleClicked?.Invoke(mod); } - public GUIMod SelectedModule + public GUIMod? SelectedModule { set { - var module = value?.ToModule(); if (value != selectedModule) { selectedModule = value; - if (module == null) + if (value == null) { ModInfoTabControl.Enabled = false; } - else + else if (manager?.CurrentInstance?.VersionCriteria() is GameVersionCriteria crit) { ModInfoTabControl.Enabled = true; - UpdateHeaderInfo(value, manager.CurrentInstance.VersionCriteria()); + UpdateHeaderInfo(value, crit); LoadTab(value); } } @@ -53,9 +52,9 @@ public void RefreshModContentsTree() Contents.RefreshModContentsTree(); } - public event Action OnDownloadClick; - public event Action OnChangeFilter; - public event Action ModuleDoubleClicked; + public event Action? OnDownloadClick; + public event Action? OnChangeFilter; + public event Action? ModuleDoubleClicked; protected override void OnResize(EventArgs e) { @@ -63,17 +62,18 @@ protected override void OnResize(EventArgs e) ModInfoTable.RowStyles[1].Height = ModInfoTable.Padding.Vertical + ModInfoTable.Margin.Vertical + tagsLabelsLinkList.TagsHeight; - if (!string.IsNullOrEmpty(MetadataModuleDescriptionTextBox?.Text)) + if (MetadataModuleDescriptionTextBox != null + && !string.IsNullOrEmpty(MetadataModuleDescriptionTextBox.Text)) { MetadataModuleDescriptionTextBox.Height = DescriptionHeight; } } - private GUIMod selectedModule; + private GUIMod? selectedModule; private void LoadTab(GUIMod gm) { - switch (ModInfoTabControl.SelectedTab.Name) + switch (ModInfoTabControl.SelectedTab?.Name) { case "MetadataTabPage": Metadata.UpdateModInfo(gm); @@ -99,12 +99,15 @@ private void LoadTab(GUIMod gm) } // When switching tabs ensure that the resulting tab is updated. - private void ModInfoTabControl_SelectedIndexChanged(object sender, EventArgs e) + private void ModInfoTabControl_SelectedIndexChanged(object? sender, EventArgs? e) { - LoadTab(SelectedModule); + if (SelectedModule != null) + { + LoadTab(SelectedModule); + } } - private GameInstanceManager manager => Main.Instance.Manager; + private static GameInstanceManager? manager => Main.Instance?.Manager; private int TextBoxStringHeight(TextBox tb) => tb.Padding.Vertical + tb.Margin.Vertical @@ -140,27 +143,29 @@ private void UpdateHeaderInfo(GUIMod gmod, GameVersionCriteria crit) }); } - private ModuleLabelList ModuleLabels => Main.Instance.ManageMods.mainModList.ModuleLabels; + private static ModuleLabelList ModuleLabels => ModuleLabelList.ModuleLabels; private void UpdateTagsAndLabels(CkanModule mod) { - var registry = RegistryManager.Instance( - manager.CurrentInstance, ServiceLocator.Container.Resolve() - ).registry; - tagsLabelsLinkList.UpdateTagsAndLabels( - registry?.Tags - .Where(t => t.Value.ModuleIdentifiers.Contains(mod.identifier)) - .OrderBy(t => t.Key) - .Select(t => t.Value), - ModuleLabels?.LabelsFor(manager.CurrentInstance.Name) - .Where(l => l.ContainsModule(Main.Instance.CurrentInstance.game, mod.identifier)) - .OrderBy(l => l.Name)); - Util.Invoke(tagsLabelsLinkList, () => + if (manager?.CurrentInstance is GameInstance inst) { - ModInfoTable.RowStyles[1].Height = ModInfoTable.Padding.Vertical - + ModInfoTable.Margin.Vertical - + tagsLabelsLinkList.TagsHeight; - }); + var registry = RegistryManager.Instance(inst, ServiceLocator.Container.Resolve()) + .registry; + tagsLabelsLinkList.UpdateTagsAndLabels( + registry?.Tags + .Where(t => t.Value.ModuleIdentifiers.Contains(mod.identifier)) + .OrderBy(t => t.Key) + .Select(t => t.Value), + ModuleLabels?.LabelsFor(inst.Name) + .Where(l => l.ContainsModule(inst.game, mod.identifier)) + .OrderBy(l => l.Name)); + Util.Invoke(tagsLabelsLinkList, () => + { + ModInfoTable.RowStyles[1].Height = ModInfoTable.Padding.Vertical + + ModInfoTable.Margin.Vertical + + tagsLabelsLinkList.TagsHeight; + }); + } } diff --git a/GUI/Controls/ModInfoTabs/Contents.cs b/GUI/Controls/ModInfoTabs/Contents.cs index 8ea9351a0f..775852d5b3 100644 --- a/GUI/Controls/ModInfoTabs/Contents.cs +++ b/GUI/Controls/ModInfoTabs/Contents.cs @@ -23,14 +23,14 @@ public Contents() InitializeComponent(); } - public GUIMod SelectedModule + public GUIMod? SelectedModule { set { if (value != selectedModule) { selectedModule = value; - Util.Invoke(ContentsPreviewTree, () => _UpdateModContentsTree(selectedModule.ToModule())); + Util.Invoke(ContentsPreviewTree, () => _UpdateModContentsTree(selectedModule?.ToModule())); } } get => selectedModule; @@ -45,43 +45,42 @@ public void RefreshModContentsTree() } } - public event Action OnDownloadClick; + public event Action? OnDownloadClick; - private GUIMod selectedModule; - private CkanModule currentModContentsModule; - private GameInstanceManager manager => Main.Instance.Manager; + private GUIMod? selectedModule; + private CkanModule? currentModContentsModule; + private GameInstanceManager? manager => Main.Instance?.Manager; - private void ContentsPreviewTree_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e) + private void ContentsPreviewTree_NodeMouseDoubleClick(object? sender, TreeNodeMouseClickEventArgs? e) { - OpenFileBrowser(e.Node); + if (e != null) + { + Utilities.OpenFileBrowser(e.Node.Name); + } } - private void ContentsDownloadButton_Click(object sender, EventArgs e) + private void ContentsDownloadButton_Click(object? sender, EventArgs? e) { - OnDownloadClick?.Invoke(SelectedModule); + if (SelectedModule != null) + { + OnDownloadClick?.Invoke(SelectedModule); + } } - private void ContentsOpenButton_Click(object sender, EventArgs e) + private void ContentsOpenButton_Click(object? sender, EventArgs? e) { - Utilities.ProcessStartURL(manager.Cache.GetCachedFilename(SelectedModule.ToModule())); + if (SelectedModule != null + && manager?.Cache?.GetCachedFilename(SelectedModule.ToModule()) is string s) + { + Utilities.ProcessStartURL(s); + } } - private void _UpdateModContentsTree(CkanModule module, bool force = false) + private void _UpdateModContentsTree(CkanModule? module, bool force = false) { - ContentsPreviewTree.BackColor = SystemColors.Window; - ContentsPreviewTree.LineColor = SystemColors.WindowText; - - if (Equals(module, currentModContentsModule) && !force) + if (module == null) { - return; - } - else - { - currentModContentsModule = module; - } - if (module.IsMetapackage) - { - NotCachedLabel.Text = Properties.Resources.ModInfoNoDownload; + NotCachedLabel.Text = ""; ContentsPreviewTree.Enabled = false; ContentsDownloadButton.Enabled = false; ContentsOpenButton.Enabled = false; @@ -89,54 +88,79 @@ private void _UpdateModContentsTree(CkanModule module, bool force = false) } else { - ContentsPreviewTree.Enabled = true; - ContentsPreviewTree.Nodes.Clear(); - var rootNode = ContentsPreviewTree.Nodes.Add("", module.ToString(), "folderZip", "folderZip"); - if (!manager.Cache.IsMaybeCachedZip(module)) + ContentsPreviewTree.BackColor = SystemColors.Window; + ContentsPreviewTree.LineColor = SystemColors.WindowText; + + if (Equals(module, currentModContentsModule) && !force) { - NotCachedLabel.Text = Properties.Resources.ModInfoNotCached; - ContentsDownloadButton.Enabled = true; - ContentsOpenButton.Enabled = false; - ContentsPreviewTree.Enabled = false; + return; } else { - rootNode.Text = Path.GetFileName( - manager.Cache.GetCachedFilename(module)); - NotCachedLabel.Text = Properties.Resources.ModInfoCached; + currentModContentsModule = module; + } + if (module.IsMetapackage) + { + NotCachedLabel.Text = Properties.Resources.ModInfoNoDownload; + ContentsPreviewTree.Enabled = false; ContentsDownloadButton.Enabled = false; - ContentsOpenButton.Enabled = true; + ContentsOpenButton.Enabled = false; + ContentsPreviewTree.Nodes.Clear(); + } + else + { ContentsPreviewTree.Enabled = true; - - UseWaitCursor = true; - Task.Factory.StartNew(() => + ContentsPreviewTree.Nodes.Clear(); + var rootNode = ContentsPreviewTree.Nodes.Add("", module.ToString(), "folderZip", "folderZip"); + if (!manager?.Cache?.IsMaybeCachedZip(module) ?? false) { - var paths = new ModuleInstaller( - manager.CurrentInstance, - manager.Cache, - Main.Instance.currentUser) - .GetModuleContentsList(module) - // Load fully in bg - .ToArray(); - // Stop if user switched to another mod - if (rootNode.TreeView != null) + NotCachedLabel.Text = Properties.Resources.ModInfoNotCached; + ContentsDownloadButton.Enabled = true; + ContentsOpenButton.Enabled = false; + ContentsPreviewTree.Enabled = false; + } + else if (manager != null + && manager?.CurrentInstance != null + && manager?.Cache != null + && Main.Instance?.currentUser != null) + { + rootNode.Text = Path.GetFileName( + manager.Cache.GetCachedFilename(module)); + NotCachedLabel.Text = Properties.Resources.ModInfoCached; + ContentsDownloadButton.Enabled = false; + ContentsOpenButton.Enabled = true; + ContentsPreviewTree.Enabled = true; + + UseWaitCursor = true; + Task.Factory.StartNew(() => { - Util.Invoke(this, () => + var paths = new ModuleInstaller( + manager.CurrentInstance, + manager.Cache, + Main.Instance.currentUser) + .GetModuleContentsList(module) + // Load fully in bg + .ToArray(); + // Stop if user switched to another mod + if (rootNode.TreeView != null) { - ContentsPreviewTree.BeginUpdate(); - foreach (string path in paths) + Util.Invoke(this, () => { - AddContentPieces( - rootNode, - path.Split(new char[] {'/'})); - } - rootNode.ExpandAll(); - rootNode.EnsureVisible(); - ContentsPreviewTree.EndUpdate(); - UseWaitCursor = false; - }); - } - }); + ContentsPreviewTree.BeginUpdate(); + foreach (string path in paths) + { + AddContentPieces( + rootNode, + path.Split(new char[] {'/'})); + } + rootNode.ExpandAll(); + rootNode.EnsureVisible(); + ContentsPreviewTree.EndUpdate(); + UseWaitCursor = false; + }); + } + }); + } } } } @@ -160,30 +184,5 @@ private void AddContentPieces(TreeNode parent, IEnumerable pieces) } } - /// - /// Opens the folder of the double-clicked node - /// in the file browser of the user's system - /// - /// A node of the ContentsPreviewTree - private void OpenFileBrowser(TreeNode node) - { - string location = manager.CurrentInstance.ToAbsoluteGameDir(node.Name); - - if (File.Exists(location)) - { - // We need the Folder of the file - // Otherwise the OS would try to open the file in its default application - location = Path.GetDirectoryName(location); - } - - if (!Directory.Exists(location)) - { - // User either selected the parent node - // or clicked on the tree node of a cached, but not installed mod - return; - } - - Utilities.ProcessStartURL(location); - } } } diff --git a/GUI/Controls/ModInfoTabs/Metadata.cs b/GUI/Controls/ModInfoTabs/Metadata.cs index 3d18ef49e2..2d42b93c17 100644 --- a/GUI/Controls/ModInfoTabs/Metadata.cs +++ b/GUI/Controls/ModInfoTabs/Metadata.cs @@ -93,7 +93,7 @@ public void UpdateModInfo(GUIMod gui_module) }); } - public event Action OnChangeFilter; + public event Action? OnChangeFilter; private void UpdateAuthorLinks(List authors) { @@ -120,36 +120,43 @@ private LinkLabel AuthorLink(string name) return link; } - private void OnAuthorClick(object sender, LinkLabelLinkClickedEventArgs e) + private void OnAuthorClick(object? sender, LinkLabelLinkClickedEventArgs? e) { - var link = sender as LinkLabel; - var author = link.Text; - var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); - OnChangeFilter?.Invoke( - new SavedSearch() - { - Name = string.Format(Properties.Resources.AuthorSearchName, author), - Values = new List() + if (sender is LinkLabel link + && link.Text is string author) + { + var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); + OnChangeFilter?.Invoke( + new SavedSearch() { - ModSearch.FromAuthors(Enumerable.Repeat(author, 1)).Combined + Name = string.Format(Properties.Resources.AuthorSearchName, author), + Values = Enumerable.Repeat(ModSearch.FromAuthors(Enumerable.Repeat(author, 1)).Combined, 1) + .OfType() + .ToList(), }, - }, - merge); + merge); + } } - private void LinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + private void LinkLabel_LinkClicked(object? sender, LinkLabelLinkClickedEventArgs? e) { - Util.HandleLinkClicked((sender as LinkLabel).Text, e); + if (sender is LinkLabel lbl) + { + Util.HandleLinkClicked(lbl.Text, e); + } } - private void LinkLabel_KeyDown(object sender, KeyEventArgs e) + private void LinkLabel_KeyDown(object? sender, KeyEventArgs? e) { - switch (e.KeyCode) + if (sender is LinkLabel lbl) { - case Keys.Apps: - Util.LinkContextMenu((sender as LinkLabel).Text); - e.Handled = true; - break; + switch (e?.KeyCode) + { + case Keys.Apps: + Util.LinkContextMenu(lbl.Text); + e.Handled = true; + break; + } } } @@ -189,7 +196,7 @@ private int RightColumnWidth - MetadataTable.Margin.Horizontal - (int)MetadataTable.ColumnStyles[0].Width; - private void AddResourceLink(string label, Uri link) + private void AddResourceLink(string label, Uri? link) { const int vPadding = 5; if (link != null) diff --git a/GUI/Controls/ModInfoTabs/Relationships.cs b/GUI/Controls/ModInfoTabs/Relationships.cs index a5e042873f..f6d45d9082 100644 --- a/GUI/Controls/ModInfoTabs/Relationships.cs +++ b/GUI/Controls/ModInfoTabs/Relationships.cs @@ -59,7 +59,7 @@ public Relationships() DependsGraphTree.BeforeExpand += BeforeExpand; } - public GUIMod SelectedModule + public GUIMod? SelectedModule { set { @@ -70,21 +70,24 @@ public GUIMod SelectedModule ReverseRelationshipsCheckbox.CheckState = CheckState.Unchecked; } selectedModule = value; - UpdateModDependencyGraph(selectedModule.ToModule()); + UpdateModDependencyGraph(selectedModule?.ToModule()); } } get => selectedModule; } - public event Action ModuleDoubleClicked; + public event Action? ModuleDoubleClicked; - private void UpdateModDependencyGraph(CkanModule module) + private void UpdateModDependencyGraph(CkanModule? module) { - Util.Invoke(DependsGraphTree, () => _UpdateModDependencyGraph(module)); + if (module != null) + { + Util.Invoke(DependsGraphTree, () => _UpdateModDependencyGraph(module)); + } } - private GUIMod selectedModule; - private GameInstanceManager manager => Main.Instance.Manager; + private GUIMod? selectedModule; + private GameInstanceManager? manager => Main.Instance?.Manager; private readonly RepositoryDataManager repoData; private void DependsGraphTree_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e) @@ -110,7 +113,7 @@ private bool ImMyOwnGrandpa(TreeNode node) return false; } - private void ReverseRelationshipsCheckbox_Click(object sender, EventArgs e) + private void ReverseRelationshipsCheckbox_Click(object? sender, EventArgs? e) { ReverseRelationshipsCheckbox.CheckState = ReverseRelationshipsCheckbox.CheckState == CheckState.Unchecked @@ -122,56 +125,61 @@ private void ReverseRelationshipsCheckbox_Click(object sender, EventArgs e) : CheckState.Unchecked; } - private void ReverseRelationshipsCheckbox_CheckedChanged(object sender, EventArgs e) + private void ReverseRelationshipsCheckbox_CheckedChanged(object? sender, EventArgs? e) { - UpdateModDependencyGraph(SelectedModule.ToModule()); + UpdateModDependencyGraph(SelectedModule?.ToModule()); } private void _UpdateModDependencyGraph(CkanModule module) { - DependsGraphTree.BeginUpdate(); - DependsGraphTree.BackColor = SystemColors.Window; - DependsGraphTree.LineColor = SystemColors.WindowText; - DependsGraphTree.Nodes.Clear(); - IRegistryQuerier registry = RegistryManager.Instance(manager.CurrentInstance, repoData).registry; - TreeNode root = new TreeNode($"{module.name} {module.version}", 0, 0) - { - Name = module.identifier, - Tag = module - }; - DependsGraphTree.Nodes.Add(root); - AddChildren(registry, root); - root.Expand(); - // Expand virtual depends nodes - foreach (var node in root.Nodes.OfType() - .Where(nd => nd.Nodes.Count > 0 - && nd.ImageIndex == (int)RelationshipType.Depends + 1)) + if (manager?.CurrentInstance != null) { - node.Expand(); + DependsGraphTree.BeginUpdate(); + DependsGraphTree.BackColor = SystemColors.Window; + DependsGraphTree.LineColor = SystemColors.WindowText; + DependsGraphTree.Nodes.Clear(); + IRegistryQuerier registry = RegistryManager.Instance(manager.CurrentInstance, repoData).registry; + TreeNode root = new TreeNode($"{module.name} {module.version}", 0, 0) + { + Name = module.identifier, + Tag = module + }; + DependsGraphTree.Nodes.Add(root); + AddChildren(registry, root); + root.Expand(); + // Expand virtual depends nodes + foreach (var node in root.Nodes.OfType() + .Where(nd => nd.Nodes.Count > 0 + && nd.ImageIndex == (int)RelationshipType.Depends + 1)) + { + node.Expand(); + } + DependsGraphTree.EndUpdate(); } - DependsGraphTree.EndUpdate(); } - private void BeforeExpand(object sender, TreeViewCancelEventArgs args) + private void BeforeExpand(object? sender, TreeViewCancelEventArgs? args) { - IRegistryQuerier registry = RegistryManager.Instance(manager.CurrentInstance, repoData).registry; - TreeNode node = args.Node; - const int modsPerUpdate = 10; - - // Load in groups to reduce flickering - UseWaitCursor = true; - int lastStart = Math.Max(0, node.Nodes.Count - modsPerUpdate); - for (int start = 0; start <= lastStart; start += modsPerUpdate) + if (manager?.CurrentInstance != null && args?.Node is TreeNode node) { - // Copy start's value to a variable that won't change as we loop - int threadStart = start; - int nodesLeft = node.Nodes.Count - start; - Task.Factory.StartNew(() => - ExpandOnePage( - registry, node, threadStart, - // If next page is small (last), add it to this one, - // so the final page will be slower rather than faster - nodesLeft >= 2 * modsPerUpdate ? modsPerUpdate : nodesLeft)); + IRegistryQuerier registry = RegistryManager.Instance(manager.CurrentInstance, repoData).registry; + const int modsPerUpdate = 10; + + // Load in groups to reduce flickering + UseWaitCursor = true; + int lastStart = Math.Max(0, node.Nodes.Count - modsPerUpdate); + for (int start = 0; start <= lastStart; start += modsPerUpdate) + { + // Copy start's value to a variable that won't change as we loop + int threadStart = start; + int nodesLeft = node.Nodes.Count - start; + Task.Factory.StartNew(() => + ExpandOnePage( + registry, node, threadStart, + // If next page is small (last), add it to this one, + // so the final page will be slower rather than faster + nodesLeft >= 2 * modsPerUpdate ? modsPerUpdate : nodesLeft)); + } } } @@ -231,18 +239,17 @@ private void AddChildren(IRegistryQuerier registry, TreeNode node) // Load one layer of grandchildren on demand private IEnumerable GetChildren(IRegistryQuerier registry, TreeNode node) - { - var crit = manager.CurrentInstance.VersionCriteria(); // Skip children of nodes from circular dependencies // Tag is null for non-indexed nodes - return !ImMyOwnGrandpa(node) && node.Tag is CkanModule module - ? ReverseRelationshipsCheckbox.CheckState == CheckState.Unchecked - ? ForwardRelationships(registry, module, crit) - : ReverseRelationships(registry, module, crit) - : Enumerable.Empty(); - } - - private IEnumerable GetModRelationships(CkanModule module, RelationshipType which) + => !ImMyOwnGrandpa(node) + && node.Tag is CkanModule module + && manager?.CurrentInstance?.VersionCriteria() is GameVersionCriteria crit + ? ReverseRelationshipsCheckbox.CheckState == CheckState.Unchecked + ? ForwardRelationships(registry, module, crit) + : ReverseRelationships(registry, module, crit) + : Enumerable.Empty(); + + private static IEnumerable GetModRelationships(CkanModule module, RelationshipType which) { switch (which) { @@ -277,10 +284,10 @@ private IEnumerable ForwardRelationships(IRegistryQuerier registry, Ck // Then give up and note the name without a module ?? nonindexedNode(dependency, relationship)))); - private TreeNode findDependencyShallow(IRegistryQuerier registry, - RelationshipDescriptor relDescr, - RelationshipType relationship, - GameVersionCriteria crit) + private TreeNode? findDependencyShallow(IRegistryQuerier registry, + RelationshipDescriptor relDescr, + RelationshipType relationship, + GameVersionCriteria? crit) { var childNodes = relDescr.LatestAvailableWithProvides( registry, crit, @@ -294,7 +301,7 @@ private TreeNode findDependencyShallow(IRegistryQuerier registry, registry.InstalledDlls.ToHashSet(), // Maybe it's a DLC? registry.InstalledDlc, - out CkanModule matched)) + out CkanModule? matched)) { if (matched == null) { @@ -331,7 +338,7 @@ private TreeNode findDependencyShallow(IRegistryQuerier registry, else { // Several found or not same id, return a "provides" node - return providesNode(relDescr.ToString(), relationship, + return providesNode(relDescr.ToString() ?? "", relationship, childNodes.ToArray()); } } @@ -372,12 +379,13 @@ private TreeNode indexedNode(IRegistryQuerier registry, CkanModule module, RelationshipType relationship, RelationshipDescriptor relDescr, - GameVersionCriteria crit) + GameVersionCriteria? crit) { int icon = (int)relationship + 1; bool missingDLC = module.IsDLC && !registry.InstalledDlc.ContainsKey(module.identifier); bool compatible = crit != null && registry.IdentifierCompatible(module.identifier, crit); - string suffix = compatible ? "" + string suffix = compatible || manager?.CurrentInstance == null + ? "" : $" ({registry.CompatibleGameVersions(manager.CurrentInstance.game, module.identifier)})"; return new TreeNode($"{module.name} {module.version}{suffix}", icon, icon) { @@ -395,7 +403,7 @@ private TreeNode indexedNode(IRegistryQuerier registry, } private TreeNode nonModuleNode(RelationshipDescriptor relDescr, - ModuleVersion version, + ModuleVersion? version, RelationshipType relationship) { int icon = (int)relationship + 1; diff --git a/GUI/Controls/ModInfoTabs/Versions.cs b/GUI/Controls/ModInfoTabs/Versions.cs index a372f207a6..a0d3936d81 100644 --- a/GUI/Controls/ModInfoTabs/Versions.cs +++ b/GUI/Controls/ModInfoTabs/Versions.cs @@ -64,18 +64,18 @@ public GUIMod SelectedModule } } - private GameInstance currentInstance => Main.Instance.CurrentInstance; - private GameInstanceManager manager => Main.Instance.Manager; - private IUser user => Main.Instance.currentUser; + private static GameInstance? currentInstance => Main.Instance?.CurrentInstance; + private static GameInstanceManager? manager => Main.Instance?.Manager; + private static IUser? user => Main.Instance?.currentUser; - private readonly RepositoryDataManager repoData; - private GUIMod visibleGuiModule; - private bool ignoreItemCheck; - private CancellationTokenSource cancelTokenSrc; + private readonly RepositoryDataManager repoData; + private GUIMod? visibleGuiModule; + private bool ignoreItemCheck; + private CancellationTokenSource? cancelTokenSrc; private void VersionsListView_ItemCheck(object sender, ItemCheckEventArgs e) { - if (!ignoreItemCheck && e.CurrentValue != e.NewValue + if (!ignoreItemCheck && e.CurrentValue != e.NewValue && visibleGuiModule != null && VersionsListView.Items[e.Index].Tag is CkanModule module) { switch (e.NewValue) @@ -105,7 +105,9 @@ private void VersionsListView_ItemCheck(object sender, ItemCheckEventArgs e) private bool installable(ModuleInstaller installer, CkanModule module, IRegistryQuerier registry) - => installable(installer, module, registry, currentInstance.VersionCriteria()); + => currentInstance != null + && installable(installer, module, registry, + currentInstance.VersionCriteria()); [ForbidGUICalls] private static bool installable(ModuleInstaller installer, @@ -119,21 +121,25 @@ private static bool installable(ModuleInstaller installer, private bool allowInstall(CkanModule module) { + if (currentInstance == null || manager?.Cache == null || user == null) + { + return false; + } IRegistryQuerier registry = RegistryManager.Instance(currentInstance, repoData).registry; var installer = new ModuleInstaller(currentInstance, manager.Cache, user); return installable(installer, module, registry) - || Main.Instance.YesNoDialog( + || (Main.Instance?.YesNoDialog( string.Format(Properties.Resources.AllModVersionsInstallPrompt, module.ToString(), currentInstance.VersionCriteria().ToSummaryString(currentInstance.game)), Properties.Resources.AllModVersionsInstallYes, - Properties.Resources.AllModVersionsInstallNo); + Properties.Resources.AllModVersionsInstallNo) ?? false); } - private void visibleGuiModule_PropertyChanged(object sender, PropertyChangedEventArgs e) + private void visibleGuiModule_PropertyChanged(object? sender, PropertyChangedEventArgs? e) { - switch (e.PropertyName) + switch (e?.PropertyName) { case "SelectedMod": UpdateSelection(); @@ -143,118 +149,136 @@ private void visibleGuiModule_PropertyChanged(object sender, PropertyChangedEven private void UpdateSelection() { - bool prevIgnore = ignoreItemCheck; - ignoreItemCheck = true; - foreach (ListViewItem item in VersionsListView.Items) + if (visibleGuiModule != null) { - CkanModule module = item.Tag as CkanModule; - item.Checked = module.Equals(visibleGuiModule.SelectedMod); + bool prevIgnore = ignoreItemCheck; + ignoreItemCheck = true; + foreach (var item in VersionsListView.Items.OfType()) + { + var module = item.Tag as CkanModule; + item.Checked = module?.Equals(visibleGuiModule.SelectedMod) ?? false; + } + ignoreItemCheck = prevIgnore; } - ignoreItemCheck = prevIgnore; } private List getVersions(GUIMod gmod) { - IRegistryQuerier registry = RegistryManager.Instance(currentInstance, repoData).registry; - - // Can't be functional because AvailableByIdentifier throws exceptions - var versions = new List(); - try - { - versions = registry.AvailableByIdentifier(gmod.Identifier).ToList(); - } - catch (ModuleNotFoundKraken) + if (currentInstance != null) { - // Identifier unknown to registry, maybe installed from local .ckan - } + var registry = RegistryManager.Instance(currentInstance, repoData).registry; - // Take the module associated with GUIMod, if any, and append it to the list if it's not already there. - var installedModule = gmod.InstalledMod?.Module; - if (installedModule != null && !versions.Contains(installedModule)) - { - versions.Add(installedModule); - } + // Can't be functional because AvailableByIdentifier throws exceptions + var versions = new List(); + try + { + versions = registry.AvailableByIdentifier(gmod.Identifier).ToList(); + } + catch (ModuleNotFoundKraken) + { + // Identifier unknown to registry, maybe installed from local .ckan + } - return versions; + // Take the module associated with GUIMod, if any, and append it to the list if it's not already there. + var installedModule = gmod.InstalledMod?.Module; + if (installedModule != null && !versions.Contains(installedModule)) + { + versions.Add(installedModule); + } + + return versions; + } + return new List(); } private ListViewItem[] getItems(GUIMod gmod, List versions) { - IRegistryQuerier registry = RegistryManager.Instance(currentInstance, repoData).registry; - ModuleVersion installedVersion = registry.InstalledVersion(gmod.Identifier); - - var items = versions.OrderByDescending(module => module.version) - .Select(module => + if (currentInstance != null) { - CkanModule.GetMinMaxVersions( - new List() {module}, - out ModuleVersion minMod, out ModuleVersion maxMod, - out GameVersion minKsp, out GameVersion maxKsp); - ListViewItem toRet = new ListViewItem(new string[] - { - module.version.ToString(), - GameVersionRange.VersionSpan(currentInstance.game, minKsp, maxKsp), - module.release_date?.ToString("g") ?? "" - }) - { - Tag = module - }; - if (installedVersion != null && installedVersion.IsEqualTo(module.version)) - { - toRet.Font = new Font(toRet.Font, FontStyle.Bold); - } - if (module.Equals(gmod.SelectedMod)) + var registry = RegistryManager.Instance(currentInstance, repoData).registry; + var installedVersion = registry.InstalledVersion(gmod.Identifier); + + var items = versions.OrderByDescending(module => module.version) + .Select(module => { - toRet.Checked = true; - } - return toRet; - }).ToArray(); + CkanModule.GetMinMaxVersions( + new List() {module}, + out ModuleVersion? minMod, out ModuleVersion? maxMod, + out GameVersion? minKsp, out GameVersion? maxKsp); + ListViewItem toRet = new ListViewItem(new string[] + { + module.version.ToString(), + GameVersionRange.VersionSpan(currentInstance.game, + minKsp ?? GameVersion.Any, + maxKsp ?? GameVersion.Any), + module.release_date?.ToString("g") ?? "" + }) + { + Tag = module + }; + if (installedVersion != null && installedVersion.IsEqualTo(module.version)) + { + toRet.Font = new Font(toRet.Font, FontStyle.Bold); + } + if (module.Equals(gmod.SelectedMod)) + { + toRet.Checked = true; + } + return toRet; + }).ToArray(); - return items; + return items; + } + return new ListViewItem[] { }; } [ForbidGUICalls] private void checkInstallable(ListViewItem[] items) { - var registry = RegistryManager.Instance(currentInstance, repoData).registry; - var installer = new ModuleInstaller(currentInstance, manager.Cache, user); - ListViewItem latestCompatible = null; - // Load balance the items so they're processed roughly in-order instead of blocks - Partitioner.Create(items, true) - // Distribute across cores - .AsParallel() - // Return them as they're processed - .WithMergeOptions(ParallelMergeOptions.NotBuffered) - // Abort when they switch to another mod - .WithCancellation(cancelTokenSrc.Token) - // Check the important ones first - .OrderBy(item => (item.Tag as CkanModule) != visibleGuiModule.InstalledMod?.Module - && (item.Tag as CkanModule) != visibleGuiModule.SelectedMod) - // Slow step to be performed across multiple cores - .Where(item => installable(installer, item.Tag as CkanModule, registry)) - // Jump back to GUI thread for the updates for each compatible item - .ForAll(item => Util.Invoke(this, () => - { - if (latestCompatible == null || item.Index < latestCompatible.Index) + if (currentInstance != null && manager?.Cache != null + && user != null && cancelTokenSrc != null && visibleGuiModule != null) + { + var registry = RegistryManager.Instance(currentInstance, repoData).registry; + var installer = new ModuleInstaller(currentInstance, manager.Cache, user); + ListViewItem? latestCompatible = null; + // Load balance the items so they're processed roughly in-order instead of blocks + Partitioner.Create(items, true) + // Distribute across cores + .AsParallel() + // Return them as they're processed + .WithMergeOptions(ParallelMergeOptions.NotBuffered) + // Abort when they switch to another mod + .WithCancellation(cancelTokenSrc.Token) + // Check the important ones first + .OrderBy(item => (item.Tag as CkanModule) != visibleGuiModule.InstalledMod?.Module + && (item.Tag as CkanModule) != visibleGuiModule.SelectedMod) + // Slow step to be performed across multiple cores + .Where(item => item.Tag is CkanModule m + && installable(installer, m, registry)) + // Jump back to GUI thread for the updates for each compatible item + .ForAll(item => Util.Invoke(this, () => { - VersionsListView.BeginUpdate(); - if (latestCompatible != null) + if (latestCompatible == null || item.Index < latestCompatible.Index) { - // Revert color of previous best guess - latestCompatible.BackColor = Color.LightGreen; - latestCompatible.ForeColor = SystemColors.ControlText; + VersionsListView.BeginUpdate(); + if (latestCompatible != null) + { + // Revert color of previous best guess + latestCompatible.BackColor = Color.LightGreen; + latestCompatible.ForeColor = SystemColors.ControlText; + } + latestCompatible = item; + item.BackColor = Color.Green; + item.ForeColor = Color.White; + VersionsListView.EndUpdate(); } - latestCompatible = item; - item.BackColor = Color.Green; - item.ForeColor = Color.White; - VersionsListView.EndUpdate(); - } - else - { - item.BackColor = Color.LightGreen; - } - })); - Util.Invoke(this, () => UseWaitCursor = false); + else + { + item.BackColor = Color.LightGreen; + } + })); + Util.Invoke(this, () => UseWaitCursor = false); + } } private void Refresh(GUIMod gmod) diff --git a/GUI/Controls/PlayTime.cs b/GUI/Controls/PlayTime.cs index d4cd26ecc6..f287f98254 100755 --- a/GUI/Controls/PlayTime.cs +++ b/GUI/Controls/PlayTime.cs @@ -44,7 +44,7 @@ public void loadAllPlayTime(GameInstanceManager manager) /// /// Invoked when the user clicks OK /// - public event Action Done; + public event Action? Done; /// /// Open the user guide when the user presses F1 @@ -72,12 +72,12 @@ private void PlayTimeGrid_CellValueChanged(object sender, DataGridViewCellEventA ShowTotal(); } - private void OKButton_Click(object sender, EventArgs e) + private void OKButton_Click(object? sender, EventArgs? e) { Done?.Invoke(); } - private List rows; + private List? rows; } /// @@ -92,7 +92,7 @@ public class PlayTimeRow public PlayTimeRow(string name, GameInstance instance) { Name = name; - PlayTime = instance.playTime; + PlayTime = instance.playTime ?? new TimeLog(); path = TimeLog.GetPath(instance.CkanDir()); } diff --git a/GUI/Controls/TagsLabelsLinkList.cs b/GUI/Controls/TagsLabelsLinkList.cs index b1a2acea0b..17c20d406e 100644 --- a/GUI/Controls/TagsLabelsLinkList.cs +++ b/GUI/Controls/TagsLabelsLinkList.cs @@ -18,8 +18,8 @@ namespace CKAN.GUI public partial class TagsLabelsLinkList : FlowLayoutPanel { [ForbidGUICalls] - public void UpdateTagsAndLabels(IEnumerable tags, - IEnumerable labels) + public void UpdateTagsAndLabels(IEnumerable? tags, + IEnumerable? labels) { Util.Invoke(this, () => { @@ -61,12 +61,12 @@ public string TagToolTipText } } - public event Action TagClicked; - public event Action LabelClicked; + public event Action? TagClicked; + public event Action? LabelClicked; private string tagToolTip = Properties.Resources.FilterLinkToolTip; - private static int LinkLabelBottom(LinkLabel lbl) + private static int LinkLabelBottom(LinkLabel? lbl) => lbl == null ? 0 : lbl.Bottom + lbl.Margin.Bottom + lbl.Padding.Bottom; @@ -93,18 +93,20 @@ private LinkLabel TagLabelLink(string name, return link; } - private void TagLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + private void TagLinkLabel_LinkClicked(object? sender, LinkLabelLinkClickedEventArgs? e) { - var link = sender as LinkLabel; - var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); - TagClicked?.Invoke(link.Tag as ModuleTag, merge); + if (sender is LinkLabel link && link.Tag is ModuleTag t) + { + TagClicked?.Invoke(t, ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift)); + } } - private void LabelLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + private void LabelLinkLabel_LinkClicked(object? sender, LinkLabelLinkClickedEventArgs? e) { - var link = sender as LinkLabel; - var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); - LabelClicked?.Invoke(link.Tag as ModuleLabel, merge); + if (sender is LinkLabel link && link.Tag is ModuleLabel l) + { + LabelClicked?.Invoke(l, ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift)); + } } private readonly ToolTip ToolTip = new ToolTip() diff --git a/GUI/Controls/ThemedTabControl.cs b/GUI/Controls/ThemedTabControl.cs index 05955fe72e..1adfc2a36e 100644 --- a/GUI/Controls/ThemedTabControl.cs +++ b/GUI/Controls/ThemedTabControl.cs @@ -38,10 +38,11 @@ protected override void OnDrawItem(DrawItemEventArgs e) var textRect = e.Bounds; // Image - var imageIndex = !string.IsNullOrEmpty(tabPage.ImageKey) - ? ImageList.Images.IndexOfKey(tabPage.ImageKey) - : tabPage.ImageIndex; - if (imageIndex > -1) + if (ImageList != null + && (!string.IsNullOrEmpty(tabPage.ImageKey) + ? ImageList.Images.IndexOfKey(tabPage.ImageKey) + : tabPage.ImageIndex) is int imageIndex + && imageIndex > -1) { var image = ImageList.Images[imageIndex]; var offsetY = (e.Bounds.Height - image.Height) / 2; diff --git a/GUI/Controls/TransparentTextBox.cs b/GUI/Controls/TransparentTextBox.cs index e85b493b82..f5840393dd 100644 --- a/GUI/Controls/TransparentTextBox.cs +++ b/GUI/Controls/TransparentTextBox.cs @@ -1,4 +1,7 @@ using System.Windows.Forms; +#if NET5_0_OR_GREATER +using System.Runtime.Versioning; +#endif namespace CKAN.GUI { @@ -9,6 +12,9 @@ namespace CKAN.GUI /// Multiline is set to true. /// Used in . /// + #if NET5_0_OR_GREATER + [SupportedOSPlatform("windows")] + #endif public class TransparentTextBox : TextBox { public TransparentTextBox() diff --git a/GUI/Controls/TriStateToggle.cs b/GUI/Controls/TriStateToggle.cs index ac677ab0fa..84f7a4f048 100644 --- a/GUI/Controls/TriStateToggle.cs +++ b/GUI/Controls/TriStateToggle.cs @@ -43,7 +43,7 @@ public TriStateToggle() public bool? Value { get => YesRadioButton.Checked ? true - : NoRadioButton.Checked ? (bool?)false + : NoRadioButton.Checked ? false : null; set { @@ -62,7 +62,7 @@ public bool? Value } } - public event Action Changed; + public event Action? Changed; private RadioButton MakeRadioButton(int index, Bitmap icon, string tooltip, bool check = false) { @@ -82,18 +82,20 @@ private RadioButton MakeRadioButton(int index, Bitmap icon, string tooltip, bool return rb; } - private void RadioButtonChanged(object sender, EventArgs e) + private void RadioButtonChanged(object? sender, EventArgs? e) { - var butt = sender as RadioButton; - // Will probably fire 3 times per click, only pass along one of them - if (butt.Checked) + if (sender is RadioButton butt) { - Changed?.Invoke(Value); - butt.BackColor = SystemColors.Highlight; - } - else - { - butt.BackColor = DefaultBackColor; + // Will probably fire 3 times per click, only pass along one of them + if (butt.Checked) + { + Changed?.Invoke(Value); + butt.BackColor = SystemColors.Highlight; + } + else + { + butt.BackColor = DefaultBackColor; + } } } diff --git a/GUI/Controls/UnmanagedFiles.cs b/GUI/Controls/UnmanagedFiles.cs index 128223e583..624c918ad1 100644 --- a/GUI/Controls/UnmanagedFiles.cs +++ b/GUI/Controls/UnmanagedFiles.cs @@ -9,6 +9,7 @@ #if NET5_0_OR_GREATER using System.Runtime.Versioning; #endif +using System.Diagnostics.CodeAnalysis; using log4net; @@ -27,7 +28,10 @@ public UnmanagedFiles() GameFolderTree.TreeViewNodeSorter = new DirsFirstSorter(); } - public void LoadFiles(GameInstance inst, RepositoryDataManager repoData, IUser user) + [MemberNotNull(nameof(inst), nameof(user), nameof(registry))] + public void LoadFiles(GameInstance inst, + RepositoryDataManager repoData, + IUser user) { this.inst = inst; this.user = user; @@ -38,7 +42,7 @@ public void LoadFiles(GameInstance inst, RepositoryDataManager repoData, IUser u /// /// Invoked when the user clicks OK /// - public event Action Done; + public event Action? Done; /// /// Open the user guide when the user presses F1 @@ -50,36 +54,39 @@ protected override void OnHelpRequested(HelpEventArgs evt) private void _UpdateGameFolderTree() { - GameFolderTree.BackColor = SystemColors.Window; - GameFolderTree.LineColor = SystemColors.WindowText; + if (inst != null && registry != null) + { + GameFolderTree.BackColor = SystemColors.Window; + GameFolderTree.LineColor = SystemColors.WindowText; - GameFolderTree.Nodes.Clear(); - var rootNode = GameFolderTree.Nodes.Add( - "", - Platform.FormatPath(inst.GameDir()), - "folder", "folder"); + GameFolderTree.Nodes.Clear(); + var rootNode = GameFolderTree.Nodes.Add( + "", + Platform.FormatPath(inst.GameDir()), + "folder", "folder"); - UseWaitCursor = true; - Task.Factory.StartNew(() => - { - var paths = inst?.UnmanagedFiles(registry).ToArray() - ?? Array.Empty(); - Util.Invoke(this, () => + UseWaitCursor = true; + Task.Factory.StartNew(() => { - GameFolderTree.BeginUpdate(); - foreach (string path in paths) + var paths = inst.UnmanagedFiles(registry).ToArray() + ?? Array.Empty(); + Util.Invoke(this, () => { - AddContentPieces(rootNode, path.Split(new char[] {'/'})); - } - rootNode.Expand(); - rootNode.EnsureVisible(); - ExpandDefaultModDir(inst.game); - // The nodes don't have children at first, so the sort needs to be re-applied after they're added - GameFolderTree.Sort(); - GameFolderTree.EndUpdate(); - UseWaitCursor = false; + GameFolderTree.BeginUpdate(); + foreach (string path in paths) + { + AddContentPieces(rootNode, path.Split(new char[] {'/'})); + } + rootNode.Expand(); + rootNode.EnsureVisible(); + ExpandDefaultModDir(inst.game); + // The nodes don't have children at first, so the sort needs to be re-applied after they're added + GameFolderTree.Sort(); + GameFolderTree.EndUpdate(); + UseWaitCursor = false; + }); }); - }); + } } private IEnumerable ParentPaths(string[] pathPieces) @@ -116,12 +123,12 @@ private void AddContentPieces(TreeNode parent, IEnumerable pieces) } } - private void RefreshButton_Click(object sender, EventArgs e) + private void RefreshButton_Click(object? sender, EventArgs? e) { Refresh(); } - private void ExpandAllButton_Click(object sender, EventArgs e) + private void ExpandAllButton_Click(object? sender, EventArgs? e) { GameFolderTree.BeginUpdate(); GameFolderTree.ExpandAll(); @@ -130,7 +137,7 @@ private void ExpandAllButton_Click(object sender, EventArgs e) GameFolderTree.Focus(); } - private void CollapseAllButton_Click(object sender, EventArgs e) + private void CollapseAllButton_Click(object? sender, EventArgs? e) { GameFolderTree.BeginUpdate(); GameFolderTree.CollapseAll(); @@ -139,114 +146,97 @@ private void CollapseAllButton_Click(object sender, EventArgs e) GameFolderTree.Focus(); } - private void ResetCollapseButton_Click(object sender, EventArgs e) + private void ResetCollapseButton_Click(object? sender, EventArgs? e) { - GameFolderTree.BeginUpdate(); - GameFolderTree.CollapseAll(); - GameFolderTree.Nodes[0].Expand(); - ExpandDefaultModDir(inst.game); - GameFolderTree.Nodes[0].EnsureVisible(); - GameFolderTree.EndUpdate(); - GameFolderTree.Focus(); + if (inst != null) + { + GameFolderTree.BeginUpdate(); + GameFolderTree.CollapseAll(); + GameFolderTree.Nodes[0].Expand(); + ExpandDefaultModDir(inst.game); + GameFolderTree.Nodes[0].EnsureVisible(); + GameFolderTree.EndUpdate(); + GameFolderTree.Focus(); + } } - private void ShowInFolderButton_Click(object sender, EventArgs e) + private void ShowInFolderButton_Click(object? sender, EventArgs? e) { - OpenFileBrowser(GameFolderTree.SelectedNode); + Utilities.OpenFileBrowser(GameFolderTree.SelectedNode.Name); GameFolderTree.Focus(); } - private void DeleteButton_Click(object sender, EventArgs e) + private void DeleteButton_Click(object? sender, EventArgs? e) { - var relPath = GameFolderTree.SelectedNode?.Name; - var absPath = inst.ToAbsoluteGameDir(relPath); - log.DebugFormat("Trying to delete {0}", absPath); - if (inst.HasManagedFiles(registry, absPath)) - { - Main.Instance.ErrorDialog(Properties.Resources.FolderContainsManagedFiles, relPath); - } - else if (!string.IsNullOrEmpty(relPath) && Main.Instance.YesNoDialog( - string.Format(Properties.Resources.DeleteUnmanagedFileConfirmation, - Platform.FormatPath(relPath)), - Properties.Resources.DeleteUnmanagedFileDelete, - Properties.Resources.DeleteUnmanagedFileCancel)) + if (inst != null && registry != null + && GameFolderTree.SelectedNode?.Name is string relPath) { - try + var absPath = inst.ToAbsoluteGameDir(relPath); + log.DebugFormat("Trying to delete {0}", absPath); + if (inst.HasManagedFiles(registry, absPath)) { - if (File.Exists(absPath)) + Main.Instance?.ErrorDialog(Properties.Resources.FolderContainsManagedFiles, relPath); + } + else if (!string.IsNullOrEmpty(relPath) + && (Main.Instance?.YesNoDialog(string.Format(Properties.Resources.DeleteUnmanagedFileConfirmation, + Platform.FormatPath(relPath)), + Properties.Resources.DeleteUnmanagedFileDelete, + Properties.Resources.DeleteUnmanagedFileCancel) ?? false)) + { + try { - File.Delete(absPath); + if (File.Exists(absPath)) + { + File.Delete(absPath); + } + else if (Directory.Exists(absPath)) + { + Directory.Delete(absPath, true); + } + GameFolderTree.Nodes.Remove(GameFolderTree.SelectedNode); } - else if (Directory.Exists(absPath)) + catch (Exception exc) { - Directory.Delete(absPath, true); + user?.RaiseError(exc.Message); } - GameFolderTree.Nodes.Remove(GameFolderTree.SelectedNode); - } - catch (Exception exc) - { - user.RaiseError(exc.Message); } } } private void GameFolderTree_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e) { - OpenFileBrowser(e.Node); + Utilities.OpenFileBrowser(e.Node.Name); } - private void OKButton_Click(object sender, EventArgs e) + private void OKButton_Click(object? sender, EventArgs? e) { Done?.Invoke(); } - /// - /// Opens the folder of the double-clicked node - /// in the file browser of the user's system - /// - /// A node of the GameFolderTree - private void OpenFileBrowser(TreeNode node) - { - if (node != null) - { - string location = inst.ToAbsoluteGameDir(node.Name); - - if (File.Exists(location)) - { - // We need the Folder of the file - // Otherwise the OS would try to open the file in its default application - location = Path.GetDirectoryName(location); - } - - if (!Directory.Exists(location)) - { - // User either selected the parent node - // or clicked on the tree node of a cached, but not installed mod - return; - } - - Utilities.ProcessStartURL(location); - } - } - - private GameInstance inst; - private IUser user; - private Registry registry; + private GameInstance? inst; + private IUser? user; + private Registry? registry; private static readonly ILog log = LogManager.GetLogger(typeof(UnmanagedFiles)); } + #if NET5_0_OR_GREATER + [SupportedOSPlatform("windows")] + #endif internal class DirsFirstSorter : IComparer, IComparer { - public int Compare(object a, object b) + public int Compare(object? a, object? b) => Compare(a as TreeNode, b as TreeNode); - public int Compare(TreeNode a, TreeNode b) - => a.Nodes.Count > 0 - ? b.Nodes.Count > 0 - ? string.Compare(a.Text, b.Text) - : -1 - : b.Nodes.Count > 0 - ? 1 - : string.Compare(a.Text, b.Text); + public int Compare(TreeNode? a, TreeNode? b) + => a == null ? b == null ? 0 + : -1 + : b == null ? 1 + : a.Nodes.Count > 0 + ? b.Nodes.Count > 0 + ? string.Compare(a.Text, b.Text) + : -1 + : b.Nodes.Count > 0 + ? 1 + : string.Compare(a.Text, b.Text); } } diff --git a/GUI/Controls/Wait.cs b/GUI/Controls/Wait.cs index 9dcd29ffab..39db5c4b10 100644 --- a/GUI/Controls/Wait.cs +++ b/GUI/Controls/Wait.cs @@ -29,10 +29,10 @@ public Wait() [ForbidGUICalls] - public void StartWaiting(Action mainWork, - Action postWork, + public void StartWaiting(Action mainWork, + Action postWork, bool cancelable, - object param) + object? param) { bgLogic = mainWork; postLogic = postWork; @@ -42,9 +42,9 @@ public void StartWaiting(Action mainWork, bgWorker.RunWorkerAsync(param); } - public event Action OnRetry; - public event Action OnCancel; - public event Action OnOk; + public event Action? OnRetry; + public event Action? OnCancel; + public event Action? OnOk; #pragma warning disable IDE0027 @@ -89,7 +89,7 @@ public void SetProgress(string label, long remaining, long total) { Util.Invoke(this, () => { - if (progressBars.TryGetValue(label, out ProgressBar pb)) + if (progressBars.TryGetValue(label, out ProgressBar? pb)) { // download_size is allowed to be 0 pb.Value = Math.Max(pb.Minimum, Math.Min(pb.Maximum, @@ -133,8 +133,8 @@ public void SetModuleProgress(CkanModule module, long remaining, long total) SetProgress(module.ToString(), remaining, total); } - private Action bgLogic; - private Action postLogic; + private Action? bgLogic; + private Action? postLogic; private readonly BackgroundWorker bgWorker = new BackgroundWorker() { @@ -142,12 +142,12 @@ public void SetModuleProgress(CkanModule module, long remaining, long total) WorkerSupportsCancellation = true, }; - private void DoWork(object sender, DoWorkEventArgs e) + private void DoWork(object? sender, DoWorkEventArgs? e) { bgLogic?.Invoke(sender, e); } - private void RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) + private void RunWorkerCompleted(object? sender, RunWorkerCompletedEventArgs? e) { postLogic?.Invoke(sender, e); } @@ -295,7 +295,7 @@ public void SetMainProgress(int percent, long bytesPerSecond, long bytesLeft) }); } - private string lastProgressMessage; + private string? lastProgressMessage; [ForbidGUICalls] private void ClearLog() @@ -309,12 +309,12 @@ public void AddLogMessage(string message) LogTextBox.AppendText(message + "\r\n"); } - private void RetryCurrentActionButton_Click(object sender, EventArgs e) + private void RetryCurrentActionButton_Click(object? sender, EventArgs? e) { OnRetry?.Invoke(); } - private void CancelCurrentActionButton_Click(object sender, EventArgs e) + private void CancelCurrentActionButton_Click(object? sender, EventArgs? e) { bgWorker.CancelAsync(); if (OnCancel != null) @@ -325,7 +325,7 @@ private void CancelCurrentActionButton_Click(object sender, EventArgs e) } } - private void OkButton_Click(object sender, EventArgs e) + private void OkButton_Click(object? sender, EventArgs? e) { OnOk?.Invoke(); } diff --git a/GUI/Dialogs/AboutDialog.cs b/GUI/Dialogs/AboutDialog.cs index 337d9b80da..3af2f12208 100644 --- a/GUI/Dialogs/AboutDialog.cs +++ b/GUI/Dialogs/AboutDialog.cs @@ -18,20 +18,25 @@ public AboutDialog() versionLabel.Text = string.Format(Properties.Resources.AboutDialogLabel2Text, Meta.GetVersion()); } - private void linkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + private void linkLabel_LinkClicked(object? sender, LinkLabelLinkClickedEventArgs? e) { - string url = (sender as LinkLabel).Text; - Util.HandleLinkClicked(url, e); + if (sender is LinkLabel l) + { + Util.HandleLinkClicked(l.Text, e); + } } - private void linkLabel_KeyDown(object sender, KeyEventArgs e) + private void linkLabel_KeyDown(object? sender, KeyEventArgs? e) { - switch (e.KeyCode) + if (sender is LinkLabel l) { - case Keys.Apps: - Util.LinkContextMenu((sender as LinkLabel).Text); - e.Handled = true; - break; + switch (e?.KeyCode) + { + case Keys.Apps: + Util.LinkContextMenu(l.Text); + e.Handled = true; + break; + } } } diff --git a/GUI/Dialogs/CloneGameInstanceDialog.cs b/GUI/Dialogs/CloneGameInstanceDialog.cs index 96ff730d98..562e5a2c2b 100644 --- a/GUI/Dialogs/CloneGameInstanceDialog.cs +++ b/GUI/Dialogs/CloneGameInstanceDialog.cs @@ -10,8 +10,6 @@ using Autofac; -using CKAN.Games; - namespace CKAN.GUI { /// @@ -26,7 +24,7 @@ public partial class CloneGameInstanceDialog : Form private readonly GameInstanceManager manager; private readonly IUser user; - public CloneGameInstanceDialog(GameInstanceManager manager, IUser user, string selectedInstanceName = null) + public CloneGameInstanceDialog(GameInstanceManager manager, IUser user, string? selectedInstanceName = null) : base() { this.manager = manager; @@ -53,18 +51,18 @@ public CloneGameInstanceDialog(GameInstanceManager manager, IUser user, string s #region clone - private void comboBoxKnownInstance_SelectedIndexChanged(object sender, EventArgs e) + private void comboBoxKnownInstance_SelectedIndexChanged(object? sender, EventArgs? e) { - string sel = comboBoxKnownInstance.SelectedItem as string; - textBoxClonePath.Text = string.IsNullOrEmpty(sel) - ? "" - : Platform.FormatPath(manager.Instances[sel].GameDir()); + textBoxClonePath.Text = comboBoxKnownInstance.SelectedItem is string sel + && string.IsNullOrEmpty(sel) + ? Platform.FormatPath(manager.Instances[sel].GameDir()) + : ""; } /// /// Open an file dialog to search for a game instance, like in ManageGameInstancesDialog. /// - private void buttonInstancePathSelection_Click(object sender, EventArgs e) + private void buttonInstancePathSelection_Click(object? sender, EventArgs? e) { // Create a new FileDialog object OpenFileDialog instanceDialog = new OpenFileDialog() @@ -93,7 +91,7 @@ private void buttonInstancePathSelection_Click(object sender, EventArgs e) /// User is done. Start cloning or faking, depending on the clicked radio button. /// Close the window if everything went right. /// - private async void buttonOK_Click(object sender, EventArgs e) + private async void buttonOK_Click(object? sender, EventArgs? e) { string existingPath = textBoxClonePath.Text; string newName = textBoxNewName.Text; @@ -123,10 +121,11 @@ private async void buttonOK_Click(object sender, EventArgs e) try { - if (!manager.Instances.TryGetValue(comboBoxKnownInstance.SelectedItem as string, out GameInstance instanceToClone) - || existingPath != Platform.FormatPath(instanceToClone.GameDir())) + if (comboBoxKnownInstance.SelectedItem is string s + && (!manager.Instances.TryGetValue(s, out GameInstance? instanceToClone) + || existingPath != Platform.FormatPath(instanceToClone.GameDir()))) { - IGame sourceGame = manager.DetermineGame(new DirectoryInfo(existingPath), user); + var sourceGame = manager.DetermineGame(new DirectoryInfo(existingPath), user); if (sourceGame == null) { // User cancelled, let them try again @@ -135,18 +134,18 @@ private async void buttonOK_Click(object sender, EventArgs e) } instanceToClone = new GameInstance( sourceGame, existingPath, "irrelevant", user); - } - await Task.Run(() => - { - if (instanceToClone.Valid) - { - manager.CloneInstance(instanceToClone, newName, newPath, checkBoxShareStock.Checked); - } - else + await Task.Run(() => { - throw new NotKSPDirKraken(instanceToClone.GameDir()); - } - }); + if (instanceToClone.Valid) + { + manager.CloneInstance(instanceToClone, newName, newPath, checkBoxShareStock.Checked); + } + else + { + throw new NotKSPDirKraken(instanceToClone.GameDir()); + } + }); + } } catch (InstanceNameTakenKraken) { @@ -164,7 +163,7 @@ await Task.Run(() => catch (PathErrorKraken kraken) { user.RaiseError(string.Format(Properties.Resources.CloneFakeKspDialogDestinationNotEmpty, - Platform.FormatPath(kraken.path))); + Platform.FormatPath(kraken?.path ?? ""))); reactivateDialog(); return; } @@ -197,7 +196,7 @@ await Task.Run(() => Close(); } - private void buttonCancel_Click(object sender, EventArgs e) + private void buttonCancel_Click(object? sender, EventArgs? e) { DialogResult = DialogResult.Cancel; Close(); @@ -217,7 +216,7 @@ private void reactivateDialog() progressBar.Hide(); } - private void buttonPathBrowser_Click(object sender, EventArgs e) + private void buttonPathBrowser_Click(object? sender, EventArgs? e) { if (folderBrowserDialogNewPath.ShowDialog(this).Equals(DialogResult.OK)) { diff --git a/GUI/Dialogs/CompatibleGameVersionsDialog.cs b/GUI/Dialogs/CompatibleGameVersionsDialog.cs index 28bd15383d..d22d5c62f1 100644 --- a/GUI/Dialogs/CompatibleGameVersionsDialog.cs +++ b/GUI/Dialogs/CompatibleGameVersionsDialog.cs @@ -65,7 +65,7 @@ protected override void OnHelpButtonClicked(CancelEventArgs evt) evt.Cancel = Util.TryOpenWebPage(HelpURLs.CompatibleGameVersions); } - private void CompatibleGameVersionsDialog_Shown(object sender, EventArgs e) + private void CompatibleGameVersionsDialog_Shown(object? sender, EventArgs? e) { if (_inst.CompatibleVersionsAreFromDifferentGameVersion) { @@ -94,7 +94,7 @@ private void SortAndAddVersionsToList(List versions, List() diff --git a/GUI/Dialogs/DownloadsFailedDialog.cs b/GUI/Dialogs/DownloadsFailedDialog.cs index 9b604f2a59..02e10d0765 100644 --- a/GUI/Dialogs/DownloadsFailedDialog.cs +++ b/GUI/Dialogs/DownloadsFailedDialog.cs @@ -73,7 +73,7 @@ public DownloadsFailedDialog( } [ForbidGUICalls] - public object[] Wait() => task.Task.Result; + public object[]? Wait() => task.Task.Result; /// /// True if user clicked the abort button, false otherwise @@ -108,7 +108,7 @@ protected override void OnHelpButtonClicked(CancelEventArgs evt) evt.Cancel = Util.TryOpenWebPage(HelpURLs.DownloadsFailed); } - private void DownloadsGrid_SelectionChanged(object sender, EventArgs e) + private void DownloadsGrid_SelectionChanged(object? sender, EventArgs? e) { // Don't clutter the screen with a highlight we don't use DownloadsGrid.ClearSelection(); @@ -152,14 +152,14 @@ private void DownloadsGrid_CellEndEdit(object sender, DataGridViewCellEventArgs } } - private void RetryButton_Click(object sender, EventArgs e) + private void RetryButton_Click(object? sender, EventArgs? e) { Abort = false; task.SetResult(Skip); Close(); } - private void AbortButton_Click(object sender, EventArgs e) + private void AbortButton_Click(object? sender, EventArgs? e) { Abort = true; task.SetResult(null); @@ -168,7 +168,7 @@ private void AbortButton_Click(object sender, EventArgs e) private readonly List rows; private readonly Func rowsLinked; - private readonly TaskCompletionSource task = new TaskCompletionSource(); + private readonly TaskCompletionSource task = new TaskCompletionSource(); } /// diff --git a/GUI/Dialogs/EditLabelsDialog.cs b/GUI/Dialogs/EditLabelsDialog.cs index 13ea333ace..46d75de1a7 100644 --- a/GUI/Dialogs/EditLabelsDialog.cs +++ b/GUI/Dialogs/EditLabelsDialog.cs @@ -6,6 +6,7 @@ #if NET5_0_OR_GREATER using System.Runtime.Versioning; #endif +using System.Diagnostics.CodeAnalysis; namespace CKAN.GUI { @@ -40,30 +41,27 @@ private void LoadTree() { LabelSelectionTree.BeginUpdate(); LabelSelectionTree.Nodes.Clear(); - var groups = labels.Labels - .GroupBy(l => l.InstanceName) - .OrderBy(g => g.Key == null) - .ThenBy(g => g.Key); - foreach (var group in groups) - { - string groupName = string.IsNullOrEmpty(group.Key) - ? Properties.Resources.ModuleLabelListGlobal - : group.Key; - LabelSelectionTree.Nodes.Add(new TreeNode( - groupName, - group.Select(mlbl => new TreeNode(mlbl.Name) - { - // Windows's TreeView has a bug where the node's visual - // width is based on the owning TreeView.Font rather - // than TreeNode.Font, so to ensure there's enough space, - // we have to make the default bold and then override it - // for non-bold nodes. - NodeFont = new Font(LabelSelectionTree.Font, FontStyle.Regular), - Tag = mlbl - }) - .ToArray() - )); - } + LabelSelectionTree.Nodes.AddRange( + labels.Labels + .GroupBy(l => l.InstanceName) + .OrderBy(g => g.Key == null) + .ThenBy(g => g.Key) + .Select(g => new TreeNode( + g.Key == null || string.IsNullOrEmpty(g.Key) + ? Properties.Resources.ModuleLabelListGlobal + : g.Key, + g.Select(mlbl => new TreeNode(mlbl.Name) + { + // Windows's TreeView has a bug where the node's visual + // width is based on the owning TreeView.Font rather + // than TreeNode.Font, so to ensure there's enough space, + // we have to make the default bold and then override it + // for non-bold nodes. + NodeFont = new Font(LabelSelectionTree.Font, FontStyle.Regular), + Tag = mlbl, + }) + .ToArray())) + .ToArray()); EnableDisableUpDownButtons(); if (currentlyEditing != null) { @@ -94,39 +92,45 @@ protected override void OnHelpButtonClicked(CancelEventArgs evt) evt.Cancel = Util.TryOpenWebPage(HelpURLs.Labels); } - private void LabelSelectionTree_BeforeSelect(object sender, TreeViewCancelEventArgs e) + private void LabelSelectionTree_BeforeSelect(object? sender, TreeViewCancelEventArgs? e) { - if (e.Node == null) - { - e.Cancel = false; - } - else if (e.Node.Tag == null) - { - e.Cancel = true; - } - else if (!TryCloseEdit()) - { - e.Cancel = true; - } - else + if (e != null) { - StartEdit(e.Node.Tag as ModuleLabel); - e.Cancel = false; + if (e.Node == null) + { + e.Cancel = false; + } + else if (e.Node.Tag == null) + { + e.Cancel = true; + } + else if (!TryCloseEdit()) + { + e.Cancel = true; + } + else if (e.Node.Tag is ModuleLabel l) + { + StartEdit(l); + e.Cancel = false; + } } } - private void LabelSelectionTree_BeforeCollapse(object sender, TreeViewCancelEventArgs e) + private void LabelSelectionTree_BeforeCollapse(object? sender, TreeViewCancelEventArgs? e) { - e.Cancel = true; + if (e != null) + { + e.Cancel = true; + } } - private void CreateButton_Click(object sender, EventArgs e) + private void CreateButton_Click(object? sender, EventArgs? e) { LabelSelectionTree.SelectedNode = null; - StartEdit(new ModuleLabel()); + StartEdit(new ModuleLabel("")); } - private void ColorButton_Click(object sender, EventArgs e) + private void ColorButton_Click(object? sender, EventArgs? e) { var dlg = new ColorDialog() { @@ -142,7 +146,7 @@ private void ColorButton_Click(object sender, EventArgs e) } } - private void SaveButton_Click(object sender, EventArgs e) + private void SaveButton_Click(object? sender, EventArgs? e) { if (TrySave(out string errMsg)) { @@ -154,23 +158,21 @@ private void SaveButton_Click(object sender, EventArgs e) } } - private void CancelEditButton_Click(object sender, EventArgs e) + private void CancelEditButton_Click(object? sender, EventArgs? e) { EditDetailsPanel.Visible = false; currentlyEditing = null; LabelSelectionTree.SelectedNode = null; } - private void DeleteButton_Click(object sender, EventArgs e) + private void DeleteButton_Click(object? sender, EventArgs? e) { - if (currentlyEditing != null && Main.Instance.YesNoDialog( - string.Format( - Properties.Resources.EditLabelsDialogConfirmDelete, - currentlyEditing.Name - ), - Properties.Resources.EditLabelsDialogDelete, - Properties.Resources.EditLabelsDialogCancel - )) + if (currentlyEditing != null + && (Main.Instance?.YesNoDialog( + string.Format(Properties.Resources.EditLabelsDialogConfirmDelete, + currentlyEditing.Name), + Properties.Resources.EditLabelsDialogDelete, + Properties.Resources.EditLabelsDialogCancel) ?? false)) { labels.Labels = labels.Labels .Except(new ModuleLabel[] { currentlyEditing }) @@ -181,7 +183,7 @@ private void DeleteButton_Click(object sender, EventArgs e) } } - private void CloseButton_Click(object sender, EventArgs e) + private void CloseButton_Click(object? sender, EventArgs? e) { if (TryCloseEdit()) { @@ -194,7 +196,7 @@ private void StartEdit(ModuleLabel lbl) currentlyEditing = lbl; NameTextBox.Text = lbl.Name; - ColorButton.BackColor = lbl.Color; + ColorButton.BackColor = lbl.Color ?? Color.Transparent; InstanceNameComboBox.SelectedItem = lbl.InstanceName; HideFromOtherFiltersCheckBox.Checked = lbl.Hide; NotifyOnChangesCheckBox.Checked = lbl.NotifyOnChange; @@ -228,7 +230,7 @@ private void EnableDisableUpDownButtons() } } - private void MoveUpButton_Click(object sender, EventArgs e) + private void MoveUpButton_Click(object? sender, EventArgs? e) { if (currentlyEditing != null) { @@ -248,7 +250,7 @@ private void MoveUpButton_Click(object sender, EventArgs e) } } - private void MoveDownButton_Click(object sender, EventArgs e) + private void MoveDownButton_Click(object? sender, EventArgs? e) { if (currentlyEditing != null) { @@ -272,11 +274,10 @@ private bool TryCloseEdit() { if (HasChanges()) { - if (Main.Instance.YesNoDialog( + if (Main.Instance?.YesNoDialog( Properties.Resources.EditLabelsDialogSavePrompt, Properties.Resources.EditLabelsDialogSave, - Properties.Resources.EditLabelsDialogDiscard - )) + Properties.Resources.EditLabelsDialogDiscard) ?? true) { if (!TrySave(out string errMsg)) { @@ -295,7 +296,8 @@ private bool TrySave(out string errMsg) currentlyEditing.Name = NameTextBox.Text; currentlyEditing.Color = ColorButton.BackColor; currentlyEditing.InstanceName = - string.IsNullOrWhiteSpace(InstanceNameComboBox.SelectedItem?.ToString()) + InstanceNameComboBox.SelectedItem == null + || string.IsNullOrWhiteSpace(InstanceNameComboBox.SelectedItem.ToString()) ? null : InstanceNameComboBox.SelectedItem.ToString(); currentlyEditing.Hide = HideFromOtherFiltersCheckBox.Checked; @@ -321,6 +323,7 @@ private bool TrySave(out string errMsg) return false; } + [MemberNotNullWhen(true, nameof(currentlyEditing))] private bool EditingValid(out string errMsg) { if (currentlyEditing == null) @@ -333,9 +336,10 @@ private bool EditingValid(out string errMsg) errMsg = Properties.Resources.EditLabelsDialogNameRequired; return false; } - var newInst = string.IsNullOrWhiteSpace(InstanceNameComboBox.SelectedItem?.ToString()) - ? null - : InstanceNameComboBox.SelectedItem.ToString(); + var newInst = InstanceNameComboBox.SelectedItem == null + || string.IsNullOrWhiteSpace(InstanceNameComboBox.SelectedItem.ToString()) + ? null + : InstanceNameComboBox.SelectedItem.ToString(); var found = labels.Labels.FirstOrDefault(l => l != currentlyEditing && l.Name == NameTextBox.Text @@ -358,9 +362,10 @@ private bool EditingValid(out string errMsg) private bool HasChanges() { - var newInst = string.IsNullOrWhiteSpace(InstanceNameComboBox.SelectedItem?.ToString()) - ? null - : InstanceNameComboBox.SelectedItem.ToString(); + var newInst = InstanceNameComboBox.SelectedItem == null + || string.IsNullOrWhiteSpace(InstanceNameComboBox.SelectedItem.ToString()) + ? null + : InstanceNameComboBox.SelectedItem.ToString(); return EditDetailsPanel.Visible && currentlyEditing != null && ( currentlyEditing.Name != NameTextBox.Text || currentlyEditing.Color != ColorButton.BackColor @@ -374,7 +379,7 @@ private bool HasChanges() ); } - private ModuleLabel currentlyEditing; + private ModuleLabel? currentlyEditing; private readonly IUser user; private readonly ModuleLabelList labels; diff --git a/GUI/Dialogs/ErrorDialog.cs b/GUI/Dialogs/ErrorDialog.cs index 123fc67aa8..4d3aa87840 100644 --- a/GUI/Dialogs/ErrorDialog.cs +++ b/GUI/Dialogs/ErrorDialog.cs @@ -53,7 +53,7 @@ public void ShowErrorDialog(Main mainForm, string text, params object[] args) }); } - private void DismissButton_Click(object sender, EventArgs e) + private void DismissButton_Click(object? sender, EventArgs? e) { Close(); } diff --git a/GUI/Dialogs/GameCommandLineOptionsDialog.cs b/GUI/Dialogs/GameCommandLineOptionsDialog.cs index 4c8d58c877..364f2bb514 100644 --- a/GUI/Dialogs/GameCommandLineOptionsDialog.cs +++ b/GUI/Dialogs/GameCommandLineOptionsDialog.cs @@ -3,9 +3,15 @@ using System.Collections.Generic; using System.Linq; using System.Windows.Forms; +#if NET5_0_OR_GREATER +using System.Runtime.Versioning; +#endif namespace CKAN.GUI { + #if NET5_0_OR_GREATER + [SupportedOSPlatform("windows")] + #endif public partial class GameCommandLineOptionsDialog : Form { public GameCommandLineOptionsDialog() @@ -42,9 +48,10 @@ protected override void OnShown(EventArgs e) CmdLineGrid.BeginEdit(false); } - public List Results => rows.Select(row => row.CmdLine) - .Where(str => !string.IsNullOrEmpty(str)) - .ToList(); + public List Results => rows?.Select(row => row.CmdLine) + .Where(str => !string.IsNullOrEmpty(str)) + .ToList() + ?? new List(); private void CmdLineGrid_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e) { @@ -61,16 +68,17 @@ private void CmdLineGrid_EditingControlShowing(object sender, DataGridViewEditin private void CmdLineGrid_UserDeletingRow(object sender, DataGridViewRowCancelEventArgs e) { // You can't delete the last row - if (rows.Count == 1) + if (rows?.Count == 1) { e.Cancel = true; } } - private void ResetToDefaultsButton_Click(object sender, EventArgs e) + private void ResetToDefaultsButton_Click(object? sender, EventArgs? e) { - rows = defaults.Select(cmdLine => new CmdLineRow(cmdLine)) - .ToList(); + rows = defaults?.Select(cmdLine => new CmdLineRow(cmdLine)) + .ToList() + ?? new List(); CmdLineGrid.DataSource = new BindingList(rows) { AllowEdit = true, @@ -78,14 +86,14 @@ private void ResetToDefaultsButton_Click(object sender, EventArgs e) }; } - private void AddButton_Click(object sender, EventArgs e) + private void AddButton_Click(object? sender, EventArgs? e) { (CmdLineGrid.DataSource as BindingList)?.AddNew(); CmdLineGrid.CurrentCell = CmdLineGrid.Rows[CmdLineGrid.RowCount - 1].Cells[0]; CmdLineGrid.BeginEdit(false); } - private void AcceptChangesButton_Click(object sender, EventArgs e) + private void AcceptChangesButton_Click(object? sender, EventArgs? e) { if (Results.Count < 1) { @@ -94,8 +102,8 @@ private void AcceptChangesButton_Click(object sender, EventArgs e) } } - private string[] defaults; - private List rows; + private string[]? defaults; + private List? rows; } public class CmdLineRow diff --git a/GUI/Dialogs/InstallFiltersDialog.cs b/GUI/Dialogs/InstallFiltersDialog.cs index b8b9b8618a..e9e1a10d6b 100644 --- a/GUI/Dialogs/InstallFiltersDialog.cs +++ b/GUI/Dialogs/InstallFiltersDialog.cs @@ -38,7 +38,7 @@ protected override void OnHelpButtonClicked(CancelEventArgs evt) evt.Cancel = Util.TryOpenWebPage(HelpURLs.Filters); } - private void InstallFiltersDialog_Load(object sender, EventArgs e) + private void InstallFiltersDialog_Load(object? sender, EventArgs? e) { GlobalFiltersTextBox.Text = string.Join(Environment.NewLine, globalConfig.GlobalInstallFilters); InstanceFiltersTextBox.Text = string.Join(Environment.NewLine, instance.InstallFilters); @@ -46,13 +46,13 @@ private void InstallFiltersDialog_Load(object sender, EventArgs e) InstanceFiltersTextBox.DeselectAll(); } - private void InstallFiltersDialog_Closing(object sender, CancelEventArgs e) + private void InstallFiltersDialog_Closing(object? sender, CancelEventArgs? e) { globalConfig.GlobalInstallFilters = GlobalFiltersTextBox.Text.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); instance.InstallFilters = InstanceFiltersTextBox.Text.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); } - private void AddMiniAVCButton_Click(object sender, EventArgs e) + private void AddMiniAVCButton_Click(object? sender, EventArgs? e) { GlobalFiltersTextBox.Text = string.Join(Environment.NewLine, GlobalFiltersTextBox.Text.Split(delimiters, StringSplitOptions.RemoveEmptyEntries) diff --git a/GUI/Dialogs/ManageGameInstancesDialog.cs b/GUI/Dialogs/ManageGameInstancesDialog.cs index 4404220ca3..6aef91af12 100644 --- a/GUI/Dialogs/ManageGameInstancesDialog.cs +++ b/GUI/Dialogs/ManageGameInstancesDialog.cs @@ -151,36 +151,34 @@ protected override void OnHelpButtonClicked(CancelEventArgs evt) evt.Cancel = Util.TryOpenWebPage(HelpURLs.ManageInstances); } - private static string FormatVersion(GameVersion v) + private static string FormatVersion(GameVersion? v) => v == null ? Properties.Resources.CompatibleGameVersionsDialogNone // The BUILD component is not useful visually - : new GameVersion(v.Major, v.Minor, v.Patch).ToString(); + : new GameVersion(v.Major, v.Minor, v.Patch).ToString() ?? ""; - private void AddToCKANMenuItem_Click(object sender, EventArgs e) + private void AddToCKANMenuItem_Click(object? sender, EventArgs? e) { - if (instanceDialog.ShowDialog(this) != DialogResult.OK - || !File.Exists(instanceDialog.FileName)) - { - return; - } - - var path = Path.GetDirectoryName(instanceDialog.FileName); try { - var instanceName = Path.GetFileName(path); - if (string.IsNullOrWhiteSpace(instanceName)) + if (instanceDialog.ShowDialog(this) == DialogResult.OK + && File.Exists(instanceDialog.FileName) + && Path.GetDirectoryName(instanceDialog.FileName) is string path) { - instanceName = path; + var instanceName = Path.GetFileName(path); + if (string.IsNullOrWhiteSpace(instanceName)) + { + instanceName = path; + } + instanceName = manager.GetNextValidInstanceName(instanceName); + manager.AddInstance(path, instanceName, user); + UpdateInstancesList(); } - instanceName = manager.GetNextValidInstanceName(instanceName); - manager.AddInstance(path, instanceName, user); - UpdateInstancesList(); } catch (NotKSPDirKraken k) { user.RaiseError(Properties.Resources.ManageGameInstancesNotValid, - new object[] { k.path }); + k.path); } catch (Exception exc) { @@ -188,7 +186,7 @@ private void AddToCKANMenuItem_Click(object sender, EventArgs e) } } - private void ImportFromSteamMenuItem_Click(object sender, EventArgs e) + private void ImportFromSteamMenuItem_Click(object? sender, EventArgs? e) { var currentDirs = manager.Instances.Values .Select(inst => inst.GameDir()) @@ -202,21 +200,24 @@ private void ImportFromSteamMenuItem_Click(object sender, EventArgs e) UpdateInstancesList(); } - private void CloneGameInstanceMenuItem_Click(object sender, EventArgs e) + private void CloneGameInstanceMenuItem_Click(object? sender, EventArgs? e) { - var old_instance = manager.CurrentInstance; - - var result = new CloneGameInstanceDialog(manager, user, (string)GameInstancesListView.SelectedItems[0].Tag).ShowDialog(this); - if (result == DialogResult.OK && !Equals(old_instance, manager.CurrentInstance)) + if (GameInstancesListView.SelectedItems[0].Tag is string instName) { - DialogResult = DialogResult.OK; - Close(); - } + var old_instance = manager.CurrentInstance; - UpdateInstancesList(); + var result = new CloneGameInstanceDialog(manager, user, instName).ShowDialog(this); + if (result == DialogResult.OK && !Equals(old_instance, manager.CurrentInstance)) + { + DialogResult = DialogResult.OK; + Close(); + } + + UpdateInstancesList(); + } } - private void SelectButton_Click(object sender, EventArgs e) + private void SelectButton_Click(object? sender, EventArgs? e) { UseSelectedInstance(); } @@ -244,7 +245,7 @@ private void UseSelectedInstance() } } - private void SetAsDefaultCheckbox_Click(object sender, EventArgs e) + private void SetAsDefaultCheckbox_Click(object? sender, EventArgs? e) { if (SetAsDefaultCheckbox.Checked) { @@ -268,17 +269,19 @@ private void SetAsDefaultCheckbox_Click(object sender, EventArgs e) } } - private void GameInstancesListView_SelectedIndexChanged(object sender, EventArgs e) + private void GameInstancesListView_SelectedIndexChanged(object? sender, EventArgs? e) { - UpdateButtonState(); - - if (GameInstancesListView.SelectedItems.Count == 0) + if (GameInstancesListView.SelectedItems[0].Tag is string instName) { - return; - } + UpdateButtonState(); - string instName = (string)GameInstancesListView.SelectedItems[0].Tag; - SetAsDefaultCheckbox.Checked = manager.AutoStartInstance?.Equals(instName) ?? false; + if (GameInstancesListView.SelectedItems.Count == 0) + { + return; + } + + SetAsDefaultCheckbox.Checked = manager.AutoStartInstance?.Equals(instName) ?? false; + } } private void GameInstancesListView_DoubleClick(object sender, EventArgs r) @@ -297,9 +300,9 @@ private void GameInstancesListView_Click(object sender, MouseEventArgs e) } } - private void GameInstancesListView_KeyDown(object sender, KeyEventArgs e) + private void GameInstancesListView_KeyDown(object? sender, KeyEventArgs? e) { - switch (e.KeyCode) + switch (e?.KeyCode) { case Keys.Apps: InstanceListContextMenuStrip.Show(Cursor.Position); @@ -308,38 +311,45 @@ private void GameInstancesListView_KeyDown(object sender, KeyEventArgs e) } } - private void OpenDirectoryMenuItem_Click(object sender, EventArgs e) + private void OpenDirectoryMenuItem_Click(object? sender, EventArgs? e) { - string path = manager.Instances[(string) GameInstancesListView.SelectedItems[0].Tag].GameDir(); - - if (!Directory.Exists(path)) + if (GameInstancesListView.SelectedItems[0].Tag is string instName) { - user.RaiseError(Properties.Resources.ManageGameInstancesDirectoryDeleted, path); - return; - } + string path = manager.Instances[instName].GameDir(); - Utilities.ProcessStartURL(path); + if (!Directory.Exists(path)) + { + user.RaiseError(Properties.Resources.ManageGameInstancesDirectoryDeleted, path); + return; + } + + Utilities.ProcessStartURL(path); + } } - private void RenameButton_Click(object sender, EventArgs e) + private void RenameButton_Click(object? sender, EventArgs? e) { - var instance = (string)GameInstancesListView.SelectedItems[0].Tag; - - // show the dialog, and only continue if the user selected "OK" - var renameInstanceDialog = new RenameInstanceDialog(); - if (renameInstanceDialog.ShowRenameInstanceDialog(instance) != DialogResult.OK) + if (GameInstancesListView.SelectedItems[0].Tag is string instName) { - return; - } + // show the dialog, and only continue if the user selected "OK" + var renameInstanceDialog = new RenameInstanceDialog(); + if (renameInstanceDialog.ShowRenameInstanceDialog(instName) != DialogResult.OK) + { + return; + } - // proceed with instance rename - manager.RenameInstance(instance, renameInstanceDialog.GetResult()); - UpdateInstancesList(); + // proceed with instance rename + manager.RenameInstance(instName, renameInstanceDialog.GetResult()); + UpdateInstancesList(); + } } - private void Forget_Click(object sender, EventArgs e) + private void Forget_Click(object? sender, EventArgs? e) { - foreach (var instance in GameInstancesListView.SelectedItems.OfType().Select(item => item.Tag as string)) + foreach (var instance in GameInstancesListView.SelectedItems + .OfType() + .Select(item => item.Tag as string) + .OfType()) { manager.RemoveInstance(instance); UpdateInstancesList(); @@ -348,9 +358,12 @@ private void Forget_Click(object sender, EventArgs e) private void UpdateButtonState() { - RenameButton.Enabled = SelectButton.Enabled = SetAsDefaultCheckbox.Enabled = CloneGameInstanceMenuItem.Enabled = HasSelections; - ForgetButton.Enabled = HasSelections && (string)GameInstancesListView.SelectedItems[0].Tag != manager.CurrentInstance?.Name; - ImportFromSteamMenuItem.Enabled = manager.SteamLibrary.Games.Length > 0; + if (GameInstancesListView.SelectedItems[0].Tag is string instName) + { + RenameButton.Enabled = SelectButton.Enabled = SetAsDefaultCheckbox.Enabled = CloneGameInstanceMenuItem.Enabled = HasSelections; + ForgetButton.Enabled = HasSelections && instName != manager.CurrentInstance?.Name; + ImportFromSteamMenuItem.Enabled = manager.SteamLibrary.Games.Length > 0; + } } private readonly GameInstanceManager manager; @@ -367,9 +380,9 @@ private void UpdateButtonState() Multiselect = false, }; - private void InstanceFileOK(object sender, CancelEventArgs e) + private void InstanceFileOK(object? sender, CancelEventArgs? e) { - if (sender is OpenFileDialog dlg) + if (e != null && sender is OpenFileDialog dlg) { // OpenFileDialog always shows shortcuts (!!!!!), // so we have to re-enforce the filter ourselves diff --git a/GUI/Dialogs/NewRepoDialog.cs b/GUI/Dialogs/NewRepoDialog.cs index 767f48c0b0..78e54626e2 100644 --- a/GUI/Dialogs/NewRepoDialog.cs +++ b/GUI/Dialogs/NewRepoDialog.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Windows.Forms; + #if NET5_0_OR_GREATER using System.Runtime.Versioning; #endif @@ -12,8 +13,9 @@ namespace CKAN.GUI #endif public partial class NewRepoDialog : Form { - public NewRepoDialog() + public NewRepoDialog(Repository[] repos) { + this.repos = repos; InitializeComponent(); StartPosition = FormStartPosition.CenterScreen; } @@ -21,46 +23,25 @@ public NewRepoDialog() public Repository Selection => new Repository(RepoNameTextBox.Text, RepoUrlTextBox.Text); - private void NewRepoDialog_Load(object sender, EventArgs e) + private void NewRepoDialog_Load(object? sender, EventArgs? e) { - RepositoryList repositories; - - try - { - repositories = Main.Instance.FetchMasterRepositoryList(); - } - catch - { - ReposListBox.Items.Add(Properties.Resources.NewRepoDialogFailed); - return; - } - ReposListBox.Items.Clear(); - - if (repositories.repositories == null) - { - ReposListBox.Items.Add(Properties.Resources.NewRepoDialogFailed); - return; - } - - ReposListBox.Items.AddRange(repositories.repositories.Select(r => - new ListViewItem(new string[] { r.name, r.uri.ToString() }) - { - Tag = r - } - ).ToArray()); + ReposListBox.Items.AddRange( + repos.Select(r => new ListViewItem(r.name, r.uri.ToString()) + { + Tag = r + }) + .ToArray()); } - private void ReposListBox_SelectedIndexChanged(object sender, EventArgs e) + private void ReposListBox_SelectedIndexChanged(object? sender, EventArgs? e) { - if (ReposListBox.SelectedItems.Count == 0) + if (ReposListBox.SelectedItems.Count > 0 + && ReposListBox.SelectedItems[0].Tag is Repository r) { - return; + RepoNameTextBox.Text = r.name; + RepoUrlTextBox.Text = r.uri.ToString(); } - - Repository r = ReposListBox.SelectedItems[0].Tag as Repository; - RepoNameTextBox.Text = r.name; - RepoUrlTextBox.Text = r.uri.ToString(); } private void ReposListBox_DoubleClick(object sender, EventArgs r) @@ -74,10 +55,12 @@ private void ReposListBox_DoubleClick(object sender, EventArgs r) Close(); } - private void RepoUrlTextBox_TextChanged(object sender, EventArgs e) + private void RepoUrlTextBox_TextChanged(object? sender, EventArgs? e) { RepoOK.Enabled = RepoNameTextBox.Text.Length > 0 && RepoUrlTextBox.Text.Length > 0; } + + private readonly Repository[] repos; } } diff --git a/GUI/Dialogs/NewUpdateDialog.cs b/GUI/Dialogs/NewUpdateDialog.cs index 85aea2decf..f876432a82 100644 --- a/GUI/Dialogs/NewUpdateDialog.cs +++ b/GUI/Dialogs/NewUpdateDialog.cs @@ -1,7 +1,13 @@ using System.Windows.Forms; +#if NET5_0_OR_GREATER +using System.Runtime.Versioning; +#endif namespace CKAN.GUI { + #if NET5_0_OR_GREATER + [SupportedOSPlatform("windows")] + #endif public partial class NewUpdateDialog : Form { /// @@ -9,11 +15,11 @@ public partial class NewUpdateDialog : Form /// /// Version number of new release /// Markdown formatted description of the new release - public NewUpdateDialog(string version, string releaseNotes) + public NewUpdateDialog(string version, string? releaseNotes) { InitializeComponent(); VersionLabel.Text = version; - ReleaseNotesTextbox.Text = releaseNotes.Trim(); + ReleaseNotesTextbox.Text = releaseNotes?.Trim() ?? ""; } } } diff --git a/GUI/Dialogs/PluginsDialog.cs b/GUI/Dialogs/PluginsDialog.cs index 1f32088654..f2e10e6ec7 100644 --- a/GUI/Dialogs/PluginsDialog.cs +++ b/GUI/Dialogs/PluginsDialog.cs @@ -17,11 +17,11 @@ public PluginsDialog() StartPosition = FormStartPosition.CenterScreen; } - private PluginController pluginController => Main.Instance.pluginController; + private PluginController? pluginController => Main.Instance?.pluginController; private readonly OpenFileDialog m_AddNewPluginDialog = new OpenFileDialog(); - private void PluginsDialog_Load(object sender, EventArgs e) + private void PluginsDialog_Load(object? sender, EventArgs? e) { DeactivateButton.Enabled = false; ReloadPluginButton.Enabled = false; @@ -37,96 +37,92 @@ private void PluginsDialog_Load(object sender, EventArgs e) private void RefreshActivePlugins() { - var activePlugins = pluginController.ActivePlugins; - - ActivePluginsListBox.Items.Clear(); - foreach (var plugin in activePlugins) + if (pluginController != null) { - ActivePluginsListBox.Items.Add(plugin); + var activePlugins = pluginController.ActivePlugins; + ActivePluginsListBox.Items.Clear(); + foreach (var plugin in activePlugins) + { + ActivePluginsListBox.Items.Add(plugin); + } } } private void RefreshDormantPlugins() { - var dormantPlugins = pluginController.DormantPlugins; - - DormantPluginsListBox.Items.Clear(); - foreach (var plugin in dormantPlugins) + if (pluginController != null) { - DormantPluginsListBox.Items.Add(plugin); + var dormantPlugins = pluginController.DormantPlugins; + DormantPluginsListBox.Items.Clear(); + foreach (var plugin in dormantPlugins) + { + DormantPluginsListBox.Items.Add(plugin); + } } } - private void ActivePluginsListBox_SelectedIndexChanged(object sender, EventArgs e) + private void ActivePluginsListBox_SelectedIndexChanged(object? sender, EventArgs? e) { bool state = ActivePluginsListBox.SelectedItem != null; DeactivateButton.Enabled = state; ReloadPluginButton.Enabled = state; } - private void DeactivateButton_Click(object sender, EventArgs e) + private void DeactivateButton_Click(object? sender, EventArgs? e) { - if (ActivePluginsListBox.SelectedItem == null) + if (pluginController != null && ActivePluginsListBox.SelectedItem != null) { - return; + var plugin = (IGUIPlugin) ActivePluginsListBox.SelectedItem; + pluginController.DeactivatePlugin(plugin); + RefreshActivePlugins(); + RefreshDormantPlugins(); } - - var plugin = (IGUIPlugin) ActivePluginsListBox.SelectedItem; - pluginController.DeactivatePlugin(plugin); - RefreshActivePlugins(); - RefreshDormantPlugins(); } - private void ReloadPluginButton_Click(object sender, EventArgs e) + private void ReloadPluginButton_Click(object? sender, EventArgs? e) { - if (ActivePluginsListBox.SelectedItem == null) + if (pluginController != null && ActivePluginsListBox.SelectedItem != null) { - return; + var plugin = (IGUIPlugin)ActivePluginsListBox.SelectedItem; + pluginController.DeactivatePlugin(plugin); + pluginController.ActivatePlugin(plugin); + RefreshActivePlugins(); + RefreshDormantPlugins(); } - - var plugin = (IGUIPlugin)ActivePluginsListBox.SelectedItem; - pluginController.DeactivatePlugin(plugin); - pluginController.ActivatePlugin(plugin); - RefreshActivePlugins(); - RefreshDormantPlugins(); } - private void DormantPluginsListBox_SelectedIndexChanged(object sender, EventArgs e) + private void DormantPluginsListBox_SelectedIndexChanged(object? sender, EventArgs? e) { bool state = DormantPluginsListBox.SelectedItem != null; ActivatePluginButton.Enabled = state; UnloadPluginButton.Enabled = state; } - private void ActivatePluginButton_Click(object sender, EventArgs e) + private void ActivatePluginButton_Click(object? sender, EventArgs? e) { - if (DormantPluginsListBox.SelectedItem == null) + if (pluginController != null && DormantPluginsListBox.SelectedItem != null) { - return; + var plugin = (IGUIPlugin)DormantPluginsListBox.SelectedItem; + pluginController.ActivatePlugin(plugin); + RefreshActivePlugins(); + RefreshDormantPlugins(); } - - var plugin = (IGUIPlugin)DormantPluginsListBox.SelectedItem; - pluginController.ActivatePlugin(plugin); - RefreshActivePlugins(); - RefreshDormantPlugins(); } - private void UnloadPluginButton_Click(object sender, EventArgs e) + private void UnloadPluginButton_Click(object? sender, EventArgs? e) { - if (DormantPluginsListBox.SelectedItem == null) + if (pluginController != null && DormantPluginsListBox.SelectedItem != null) { - return; + var plugin = (IGUIPlugin)DormantPluginsListBox.SelectedItem; + pluginController.UnloadPlugin(plugin); + RefreshActivePlugins(); + RefreshDormantPlugins(); } - - var plugin = (IGUIPlugin)DormantPluginsListBox.SelectedItem; - pluginController.UnloadPlugin(plugin); - RefreshActivePlugins(); - RefreshDormantPlugins(); } - private void AddNewPluginButton_Click(object sender, EventArgs e) + private void AddNewPluginButton_Click(object? sender, EventArgs? e) { - if (m_AddNewPluginDialog.ShowDialog(this) == DialogResult.OK) + if (pluginController != null && m_AddNewPluginDialog.ShowDialog(this) == DialogResult.OK) { var path = m_AddNewPluginDialog.FileName; pluginController.AddNewAssemblyToPluginsPath(path); diff --git a/GUI/Dialogs/PreferredHostsDialog.cs b/GUI/Dialogs/PreferredHostsDialog.cs index f5740327b8..0a7059de34 100644 --- a/GUI/Dialogs/PreferredHostsDialog.cs +++ b/GUI/Dialogs/PreferredHostsDialog.cs @@ -44,11 +44,12 @@ protected override void OnHelpButtonClicked(CancelEventArgs evt) evt.Cancel = Util.TryOpenWebPage(HelpURLs.PreferredHosts); } - private void PreferredHostsDialog_Load(object sender, EventArgs e) + private void PreferredHostsDialog_Load(object? sender, EventArgs? e) { - AvailableHostsListBox.Items.AddRange(allHosts - .Except(config.PreferredHosts) - .ToArray()); + AvailableHostsListBox.Items.AddRange( + allHosts.Except(config.PreferredHosts) + .OfType() + .ToArray()); PreferredHostsListBox.Items.AddRange(config.PreferredHosts .Select(host => host ?? placeholder) .ToArray()); @@ -56,39 +57,40 @@ private void PreferredHostsDialog_Load(object sender, EventArgs e) PreferredHostsListBox_SelectedIndexChanged(null, null); } - private void PreferredHostsDialog_Closing(object sender, CancelEventArgs e) + private void PreferredHostsDialog_Closing(object? sender, CancelEventArgs? e) { - config.PreferredHosts = PreferredHostsListBox.Items.Cast() - .Select(h => h == placeholder ? null : h) - .ToArray(); + config.PreferredHosts = PreferredHostsListBox.Items + .OfType() + .Select(h => h == placeholder ? null : h) + .ToArray(); } - private void AvailableHostsListBox_SelectedIndexChanged(object sender, EventArgs e) + private void AvailableHostsListBox_SelectedIndexChanged(object? sender, EventArgs? e) { MoveRightButton.Enabled = AvailableHostsListBox.SelectedIndex > -1; } - private void PreferredHostsListBox_SelectedIndexChanged(object sender, EventArgs e) + private void PreferredHostsListBox_SelectedIndexChanged(object? sender, EventArgs? e) { var haveSelection = PreferredHostsListBox.SelectedIndex > -1; MoveLeftButton.Enabled = haveSelection - && (string)PreferredHostsListBox.SelectedItem != placeholder; + && (PreferredHostsListBox.SelectedItem as string) != placeholder; MoveUpButton.Enabled = PreferredHostsListBox.SelectedIndex > 0; MoveDownButton.Enabled = haveSelection && PreferredHostsListBox.SelectedIndex < PreferredHostsListBox.Items.Count - 1; } - private void AvailableHostsListBox_DoubleClick(object sender, EventArgs e) + private void AvailableHostsListBox_DoubleClick(object? sender, EventArgs? e) { MoveRightButton_Click(null, null); } - private void PreferredHostsListBox_DoubleClick(object sender, EventArgs e) + private void PreferredHostsListBox_DoubleClick(object? sender, EventArgs? e) { MoveLeftButton_Click(null, null); } - private void MoveRightButton_Click(object sender, EventArgs e) + private void MoveRightButton_Click(object? sender, EventArgs? e) { if (AvailableHostsListBox.SelectedIndex > -1) { @@ -97,10 +99,12 @@ private void MoveRightButton_Click(object sender, EventArgs e) PreferredHostsListBox.Items.Add(placeholder); } var fromWhere = AvailableHostsListBox.SelectedIndex; - var selected = AvailableHostsListBox.SelectedItem; - var toWhere = PreferredHostsListBox.Items.IndexOf(placeholder); - AvailableHostsListBox.Items.Remove(selected); - PreferredHostsListBox.Items.Insert(toWhere, selected); + if (AvailableHostsListBox.SelectedItem is object selected) + { + var toWhere = PreferredHostsListBox.Items.IndexOf(placeholder); + AvailableHostsListBox.Items.Remove(selected); + PreferredHostsListBox.Items.Insert(toWhere, selected); + } // Preserve selection on same line if (AvailableHostsListBox.Items.Count > 0) { @@ -116,13 +120,13 @@ private void MoveRightButton_Click(object sender, EventArgs e) } } - private void MoveLeftButton_Click(object sender, EventArgs e) + private void MoveLeftButton_Click(object? sender, EventArgs? e) { if (PreferredHostsListBox.SelectedIndex > -1) { var fromWhere = PreferredHostsListBox.SelectedIndex; - var selected = (string)PreferredHostsListBox.SelectedItem; - if (selected != placeholder) + var selected = PreferredHostsListBox.SelectedItem as string; + if (selected != placeholder && selected != null) { PreferredHostsListBox.Items.Remove(selected); // Regenerate the list to put the item back in the original order @@ -145,7 +149,7 @@ private void MoveLeftButton_Click(object sender, EventArgs e) } } - private void MoveUpButton_Click(object sender, EventArgs e) + private void MoveUpButton_Click(object? sender, EventArgs? e) { if (PreferredHostsListBox.SelectedIndex > 0) { @@ -156,7 +160,7 @@ private void MoveUpButton_Click(object sender, EventArgs e) } } - private void MoveDownButton_Click(object sender, EventArgs e) + private void MoveDownButton_Click(object? sender, EventArgs? e) { if (PreferredHostsListBox.SelectedIndex > -1 && PreferredHostsListBox.SelectedIndex < PreferredHostsListBox.Items.Count - 1) diff --git a/GUI/Dialogs/RenameInstanceDialog.cs b/GUI/Dialogs/RenameInstanceDialog.cs index f8d07bfee8..60ee881d5d 100644 --- a/GUI/Dialogs/RenameInstanceDialog.cs +++ b/GUI/Dialogs/RenameInstanceDialog.cs @@ -1,7 +1,13 @@ using System.Windows.Forms; +#if NET5_0_OR_GREATER +using System.Runtime.Versioning; +#endif namespace CKAN.GUI { + #if NET5_0_OR_GREATER + [SupportedOSPlatform("windows")] + #endif public partial class RenameInstanceDialog : Form { public RenameInstanceDialog() diff --git a/GUI/Dialogs/SelectionDialog.cs b/GUI/Dialogs/SelectionDialog.cs index 87d0f172a2..62105f23a9 100644 --- a/GUI/Dialogs/SelectionDialog.cs +++ b/GUI/Dialogs/SelectionDialog.cs @@ -93,7 +93,7 @@ public int ShowSelectionDialog (string message, params object[] args) } else { - Util.Invoke(OptionsList, () => OptionsList.Items.Add(args[i].ToString())); + Util.Invoke(OptionsList, () => OptionsList.Items.Add(args[i].ToString() ?? "")); } } @@ -123,7 +123,7 @@ public void HideYesNoDialog () Util.Invoke(this, Close); } - private void OptionsList_SelectedIndexChanged(object sender, EventArgs e) + private void OptionsList_SelectedIndexChanged(object? sender, EventArgs? e) { currentSelected = OptionsList.SelectedIndex; } diff --git a/GUI/Dialogs/SettingsDialog.cs b/GUI/Dialogs/SettingsDialog.cs index dc6dc74a28..07f5deeeee 100644 --- a/GUI/Dialogs/SettingsDialog.cs +++ b/GUI/Dialogs/SettingsDialog.cs @@ -25,7 +25,7 @@ public partial class SettingsDialog : Form public bool RepositoryRemoved { get; private set; } = false; public bool RepositoryMoved { get; private set; } = false; - private GameInstanceManager manager => Main.Instance.Manager; + private static GameInstanceManager? manager => Main.Instance?.Manager; private readonly IConfiguration coreConfig; private readonly GUIConfiguration guiConfig; @@ -54,7 +54,7 @@ public SettingsDialog(IConfiguration coreConfig, } } - private void SettingsDialog_Load(object sender, EventArgs e) + private void SettingsDialog_Load(object? sender, EventArgs? e) { UpdateDialog(); } @@ -78,7 +78,10 @@ public void UpdateDialog() UpdateRefreshRate(); - UpdateCacheInfo(coreConfig.DownloadCacheDir); + if (coreConfig.DownloadCacheDir != null) + { + UpdateCacheInfo(coreConfig.DownloadCacheDir); + } } private void UpdateAutoUpdate() @@ -88,9 +91,9 @@ private void UpdateAutoUpdate() { var latestVersion = updater.GetUpdate(coreConfig.DevBuilds ?? false) .Version; - LatestVersionLabel.Text = latestVersion.ToString(); + LatestVersionLabel.Text = latestVersion?.ToString() ?? ""; // Allow downgrading in case they want to stop using dev builds - InstallUpdateButton.Enabled = !latestVersion.Equals(new ModuleVersion(Meta.GetVersion())); + InstallUpdateButton.Enabled = !latestVersion?.Equals(new ModuleVersion(Meta.GetVersion())) ?? false; } catch { @@ -105,7 +108,8 @@ private void UpdateAutoUpdate() protected override void OnFormClosing(FormClosingEventArgs e) { if (CachePath.Text != coreConfig.DownloadCacheDir - && !manager.TrySetupCache(CachePath.Text, out string failReason)) + && manager != null + && !manager.TrySetupCache(CachePath.Text, out string? failReason)) { user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); e.Cancel = true; @@ -118,11 +122,14 @@ protected override void OnFormClosing(FormClosingEventArgs e) private void UpdateRefreshRate() { - int rate = coreConfig.RefreshRate; - RefreshTextBox.Text = rate.ToString(); - PauseRefreshCheckBox.Enabled = rate != 0; - Main.Instance.pauseToolStripMenuItem.Enabled = coreConfig.RefreshRate != 0; - Main.Instance.UpdateRefreshTimer(); + if (Main.Instance != null) + { + int rate = coreConfig.RefreshRate; + RefreshTextBox.Text = rate.ToString(); + PauseRefreshCheckBox.Enabled = rate != 0; + Main.Instance.pauseToolStripMenuItem.Enabled = coreConfig.RefreshRate != 0; + Main.Instance.UpdateRefreshTimer(); + } } private void RefreshReposListBox(bool saveChanges = true) @@ -161,7 +168,6 @@ private void RefreshReposListBox(bool saveChanges = true) private void UpdateLanguageSelectionComboBox() { LanguageSelectionComboBox.Items.Clear(); - LanguageSelectionComboBox.Items.AddRange(Utilities.AvailableLanguages); // If the current language is supported by CKAN, set is as selected. // Else display a blank field. @@ -212,12 +218,12 @@ private void UpdateCacheInfo(string newPath) }); } - private void CachePath_TextChanged(object sender, EventArgs e) + private void CachePath_TextChanged(object? sender, EventArgs? e) { UpdateCacheInfo(CachePath.Text); } - private void CacheLimit_TextChanged(object sender, EventArgs e) + private void CacheLimit_TextChanged(object? sender, EventArgs? e) { if (string.IsNullOrEmpty(CacheLimit.Text)) { @@ -239,82 +245,88 @@ private void CacheLimit_KeyPress(object sender, KeyPressEventArgs e) } } - private void ChangeCacheButton_Click(object sender, EventArgs e) + private void ChangeCacheButton_Click(object? sender, EventArgs? e) { - FolderBrowserDialog cacheChooser = new FolderBrowserDialog() + var cacheChooser = new FolderBrowserDialog() { Description = Properties.Resources.SettingsDialogCacheDescrip, RootFolder = Environment.SpecialFolder.MyComputer, - SelectedPath = coreConfig.DownloadCacheDir, + SelectedPath = coreConfig.DownloadCacheDir + ?? JsonConfiguration.DefaultDownloadCacheDir, ShowNewFolderButton = true }; - DialogResult result = cacheChooser.ShowDialog(this); - if (result == DialogResult.OK) + if (cacheChooser.ShowDialog(this) == DialogResult.OK) { UpdateCacheInfo(cacheChooser.SelectedPath); } } - private void PurgeToLimitMenuItem_Click(object sender, EventArgs e) + private void PurgeToLimitMenuItem_Click(object? sender, EventArgs? e) { // Purge old downloads if we're over the limit - if (coreConfig.CacheSizeLimit.HasValue) + if (coreConfig.CacheSizeLimit.HasValue && manager != null && coreConfig.DownloadCacheDir != null) { // Switch main cache since user seems committed to this path if (CachePath.Text != coreConfig.DownloadCacheDir - && !manager.TrySetupCache(CachePath.Text, out string failReason)) + && !manager.TrySetupCache(CachePath.Text, out string? failReason)) { user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); return; } - manager.Cache.EnforceSizeLimit( + manager.Cache?.EnforceSizeLimit( coreConfig.CacheSizeLimit.Value, regMgr.registry); UpdateCacheInfo(coreConfig.DownloadCacheDir); } } - private void PurgeAllMenuItem_Click(object sender, EventArgs e) + private void PurgeAllMenuItem_Click(object? sender, EventArgs? e) { - // Switch main cache since user seems committed to this path - if (CachePath.Text != coreConfig.DownloadCacheDir - && !manager.TrySetupCache(CachePath.Text, out string failReason)) + if (manager?.Cache != null) { - user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); - return; - } + // Switch main cache since user seems committed to this path + if (CachePath.Text != coreConfig.DownloadCacheDir + && !manager.TrySetupCache(CachePath.Text, out string? failReason)) + { + user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); + return; + } - manager.Cache.GetSizeInfo( - out int cacheFileCount, out long cacheSize, out _); + manager.Cache.GetSizeInfo( + out int cacheFileCount, out long cacheSize, out _); - YesNoDialog deleteConfirmationDialog = new YesNoDialog(); - string confirmationText = string.Format( - Properties.Resources.SettingsDialogDeleteConfirm, - cacheFileCount, - CkanModule.FmtSize(cacheSize)); + YesNoDialog deleteConfirmationDialog = new YesNoDialog(); + string confirmationText = string.Format( + Properties.Resources.SettingsDialogDeleteConfirm, + cacheFileCount, + CkanModule.FmtSize(cacheSize)); - if (deleteConfirmationDialog.ShowYesNoDialog(this, confirmationText) == DialogResult.Yes) - { - // Tell the cache object to nuke itself - manager.Cache.RemoveAll(); + if (deleteConfirmationDialog.ShowYesNoDialog(this, confirmationText) == DialogResult.Yes) + { + // Tell the cache object to nuke itself + manager.Cache.RemoveAll(); - UpdateCacheInfo(coreConfig.DownloadCacheDir); + if (coreConfig.DownloadCacheDir != null) + { + UpdateCacheInfo(coreConfig.DownloadCacheDir); + } + } } } - private void ResetCacheButton_Click(object sender, EventArgs e) + private void ResetCacheButton_Click(object? sender, EventArgs? e) { // Reset to default cache path UpdateCacheInfo(JsonConfiguration.DefaultDownloadCacheDir); } - private void OpenCacheButton_Click(object sender, EventArgs e) + private void OpenCacheButton_Click(object? sender, EventArgs? e) { - Utilities.ProcessStartURL(coreConfig.DownloadCacheDir); + Utilities.ProcessStartURL(coreConfig.DownloadCacheDir ?? JsonConfiguration.DefaultDownloadCacheDir); } - private void ReposListBox_SelectedIndexChanged(object sender, EventArgs e) + private void ReposListBox_SelectedIndexChanged(object? sender, EventArgs? e) { EnableDisableRepoButtons(); } @@ -334,20 +346,16 @@ private void EnableDisableRepoButtons() && ReposListBox.SelectedIndices[0] >= 0; } - private void DeleteRepoButton_Click(object sender, EventArgs e) + private void DeleteRepoButton_Click(object? sender, EventArgs? e) { - if (ReposListBox.SelectedItems.Count == 0) - { - return; - } - - var repo = ReposListBox.SelectedItems[0].Tag as Repository; YesNoDialog deleteConfirmationDialog = new YesNoDialog(); - if (deleteConfirmationDialog.ShowYesNoDialog(this, - string.Format(Properties.Resources.SettingsDialogRepoDeleteConfirm, - repo.name), - Properties.Resources.SettingsDialogRepoDeleteDelete, - Properties.Resources.SettingsDialogRepoDeleteCancel) + if (ReposListBox.SelectedItems.Count > 0 + && ReposListBox.SelectedItems[0].Tag is Repository repo + && deleteConfirmationDialog.ShowYesNoDialog(this, + string.Format(Properties.Resources.SettingsDialogRepoDeleteConfirm, + repo.name), + Properties.Resources.SettingsDialogRepoDeleteDelete, + Properties.Resources.SettingsDialogRepoDeleteCancel) == DialogResult.Yes) { var registry = regMgr.registry; @@ -358,74 +366,77 @@ private void DeleteRepoButton_Click(object sender, EventArgs e) } } - private void NewRepoButton_Click(object sender, EventArgs e) + private void NewRepoButton_Click(object? sender, EventArgs? e) { - var dialog = new NewRepoDialog(); - if (dialog.ShowDialog(this) == DialogResult.OK) + if (manager?.CurrentInstance != null + && RepositoryList.DefaultRepositories(manager.CurrentInstance.game)?.repositories + is Repository[] repos) { - var repo = dialog.Selection; - var registry = regMgr.registry; - if (registry.Repositories.Values.Any(other => other.uri == repo.uri)) + var dialog = new NewRepoDialog(repos); + if (dialog.ShowDialog(this) == DialogResult.OK) { - user.RaiseError(Properties.Resources.SettingsDialogRepoAddDuplicateURL, repo.uri); - return; - } - if (registry.Repositories.TryGetValue(repo.name, out Repository existing)) - { - repo.priority = existing.priority; - registry.RepositoriesRemove(repo.name); - } - else - { - repo.priority = registry.Repositories.Count; - } - registry.RepositoriesAdd(repo); - RepositoryAdded = true; + var repo = dialog.Selection; + var registry = regMgr.registry; + if (registry.Repositories.Values.Any(other => other.uri == repo.uri)) + { + user.RaiseError(Properties.Resources.SettingsDialogRepoAddDuplicateURL, repo.uri); + return; + } + if (registry.Repositories.TryGetValue(repo.name, out Repository? existing)) + { + repo.priority = existing.priority; + registry.RepositoriesRemove(repo.name); + } + else + { + repo.priority = registry.Repositories.Count; + } + registry.RepositoriesAdd(repo); + RepositoryAdded = true; - RefreshReposListBox(); + RefreshReposListBox(); + } } } - private void UpRepoButton_Click(object sender, EventArgs e) + private void UpRepoButton_Click(object? sender, EventArgs? e) { - if (ReposListBox.SelectedIndices.Count == 0 - || ReposListBox.SelectedIndices[0] == 0) + if (ReposListBox.SelectedIndices.Count > 0 + && ReposListBox.SelectedIndices[0] != 0 + && ReposListBox.SelectedItems[0].Tag is Repository selected) { - return; - } - - var selected = ReposListBox.SelectedItems[0].Tag as Repository; - var prev = ReposListBox.Items.Cast() + var prev = ReposListBox.Items.OfType() .Select(item => item.Tag as Repository) + .OfType() .FirstOrDefault(r => r.priority == selected.priority - 1); - --selected.priority; - RepositoryMoved = true; - if (prev != null) - { - ++prev.priority; + --selected.priority; + RepositoryMoved = true; + if (prev != null) + { + ++prev.priority; + } + RefreshReposListBox(); } - RefreshReposListBox(); } - private void DownRepoButton_Click(object sender, EventArgs e) + private void DownRepoButton_Click(object? sender, EventArgs? e) { - if (ReposListBox.SelectedIndices.Count == 0 - || ReposListBox.SelectedIndices[0] == ReposListBox.Items.Count - 1) + if (ReposListBox.SelectedIndices.Count > 0 + && ReposListBox.SelectedIndices[0] != ReposListBox.Items.Count - 1 + && ReposListBox.SelectedItems[0].Tag is Repository selected) { - return; - } - - var selected = ReposListBox.SelectedItems[0].Tag as Repository; - var next = ReposListBox.Items.Cast() - .Select(item => item.Tag as Repository) - .FirstOrDefault(r => r.priority == selected.priority + 1); - ++selected.priority; - RepositoryMoved = true; - if (next != null) - { - --next.priority; + var next = ReposListBox.Items.Cast() + .Select(item => item.Tag as Repository) + .OfType() + .FirstOrDefault(r => r.priority == selected.priority + 1); + ++selected.priority; + RepositoryMoved = true; + if (next != null) + { + --next.priority; + } + RefreshReposListBox(); } - RefreshReposListBox(); } private void RefreshAuthTokensListBox() @@ -433,7 +444,7 @@ private void RefreshAuthTokensListBox() AuthTokensListBox.Items.Clear(); foreach (string host in coreConfig.GetAuthTokenHosts()) { - if (coreConfig.TryGetAuthToken(host, out string token)) + if (coreConfig.TryGetAuthToken(host, out string? token)) { AuthTokensListBox.Items.Add(new ListViewItem( new string[] { host, token }) @@ -444,12 +455,12 @@ private void RefreshAuthTokensListBox() } } - private void AuthTokensListBox_SelectedIndexChanged(object sender, EventArgs e) + private void AuthTokensListBox_SelectedIndexChanged(object? sender, EventArgs? e) { DeleteAuthTokenButton.Enabled = AuthTokensListBox.SelectedItems.Count > 0; } - private void NewAuthTokenButton_Click(object sender, EventArgs e) + private void NewAuthTokenButton_Click(object? sender, EventArgs? e) { // Inspired by https://stackoverflow.com/a/17546909/2422988 Form newAuthTokenPopup = new Form() @@ -560,20 +571,22 @@ private bool validNewAuthToken(string host, string token) return true; } - private void DeleteAuthTokenButton_Click(object sender, EventArgs e) + private void DeleteAuthTokenButton_Click(object? sender, EventArgs? e) { if (AuthTokensListBox.SelectedItems.Count > 0) { - string item = AuthTokensListBox.SelectedItems[0].Tag as string; - string host = item?.Split('|')[0].Trim(); - - coreConfig.SetAuthToken(host, null); - RefreshAuthTokensListBox(); - DeleteAuthTokenButton.Enabled = false; + var item = AuthTokensListBox.SelectedItems[0].Tag as string; + var host = item?.Split('|')[0].Trim(); + if (host != null) + { + coreConfig.SetAuthToken(host, null); + RefreshAuthTokensListBox(); + DeleteAuthTokenButton.Enabled = false; + } } } - private void CheckForUpdatesButton_Click(object sender, EventArgs e) + private void CheckForUpdatesButton_Click(object? sender, EventArgs? e) { try { @@ -585,9 +598,9 @@ private void CheckForUpdatesButton_Click(object sender, EventArgs e) } } - private void InstallUpdateButton_Click(object sender, EventArgs e) + private void InstallUpdateButton_Click(object? sender, EventArgs? e) { - if (Main.Instance.CheckForCKANUpdate()) + if (Main.Instance?.CheckForCKANUpdate() ?? false) { Hide(); Main.Instance.UpdateCKAN(); @@ -598,28 +611,28 @@ private void InstallUpdateButton_Click(object sender, EventArgs e) } } - private void CheckUpdateOnLaunchCheckbox_CheckedChanged(object sender, EventArgs e) + private void CheckUpdateOnLaunchCheckbox_CheckedChanged(object? sender, EventArgs? e) { guiConfig.CheckForUpdatesOnLaunch = CheckUpdateOnLaunchCheckbox.Checked; } - private void DevBuildsCheckbox_CheckedChanged(object sender, EventArgs e) + private void DevBuildsCheckbox_CheckedChanged(object? sender, EventArgs? e) { coreConfig.DevBuilds = DevBuildsCheckbox.Checked; UpdateAutoUpdate(); } - private void RefreshOnStartupCheckbox_CheckedChanged(object sender, EventArgs e) + private void RefreshOnStartupCheckbox_CheckedChanged(object? sender, EventArgs? e) { guiConfig.RefreshOnStartup = RefreshOnStartupCheckbox.Checked; } - private void HideEpochsCheckbox_CheckedChanged(object sender, EventArgs e) + private void HideEpochsCheckbox_CheckedChanged(object? sender, EventArgs? e) { guiConfig.HideEpochs = HideEpochsCheckbox.Checked; } - private void HideVCheckbox_CheckedChanged(object sender, EventArgs e) + private void HideVCheckbox_CheckedChanged(object? sender, EventArgs? e) { guiConfig.HideV = HideVCheckbox.Checked; } @@ -633,29 +646,29 @@ private void LanguageSelectionComboBox_MouseWheel(object sender, MouseEventArgs } } - private void LanguageSelectionComboBox_SelectionChanged(object sender, EventArgs e) + private void LanguageSelectionComboBox_SelectionChanged(object? sender, EventArgs? e) { - coreConfig.Language = LanguageSelectionComboBox.SelectedItem.ToString(); + coreConfig.Language = LanguageSelectionComboBox.SelectedItem?.ToString(); } - private void AutoSortUpdateCheckBox_CheckedChanged(object sender, EventArgs e) + private void AutoSortUpdateCheckBox_CheckedChanged(object? sender, EventArgs? e) { guiConfig.AutoSortByUpdate = AutoSortUpdateCheckBox.Checked; } - private void EnableTrayIconCheckBox_CheckedChanged(object sender, EventArgs e) + private void EnableTrayIconCheckBox_CheckedChanged(object? sender, EventArgs? e) { MinimizeToTrayCheckBox.Enabled = guiConfig.EnableTrayIcon = EnableTrayIconCheckBox.Checked; - Main.Instance.CheckTrayState(); + Main.Instance?.CheckTrayState(); } - private void MinimizeToTrayCheckBox_CheckedChanged(object sender, EventArgs e) + private void MinimizeToTrayCheckBox_CheckedChanged(object? sender, EventArgs? e) { guiConfig.MinimizeToTray = MinimizeToTrayCheckBox.Checked; - Main.Instance.CheckTrayState(); + Main.Instance?.CheckTrayState(); } - private void RefreshTextBox_TextChanged(object sender, EventArgs e) + private void RefreshTextBox_TextChanged(object? sender, EventArgs? e) { coreConfig.RefreshRate = string.IsNullOrEmpty(RefreshTextBox.Text) ? 0 : int.Parse(RefreshTextBox.Text); UpdateRefreshRate(); @@ -669,17 +682,17 @@ private void RefreshTextBox_KeyPress(object sender, KeyPressEventArgs e) } } - private void PauseRefreshCheckBox_CheckedChanged(object sender, EventArgs e) + private void PauseRefreshCheckBox_CheckedChanged(object? sender, EventArgs? e) { guiConfig.RefreshPaused = PauseRefreshCheckBox.Checked; if (guiConfig.RefreshPaused) { - Main.Instance.refreshTimer.Stop(); + Main.Instance?.refreshTimer?.Stop(); } else { - Main.Instance.refreshTimer.Start(); + Main.Instance?.refreshTimer?.Start(); } } } diff --git a/GUI/Dialogs/YesNoDialog.cs b/GUI/Dialogs/YesNoDialog.cs index 1414b4485d..34a1d042cf 100644 --- a/GUI/Dialogs/YesNoDialog.cs +++ b/GUI/Dialogs/YesNoDialog.cs @@ -23,7 +23,7 @@ public YesNoDialog() } [ForbidGUICalls] - public DialogResult ShowYesNoDialog(Form parentForm, string text, string yesText = null, string noText = null) + public DialogResult ShowYesNoDialog(Form parentForm, string text, string? yesText = null, string? noText = null) { task = new TaskCompletionSource>(); @@ -37,7 +37,7 @@ public DialogResult ShowYesNoDialog(Form parentForm, string text, string yesText } [ForbidGUICalls] - public Tuple ShowSuppressableYesNoDialog(Form parentForm, string text, string suppressText, string yesText = null, string noText = null) + public Tuple ShowSuppressableYesNoDialog(Form parentForm, string text, string suppressText, string? yesText = null, string? noText = null) { task = new TaskCompletionSource>(); @@ -50,7 +50,7 @@ public Tuple ShowSuppressableYesNoDialog(Form parentForm, st return task.Task.Result; } - private void Setup(string text, string yesText, string noText) + private void Setup(string text, string? yesText, string? noText) { var height = Util.StringHeight(CreateGraphics(), text, DescriptionLabel.Font, ClientSize.Width - 25) + (2 * 54); DescriptionLabel.Text = text; @@ -70,7 +70,7 @@ private void Setup(string text, string yesText, string noText) ActiveControl = YesButton; } - private void SetupSuppressable(string text, string yesText, string noText, string suppressText) + private void SetupSuppressable(string text, string? yesText, string? noText, string suppressText) { Setup(text, yesText, noText); SuppressCheckbox.Checked = false; @@ -84,7 +84,7 @@ public void HideYesNoDialog() } private const int maxHeight = 600; - private TaskCompletionSource> task; + private TaskCompletionSource>? task; private readonly string defaultYes; private readonly string defaultNo; } diff --git a/GUI/FormCompatibility.cs b/GUI/FormCompatibility.cs index 9afa43a5e1..53f7ad9aef 100644 --- a/GUI/FormCompatibility.cs +++ b/GUI/FormCompatibility.cs @@ -1,5 +1,8 @@ using System.Drawing; using System.Windows.Forms; +#if NET5_0_OR_GREATER +using System.Runtime.Versioning; +#endif namespace CKAN.GUI { @@ -7,6 +10,9 @@ namespace CKAN.GUI /// Inheriting from this class ensures that forms are equally sized on Windows and on Linux/MacOSX /// Choose the form size so that it is the right one for Windows. /// + #if NET5_0_OR_GREATER + [SupportedOSPlatform("windows")] + #endif public class FormCompatibility : Form { private const int formHeightDifference = 24; diff --git a/GUI/Labels/ModuleLabel.cs b/GUI/Labels/ModuleLabel.cs index 2ba6a4d220..e8b1b154ec 100644 --- a/GUI/Labels/ModuleLabel.cs +++ b/GUI/Labels/ModuleLabel.cs @@ -13,14 +13,25 @@ namespace CKAN.GUI [JsonConverter(typeof(ModuleIdentifiersRenamedConverter))] public class ModuleLabel { + [JsonConstructor] + public ModuleLabel() + { + Name = ""; + } + + public ModuleLabel(string name) + { + Name = name; + } + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name; [JsonProperty("color", NullValueHandling = NullValueHandling.Ignore)] - public Color Color; + public Color? Color; [JsonProperty("instance_name", NullValueHandling = NullValueHandling.Ignore)] - public string InstanceName; + public string? InstanceName; [JsonProperty("hide", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] [DefaultValue(false)] @@ -57,7 +68,7 @@ public class ModuleLabel /// Game to check /// Number of modules public int ModuleCount(IGame game) - => ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet identifiers) + => ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet? identifiers) ? identifiers.Count : 0; @@ -68,7 +79,7 @@ public int ModuleCount(IGame game) /// The identifier to check /// true if this label applies to this identifier, false otherwise public bool ContainsModule(IGame game, string identifier) - => ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet identifiers) + => ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet? identifiers) && identifiers.Contains(identifier); /// @@ -82,7 +93,7 @@ public bool AppliesTo(string instanceName) => InstanceName == null || InstanceName == instanceName; public IEnumerable IdentifiersFor(IGame game) - => ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet idents) + => ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet? idents) ? idents : Enumerable.Empty(); @@ -92,7 +103,7 @@ public IEnumerable IdentifiersFor(IGame game) /// The identifier of the module to add public void Add(IGame game, string identifier) { - if (ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet identifiers)) + if (ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet? identifiers)) { identifiers.Add(identifier); } @@ -108,7 +119,7 @@ public void Add(IGame game, string identifier) /// The identifier of the module to remove public void Remove(IGame game, string identifier) { - if (ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet identifiers)) + if (ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet? identifiers)) { identifiers.Remove(identifier); if (identifiers.Count < 1) diff --git a/GUI/Labels/ModuleLabelList.cs b/GUI/Labels/ModuleLabelList.cs index fb3f069d37..902d97c124 100644 --- a/GUI/Labels/ModuleLabelList.cs +++ b/GUI/Labels/ModuleLabelList.cs @@ -21,32 +21,31 @@ public IEnumerable LabelsFor(string instanceName) public static readonly string DefaultPath = Path.Combine(CKANPathUtils.AppDataPath, "labels.json"); + public static readonly ModuleLabelList ModuleLabels = Load(DefaultPath) ?? GetDefaultLabels(); + public static ModuleLabelList GetDefaultLabels() => new ModuleLabelList() { Labels = new ModuleLabel[] { - new ModuleLabel() + new ModuleLabel(Properties.Resources.ModuleLabelListFavourites) { - Name = Properties.Resources.ModuleLabelListFavourites, Color = Color.PaleGreen, }, - new ModuleLabel() + new ModuleLabel(Properties.Resources.ModuleLabelListHidden) { - Name = Properties.Resources.ModuleLabelListHidden, Hide = true, Color = Color.PaleVioletRed, }, - new ModuleLabel() + new ModuleLabel(Properties.Resources.ModuleLabelListHeld) { - Name = Properties.Resources.ModuleLabelListHeld, HoldVersion = true, Color = Color.FromArgb(255, 255, 176), } } }; - public static ModuleLabelList Load(string path) + public static ModuleLabelList? Load(string path) { try { diff --git a/GUI/Main/Main.cs b/GUI/Main/Main.cs index d5dfc211fa..7e0d3be90a 100644 --- a/GUI/Main/Main.cs +++ b/GUI/Main/Main.cs @@ -34,21 +34,21 @@ public partial class Main : Form, IMessageFilter // Stuff we set in the constructor and never change public readonly IUser currentUser; public readonly GameInstanceManager Manager; - public GameInstance CurrentInstance => Manager.CurrentInstance; + public GameInstance? CurrentInstance => Manager.CurrentInstance; private readonly RepositoryDataManager repoData; private readonly AutoUpdate updater = new AutoUpdate(); // Stuff we set when the game instance changes - public GUIConfiguration configuration; - public PluginController pluginController; + public GUIConfiguration? configuration; + public PluginController? pluginController; private readonly TabController tabController; - private string focusIdent; + private string? focusIdent; private bool needRegistrySave = false; [Obsolete("Main.Instance is a global singleton. Find a better way to access this object.")] - public static Main Instance { get; private set; } + public static Main? Instance { get; private set; } /// /// Set up the main form's core properties quickly. @@ -58,7 +58,7 @@ public partial class Main : Form, IMessageFilter /// /// The strings from the command line that launched us /// Game instance manager created by the cmdline handler - public Main(string[] cmdlineArgs, GameInstanceManager mgr) + public Main(string[] cmdlineArgs, GameInstanceManager? mgr) { log.Info("Starting the GUI"); if (cmdlineArgs.Length >= 2) @@ -66,16 +66,16 @@ public Main(string[] cmdlineArgs, GameInstanceManager mgr) focusIdent = cmdlineArgs[1]; if (focusIdent.StartsWith("//")) { - focusIdent = focusIdent.Substring(2); + focusIdent = focusIdent[2..]; } else if (focusIdent.StartsWith("ckan://")) { - focusIdent = focusIdent.Substring(7); + focusIdent = focusIdent[7..]; } if (focusIdent.EndsWith("/")) { - focusIdent = focusIdent.Substring(0, focusIdent.Length - 1); + focusIdent = focusIdent[..^1]; } } @@ -151,8 +151,11 @@ public Main(string[] cmdlineArgs, GameInstanceManager mgr) Manager = new GameInstanceManager(currentUser); } - Manager.Cache.ModStored += OnModStoredOrPurged; - Manager.Cache.ModPurged += OnModStoredOrPurged; + if (Manager.Cache != null) + { + Manager.Cache.ModStored += OnModStoredOrPurged; + Manager.Cache.ModPurged += OnModStoredOrPurged; + } tabController = new TabController(MainTabControl); tabController.ShowTab("ManageModsTabPage"); @@ -204,7 +207,7 @@ protected override void OnLoad(EventArgs e) private static string GUIConfigPath(GameInstance inst) => Path.Combine(inst.CkanDir(), GUIConfigFilename); - private static GUIConfiguration GUIConfigForInstance(SteamLibrary steamLib, GameInstance inst) + private static GUIConfiguration GUIConfigForInstance(SteamLibrary steamLib, GameInstance? inst) => inst == null ? new GUIConfiguration() : GUIConfiguration.LoadOrCreateConfiguration( GUIConfigPath(inst), @@ -212,7 +215,7 @@ private static GUIConfiguration GUIConfigForInstance(SteamLibrary steamLib, Game new DirectoryInfo(inst.GameDir())) .ToList()); - private static GameInstance InstanceWithNewestGUIConfig(IEnumerable instances) + private static GameInstance? InstanceWithNewestGUIConfig(IEnumerable instances) => instances.Where(inst => inst.Valid) .OrderByDescending(inst => File.GetLastWriteTime(GUIConfigPath(inst))) .ThenBy(inst => inst.Name) @@ -234,85 +237,91 @@ protected override void OnShown(EventArgs e) Wait.StartWaiting( (sender, evt) => { - currentUser.RaiseMessage(Properties.Resources.MainModListLoadingRegistry); - // Make sure we have a lockable instance - do + if (evt != null) { - if (CurrentInstance == null && !InstancePromptAtStart()) - { - // User cancelled, give up - evt.Result = false; - return; - } - for (RegistryManager regMgr = null; - CurrentInstance != null && regMgr == null;) + currentUser.RaiseMessage(Properties.Resources.MainModListLoadingRegistry); + // Make sure we have a lockable instance + do { - // We now have a tentative instance. Check if it's locked. - try + if (CurrentInstance == null && !InstancePromptAtStart()) { - // This will throw RegistryInUseKraken if locked by another process - regMgr = RegistryManager.Instance(CurrentInstance, repoData); - // Tell the user their registry was reset if it was corrupted - if (!string.IsNullOrEmpty(regMgr.previousCorruptedMessage) - && !string.IsNullOrEmpty(regMgr.previousCorruptedPath)) - { - errorDialog.ShowErrorDialog(this, - Properties.Resources.MainCorruptedRegistry, - regMgr.previousCorruptedPath, regMgr.previousCorruptedMessage, - Path.Combine(Path.GetDirectoryName(regMgr.previousCorruptedPath) ?? "", regMgr.LatestInstalledExportFilename())); - regMgr.previousCorruptedMessage = null; - regMgr.previousCorruptedPath = null; - // But the instance is actually fine because a new registry was just created - } + // User cancelled, give up + evt.Result = false; + return; } - catch (RegistryInUseKraken kraken) + for (RegistryManager? regMgr = null; + CurrentInstance != null && regMgr == null;) { - if (Instance.YesNoDialog( - kraken.ToString(), - Properties.Resources.MainDeleteLockfileYes, - Properties.Resources.MainDeleteLockfileNo)) + // We now have a tentative instance. Check if it's locked. + try { - // Delete it - File.Delete(kraken.lockfilePath); - // Loop back around to re-acquire the lock + // This will throw RegistryInUseKraken if locked by another process + regMgr = RegistryManager.Instance(CurrentInstance, repoData); + // Tell the user their registry was reset if it was corrupted + if (regMgr.previousCorruptedMessage != null + && regMgr.previousCorruptedPath != null) + { + errorDialog.ShowErrorDialog(this, + Properties.Resources.MainCorruptedRegistry, + regMgr.previousCorruptedPath, regMgr.previousCorruptedMessage, + Path.Combine(Path.GetDirectoryName(regMgr.previousCorruptedPath) ?? "", regMgr.LatestInstalledExportFilename())); + regMgr.previousCorruptedMessage = null; + regMgr.previousCorruptedPath = null; + // But the instance is actually fine because a new registry was just created + } } - else + catch (RegistryInUseKraken kraken) { - // Couldn't get the lock, there is no current instance - Manager.CurrentInstance = null; - if (Manager.Instances.Values.All(inst => !inst.Valid || inst.IsMaybeLocked)) + if (YesNoDialog( + kraken.ToString(), + Properties.Resources.MainDeleteLockfileYes, + Properties.Resources.MainDeleteLockfileNo)) { - // Everything's invalid or locked, give up - evt.Result = false; - return; + // Delete it + File.Delete(kraken.lockfilePath); + // Loop back around to re-acquire the lock + } + else + { + // Couldn't get the lock, there is no current instance + Manager.CurrentInstance = null; + if (Manager.Instances.Values.All(inst => !inst.Valid || inst.IsMaybeLocked)) + { + // Everything's invalid or locked, give up + evt.Result = false; + return; + } } } } - } - } while (CurrentInstance == null); - // We can only reach this point if CurrentInstance is not null - // AND we acquired the lock for it successfully - evt.Result = true; + } while (CurrentInstance == null); + // We can only reach this point if CurrentInstance is not null + // AND we acquired the lock for it successfully + evt.Result = true; + } }, (sender, evt) => { - // Application.Exit doesn't work if the window is disabled! - EnableMainWindow(); - if ((bool)evt.Result) + if (evt != null) { - HideWaitDialog(); - CheckTrayState(); - Console.CancelKeyPress += (sender2, evt2) => + // Application.Exit doesn't work if the window is disabled! + EnableMainWindow(); + if (evt.Result is bool b && b) { - // Hide tray icon on Ctrl-C - minimizeNotifyIcon.Visible = false; - }; - InitRefreshTimer(); - CurrentInstanceUpdated(); - } - else - { - Application.Exit(); + HideWaitDialog(); + CheckTrayState(); + Console.CancelKeyPress += (sender2, evt2) => + { + // Hide tray icon on Ctrl-C + minimizeNotifyIcon.Visible = false; + }; + InitRefreshTimer(); + CurrentInstanceUpdated(); + } + else + { + Application.Exit(); + } } }, false, @@ -332,7 +341,7 @@ private bool InstancePromptAtStart() return gotInstance; } - private void manageGameInstancesMenuItem_Click(object sender, EventArgs e) + private void manageGameInstancesMenuItem_Click(object? sender, EventArgs? e) { var old_instance = CurrentInstance; var result = new ManageGameInstancesDialog(Manager, !actuallyVisible, currentUser).ShowDialog(this); @@ -348,7 +357,7 @@ private void manageGameInstancesMenuItem_Click(object sender, EventArgs e) } catch (RegistryInUseKraken kraken) { - if (Instance.YesNoDialog( + if (YesNoDialog( kraken.ToString(), Properties.Resources.MainDeleteLockfileYes, Properties.Resources.MainDeleteLockfileNo)) @@ -370,14 +379,18 @@ private void manageGameInstancesMenuItem_Click(object sender, EventArgs e) private void UpdateStatusBar() { - StatusInstanceLabel.Text = string.Format( - CurrentInstance.playTime.Time > TimeSpan.Zero - ? Properties.Resources.StatusInstanceLabelTextWithPlayTime - : Properties.Resources.StatusInstanceLabelText, - CurrentInstance.Name, - CurrentInstance.game.ShortName, - CurrentInstance.Version()?.ToString(), - CurrentInstance.playTime.ToString()); + if (CurrentInstance != null) + { + StatusInstanceLabel.Text = string.Format( + CurrentInstance.playTime != null + && CurrentInstance.playTime.Time > TimeSpan.Zero + ? Properties.Resources.StatusInstanceLabelTextWithPlayTime + : Properties.Resources.StatusInstanceLabelText, + CurrentInstance.Name, + CurrentInstance.game.ShortName, + CurrentInstance.Version()?.ToString(), + CurrentInstance.playTime?.ToString() ?? ""); + } } /// @@ -386,6 +399,10 @@ private void UpdateStatusBar() /// true if a repo update is allowed if needed (e.g. on initial load), false otherwise private void CurrentInstanceUpdated() { + if (CurrentInstance == null) + { + return; + } // This will throw RegistryInUseKraken if locked by another process var regMgr = RegistryManager.Instance(CurrentInstance, repoData); log.Debug("Current instance updated, scanning"); @@ -403,8 +420,8 @@ private void CurrentInstanceUpdated() } var registry = regMgr.registry; - if (!string.IsNullOrEmpty(regMgr.previousCorruptedMessage) - && !string.IsNullOrEmpty(regMgr.previousCorruptedPath)) + if (regMgr.previousCorruptedMessage != null + && regMgr.previousCorruptedPath != null) { errorDialog.ShowErrorDialog(this, Properties.Resources.MainCorruptedRegistry, @@ -461,7 +478,7 @@ protected override void OnFormClosed(FormClosedEventArgs e) { if (CurrentInstance != null) { - RegistryManager.DisposeInstance(Manager.CurrentInstance); + RegistryManager.DisposeInstance(CurrentInstance); } // Stop all running play time timers @@ -469,7 +486,7 @@ protected override void OnFormClosed(FormClosedEventArgs e) { if (inst.Valid) { - inst.playTime.Stop(inst.CkanDir()); + inst.playTime?.Stop(inst.CkanDir()); } } Application.RemoveMessageFilter(this); @@ -479,31 +496,34 @@ protected override void OnFormClosed(FormClosedEventArgs e) private void SetStartPosition() { - Screen screen = Util.FindScreen(configuration.WindowLoc, configuration.WindowSize); - if (screen == null) + if (configuration != null) { - // Start at center of screen if we have an invalid location saved in the config - // (such as -32000,-32000, which Windows uses when you're minimized) - StartPosition = FormStartPosition.CenterScreen; - } - else if (configuration.WindowLoc.X == -1 && configuration.WindowLoc.Y == -1) - { - // Center on screen for first launch - StartPosition = FormStartPosition.CenterScreen; - } - else if (Platform.IsMac) - { - // Make sure there's room at the top for the MacOSX menu bar - Location = Util.ClampedLocationWithMargins( - configuration.WindowLoc, configuration.WindowSize, - new Size(0, 30), new Size(0, 0), - screen - ); - } - else - { - // Just make sure it's fully on screen - Location = Util.ClampedLocation(configuration.WindowLoc, configuration.WindowSize, screen); + var screen = Util.FindScreen(configuration.WindowLoc, configuration.WindowSize); + if (screen == null) + { + // Start at center of screen if we have an invalid location saved in the config + // (such as -32000,-32000, which Windows uses when you're minimized) + StartPosition = FormStartPosition.CenterScreen; + } + else if (configuration.WindowLoc.X == -1 && configuration.WindowLoc.Y == -1) + { + // Center on screen for first launch + StartPosition = FormStartPosition.CenterScreen; + } + else if (Platform.IsMac) + { + // Make sure there's room at the top for the MacOSX menu bar + Location = Util.ClampedLocationWithMargins( + configuration.WindowLoc, configuration.WindowSize, + new Size(0, 30), new Size(0, 0), + screen + ); + } + else + { + // Just make sure it's fully on screen + Location = Util.ClampedLocation(configuration.WindowLoc, configuration.WindowSize, screen); + } } } @@ -522,22 +542,25 @@ protected override void OnFormClosing(FormClosingEventArgs e) return; } - // Copy window location to app settings - configuration.WindowLoc = WindowState == FormWindowState.Normal ? Location : RestoreBounds.Location; + if (configuration != null) + { + // Copy window location to app settings + configuration.WindowLoc = WindowState == FormWindowState.Normal ? Location : RestoreBounds.Location; - // Copy window size to app settings if not maximized - configuration.WindowSize = WindowState == FormWindowState.Normal ? Size : RestoreBounds.Size; + // Copy window size to app settings if not maximized + configuration.WindowSize = WindowState == FormWindowState.Normal ? Size : RestoreBounds.Size; - //copy window maximized state to app settings - configuration.IsWindowMaximised = WindowState == FormWindowState.Maximized; + //copy window maximized state to app settings + configuration.IsWindowMaximised = WindowState == FormWindowState.Maximized; - // Copy panel position to app settings - configuration.PanelPosition = splitContainer1.SplitterDistance; + // Copy panel position to app settings + configuration.PanelPosition = splitContainer1.SplitterDistance; - // Save settings - configuration.Save(); + // Save settings + configuration.Save(); + } - if (needRegistrySave) + if (needRegistrySave && CurrentInstance != null) { using (var transaction = CkanTransaction.CreateTransactionScope()) { @@ -583,101 +606,121 @@ protected override void OnMove(EventArgs e) private void SetupDefaultSearch() { - var registry = RegistryManager.Instance(CurrentInstance, repoData).registry; - var def = configuration.DefaultSearches; - if (def == null || def.Count < 1) - { - // Fall back to old setting - ManageMods.Filter(ModList.FilterToSavedSearch( - (GUIModFilter)configuration.ActiveFilter, - registry.Tags.GetOrDefault(configuration.TagFilter), - ManageMods.mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name) - .FirstOrDefault(l => l.Name == configuration.CustomLabelFilter) - ), false); - // Clear the old filter so it doesn't get pulled forward again - configuration.ActiveFilter = (int)GUIModFilter.All; - } - else + if (CurrentInstance != null && configuration != null) { - var labels = ManageMods.mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name).ToList(); - var searches = def.Select(s => ModSearch.Parse(s, labels)).ToList(); - ManageMods.SetSearches(searches); + var registry = RegistryManager.Instance(CurrentInstance, repoData).registry; + var def = configuration.DefaultSearches; + if (def == null || def.Count < 1) + { + // Fall back to old setting + ManageMods.Filter( + ModList.FilterToSavedSearch( + (GUIModFilter)configuration.ActiveFilter, + configuration.TagFilter == null + ? null + : registry.Tags.GetOrDefault(configuration.TagFilter), + ModuleLabelList.ModuleLabels.LabelsFor(CurrentInstance.Name) + .FirstOrDefault(l => l.Name == configuration.CustomLabelFilter)), + false); + // Clear the old filter so it doesn't get pulled forward again + configuration.ActiveFilter = (int)GUIModFilter.All; + } + else + { + var labels = ModuleLabelList.ModuleLabels.LabelsFor(CurrentInstance.Name).ToList(); + var searches = def.Select(s => ModSearch.Parse(s, labels)) + .OfType() + .ToList(); + ManageMods.SetSearches(searches); + } } } - private void ExitToolButton_Click(object sender, EventArgs e) + private void ExitToolButton_Click(object? sender, EventArgs? e) { Close(); } - private void aboutToolStripMenuItem_Click(object sender, EventArgs e) + private void aboutToolStripMenuItem_Click(object? sender, EventArgs? e) { new AboutDialog().ShowDialog(this); } - private void GameCommandlineToolStripMenuItem_Click(object sender, EventArgs e) + private void GameCommandlineToolStripMenuItem_Click(object? sender, EventArgs? e) { EditCommandLines(); } private void EditCommandLines() { - var dialog = new GameCommandLineOptionsDialog(); - var defaults = CurrentInstance.game.DefaultCommandLines(Manager.SteamLibrary, - new DirectoryInfo(CurrentInstance.GameDir())); - if (dialog.ShowGameCommandLineOptionsDialog(this, configuration.CommandLines, defaults) == DialogResult.OK) + if (CurrentInstance != null && configuration != null) { - configuration.CommandLines = dialog.Results; + var dialog = new GameCommandLineOptionsDialog(); + var defaults = CurrentInstance.game.DefaultCommandLines(Manager.SteamLibrary, + new DirectoryInfo(CurrentInstance.GameDir())); + if (dialog.ShowGameCommandLineOptionsDialog(this, configuration.CommandLines, defaults) == DialogResult.OK) + { + configuration.CommandLines = dialog.Results; + } } } - private void CKANSettingsToolStripMenuItem_Click(object sender, EventArgs e) + private void CKANSettingsToolStripMenuItem_Click(object? sender, EventArgs? e) { - // Flipping enabled here hides the main form itself. - Enabled = false; - var dialog = new SettingsDialog(ServiceLocator.Container.Resolve(), - configuration, - RegistryManager.Instance(CurrentInstance, repoData), - updater, - currentUser); - dialog.ShowDialog(this); - Enabled = true; - if (dialog.RepositoryAdded) - { - UpdateRepo(refreshWithoutChanges: true); - } - else if (dialog.RepositoryRemoved || dialog.RepositoryMoved) + if (CurrentInstance != null && configuration != null) { - RefreshModList(false); + // Flipping enabled here hides the main form itself. + Enabled = false; + var dialog = new SettingsDialog(ServiceLocator.Container.Resolve(), + configuration, + RegistryManager.Instance(CurrentInstance, repoData), + updater, + currentUser); + dialog.ShowDialog(this); + Enabled = true; + if (dialog.RepositoryAdded) + { + UpdateRepo(refreshWithoutChanges: true); + } + else if (dialog.RepositoryRemoved || dialog.RepositoryMoved) + { + RefreshModList(false); + } } } - private void pluginsToolStripMenuItem_Click(object sender, EventArgs e) + private void pluginsToolStripMenuItem_Click(object? sender, EventArgs? e) { Enabled = false; pluginsDialog.ShowDialog(this); Enabled = true; } - private void preferredHostsToolStripMenuItem_Click(object sender, EventArgs e) + private void preferredHostsToolStripMenuItem_Click(object? sender, EventArgs? e) { - Enabled = false; - var dlg = new PreferredHostsDialog( - ServiceLocator.Container.Resolve(), - RegistryManager.Instance(CurrentInstance, repoData).registry); - dlg.ShowDialog(this); - Enabled = true; + if (CurrentInstance != null) + { + Enabled = false; + var dlg = new PreferredHostsDialog( + ServiceLocator.Container.Resolve(), + RegistryManager.Instance(CurrentInstance, repoData).registry); + dlg.ShowDialog(this); + Enabled = true; + } } - private void installFiltersToolStripMenuItem_Click(object sender, EventArgs e) + private void installFiltersToolStripMenuItem_Click(object? sender, EventArgs? e) { - Enabled = false; - var dlg = new InstallFiltersDialog(ServiceLocator.Container.Resolve(), CurrentInstance); - dlg.ShowDialog(this); - Enabled = true; + if (CurrentInstance != null) + { + Enabled = false; + var dlg = new InstallFiltersDialog(ServiceLocator.Container.Resolve(), CurrentInstance); + dlg.ShowDialog(this); + Enabled = true; + } } - private void installFromckanToolStripMenuItem_Click(object sender, EventArgs e) + private void installFromckanToolStripMenuItem_Click(object? sender, EventArgs? e) { OpenFileDialog open_file_dialog = new OpenFileDialog() { @@ -693,6 +736,10 @@ private void installFromckanToolStripMenuItem_Click(object sender, EventArgs e) private void InstallFromCkanFiles(string[] files) { + if (CurrentInstance == null) + { + return; + } // We'll need to make some registry changes to do this. var registry_manager = RegistryManager.Instance(CurrentInstance, repoData); var crit = CurrentInstance.VersionCriteria(); @@ -748,8 +795,9 @@ private void InstallFromCkanFiles(string[] files) { CkanModule.GetMinMaxVersions(modpacks, out _, out _, - out GameVersion minGame, out GameVersion maxGame); - var filesRange = new GameVersionRange(minGame, maxGame); + out GameVersion? minGame, out GameVersion? maxGame); + var filesRange = new GameVersionRange(minGame ?? GameVersion.Any, + maxGame ?? GameVersion.Any); var instRanges = crit.Versions.Select(gv => gv.ToVersionRange()) .ToList(); var missing = CurrentInstance.game @@ -795,16 +843,17 @@ private void InstallFromCkanFiles(string[] files) } } - private void CompatibleGameVersionsToolStripMenuItem_Click(object sender, EventArgs e) + private void CompatibleGameVersionsToolStripMenuItem_Click(object? sender, EventArgs? e) { - CompatibleGameVersionsDialog dialog = new CompatibleGameVersionsDialog( - Instance.Manager.CurrentInstance, - !actuallyVisible - ); - if (dialog.ShowDialog(this) != DialogResult.Cancel) + if (CurrentInstance != null) { - // This takes a while, so don't do it if they cancel out - RefreshModList(false); + var dialog = new CompatibleGameVersionsDialog(CurrentInstance, + !actuallyVisible); + if (dialog.ShowDialog(this) != DialogResult.Cancel) + { + // This takes a while, so don't do it if they cancel out + RefreshModList(false); + } } } @@ -840,7 +889,7 @@ private void ManageMods_OnSelectedModuleChanged(GUIMod m) } } - private GUIMod ActiveModInfo + private GUIMod? ActiveModInfo { set { if (value?.ToModule() == null) @@ -858,21 +907,25 @@ private GUIMod ActiveModInfo } } - private void ShowSelectionModInfo(CkanModule module) + private void ShowSelectionModInfo(CkanModule? module) { - ActiveModInfo = module == null ? null : new GUIMod( - module, - repoData, - RegistryManager.Instance(CurrentInstance, repoData).registry, - CurrentInstance.VersionCriteria(), - null, - configuration.HideEpochs, - configuration.HideV); + if (CurrentInstance != null && configuration != null) + { + ActiveModInfo = module == null ? null : new GUIMod( + module, + repoData, + RegistryManager.Instance(CurrentInstance, repoData).registry, + CurrentInstance.VersionCriteria(), + null, + configuration.HideEpochs, + configuration.HideV); + } } private void ShowSelectionModInfo(ListView.SelectedListViewItemCollection selection) { - ShowSelectionModInfo(selection?.Cast().FirstOrDefault()?.Tag as CkanModule); + ShowSelectionModInfo(selection?.OfType() + .FirstOrDefault()?.Tag as CkanModule); } private void ManageMods_OnChangeSetChanged(List changeset, Dictionary conflicts) @@ -918,7 +971,7 @@ private void ManageMods_ClearStatusBar() StatusLabel.ToolTipText = StatusLabel.Text = ""; } - private void MainTabControl_OnSelectedIndexChanged(object sender, EventArgs e) + private void MainTabControl_OnSelectedIndexChanged(object? sender, EventArgs? e) { switch (MainTabControl.SelectedTab?.Name) { @@ -940,87 +993,98 @@ private void MainTabControl_OnSelectedIndexChanged(object sender, EventArgs e) break; default: - ShowSelectionModInfo((CkanModule)null); + ShowSelectionModInfo(null as CkanModule); break; } } - private void userGuideToolStripMenuItem_Click(object sender, EventArgs e) + private void userGuideToolStripMenuItem_Click(object? sender, EventArgs? e) { Utilities.ProcessStartURL(HelpURLs.UserGuide); } - private void discordToolStripMenuItem_Click(object sender, EventArgs e) + private void discordToolStripMenuItem_Click(object? sender, EventArgs? e) { Utilities.ProcessStartURL(HelpURLs.CKANDiscord); } - private void modSupportToolStripMenuItem_Click(object sender, EventArgs e) + private void modSupportToolStripMenuItem_Click(object? sender, EventArgs? e) { - Utilities.ProcessStartURL(Manager.CurrentInstance.game.ModSupportURL.ToString()); + if (CurrentInstance != null) + { + Utilities.ProcessStartURL(CurrentInstance.game.ModSupportURL.ToString()); + } } - private void reportClientIssueToolStripMenuItem_Click(object sender, EventArgs e) + private void reportClientIssueToolStripMenuItem_Click(object? sender, EventArgs? e) { Utilities.ProcessStartURL(HelpURLs.CKANIssues); } - private void reportMetadataIssueToolStripMenuItem_Click(object sender, EventArgs e) + private void reportMetadataIssueToolStripMenuItem_Click(object? sender, EventArgs? e) { - Utilities.ProcessStartURL(Manager.CurrentInstance.game.MetadataBugtrackerURL.ToString()); + if (CurrentInstance != null) + { + Utilities.ProcessStartURL(CurrentInstance.game.MetadataBugtrackerURL.ToString()); + } } - private void Main_Resize(object sender, EventArgs e) + private void Main_Resize(object? sender, EventArgs? e) { UpdateTrayState(); } - private void openGameDirectoryToolStripMenuItem_Click(object sender, EventArgs e) + private void openGameDirectoryToolStripMenuItem_Click(object? sender, EventArgs? e) { - Utilities.ProcessStartURL(Manager.CurrentInstance.GameDir()); + if (CurrentInstance != null) + { + Utilities.ProcessStartURL(CurrentInstance.GameDir()); + } } - private void openGameToolStripMenuItem_Click(object sender, EventArgs e) + private void openGameToolStripMenuItem_Click(object? sender, EventArgs? e) { LaunchGame(); } - private void LaunchGame(string command = null) + private void LaunchGame(string? command = null) { - var registry = RegistryManager.Instance(CurrentInstance, repoData).registry; - var suppressedIdentifiers = CurrentInstance.GetSuppressedCompatWarningIdentifiers; - var incomp = registry.IncompatibleInstalled(CurrentInstance.VersionCriteria()) - .Where(m => !m.Module.IsDLC && !suppressedIdentifiers.Contains(m.identifier)) - .ToList(); - if (incomp.Any()) + if (CurrentInstance != null && configuration != null) { - // Warn that it might not be safe to run game with incompatible modules installed - string incompatDescrip = incomp - .Select(m => $"{m.Module} ({m.Module.CompatibleGameVersions(CurrentInstance.game)})") - .Aggregate((a, b) => $"{a}{Environment.NewLine}{b}"); - var ver = CurrentInstance.Version(); - var result = SuppressableYesNoDialog( - string.Format(Properties.Resources.MainLaunchWithIncompatible, incompatDescrip), - string.Format(Properties.Resources.MainLaunchDontShow, - CurrentInstance.game.ShortName, - new GameVersion(ver.Major, ver.Minor, ver.Patch)), - Properties.Resources.MainLaunch, - Properties.Resources.MainGoBack - ); - if (result.Item1 != DialogResult.Yes) + var registry = RegistryManager.Instance(CurrentInstance, repoData).registry; + var suppressedIdentifiers = CurrentInstance.GetSuppressedCompatWarningIdentifiers; + var incomp = registry.IncompatibleInstalled(CurrentInstance.VersionCriteria()) + .Where(m => !m.Module.IsDLC && !suppressedIdentifiers.Contains(m.identifier)) + .ToList(); + if (incomp.Any() && CurrentInstance.Version() is GameVersion gv) { - return; - } - else if (result.Item2) - { - CurrentInstance.AddSuppressedCompatWarningIdentifiers( - incomp.Select(m => m.identifier).ToHashSet() - ); + // Warn that it might not be safe to run game with incompatible modules installed + string incompatDescrip = incomp + .Select(m => $"{m.Module} ({m.Module.CompatibleGameVersions(CurrentInstance.game)})") + .Aggregate((a, b) => $"{a}{Environment.NewLine}{b}"); + var result = SuppressableYesNoDialog( + string.Format(Properties.Resources.MainLaunchWithIncompatible, + incompatDescrip), + string.Format(Properties.Resources.MainLaunchDontShow, + CurrentInstance.game.ShortName, + gv.WithoutBuild), + Properties.Resources.MainLaunch, + Properties.Resources.MainGoBack); + if (result.Item1 != DialogResult.Yes) + { + return; + } + else if (result.Item2) + { + CurrentInstance.AddSuppressedCompatWarningIdentifiers( + incomp.Select(m => m.identifier) + .ToHashSet()); + } } - } - CurrentInstance.PlayGame(command ?? configuration.CommandLines.First(), - UpdateStatusBar); + CurrentInstance.PlayGame(command ?? configuration.CommandLines.First(), + UpdateStatusBar); + } } // This is used by Reinstall @@ -1032,7 +1096,7 @@ private void ManageMods_StartChangeSet(List changeset, Dictionary oldModules = null) + private void RefreshModList(bool allowAutoUpdate, Dictionary? oldModules = null) { tabController.RenameTab("WaitTabPage", Properties.Resources.MainModListWaitTitle); ShowWaitDialog(); @@ -1041,22 +1105,25 @@ private void RefreshModList(bool allowAutoUpdate, Dictionary oldMo ManageMods.Update, (sender, e) => { - if (allowAutoUpdate && !(bool)e.Result) - { - UpdateRepo(); - } - else + if (e != null) { - UpdateTrayInfo(); - HideWaitDialog(); - EnableMainWindow(); - SetupDefaultSearch(); - if (!string.IsNullOrEmpty(focusIdent)) + if (allowAutoUpdate && e.Result is bool b && !b) { - log.Debug("Attempting to select mod from startup parameters"); - ManageMods.FocusMod(focusIdent, true, true); - // Only do it the first time - focusIdent = null; + UpdateRepo(); + } + else + { + UpdateTrayInfo(); + HideWaitDialog(); + EnableMainWindow(); + SetupDefaultSearch(); + if (focusIdent != null) + { + log.Debug("Attempting to select mod from startup parameters"); + ManageMods.FocusMod(focusIdent, true, true); + // Only do it the first time + focusIdent = null; + } } } }, diff --git a/GUI/Main/MainAutoUpdate.cs b/GUI/Main/MainAutoUpdate.cs index 3afb708308..77768276db 100644 --- a/GUI/Main/MainAutoUpdate.cs +++ b/GUI/Main/MainAutoUpdate.cs @@ -5,6 +5,7 @@ using Autofac; using CKAN.Configuration; +using CKAN.Versioning; // Don't warn if we use our own obsolete properties #pragma warning disable 0618 @@ -56,9 +57,9 @@ public bool CheckForCKANUpdate() log.Info("Making auto-update call"); var mainConfig = ServiceLocator.Container.Resolve(); var update = updater.GetUpdate(mainConfig.DevBuilds ?? false); - var latestVersion = update.Version; - if (latestVersion.IsGreaterThan(Meta.ReleaseVersion)) + if (update.Version is CkanModuleVersion latestVersion + && latestVersion.IsGreaterThan(Meta.ReleaseVersion)) { log.DebugFormat("Found higher CKAN version: {0}", latestVersion); var releaseNotes = update.ReleaseNotes; @@ -100,7 +101,7 @@ public void UpdateCKAN() null); } - private void UpdateReady(object sender, RunWorkerCompletedEventArgs e) + private void UpdateReady(object? sender, RunWorkerCompletedEventArgs? e) { // Close will be cancelled if the window is still disabled EnableMainWindow(); diff --git a/GUI/Main/MainChangeset.cs b/GUI/Main/MainChangeset.cs index cc21fe77e3..537ef6757f 100644 --- a/GUI/Main/MainChangeset.cs +++ b/GUI/Main/MainChangeset.cs @@ -9,14 +9,17 @@ namespace CKAN.GUI { public partial class Main { - private void UpdateChangesDialog(List changeset, Dictionary conflicts) + private void UpdateChangesDialog(List changeset, Dictionary? conflicts) { - Changeset.LoadChangeset( - changeset, - ManageMods.mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name) - .Where(l => l.AlertOnInstall) - .ToList(), - conflicts); + if (CurrentInstance != null) + { + Changeset.LoadChangeset( + changeset, + ModuleLabelList.ModuleLabels.LabelsFor(CurrentInstance.Name) + .Where(l => l.AlertOnInstall) + .ToList(), + conflicts); + } } private void Changeset_OnSelectedItemsChanged(CkanModule item) diff --git a/GUI/Main/MainDialogs.cs b/GUI/Main/MainDialogs.cs index da88273406..1a5ff3059e 100644 --- a/GUI/Main/MainDialogs.cs +++ b/GUI/Main/MainDialogs.cs @@ -1,5 +1,6 @@ using System; using System.Windows.Forms; +using System.Diagnostics.CodeAnalysis; using CKAN.GUI.Attributes; @@ -7,18 +8,17 @@ namespace CKAN.GUI { public partial class Main { - private ErrorDialog errorDialog; - private PluginsDialog pluginsDialog; - private YesNoDialog yesNoDialog; + public ControlFactory controlFactory; + private ErrorDialog errorDialog; + private PluginsDialog pluginsDialog; + private YesNoDialog yesNoDialog; private SelectionDialog selectionDialog; - public ControlFactory controlFactory; + [MemberNotNull(nameof(controlFactory), nameof(errorDialog), nameof(pluginsDialog), + nameof(yesNoDialog), nameof(selectionDialog))] public void RecreateDialogs() { - if (controlFactory == null) - { - controlFactory = new ControlFactory(); - } + controlFactory ??= new ControlFactory(); errorDialog = controlFactory.CreateControl(); pluginsDialog = controlFactory.CreateControl(); yesNoDialog = controlFactory.CreateControl(); @@ -32,7 +32,7 @@ public void ErrorDialog(string text, params object[] args) } [ForbidGUICalls] - public bool YesNoDialog(string text, string yesText = null, string noText = null) + public bool YesNoDialog(string text, string? yesText = null, string? noText = null) => yesNoDialog.ShowYesNoDialog(this, text, yesText, noText) == DialogResult.Yes; /// @@ -41,7 +41,7 @@ public bool YesNoDialog(string text, string yesText = null, string noText = null /// A tuple of the dialog result and a bool indicating whether /// the suppress-checkbox has been checked (true) [ForbidGUICalls] - public Tuple SuppressableYesNoDialog(string text, string suppressText, string yesText = null, string noText = null) + public Tuple SuppressableYesNoDialog(string text, string suppressText, string? yesText = null, string? noText = null) => yesNoDialog.ShowSuppressableYesNoDialog(this, text, suppressText, yesText, noText); [ForbidGUICalls] diff --git a/GUI/Main/MainDownload.cs b/GUI/Main/MainDownload.cs index c42d35fd86..1cdafe0209 100644 --- a/GUI/Main/MainDownload.cs +++ b/GUI/Main/MainDownload.cs @@ -9,14 +9,14 @@ namespace CKAN.GUI { public partial class Main { - private NetAsyncModulesDownloader downloader; + private NetAsyncModulesDownloader? downloader; private void ModInfo_OnDownloadClick(GUIMod gmod) { StartDownload(gmod); } - public void StartDownload(GUIMod module) + public void StartDownload(GUIMod? module) { if (module == null || !module.IsCKAN) { @@ -40,26 +40,34 @@ public void StartDownload(GUIMod module) } [ForbidGUICalls] - private void CacheMod(object sender, DoWorkEventArgs e) + private void CacheMod(object? sender, DoWorkEventArgs? e) { - GUIMod gm = e.Argument as GUIMod; - downloader = new NetAsyncModulesDownloader(currentUser, Manager.Cache); - downloader.Progress += Wait.SetModuleProgress; - downloader.AllComplete += Wait.DownloadsComplete; - downloader.StoreProgress += (module, remaining, total) => - Wait.SetProgress(string.Format(Properties.Resources.ValidatingDownload, module), - remaining, total); - Wait.OnCancel += downloader.CancelDownload; - downloader.DownloadModules(new List { gm.ToCkanModule() }); - e.Result = e.Argument; + if (e != null + && e.Argument is GUIMod gm + && Manager?.Cache != null) + { + downloader = new NetAsyncModulesDownloader(currentUser, Manager.Cache); + downloader.Progress += Wait.SetModuleProgress; + downloader.AllComplete += Wait.DownloadsComplete; + downloader.StoreProgress += (module, remaining, total) => + Wait.SetProgress(string.Format(Properties.Resources.ValidatingDownload, + module), + remaining, total); + Wait.OnCancel += downloader.CancelDownload; + downloader.DownloadModules(new List { gm.ToCkanModule() }); + e.Result = e.Argument; + } } - public void PostModCaching(object sender, RunWorkerCompletedEventArgs e) + public void PostModCaching(object? sender, RunWorkerCompletedEventArgs? e) { - Wait.OnCancel -= downloader.CancelDownload; - downloader = null; + if (downloader != null) + { + Wait.OnCancel -= downloader.CancelDownload; + downloader = null; + } // Can't access e.Result if there's an error - if (e.Error != null) + if (e?.Error != null) { switch (e.Error) { @@ -87,13 +95,13 @@ public void PostModCaching(object sender, RunWorkerCompletedEventArgs e) } [ForbidGUICalls] - private void UpdateCachedByDownloads(CkanModule module) + private void UpdateCachedByDownloads(CkanModule? module) { var allGuiMods = ManageMods.AllGUIMods(); var affectedMods = module?.GetDownloadsGroup(allGuiMods.Values .Select(guiMod => guiMod.ToModule()) - .Where(mod => mod != null)) + .OfType()) .Select(other => allGuiMods[other.identifier]) ?? allGuiMods.Values; foreach (var otherMod in affectedMods) @@ -103,7 +111,7 @@ private void UpdateCachedByDownloads(CkanModule module) } [ForbidGUICalls] - private void OnModStoredOrPurged(CkanModule module) + private void OnModStoredOrPurged(CkanModule? module) { UpdateCachedByDownloads(module); diff --git a/GUI/Main/MainExport.cs b/GUI/Main/MainExport.cs index f2bad1a267..ecbd8fdb01 100644 --- a/GUI/Main/MainExport.cs +++ b/GUI/Main/MainExport.cs @@ -18,35 +18,44 @@ public partial class Main /// /// Exports installed mods to a .ckan file. /// - private void exportModPackToolStripMenuItem_Click(object sender, EventArgs e) + private void exportModPackToolStripMenuItem_Click(object? sender, EventArgs? e) { - // Background thread so GUI thread can work with the controls - Task.Factory.StartNew(() => + if (CurrentInstance != null) { - currentUser.RaiseMessage(""); - tabController.ShowTab("EditModpackTabPage", 2); - DisableMainWindow(); - var mgr = RegistryManager.Instance(CurrentInstance, repoData); - EditModpack.LoadModule(mgr.GenerateModpack(false, true), mgr.registry); - // This will block till the user is done - EditModpack.Wait(currentUser); - tabController.ShowTab("ManageModsTabPage"); - tabController.HideTab("EditModpackTabPage"); - EnableMainWindow(); - }); + // Background thread so GUI thread can work with the controls + Task.Factory.StartNew(() => + { + currentUser.RaiseMessage(""); + tabController.ShowTab("EditModpackTabPage", 2); + DisableMainWindow(); + var mgr = RegistryManager.Instance(CurrentInstance, repoData); + EditModpack.LoadModule(mgr.GenerateModpack(false, true), mgr.registry); + // This will block till the user is done + EditModpack.Wait(currentUser); + tabController.ShowTab("ManageModsTabPage"); + tabController.HideTab("EditModpackTabPage"); + EnableMainWindow(); + }); + } } private void EditModpack_OnSelectedItemsChanged(ListView.SelectedListViewItemCollection items) { - var first = items.Cast().FirstOrDefault()?.Tag as ModuleRelationshipDescriptor; - var ident = first?.name; - if (!string.IsNullOrEmpty(ident) && ManageMods.mainModList.full_list_of_mod_rows.TryGetValue(ident, out DataGridViewRow row)) + if (items.OfType() + .FirstOrDefault() + ?.Tag + is ModuleRelationshipDescriptor first) { - ActiveModInfo = row.Tag as GUIMod; - } - else - { - ActiveModInfo = null; + var ident = first.name; + if (!string.IsNullOrEmpty(ident) + && ManageMods.mainModList.full_list_of_mod_rows.TryGetValue(ident, out DataGridViewRow? row)) + { + ActiveModInfo = row.Tag as GUIMod; + } + else + { + ActiveModInfo = null; + } } } @@ -62,21 +71,24 @@ private void EditModpack_OnSelectedItemsChanged(ListView.SelectedListViewItemCol /// /// Exports installed mods to a non-.ckan file. /// - private void exportModListToolStripMenuItem_Click(object sender, EventArgs e) + private void exportModListToolStripMenuItem_Click(object? sender, EventArgs? e) { - var dlg = new SaveFileDialog() + if (CurrentInstance != null) { - Filter = string.Join("|", specialExportOptions.Select(i => i.ToString()).ToArray()), - Title = Properties.Resources.ExportInstalledModsDialogTitle - }; - if (dlg.ShowDialog(this) == DialogResult.OK) - { - var fileMode = File.Exists(dlg.FileName) ? FileMode.Truncate : FileMode.CreateNew; - using (var stream = new FileStream(dlg.FileName, fileMode)) + var dlg = new SaveFileDialog() + { + Filter = string.Join("|", specialExportOptions.Select(i => i.ToString()).ToArray()), + Title = Properties.Resources.ExportInstalledModsDialogTitle + }; + if (dlg.ShowDialog(this) == DialogResult.OK) { - new Exporter(specialExportOptions[dlg.FilterIndex - 1].ExportFileType).Export( - RegistryManager.Instance(CurrentInstance, repoData).registry, - stream); + var fileMode = File.Exists(dlg.FileName) ? FileMode.Truncate : FileMode.CreateNew; + using (var stream = new FileStream(dlg.FileName, fileMode)) + { + new Exporter(specialExportOptions[dlg.FilterIndex - 1].ExportFileType).Export( + RegistryManager.Instance(CurrentInstance, repoData).registry, + stream); + } } } } diff --git a/GUI/Main/MainHistory.cs b/GUI/Main/MainHistory.cs index cb21b51551..7bcba73044 100644 --- a/GUI/Main/MainHistory.cs +++ b/GUI/Main/MainHistory.cs @@ -8,23 +8,29 @@ namespace CKAN.GUI { public partial class Main { - private void installationHistoryStripMenuItem_Click(object sender, EventArgs e) + private void installationHistoryStripMenuItem_Click(object? sender, EventArgs? e) { - InstallationHistory.LoadHistory(Manager.CurrentInstance, configuration, repoData); - tabController.ShowTab("InstallationHistoryTabPage", 2); - DisableMainWindow(); + if (CurrentInstance != null && configuration != null) + { + InstallationHistory.LoadHistory(CurrentInstance, configuration, repoData); + tabController.ShowTab("InstallationHistoryTabPage", 2); + DisableMainWindow(); + } } private void InstallationHistory_Install(CkanModule[] modules) { - InstallationHistory_Done(); - var tuple = ManageMods.mainModList.ComputeFullChangeSetFromUserChangeSet( - RegistryManager.Instance(CurrentInstance, repoData).registry, - modules.Select(mod => new ModChange(mod, GUIModChangeType.Install)) - .ToHashSet(), - CurrentInstance.VersionCriteria()); - UpdateChangesDialog(tuple.Item1.ToList(), tuple.Item2); - tabController.ShowTab("ChangesetTabPage", 1); + if (CurrentInstance != null) + { + InstallationHistory_Done(); + var tuple = ManageMods.mainModList.ComputeFullChangeSetFromUserChangeSet( + RegistryManager.Instance(CurrentInstance, repoData).registry, + modules.Select(mod => new ModChange(mod, GUIModChangeType.Install)) + .ToHashSet(), + CurrentInstance.VersionCriteria()); + UpdateChangesDialog(tuple.Item1.ToList(), tuple.Item2); + tabController.ShowTab("ChangesetTabPage", 1); + } } private void InstallationHistory_Done() @@ -37,14 +43,17 @@ private void InstallationHistory_Done() private void InstallationHistory_OnSelectedModuleChanged(CkanModule m) { - ActiveModInfo = m == null - ? null - : new GUIMod(m, repoData, - RegistryManager.Instance(CurrentInstance, repoData).registry, - CurrentInstance.VersionCriteria(), - null, - configuration.HideEpochs, - configuration.HideV); + if (CurrentInstance != null) + { + ActiveModInfo = m == null + ? null + : new GUIMod(m, repoData, + RegistryManager.Instance(CurrentInstance, repoData).registry, + CurrentInstance.VersionCriteria(), + null, + configuration?.HideEpochs ?? false, + configuration?.HideV ?? false); + } } } diff --git a/GUI/Main/MainImport.cs b/GUI/Main/MainImport.cs index afd5a9e8b0..800a36c64d 100644 --- a/GUI/Main/MainImport.cs +++ b/GUI/Main/MainImport.cs @@ -11,71 +11,77 @@ namespace CKAN.GUI { public partial class Main { - private void importDownloadsToolStripMenuItem_Click(object sender, EventArgs e) + private void importDownloadsToolStripMenuItem_Click(object? sender, EventArgs? e) { ImportModules(); } private void ImportModules() { - // Prompt the user to select one or more ZIP files - var dlg = new OpenFileDialog() + if (CurrentInstance != null) { - Title = Properties.Resources.MainImportTitle, - AddExtension = true, - CheckFileExists = true, - CheckPathExists = true, - InitialDirectory = FindDownloadsPath(CurrentInstance), - DefaultExt = "zip", - Filter = Properties.Resources.MainImportFilter, - Multiselect = true - }; - if (dlg.ShowDialog(this) == DialogResult.OK - && dlg.FileNames.Length > 0) - { - // Show WaitTabPage (status page) and lock it. - tabController.RenameTab("WaitTabPage", Properties.Resources.MainImportWaitTitle); - ShowWaitDialog(); - DisableMainWindow(); - Wait.StartWaiting( - (sender, e) => - { - e.Result = new ModuleInstaller(CurrentInstance, Manager.Cache, currentUser).ImportFiles( - GetFiles(dlg.FileNames), - currentUser, - (CkanModule mod) => - { - if (ManageMods.mainModList - .full_list_of_mod_rows - .TryGetValue(mod.identifier, - out DataGridViewRow row) - && row.Tag is GUIMod gmod) - { - gmod.SelectedMod = mod; - } - }, - RegistryManager.Instance(CurrentInstance, repoData).registry); - }, - (sender, e) => - { - if (e.Error == null && e.Result is bool result && result) + // Prompt the user to select one or more ZIP files + var dlg = new OpenFileDialog() + { + Title = Properties.Resources.MainImportTitle, + AddExtension = true, + CheckFileExists = true, + CheckPathExists = true, + InitialDirectory = FindDownloadsPath(CurrentInstance), + DefaultExt = "zip", + Filter = Properties.Resources.MainImportFilter, + Multiselect = true + }; + if (dlg.ShowDialog(this) == DialogResult.OK + && dlg.FileNames.Length > 0) + { + // Show WaitTabPage (status page) and lock it. + tabController.RenameTab("WaitTabPage", Properties.Resources.MainImportWaitTitle); + ShowWaitDialog(); + DisableMainWindow(); + Wait.StartWaiting( + (sender, e) => { - // Put GUI back the way we found it - HideWaitDialog(); - } - else + if (e != null && Manager?.Cache != null) + { + e.Result = new ModuleInstaller(CurrentInstance, Manager.Cache, currentUser).ImportFiles( + GetFiles(dlg.FileNames), + currentUser, + (CkanModule mod) => + { + if (ManageMods.mainModList + .full_list_of_mod_rows + .TryGetValue(mod.identifier, + out DataGridViewRow? row) + && row.Tag is GUIMod gmod) + { + gmod.SelectedMod = mod; + } + }, + RegistryManager.Instance(CurrentInstance, repoData).registry); + } + }, + (sender, e) => { - if (e.Error is Exception exc) + if (e?.Error == null && e?.Result is bool result && result) + { + // Put GUI back the way we found it + HideWaitDialog(); + } + else { - log.Error(exc.Message, exc); - currentUser.RaiseMessage(exc.Message); + if (e?.Error is Exception exc) + { + log.Error(exc.Message, exc); + currentUser.RaiseMessage(exc.Message); + } + Wait.Finish(); } - Wait.Finish(); - } - EnableMainWindow(); - }, - false, - null); + EnableMainWindow(); + }, + false, + null); + } } } diff --git a/GUI/Main/MainInstall.cs b/GUI/Main/MainInstall.cs index 907621fa3d..b0a575cba2 100644 --- a/GUI/Main/MainInstall.cs +++ b/GUI/Main/MainInstall.cs @@ -50,7 +50,7 @@ public void InstallModuleDriver(IRegistryQuerier registry, IEnumerable(); foreach (var module in modules) { - InstalledModule installed = registry.InstalledModule(module.identifier); + var installed = registry.InstalledModule(module.identifier); if (installed != null) { // Already installed, remove it first @@ -76,257 +76,262 @@ public void InstallModuleDriver(IRegistryQuerier registry, IEnumerable changes, RelationshipResolverOptions options) = (InstallArgument)e.Argument; - - var registry_manager = RegistryManager.Instance(Manager.CurrentInstance, repoData); - var registry = registry_manager.registry; - var installer = new ModuleInstaller(CurrentInstance, Manager.Cache, currentUser); - // Avoid accumulating multiple event handlers - installer.onReportModInstalled -= OnModInstalled; - installer.onReportModInstalled += OnModInstalled; - - // this will be the final list of mods we want to install - var toInstall = new List(); - var toUninstall = new HashSet(); - var toUpgrade = new HashSet(); - - // Check whether we need an explicit Remove call for auto-removals. - // If there's an Upgrade or a user-initiated Remove, they'll take care of it. - var needRemoveForAuto = changes.All(ch => ch.ChangeType == GUIModChangeType.Install - || ch.IsAutoRemoval); - - // First compose sets of what the user wants installed, upgraded, and removed. - foreach (ModChange change in changes) + if (CurrentInstance != null + && Manager.Cache != null + && e?.Argument is (List changes, RelationshipResolverOptions options)) { - switch (change.ChangeType) + var registry_manager = RegistryManager.Instance(CurrentInstance, repoData); + var registry = registry_manager.registry; + var installer = new ModuleInstaller(CurrentInstance, Manager.Cache, currentUser); + // Avoid accumulating multiple event handlers + installer.onReportModInstalled -= OnModInstalled; + installer.onReportModInstalled += OnModInstalled; + + // this will be the final list of mods we want to install + var toInstall = new List(); + var toUninstall = new HashSet(); + var toUpgrade = new HashSet(); + + // Check whether we need an explicit Remove call for auto-removals. + // If there's an Upgrade or a user-initiated Remove, they'll take care of it. + var needRemoveForAuto = changes.All(ch => ch.ChangeType == GUIModChangeType.Install + || ch.IsAutoRemoval); + + // First compose sets of what the user wants installed, upgraded, and removed. + foreach (ModChange change in changes) { - case GUIModChangeType.Remove: - // Let Upgrade and Remove handle auto-removals to avoid cascade-removal of depending mods. - // Unless auto-removal is the ONLY thing in the changeset, in which case - // filtering these out would give us a completely empty changeset. - if (needRemoveForAuto || !change.IsAutoRemoval) - { - toUninstall.Add(change.Mod); - } - break; - case GUIModChangeType.Update: - toUpgrade.Add(change is ModUpgrade mu ? mu.targetMod - : change.Mod); - break; - case GUIModChangeType.Install: - toInstall.Add(change.Mod); - break; - case GUIModChangeType.Replace: - ModuleReplacement repl = registry.GetReplacement(change.Mod, CurrentInstance.VersionCriteria()); - if (repl != null) - { - toUninstall.Add(repl.ToReplace); - if (!toInstall.Contains(repl.ReplaceWith)) + switch (change.ChangeType) + { + case GUIModChangeType.Remove: + // Let Upgrade and Remove handle auto-removals to avoid cascade-removal of depending mods. + // Unless auto-removal is the ONLY thing in the changeset, in which case + // filtering these out would give us a completely empty changeset. + if (needRemoveForAuto || !change.IsAutoRemoval) { - toInstall.Add(repl.ReplaceWith); + toUninstall.Add(change.Mod); } - } - break; + break; + case GUIModChangeType.Update: + toUpgrade.Add(change is ModUpgrade mu ? mu.targetMod + : change.Mod); + break; + case GUIModChangeType.Install: + toInstall.Add(change.Mod); + break; + case GUIModChangeType.Replace: + var repl = registry.GetReplacement(change.Mod, CurrentInstance.VersionCriteria()); + if (repl != null) + { + toUninstall.Add(repl.ToReplace); + if (!toInstall.Contains(repl.ReplaceWith)) + { + toInstall.Add(repl.ReplaceWith); + } + } + break; + } } - } - Util.Invoke(this, () => UseWaitCursor = true); - try - { - // Prompt for recommendations and suggestions, if any - if (installer.FindRecommendations( - changes.Where(ch => ch.ChangeType == GUIModChangeType.Install) - .Select(ch => ch.Mod) - .ToHashSet(), - toInstall, - registry, - out Dictionary>> recommendations, - out Dictionary> suggestions, - out Dictionary> supporters)) + Util.Invoke(this, () => UseWaitCursor = true); + try { - tabController.ShowTab("ChooseRecommendedModsTabPage", 3); - ChooseRecommendedMods.LoadRecommendations( - registry, toInstall, toUninstall, - CurrentInstance.VersionCriteria(), Manager.Cache, - CurrentInstance.game, - ManageMods.mainModList.ModuleLabels - .LabelsFor(CurrentInstance.Name) - .ToList(), - configuration, - recommendations, suggestions, supporters); - tabController.SetTabLock(true); - Util.Invoke(this, () => UseWaitCursor = false); - var result = ChooseRecommendedMods.Wait(); - tabController.SetTabLock(false); - tabController.HideTab("ChooseRecommendedModsTabPage"); - if (result == null) - { - e.Result = new InstallResult(false, changes); - throw new CancelledActionKraken(); - } - else + // Prompt for recommendations and suggestions, if any + if (installer.FindRecommendations( + changes.Where(ch => ch.ChangeType == GUIModChangeType.Install) + .Select(ch => ch.Mod) + .ToHashSet(), + toInstall, + registry, + out Dictionary>> recommendations, + out Dictionary> suggestions, + out Dictionary> supporters)) { - toInstall = toInstall.Concat(result).Distinct().ToList(); + tabController.ShowTab("ChooseRecommendedModsTabPage", 3); + ChooseRecommendedMods.LoadRecommendations( + registry, toInstall, toUninstall, + CurrentInstance.VersionCriteria(), Manager.Cache, + CurrentInstance.game, + ModuleLabelList.ModuleLabels + .LabelsFor(CurrentInstance.Name) + .ToList(), + configuration, + recommendations, suggestions, supporters); + tabController.SetTabLock(true); + Util.Invoke(this, () => UseWaitCursor = false); + var result = ChooseRecommendedMods.Wait(); + tabController.SetTabLock(false); + tabController.HideTab("ChooseRecommendedModsTabPage"); + if (result == null) + { + e.Result = new InstallResult(false, changes); + throw new CancelledActionKraken(); + } + else + { + toInstall = toInstall.Concat(result).Distinct().ToList(); + } } } - } - finally - { - // Make sure the progress tab always shows up with a normal cursor even if an exception is thrown - Util.Invoke(this, () => UseWaitCursor = false); - ShowWaitDialog(); - } + finally + { + // Make sure the progress tab always shows up with a normal cursor even if an exception is thrown + Util.Invoke(this, () => UseWaitCursor = false); + ShowWaitDialog(); + } - // Now let's make all our changes. - Util.Invoke(this, () => - { - // Need to be on the GUI thread to get the translated string - tabController.RenameTab("WaitTabPage", Properties.Resources.MainInstallWaitTitle); - }); - tabController.SetTabLock(true); - - IDownloader downloader = new NetAsyncModulesDownloader(currentUser, Manager.Cache); - downloader.Progress += Wait.SetModuleProgress; - downloader.AllComplete += Wait.DownloadsComplete; - downloader.StoreProgress += (module, remaining, total) => - Wait.SetProgress(string.Format(Properties.Resources.ValidatingDownload, module), - remaining, total); - - Wait.OnCancel += () => - { - canceled = true; - downloader.CancelDownload(); - }; + // Now let's make all our changes. + Util.Invoke(this, () => + { + // Need to be on the GUI thread to get the translated string + tabController.RenameTab("WaitTabPage", Properties.Resources.MainInstallWaitTitle); + }); + tabController.SetTabLock(true); + + IDownloader downloader = new NetAsyncModulesDownloader(currentUser, Manager.Cache); + downloader.Progress += Wait.SetModuleProgress; + downloader.AllComplete += Wait.DownloadsComplete; + downloader.StoreProgress += (module, remaining, total) => + Wait.SetProgress(string.Format(Properties.Resources.ValidatingDownload, module), + remaining, total); + + Wait.OnCancel += () => + { + canceled = true; + downloader.CancelDownload(); + }; - HashSet possibleConfigOnlyDirs = null; + HashSet? possibleConfigOnlyDirs = null; - // Treat whole changeset as atomic - using (TransactionScope transaction = CkanTransaction.CreateTransactionScope()) - { - // Checks if all actions were successful - // Uninstall/installs/upgrades until every list is empty - // If the queue is NOT empty, resolvedAllProvidedMods is false until the action is done - for (bool resolvedAllProvidedMods = false; !resolvedAllProvidedMods;) + // Treat whole changeset as atomic + using (TransactionScope transaction = CkanTransaction.CreateTransactionScope()) { - try + // Checks if all actions were successful + // Uninstall/installs/upgrades until every list is empty + // If the queue is NOT empty, resolvedAllProvidedMods is false until the action is done + for (bool resolvedAllProvidedMods = false; !resolvedAllProvidedMods;) { - e.Result = new InstallResult(false, changes); - if (!canceled && toUninstall.Count > 0) - { - installer.UninstallList(toUninstall.Select(m => m.identifier), - ref possibleConfigOnlyDirs, registry_manager, false, toInstall); - toUninstall.Clear(); - } - if (!canceled && toInstall.Count > 0) - { - installer.InstallList(toInstall, options, registry_manager, ref possibleConfigOnlyDirs, downloader, false); - toInstall.Clear(); - } - if (!canceled && toUpgrade.Count > 0) - { - installer.Upgrade(toUpgrade, downloader, ref possibleConfigOnlyDirs, registry_manager, true, true, false); - toUpgrade.Clear(); - } - if (canceled) + try { e.Result = new InstallResult(false, changes); - throw new CancelledActionKraken(); + if (!canceled && toUninstall.Count > 0) + { + installer.UninstallList(toUninstall.Select(m => m.identifier), + ref possibleConfigOnlyDirs, registry_manager, false, toInstall); + toUninstall.Clear(); + } + if (!canceled && toInstall.Count > 0) + { + installer.InstallList(toInstall, options, registry_manager, ref possibleConfigOnlyDirs, downloader, false); + toInstall.Clear(); + } + if (!canceled && toUpgrade.Count > 0) + { + installer.Upgrade(toUpgrade, downloader, ref possibleConfigOnlyDirs, registry_manager, true, true, false); + toUpgrade.Clear(); + } + if (canceled) + { + e.Result = new InstallResult(false, changes); + throw new CancelledActionKraken(); + } + resolvedAllProvidedMods = true; } - resolvedAllProvidedMods = true; - } - catch (ModuleDownloadErrorsKraken k) - { - // Get full changeset (toInstall only includes user's selections, not dependencies) - var crit = CurrentInstance.VersionCriteria(); - var fullChangeset = new RelationshipResolver( - toInstall.Concat(toUpgrade), toUninstall, options, registry, crit - ).ModList().ToList(); - DownloadsFailedDialog dfd = null; - Util.Invoke(this, () => - { - dfd = new DownloadsFailedDialog( - Properties.Resources.ModDownloadsFailedMessage, - Properties.Resources.ModDownloadsFailedColHdr, - Properties.Resources.ModDownloadsFailedAbortBtn, - k.Exceptions.Select(kvp => new KeyValuePair( - fullChangeset.Where(m => m.download == kvp.Key.download).ToArray(), - kvp.Value)), - (m1, m2) => (m1 as CkanModule)?.download == (m2 as CkanModule)?.download); - dfd.ShowDialog(this); - }); - var skip = dfd.Wait()?.Select(m => m as CkanModule).ToArray(); - var abort = dfd.Abort; - dfd.Dispose(); - if (abort) + catch (ModuleDownloadErrorsKraken k) { - canceled = true; - e.Result = new InstallResult(false, changes); - throw new CancelledActionKraken(); - } + // Get full changeset (toInstall only includes user's selections, not dependencies) + var crit = CurrentInstance.VersionCriteria(); + var fullChangeset = new RelationshipResolver( + toInstall.Concat(toUpgrade), toUninstall, options, registry, crit + ).ModList().ToList(); + DownloadsFailedDialog? dfd = null; + Util.Invoke(this, () => + { + dfd = new DownloadsFailedDialog( + Properties.Resources.ModDownloadsFailedMessage, + Properties.Resources.ModDownloadsFailedColHdr, + Properties.Resources.ModDownloadsFailedAbortBtn, + k.Exceptions.Select(kvp => new KeyValuePair( + fullChangeset.Where(m => m.download == kvp.Key.download).ToArray(), + kvp.Value)), + (m1, m2) => (m1 as CkanModule)?.download == (m2 as CkanModule)?.download); + dfd.ShowDialog(this); + }); + var skip = dfd?.Wait()?.Select(m => m as CkanModule) + .OfType() + .ToArray(); + var abort = dfd?.Abort; + dfd?.Dispose(); + if (abort ?? false) + { + canceled = true; + e.Result = new InstallResult(false, changes); + throw new CancelledActionKraken(); + } - if (skip.Length > 0) - { - // Remove mods from changeset that user chose to skip - // and any mods depending on them - var dependers = registry.FindReverseDependencies( - skip.Select(s => s.identifier).ToList(), - fullChangeset, - // Consider virtual dependencies satisfied so user can make a new choice if they skip - rel => rel.LatestAvailableWithProvides(registry, crit).Count > 1) - .ToHashSet(); - toInstall.RemoveAll(m => dependers.Contains(m.identifier)); - } + if (skip != null && skip.Length > 0) + { + // Remove mods from changeset that user chose to skip + // and any mods depending on them + var dependers = registry.FindReverseDependencies( + skip.Select(s => s.identifier).ToList(), + fullChangeset, + // Consider virtual dependencies satisfied so user can make a new choice if they skip + rel => rel.LatestAvailableWithProvides(registry, crit).Count > 1) + .ToHashSet(); + toInstall.RemoveAll(m => dependers.Contains(m.identifier)); + } - // Now we loop back around again - } - catch (TooManyModsProvideKraken k) - { - // Prompt user to choose which mod to use - tabController.ShowTab("ChooseProvidedModsTabPage", 3); - Util.Invoke(this, () => StatusProgress.Visible = false); - var repoData = ServiceLocator.Container.Resolve(); - ChooseProvidedMods.LoadProviders( - k.Message, - k.modules.OrderByDescending(m => repoData.GetDownloadCount(registry.Repositories.Values, - m.identifier) - ?? 0) - .ThenByDescending(m => m.identifier == k.requested) - .ThenBy(m => m.name) - .ToList(), - Manager.Cache); - tabController.SetTabLock(true); - CkanModule chosen = ChooseProvidedMods.Wait(); - // Close the selection prompt - tabController.SetTabLock(false); - tabController.HideTab("ChooseProvidedModsTabPage"); - if (chosen != null) - { - // User picked a mod, queue it up for installation - toInstall.Add(chosen); - // DON'T return so we can loop around and try the above InstallList call again - tabController.ShowTab("WaitTabPage"); - Util.Invoke(this, () => StatusProgress.Visible = true); + // Now we loop back around again } - else + catch (TooManyModsProvideKraken k) { - e.Result = new InstallResult(false, changes); - throw new CancelledActionKraken(); + // Prompt user to choose which mod to use + tabController.ShowTab("ChooseProvidedModsTabPage", 3); + Util.Invoke(this, () => StatusProgress.Visible = false); + var repoData = ServiceLocator.Container.Resolve(); + ChooseProvidedMods.LoadProviders( + k.Message, + k.modules.OrderByDescending(m => repoData.GetDownloadCount(registry.Repositories.Values, + m.identifier) + ?? 0) + .ThenByDescending(m => m.identifier == k.requested) + .ThenBy(m => m.name) + .ToList(), + Manager.Cache); + tabController.SetTabLock(true); + var chosen = ChooseProvidedMods.Wait(); + // Close the selection prompt + tabController.SetTabLock(false); + tabController.HideTab("ChooseProvidedModsTabPage"); + if (chosen != null) + { + // User picked a mod, queue it up for installation + toInstall.Add(chosen); + // DON'T return so we can loop around and try the above InstallList call again + tabController.ShowTab("WaitTabPage"); + Util.Invoke(this, () => StatusProgress.Visible = true); + } + else + { + e.Result = new InstallResult(false, changes); + throw new CancelledActionKraken(); + } } } + transaction.Complete(); } - transaction.Complete(); + HandlePossibleConfigOnlyDirs(registry, possibleConfigOnlyDirs); + e.Result = new InstallResult(true, changes); } - HandlePossibleConfigOnlyDirs(registry, possibleConfigOnlyDirs); - e.Result = new InstallResult(true, changes); } [ForbidGUICalls] - private void HandlePossibleConfigOnlyDirs(Registry registry, HashSet possibleConfigOnlyDirs) + private void HandlePossibleConfigOnlyDirs(Registry registry, HashSet? possibleConfigOnlyDirs) { - if (possibleConfigOnlyDirs != null) + if (CurrentInstance != null && possibleConfigOnlyDirs != null) { // Check again for registered files, since we may // just have installed or upgraded some @@ -344,7 +349,7 @@ private void HandlePossibleConfigOnlyDirs(Registry registry, HashSet pos DeleteDirectories.LoadDirs(CurrentInstance, possibleConfigOnlyDirs); // Wait here for the GUI process to finish dealing with the user - if (DeleteDirectories.Wait(out HashSet toDelete)) + if (DeleteDirectories.Wait(out HashSet? toDelete)) { foreach (string dir in toDelete) { @@ -373,9 +378,9 @@ private void OnModInstalled(CkanModule mod) LabelsAfterInstall(mod); } - private void PostInstallMods(object sender, RunWorkerCompletedEventArgs e) + private void PostInstallMods(object? sender, RunWorkerCompletedEventArgs? e) { - if (e.Error != null) + if (e?.Error != null) { switch (e.Error) { @@ -388,7 +393,8 @@ private void PostInstallMods(object sender, RunWorkerCompletedEventArgs e) break; case BadMetadataKraken exc: - currentUser.RaiseMessage(Properties.Resources.MainInstallBadMetadata, exc.module, exc.Message); + currentUser.RaiseMessage(Properties.Resources.MainInstallBadMetadata, + exc.module?.ToString() ?? "", exc.Message); break; case NotEnoughSpaceKraken exc: @@ -400,16 +406,14 @@ private void PostInstallMods(object sender, RunWorkerCompletedEventArgs e) { currentUser.RaiseMessage( Properties.Resources.MainInstallFileExists, - exc.filename, exc.installingModule, exc.owningModule, - Meta.GetVersion() - ); + exc.filename, exc.installingModule?.ToString() ?? "", exc.owningModule, + Meta.GetVersion()); } else { currentUser.RaiseMessage( Properties.Resources.MainInstallUnownedFileExists, - exc.installingModule, exc.filename - ); + exc.installingModule?.ToString() ?? "", exc.filename); } currentUser.RaiseMessage(Properties.Resources.MainInstallGameDataReverted); break; @@ -431,9 +435,10 @@ private void PostInstallMods(object sender, RunWorkerCompletedEventArgs e) case DownloadThrottledKraken exc: string msg = exc.ToString(); currentUser.RaiseMessage(msg); - if (YesNoDialog(string.Format(Properties.Resources.MainInstallOpenSettingsPrompt, msg), - Properties.Resources.MainInstallOpenSettings, - Properties.Resources.MainInstallNo)) + if (configuration != null && CurrentInstance != null + && YesNoDialog(string.Format(Properties.Resources.MainInstallOpenSettingsPrompt, msg), + Properties.Resources.MainInstallOpenSettings, + Properties.Resources.MainInstallNo)) { // Launch the URL describing this host's throttling practices, if any if (exc.infoUrl != null) @@ -490,10 +495,9 @@ private void PostInstallMods(object sender, RunWorkerCompletedEventArgs e) Properties.Resources.MainInstallKnownError, Properties.Resources.MainInstallFailed); } - else + // The Result property throws if InstallMods threw (!!!) + else if (e?.Result is (bool success, List changes)) { - // The Result property throws if InstallMods threw (!!!) - (bool success, List changes) = (InstallResult)e.Result; currentUser.RaiseMessage(Properties.Resources.MainInstallSuccess); // Rebuilds the list of GUIMods RefreshModList(false); diff --git a/GUI/Main/MainLabels.cs b/GUI/Main/MainLabels.cs index 4266ebb522..c08a2591e9 100644 --- a/GUI/Main/MainLabels.cs +++ b/GUI/Main/MainLabels.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Windows.Forms; using System.Collections.Generic; + using CKAN.Extensions; namespace CKAN.GUI @@ -11,54 +12,61 @@ public partial class Main private void ManageMods_LabelsAfterUpdate(IEnumerable mods) { - Util.Invoke(this, () => + if (CurrentInstance != null) { - mods = mods.Memoize(); - var notifLabs = ManageMods.mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name) - .Where(l => l.NotifyOnChange) - .Memoize(); - var toNotif = mods - .Where(m => - notifLabs.Any(l => - l.ContainsModule(CurrentInstance.game, m.Identifier))) - .Select(m => m.Name) - .Memoize(); - if (toNotif.Any()) + Util.Invoke(this, () => { - MessageBox.Show( - string.Format( - Properties.Resources.MainLabelsUpdateMessage, - string.Join("\r\n", toNotif) - ), - Properties.Resources.MainLabelsUpdateTitle, - MessageBoxButtons.OK - ); - } + mods = mods.Memoize(); + var notifLabs = ModuleLabelList.ModuleLabels.LabelsFor(CurrentInstance.Name) + .Where(l => l.NotifyOnChange) + .Memoize(); + var toNotif = mods + .Where(m => + notifLabs.Any(l => + l.ContainsModule(CurrentInstance.game, m.Identifier))) + .Select(m => m.Name) + .Memoize(); + if (toNotif.Any()) + { + MessageBox.Show( + string.Format( + Properties.Resources.MainLabelsUpdateMessage, + string.Join("\r\n", toNotif) + ), + Properties.Resources.MainLabelsUpdateTitle, + MessageBoxButtons.OK + ); + } - foreach (GUIMod mod in mods) - { - foreach (ModuleLabel l in ManageMods.mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name) - .Where(l => l.RemoveOnChange - && l.ContainsModule(CurrentInstance.game, mod.Identifier))) + foreach (GUIMod mod in mods) { - l.Remove(CurrentInstance.game, mod.Identifier); + foreach (ModuleLabel l in ModuleLabelList.ModuleLabels.LabelsFor(CurrentInstance.Name) + .Where(l => l.RemoveOnChange + && l.ContainsModule(CurrentInstance.game, mod.Identifier))) + { + l.Remove(CurrentInstance.game, mod.Identifier); + } } - } - }); + }); + } } private void LabelsAfterInstall(CkanModule mod) { - foreach (ModuleLabel l in ManageMods.mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name) - .Where(l => l.RemoveOnInstall && l.ContainsModule(CurrentInstance.game, mod.identifier))) + if (CurrentInstance != null) { - l.Remove(CurrentInstance.game, mod.identifier); + foreach (var l in ModuleLabelList.ModuleLabels.LabelsFor(CurrentInstance.Name) + .Where(l => l.RemoveOnInstall && l.ContainsModule(CurrentInstance.game, mod.identifier))) + { + l.Remove(CurrentInstance.game, mod.identifier); + } } } public bool LabelsHeld(string identifier) - => ManageMods.mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name) - .Any(l => l.HoldVersion && l.ContainsModule(CurrentInstance.game, identifier)); + => CurrentInstance != null + && ModuleLabelList.ModuleLabels.LabelsFor(CurrentInstance.Name) + .Any(l => l.HoldVersion && l.ContainsModule(CurrentInstance.game, identifier)); #endregion } diff --git a/GUI/Main/MainRecommendations.cs b/GUI/Main/MainRecommendations.cs index 79a8bec964..59535b225a 100644 --- a/GUI/Main/MainRecommendations.cs +++ b/GUI/Main/MainRecommendations.cs @@ -24,52 +24,56 @@ private void ChooseRecommendedMods_OnConflictFound(string message) StatusLabel.ToolTipText = StatusLabel.Text = message; } - private void auditRecommendationsMenuItem_Click(object sender, EventArgs e) + private void auditRecommendationsMenuItem_Click(object? sender, EventArgs? e) { - // Run in a background task so GUI thread can react to user - Task.Factory.StartNew(() => AuditRecommendations( - RegistryManager.Instance(CurrentInstance, repoData).registry, - CurrentInstance.VersionCriteria() - )); + if (CurrentInstance != null) + { + // Run in a background task so GUI thread can react to user + Task.Factory.StartNew(() => AuditRecommendations( + RegistryManager.Instance(CurrentInstance, repoData).registry, + CurrentInstance.VersionCriteria())); + } } [ForbidGUICalls] - private void AuditRecommendations(IRegistryQuerier registry, GameVersionCriteria versionCriteria) + private void AuditRecommendations(Registry registry, GameVersionCriteria versionCriteria) { - var installer = new ModuleInstaller(CurrentInstance, Manager.Cache, currentUser); - if (installer.FindRecommendations( - registry.InstalledModules.Select(im => im.Module).ToHashSet(), - new List(), - registry as Registry, - out Dictionary>> recommendations, - out Dictionary> suggestions, - out Dictionary> supporters - )) + if (CurrentInstance != null && Manager.Cache != null) { - tabController.ShowTab("ChooseRecommendedModsTabPage", 3); - ChooseRecommendedMods.LoadRecommendations( - registry, new List(), new HashSet(), - versionCriteria, Manager.Cache, - CurrentInstance.game, - ManageMods.mainModList.ModuleLabels - .LabelsFor(CurrentInstance.Name) - .ToList(), - configuration, - recommendations, suggestions, supporters); - var result = ChooseRecommendedMods.Wait(); - tabController.HideTab("ChooseRecommendedModsTabPage"); - if (result != null && result.Any()) + var installer = new ModuleInstaller(CurrentInstance, Manager.Cache, currentUser); + if (installer.FindRecommendations( + registry.InstalledModules.Select(im => im.Module).ToHashSet(), + new List(), + registry, + out Dictionary>> recommendations, + out Dictionary> suggestions, + out Dictionary> supporters)) { - Wait.StartWaiting(InstallMods, PostInstallMods, true, - new InstallArgument( - result.Select(mod => new ModChange(mod, GUIModChangeType.Install)) - .ToList(), - RelationshipResolverOptions.DependsOnlyOpts())); + tabController.ShowTab("ChooseRecommendedModsTabPage", 3); + ChooseRecommendedMods.LoadRecommendations( + registry, new List(), new HashSet(), + versionCriteria, Manager.Cache, + CurrentInstance.game, + ModuleLabelList.ModuleLabels + .LabelsFor(CurrentInstance.Name) + .ToList(), + configuration, + recommendations, suggestions, supporters); + var result = ChooseRecommendedMods.Wait(); + tabController.HideTab("ChooseRecommendedModsTabPage"); + if (result != null && result.Any()) + { + Wait.StartWaiting(InstallMods, PostInstallMods, true, + new InstallArgument( + result.Select(mod => new ModChange(mod, GUIModChangeType.Install)) + .ToList(), + RelationshipResolverOptions.DependsOnlyOpts())); + } + } + else + { + currentUser.RaiseError(Properties.Resources.MainRecommendationsNoneFound); } - } - else - { - currentUser.RaiseError(Properties.Resources.MainRecommendationsNoneFound); } } } diff --git a/GUI/Main/MainRepo.cs b/GUI/Main/MainRepo.cs index a2e160053d..cb4aa83c31 100644 --- a/GUI/Main/MainRepo.cs +++ b/GUI/Main/MainRepo.cs @@ -10,7 +10,6 @@ using System.Runtime.Versioning; #endif -using Newtonsoft.Json; using Autofac; using CKAN.Configuration; @@ -30,18 +29,7 @@ namespace CKAN.GUI #endif public partial class Main { - public Timer refreshTimer; - - public RepositoryList FetchMasterRepositoryList(Uri master_uri = null) - { - if (master_uri == null) - { - master_uri = CurrentInstance.game.RepositoryListURL; - } - - string json = Net.DownloadText(master_uri); - return JsonConvert.DeserializeObject(json); - } + public Timer? refreshTimer; public void UpdateRepo(bool forceFullRefresh = false, bool refreshWithoutChanges = false) { @@ -64,121 +52,126 @@ public void UpdateRepo(bool forceFullRefresh = false, bool refreshWithoutChanges } [ForbidGUICalls] - private void UpdateRepo(object sender, DoWorkEventArgs e) + private void UpdateRepo(object? sender, DoWorkEventArgs? e) { - (bool forceFullRefresh, bool refreshWithoutChanges) = (RepoArgument)e.Argument; - // Don't repeat this stuff if downloads fail - currentUser.RaiseMessage(Properties.Resources.MainRepoScanning); - log.Debug("Scanning before repo update"); - var regMgr = RegistryManager.Instance(CurrentInstance, repoData); - bool scanChanged = regMgr.ScanUnmanagedFiles(); - - // Note the current mods' compatibility for the NewlyCompatible filter - var registry = regMgr.registry; - - // Load cached data with progress bars instead of without if not already loaded - // (which happens if auto-update is enabled, otherwise this is a no-op). - // We need the old data to alert the user of newly compatible modules after update. - repoData.Prepopulate( - registry.Repositories.Values.ToList(), - new Progress(p => currentUser.RaiseProgress(Properties.Resources.LoadingCachedRepoData, p))); - - var versionCriteria = CurrentInstance.VersionCriteria(); - var oldModules = registry.CompatibleModules(versionCriteria) - .ToDictionary(m => m.identifier, m => false); - registry.IncompatibleModules(versionCriteria) - .Where(m => !oldModules.ContainsKey(m.identifier)) - .ToList() - .ForEach(m => oldModules.Add(m.identifier, true)); - - using (var transaction = CkanTransaction.CreateTransactionScope()) + if (e?.Argument is (bool forceFullRefresh, bool refreshWithoutChanges) + && CurrentInstance != null) { - // Only way out is to return or throw - while (true) + // Don't repeat this stuff if downloads fail + currentUser.RaiseMessage(Properties.Resources.MainRepoScanning); + log.Debug("Scanning before repo update"); + var regMgr = RegistryManager.Instance(CurrentInstance, repoData); + bool scanChanged = regMgr.ScanUnmanagedFiles(); + + // Note the current mods' compatibility for the NewlyCompatible filter + var registry = regMgr.registry; + + // Load cached data with progress bars instead of without if not already loaded + // (which happens if auto-update is enabled, otherwise this is a no-op). + // We need the old data to alert the user of newly compatible modules after update. + repoData.Prepopulate( + registry.Repositories.Values.ToList(), + new Progress(p => currentUser.RaiseProgress(Properties.Resources.LoadingCachedRepoData, p))); + + var versionCriteria = CurrentInstance.VersionCriteria(); + var oldModules = registry.CompatibleModules(versionCriteria) + .ToDictionary(m => m.identifier, m => false); + registry.IncompatibleModules(versionCriteria) + .Where(m => !oldModules.ContainsKey(m.identifier)) + .ToList() + .ForEach(m => oldModules.Add(m.identifier, true)); + + using (var transaction = CkanTransaction.CreateTransactionScope()) { - var repos = registry.Repositories.Values.ToArray(); - try + // Only way out is to return or throw + while (true) { - bool canceled = false; - var downloader = new NetAsyncDownloader(currentUser); - downloader.Progress += (target, remaining, total) => + var repos = registry.Repositories.Values.ToArray(); + try { - var repo = repos.Where(r => target.urls.Contains(r.uri)) - .FirstOrDefault(); - if (repo != null) + bool canceled = false; + var downloader = new NetAsyncDownloader(currentUser); + downloader.Progress += (target, remaining, total) => { - Wait.SetProgress(repo.name, remaining, total); - } - }; - Wait.OnCancel += () => - { - canceled = true; - downloader.CancelDownload(); - }; + var repo = repos.Where(r => target.urls.Contains(r.uri)) + .FirstOrDefault(); + if (repo != null) + { + Wait.SetProgress(repo.name, remaining, total); + } + }; + Wait.OnCancel += () => + { + canceled = true; + downloader.CancelDownload(); + }; - currentUser.RaiseMessage(Properties.Resources.MainRepoUpdating); + currentUser.RaiseMessage(Properties.Resources.MainRepoUpdating); - var updateResult = repoData.Update(repos, CurrentInstance.game, - forceFullRefresh, downloader, currentUser); + var updateResult = repoData.Update(repos, CurrentInstance.game, + forceFullRefresh, downloader, currentUser); - if (canceled) - { - throw new CancelledActionKraken(); - } + if (canceled) + { + throw new CancelledActionKraken(); + } - if (updateResult == RepositoryDataManager.UpdateResult.NoChanges && scanChanged) - { - updateResult = RepositoryDataManager.UpdateResult.Updated; - } - e.Result = new RepoResult(updateResult, oldModules, refreshWithoutChanges); + if (updateResult == RepositoryDataManager.UpdateResult.NoChanges && scanChanged) + { + updateResult = RepositoryDataManager.UpdateResult.Updated; + } + e.Result = new RepoResult(updateResult, oldModules, refreshWithoutChanges); - // If we make it to the end, we are done - transaction.Complete(); - return; - } - catch (DownloadErrorsKraken k) - { - log.Debug("Caught download errors kraken"); - DownloadsFailedDialog dfd = null; - Util.Invoke(this, () => - { - dfd = new DownloadsFailedDialog( - Properties.Resources.RepoDownloadsFailedMessage, - Properties.Resources.RepoDownloadsFailedColHdr, - Properties.Resources.RepoDownloadsFailedAbortBtn, - k.Exceptions.Select(kvp => new KeyValuePair( - new object[] { repos[kvp.Key] }, kvp.Value)), - // Rows are only linked to themselves - (r1, r2) => r1 == r2); - dfd.ShowDialog(this); - }); - var skip = dfd.Wait()?.Select(r => r as Repository).ToArray(); - var abort = dfd.Abort; - dfd.Dispose(); - if (abort) - { - e.Result = new RepoResult(RepositoryDataManager.UpdateResult.Failed, - oldModules, refreshWithoutChanges); - throw new CancelledActionKraken(); + // If we make it to the end, we are done + transaction.Complete(); + return; } - if (skip.Length > 0) + catch (DownloadErrorsKraken k) { - foreach (var r in skip) + log.Debug("Caught download errors kraken"); + DownloadsFailedDialog? dfd = null; + Util.Invoke(this, () => { - registry.RepositoriesRemove(r.name); - needRegistrySave = true; + dfd = new DownloadsFailedDialog( + Properties.Resources.RepoDownloadsFailedMessage, + Properties.Resources.RepoDownloadsFailedColHdr, + Properties.Resources.RepoDownloadsFailedAbortBtn, + k.Exceptions.Select(kvp => new KeyValuePair( + new object[] { repos[kvp.Key] }, kvp.Value)), + // Rows are only linked to themselves + (r1, r2) => r1 == r2); + dfd.ShowDialog(this); + }); + var skip = dfd?.Wait()?.Select(r => r as Repository) + .OfType() + .ToArray(); + var abort = dfd?.Abort; + dfd?.Dispose(); + if (abort ?? false) + { + e.Result = new RepoResult(RepositoryDataManager.UpdateResult.Failed, + oldModules, refreshWithoutChanges); + throw new CancelledActionKraken(); + } + if (skip != null && skip.Length > 0) + { + foreach (var r in skip) + { + registry.RepositoriesRemove(r.name); + needRegistrySave = true; + } } - } - // Loop back around to retry + // Loop back around to retry + } } } } } - private void PostUpdateRepo(object sender, RunWorkerCompletedEventArgs e) + private void PostUpdateRepo(object? sender, RunWorkerCompletedEventArgs? e) { - if (e.Error != null) + if (e?.Error != null) { switch (e.Error) { @@ -224,12 +217,10 @@ private void PostUpdateRepo(object sender, RunWorkerCompletedEventArgs e) break; } } - else + else if (e?.Result is(RepositoryDataManager.UpdateResult updateResult, + Dictionary oldModules, + bool refreshWithoutChanges)) { - (RepositoryDataManager.UpdateResult updateResult, - Dictionary oldModules, - bool refreshWithoutChanges) = (RepoResult)e.Result; - switch (updateResult) { case RepositoryDataManager.UpdateResult.NoChanges: @@ -277,7 +268,7 @@ private void PostUpdateRepo(object sender, RunWorkerCompletedEventArgs e) private void ShowRefreshQuestion() { - if (!configuration.RefreshOnStartupNoNag) + if (configuration != null && !configuration.RefreshOnStartupNoNag) { configuration.RefreshOnStartupNoNag = true; if (!currentUser.RaiseYesNoDialog(Properties.Resources.MainRepoAutoRefreshPrompt)) @@ -303,20 +294,20 @@ public void InitRefreshTimer() public void UpdateRefreshTimer() { - refreshTimer.Stop(); + refreshTimer?.Stop(); IConfiguration cfg = ServiceLocator.Container.Resolve(); // Interval is set to 1 minute * RefreshRate - if (cfg.RefreshRate > 0) + if (cfg.RefreshRate > 0 && refreshTimer != null) { refreshTimer.Interval = 1000 * 60 * cfg.RefreshRate; - refreshTimer.Start(); + refreshTimer?.Start(); } } - private void OnRefreshTimer(object sender, ElapsedEventArgs e) + private void OnRefreshTimer(object? sender, ElapsedEventArgs e) { - if (menuStrip1.Enabled && !configuration.RefreshPaused) + if (menuStrip1.Enabled && configuration != null && !configuration.RefreshPaused) { // Just a safety check UpdateRepo(); diff --git a/GUI/Main/MainTime.cs b/GUI/Main/MainTime.cs index 36256a6349..51ea957231 100644 --- a/GUI/Main/MainTime.cs +++ b/GUI/Main/MainTime.cs @@ -7,7 +7,7 @@ namespace CKAN.GUI { public partial class Main { - private void viewPlayTimeStripMenuItem_Click(object sender, EventArgs e) + private void viewPlayTimeStripMenuItem_Click(object? sender, EventArgs? e) { PlayTime.loadAllPlayTime(Manager); tabController.ShowTab("PlayTimeTabPage", 2); diff --git a/GUI/Main/MainTrayIcon.cs b/GUI/Main/MainTrayIcon.cs index 6f6073347e..fba1229f5b 100644 --- a/GUI/Main/MainTrayIcon.cs +++ b/GUI/Main/MainTrayIcon.cs @@ -13,29 +13,27 @@ public partial class Main { #region Tray Behaviour - private bool enableTrayIcon; - private bool minimizeToTray; - public void CheckTrayState() { - enableTrayIcon = configuration.EnableTrayIcon; - minimizeToTray = configuration.MinimizeToTray; - pauseToolStripMenuItem.Enabled = ServiceLocator.Container.Resolve().RefreshRate != 0; - pauseToolStripMenuItem.Text = configuration.RefreshPaused - ? Properties.Resources.MainTrayIconResume - : Properties.Resources.MainTrayIconPause; - UpdateTrayState(); + if (configuration != null) + { + pauseToolStripMenuItem.Enabled = ServiceLocator.Container.Resolve().RefreshRate != 0; + pauseToolStripMenuItem.Text = configuration.RefreshPaused + ? Properties.Resources.MainTrayIconResume + : Properties.Resources.MainTrayIconPause; + UpdateTrayState(); + } } private void UpdateTrayState() { - if (enableTrayIcon) + if (configuration != null && configuration.EnableTrayIcon) { minimizeNotifyIcon.Visible = true; if (WindowState == FormWindowState.Minimized) { - if (minimizeToTray) + if (configuration.MinimizeToTray) { // Remove our taskbar entry Hide(); @@ -75,7 +73,7 @@ private void UpdateTrayInfo() public void OpenWindow() { Show(); - WindowState = configuration.IsWindowMaximised ? FormWindowState.Maximized : FormWindowState.Normal; + WindowState = configuration?.IsWindowMaximised ?? false ? FormWindowState.Maximized : FormWindowState.Normal; } private void minimizeNotifyIcon_MouseDoubleClick(object sender, MouseEventArgs e) @@ -83,59 +81,64 @@ private void minimizeNotifyIcon_MouseDoubleClick(object sender, MouseEventArgs e OpenWindow(); } - private void updatesToolStripMenuItem_Click(object sender, EventArgs e) + private void updatesToolStripMenuItem_Click(object? sender, EventArgs? e) { OpenWindow(); ManageMods.MarkAllUpdates(); } - private void refreshToolStripMenuItem_Click(object sender, EventArgs e) + private void refreshToolStripMenuItem_Click(object? sender, EventArgs? e) { UpdateRepo(); } - private void pauseToolStripMenuItem_Click(object sender, EventArgs e) + private void pauseToolStripMenuItem_Click(object? sender, EventArgs? e) { - configuration.RefreshPaused = !configuration.RefreshPaused; - if (configuration.RefreshPaused) + if (configuration != null) { - refreshTimer.Stop(); - pauseToolStripMenuItem.Text = Properties.Resources.MainTrayIconResume; - } - else - { - refreshTimer.Start(); - pauseToolStripMenuItem.Text = Properties.Resources.MainTrayIconPause; + configuration.RefreshPaused = !configuration.RefreshPaused; + if (configuration.RefreshPaused) + { + refreshTimer?.Stop(); + pauseToolStripMenuItem.Text = Properties.Resources.MainTrayIconResume; + } + else + { + refreshTimer?.Start(); + pauseToolStripMenuItem.Text = Properties.Resources.MainTrayIconPause; + } } } - private void openCKANToolStripMenuItem_Click(object sender, EventArgs e) + private void openCKANToolStripMenuItem_Click(object? sender, EventArgs? e) { OpenWindow(); } - private void cKANSettingsToolStripMenuItem1_Click(object sender, EventArgs e) + private void cKANSettingsToolStripMenuItem1_Click(object? sender, EventArgs? e) { OpenWindow(); - new SettingsDialog(ServiceLocator.Container.Resolve(), - configuration, - RegistryManager.Instance(CurrentInstance, repoData), - updater, - currentUser) - .ShowDialog(this); + if (configuration != null && CurrentInstance != null) + { + new SettingsDialog(ServiceLocator.Container.Resolve(), + configuration, + RegistryManager.Instance(CurrentInstance, repoData), + updater, + currentUser) + .ShowDialog(this); + } } - private void minimizedContextMenuStrip_Opening(object sender, CancelEventArgs e) + private void minimizedContextMenuStrip_Opening(object? sender, CancelEventArgs? e) { // The menu location can be partly off-screen by default. // Fix it. minimizedContextMenuStrip.Location = Util.ClampedLocation( minimizedContextMenuStrip.Location, - minimizedContextMenuStrip.Size - ); + minimizedContextMenuStrip.Size); } - private void minimizeNotifyIcon_BalloonTipClicked(object sender, EventArgs e) + private void minimizeNotifyIcon_BalloonTipClicked(object? sender, EventArgs? e) { // Unminimize OpenWindow(); diff --git a/GUI/Main/MainUnmanaged.cs b/GUI/Main/MainUnmanaged.cs index a62f7cfe17..18527267fd 100644 --- a/GUI/Main/MainUnmanaged.cs +++ b/GUI/Main/MainUnmanaged.cs @@ -7,11 +7,14 @@ namespace CKAN.GUI { public partial class Main { - private void viewUnmanagedFilesStripMenuItem_Click(object sender, EventArgs e) + private void viewUnmanagedFilesStripMenuItem_Click(object? sender, EventArgs? e) { - UnmanagedFiles.LoadFiles(Manager.CurrentInstance, repoData, currentUser); - tabController.ShowTab("UnmanagedFilesTabPage", 2); - DisableMainWindow(); + if (Manager.CurrentInstance != null) + { + UnmanagedFiles.LoadFiles(Manager.CurrentInstance, repoData, currentUser); + tabController.ShowTab("UnmanagedFilesTabPage", 2); + DisableMainWindow(); + } } private void UnmanagedFiles_Done() diff --git a/GUI/Model/GUIConfiguration.cs b/GUI/Model/GUIConfiguration.cs index d8ad9cd3f0..2fdc57d867 100644 --- a/GUI/Model/GUIConfiguration.cs +++ b/GUI/Model/GUIConfiguration.cs @@ -11,7 +11,7 @@ namespace CKAN.GUI [XmlRoot("Configuration")] public class GUIConfiguration { - public string CommandLineArguments = null; + public string? CommandLineArguments = null; [XmlArray, XmlArrayItem(ElementName = "CommandLine")] public List CommandLines = new List(); @@ -41,15 +41,15 @@ public class GUIConfiguration /// /// Name of the tag filter the user chose, if any /// - public string TagFilter = null; + public string? TagFilter = null; /// /// Name of the label filter the user chose, if any /// - public string CustomLabelFilter = null; + public string? CustomLabelFilter = null; [XmlArray, XmlArrayItem(ElementName = "Search")] - public List DefaultSearches = null; + public List? DefaultSearches = null; public List SortColumns = new List(); public List MultiSortDescending = new List(); @@ -139,7 +139,7 @@ private static GUIConfiguration LoadConfiguration(string path, { try { - configuration = (GUIConfiguration) serializer.Deserialize(stream); + configuration = serializer.Deserialize(stream); } catch (Exception e) { @@ -200,6 +200,7 @@ private static bool DeserializationFixes(GUIConfiguration configuration, configuration.CommandLines.AddRange( Enumerable.Repeat(configuration.CommandLineArguments, 1) .Concat(defaultCommandLines) + .OfType() .Distinct()); configuration.CommandLineArguments = null; needsSave = true; @@ -227,8 +228,7 @@ private static bool FixColumnName(List columnNames, string oldName, stri private static void SaveConfiguration(GUIConfiguration configuration) { - var serializer = new XmlSerializer(typeof (GUIConfiguration)); - + var serializer = new XmlSerializer(typeof(GUIConfiguration)); using (var writer = new StreamWriter(configuration.path)) { serializer.Serialize(writer, configuration); @@ -240,9 +240,20 @@ private static void SaveConfiguration(GUIConfiguration configuration) [XmlRoot("SavedSearch")] public class SavedSearch { - public string Name; + public string Name = ""; [XmlArray, XmlArrayItem(ElementName = "Search")] - public List Values; + public List Values = new List(); + } + + /// + /// We only use XML format for the GUI config + /// + public static class XmlSerializerExtensions + { + public static T Deserialize(this XmlSerializer serializer, + StreamReader s) + where T: class + => (serializer.Deserialize(s) as T)!; } } diff --git a/GUI/Model/GUIMod.cs b/GUI/Model/GUIMod.cs index bdb3aa29b7..3ee8c74463 100644 --- a/GUI/Model/GUIMod.cs +++ b/GUI/Model/GUIMod.cs @@ -19,20 +19,20 @@ namespace CKAN.GUI #endif public sealed class GUIMod : INotifyPropertyChanged { - private CkanModule Mod { get; set; } - public CkanModule LatestCompatibleMod { get; private set; } - public CkanModule LatestAvailableMod { get; private set; } - public InstalledModule InstalledMod { get; private set; } + private CkanModule Mod { get; set; } + public CkanModule? LatestCompatibleMod { get; private set; } + public CkanModule? LatestAvailableMod { get; private set; } + public InstalledModule? InstalledMod { get; private set; } - private GameInstance currentInstance => Main.Instance?.CurrentInstance; - private GameInstanceManager manager => Main.Instance?.Manager; + private GameInstance? currentInstance => Main.Instance?.CurrentInstance; + private GameInstanceManager? manager => Main.Instance?.Manager; /// /// The module of the checkbox that is checked in the MainAllModVersions list if any, /// null otherwise. /// Used for generating this mod's part of the change set. /// - public CkanModule SelectedMod + public CkanModule? SelectedMod { get => selectedMod; set @@ -44,14 +44,14 @@ public CkanModule SelectedMod } } } - private CkanModule selectedMod = null; + private CkanModule? selectedMod = null; /// /// Notify listeners when certain properties change. /// Currently used to tell MainAllModVersions to update its checkboxes. /// - public event PropertyChangedEventHandler PropertyChanged; - private void OnPropertyChanged([CallerMemberName] string name = null) + public event PropertyChangedEventHandler? PropertyChanged; + private void OnPropertyChanged([CallerMemberName] string? name = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } @@ -64,7 +64,7 @@ private void OnPropertyChanged([CallerMemberName] string name = null) public bool IsIncompatible { get; private set; } public bool IsAutodetected { get; private set; } public List Authors => Mod.author ?? new List(); - public string InstalledVersion { get; private set; } + public string? InstalledVersion { get; private set; } public DateTime? InstallDate { get; private set; } public string LatestVersion { get; private set; } public string DownloadSize { get; private set; } @@ -75,8 +75,8 @@ private void OnPropertyChanged([CallerMemberName] string name = null) // These indicate the maximum game version that the maximum available // version of this mod can handle. The "Long" version also indicates // to the user if a mod upgrade would be required. (#1270) - public GameVersion GameCompatibilityVersion { get; private set; } - public string GameCompatibility + public GameVersion? GameCompatibilityVersion { get; private set; } + public string? GameCompatibility => GameCompatibilityVersion == null ? Properties.Resources.GUIModUnknown : GameCompatibilityVersion.IsAny ? GameVersion.AnyString : GameCompatibilityVersion.ToString(); @@ -108,7 +108,7 @@ public bool IsInstallable() => !IsIncompatible || IsInstalled; public string Version - => IsInstalled ? InstalledVersion : LatestVersion; + => InstalledVersion ?? LatestVersion; /// /// Initialize a GUIMod based on an InstalledModule @@ -156,70 +156,21 @@ public GUIMod(CkanModule mod, bool? incompatible, bool hideEpochs, bool hideV) - : this(mod.identifier, repoDataMgr, registry, current_game_version, incompatible, hideEpochs, hideV) { - Mod = mod; - - Name = mod.name.Trim(); - Abstract = mod.@abstract.Trim(); - Description = mod.description?.Trim() ?? string.Empty; - Abbrevation = new string(Name.Split(' ').Where(s => s.Length > 0).Select(s => s[0]).ToArray()); - - HasReplacement = registry.GetReplacement(mod, current_game_version) != null; - DownloadSize = mod.download_size == 0 ? Properties.Resources.GUIModNSlashA : CkanModule.FmtSize(mod.download_size); - InstallSize = mod.install_size == 0 ? Properties.Resources.GUIModNSlashA : CkanModule.FmtSize(mod.install_size); - - // Get the Searchables. - SearchableName = mod.SearchableName; - SearchableAbstract = mod.SearchableAbstract; - SearchableDescription = mod.SearchableDescription; - SearchableAuthors = mod.SearchableAuthors; - - // If not set in GUIMod(identifier, ...) (because the mod is not known to the registry), - // set based on the the data we have from the CkanModule. - if (GameCompatibilityVersion == null) - { - GameCompatibilityVersion = mod.LatestCompatibleGameVersion(); - if (GameCompatibilityVersion.IsAny) - { - GameCompatibilityVersion = mod.LatestCompatibleRealGameVersion( - currentInstance?.game.KnownVersions - ?? new List() {}); - } - } - - UpdateIsCached(); - } - - /// - /// Initialize a GUIMod based on just an identifier - /// - /// The id of the module to represent - /// CKAN registry object for current game instance - /// Current game version - /// If true, mark this module as incompatible - private GUIMod(string identifier, - RepositoryDataManager repoDataMgr, - IRegistryQuerier registry, - GameVersionCriteria current_game_version, - bool? incompatible, - bool hideEpochs, - bool hideV) - { - Identifier = identifier; - IsAutodetected = registry.IsAutodetected(identifier); - DownloadCount = repoDataMgr.GetDownloadCount(registry.Repositories.Values, identifier); + Identifier = mod.identifier; + IsAutodetected = registry.IsAutodetected(Identifier); + DownloadCount = repoDataMgr.GetDownloadCount(registry.Repositories.Values, Identifier); if (IsAutodetected) { IsInstalled = true; } - ModuleVersion latest_version = null; + ModuleVersion? latest_version = null; if (incompatible != true) { try { - LatestCompatibleMod = registry.LatestAvailable(identifier, current_game_version); + LatestCompatibleMod = registry.LatestAvailable(Identifier, current_game_version); latest_version = LatestCompatibleMod?.version; } catch (ModuleNotFoundKraken) @@ -238,18 +189,18 @@ private GUIMod(string identifier, try { - LatestAvailableMod = registry.LatestAvailable(identifier, null); + LatestAvailableMod = registry.LatestAvailable(Identifier, null); } catch { } // If there's known information for this mod in any form, calculate the highest compatible - // KSP. + // game version. if (LatestAvailableMod != null) { GameCompatibilityVersion = registry.LatestCompatibleGameVersion( currentInstance?.game.KnownVersions ?? new List() {}, - identifier); + Identifier); } if (latest_version != null) @@ -266,6 +217,38 @@ private GUIMod(string identifier, } SearchableIdentifier = CkanModule.nonAlphaNums.Replace(Identifier, ""); + + Mod = mod; + + Name = mod.name.Trim(); + Abstract = mod.@abstract.Trim(); + Description = mod.description?.Trim() ?? string.Empty; + Abbrevation = new string(Name.Split(' ').Where(s => s.Length > 0).Select(s => s[0]).ToArray()); + + HasReplacement = registry.GetReplacement(mod, current_game_version) != null; + DownloadSize = mod.download_size == 0 ? Properties.Resources.GUIModNSlashA : CkanModule.FmtSize(mod.download_size); + InstallSize = mod.install_size == 0 ? Properties.Resources.GUIModNSlashA : CkanModule.FmtSize(mod.install_size); + + // Get the Searchables. + SearchableName = mod.SearchableName; + SearchableAbstract = mod.SearchableAbstract; + SearchableDescription = mod.SearchableDescription; + SearchableAuthors = mod.SearchableAuthors; + + // If not set in GUIMod(identifier, ...) (because the mod is not known to the registry), + // set based on the the data we have from the CkanModule. + if (GameCompatibilityVersion == null) + { + GameCompatibilityVersion = mod.LatestCompatibleGameVersion(); + if (GameCompatibilityVersion.IsAny) + { + GameCompatibilityVersion = mod.LatestCompatibleRealGameVersion( + currentInstance?.game.KnownVersions + ?? new List() {}); + } + } + + UpdateIsCached(); } /// @@ -315,7 +298,9 @@ public IEnumerable GetModChanges(bool upgradeChecked, bool replaceChe // Both null ?? true)) { - if (InstalledMod != null && SelectedMod == LatestAvailableMod) + if (InstalledMod != null + && SelectedMod != null + && SelectedMod == LatestAvailableMod) { yield return new ModUpgrade(Mod, GUIModChangeType.Update, @@ -326,7 +311,7 @@ public IEnumerable GetModChanges(bool upgradeChecked, bool replaceChe { if (InstalledMod != null) { - yield return new ModChange(InstalledMod.Module, GUIModChangeType.Remove); + yield return new ModChange(InstalledMod.Module!, GUIModChangeType.Remove); } if (SelectedMod != null) { @@ -334,7 +319,7 @@ public IEnumerable GetModChanges(bool upgradeChecked, bool replaceChe } } } - else if (upgradeChecked) + else if (upgradeChecked && SelectedMod != null) { // Reinstall yield return new ModUpgrade(Mod, @@ -346,7 +331,8 @@ public IEnumerable GetModChanges(bool upgradeChecked, bool replaceChe public void SetAutoInstallChecked(DataGridViewRow row, DataGridViewColumn col, bool? set_value_to = null) { - if (row.Cells[col.Index] is DataGridViewCheckBoxCell auto_cell) + if (row.Cells[col.Index] is DataGridViewCheckBoxCell auto_cell + && InstalledMod != null) { var old_value = (bool) auto_cell.Value; @@ -361,9 +347,9 @@ public void SetAutoInstallChecked(DataGridViewRow row, DataGridViewColumn col, b } } - private bool Equals(GUIMod other) => Equals(Identifier, other.Identifier); + private bool Equals(GUIMod? other) => Equals(Identifier, other?.Identifier); - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is null) { diff --git a/GUI/Model/ModChange.cs b/GUI/Model/ModChange.cs index 0db9904c94..6767dcd9f9 100644 --- a/GUI/Model/ModChange.cs +++ b/GUI/Model/ModChange.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.ComponentModel.DataAnnotations; + #if NET5_0_OR_GREATER using System.Runtime.Versioning; #endif @@ -82,7 +83,7 @@ public ModChange(CkanModule mod, GUIModChangeType changeType, IEnumerable