diff --git a/CKAN.schema b/CKAN.schema index 1d892dbf96..71561e12c2 100644 --- a/CKAN.schema +++ b/CKAN.schema @@ -30,7 +30,7 @@ }, "kind" : { "description" : "Package type, defaults to package.", - "enum" : [ "package", "metapackage" ] + "enum" : [ "package", "metapackage", "dlc" ] }, "abstract" : { "description" : "Short description of the mod", @@ -219,6 +219,16 @@ "description" : "Mod's remote hosted netkan", "type" : "string", "format" : "uri" + }, + "store": { + "description" : "Purchase DLC here", + "type" : "string", + "format" : "uri" + }, + "steamstore": { + "description" : "Purchase DLC on Steam here", + "type" : "string", + "format" : "uri" } } }, @@ -316,7 +326,7 @@ { "properties": { "kind": { - "enum": [ "metapackage" ] + "enum": [ "metapackage", "dlc" ] } }, "not": { diff --git a/Cmdline/Action/Available.cs b/Cmdline/Action/Available.cs index bbf140fb13..2bd76b275f 100644 --- a/Cmdline/Action/Available.cs +++ b/Cmdline/Action/Available.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Collections.Generic; namespace CKAN.CmdLine @@ -15,7 +16,10 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) { AvailableOptions opts = (AvailableOptions)raw_options; IRegistryQuerier registry = RegistryManager.Instance(ksp).registry; - var compatible = registry.CompatibleModules(ksp.VersionCriteria()); + + var compatible = registry + .CompatibleModules(ksp.VersionCriteria()) + .Where(m => !m.IsDLC); user.RaiseMessage("Modules compatible with KSP {0}", ksp.Version()); user.RaiseMessage(""); diff --git a/Cmdline/Action/Install.cs b/Cmdline/Action/Install.cs index 832210e24c..4fc91639e5 100644 --- a/Cmdline/Action/Install.cs +++ b/Cmdline/Action/Install.cs @@ -288,6 +288,19 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) user.RaiseMessage("\r\n{0}", kraken.Message); return Exit.ERROR; } + catch (ModuleIsDLCKraken kraken) + { + user.RaiseMessage($"CKAN can't install expansion '{kraken.module.name}' for you."); + var res = kraken?.module?.resources; + var storePagesMsg = new Uri[] { res?.store, res?.steamstore } + .Where(u => u != null) + .Aggregate("", (a, b) => $"{a}\r\n- {b}"); + if (!string.IsNullOrEmpty(storePagesMsg)) + { + user.RaiseMessage($"To install this expansion, purchase it from one of its store pages:\r\n{storePagesMsg}"); + } + return Exit.ERROR; + } } return Exit.OK; diff --git a/Cmdline/Action/Mark.cs b/Cmdline/Action/Mark.cs index 485dc1b9fb..fb52af08bf 100644 --- a/Cmdline/Action/Mark.cs +++ b/Cmdline/Action/Mark.cs @@ -95,8 +95,16 @@ private int MarkAuto(MarkAutoOptions opts, bool value, string verb, string descr else { user.RaiseMessage("Marking {0} as {1}...", id, descrip); - im.AutoInstalled = value; - needSave = true; + try + { + im.AutoInstalled = value; + needSave = true; + } + catch (ModuleIsDLCKraken kraken) + { + user.RaiseMessage($"Can't mark expansion '{kraken.module.name}' as auto-installed."); + return Exit.BADOPT; + } } } if (needSave) diff --git a/Cmdline/Action/Remove.cs b/Cmdline/Action/Remove.cs index 5ed6fa8cff..045a898670 100644 --- a/Cmdline/Action/Remove.cs +++ b/Cmdline/Action/Remove.cs @@ -1,7 +1,8 @@ -using log4net; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using log4net; namespace CKAN.CmdLine { @@ -65,7 +66,9 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) log.Debug("Removing all mods"); // Add the list of installed modules to the list that should be uninstalled options.modules.AddRange( - regMgr.registry.InstalledModules.Select(mod => mod.identifier) + regMgr.registry.InstalledModules + .Where(mod => !mod.Module.IsDLC) + .Select(mod => mod.identifier) ); } @@ -84,6 +87,19 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) user.RaiseMessage("Try `ckan list` for a list of installed mods."); return Exit.BADOPT; } + catch (ModuleIsDLCKraken kraken) + { + user.RaiseMessage($"CKAN can't remove expansion '{kraken.module.name}' for you."); + var res = kraken?.module?.resources; + var storePagesMsg = new Uri[] { res?.store, res?.steamstore } + .Where(u => u != null) + .Aggregate("", (a, b) => $"{a}\r\n- {b}"); + if (!string.IsNullOrEmpty(storePagesMsg)) + { + user.RaiseMessage($"To remove this expansion, follow the instructions for the store page from which you purchased it:\r\n{storePagesMsg}"); + } + return Exit.BADOPT; + } } else { diff --git a/Cmdline/Action/Show.cs b/Cmdline/Action/Show.cs index 98f990afaf..fda6dd4c66 100644 --- a/Cmdline/Action/Show.cs +++ b/Cmdline/Action/Show.cs @@ -107,10 +107,13 @@ public int ShowMod(InstalledModule module) ICollection files = module.Files as ICollection; if (files == null) throw new InvalidCastException(); - user.RaiseMessage("\r\nShowing {0} installed files:", files.Count); - foreach (string file in files) + if (!module.Module.IsDLC) { - user.RaiseMessage("- {0}", file); + user.RaiseMessage("\r\nShowing {0} installed files:", files.Count); + foreach (string file in files) + { + user.RaiseMessage("- {0}", file); + } } return return_value; @@ -192,22 +195,43 @@ public int ShowMod(CkanModule module) if (module.resources != null) { if (module.resources.bugtracker != null) + { user.RaiseMessage("- bugtracker: {0}", Uri.EscapeUriString(module.resources.bugtracker.ToString())); + } if (module.resources.homepage != null) + { user.RaiseMessage("- homepage: {0}", Uri.EscapeUriString(module.resources.homepage.ToString())); + } if (module.resources.spacedock != null) + { user.RaiseMessage("- spacedock: {0}", Uri.EscapeUriString(module.resources.spacedock.ToString())); + } if (module.resources.repository != null) + { user.RaiseMessage("- repository: {0}", Uri.EscapeUriString(module.resources.repository.ToString())); + } if (module.resources.curse != null) + { user.RaiseMessage("- curse: {0}", Uri.EscapeUriString(module.resources.curse.ToString())); + } + if (module.resources.store != null) + { + user.RaiseMessage("- store: {0}", Uri.EscapeUriString(module.resources.store.ToString())); + } + if (module.resources.steamstore != null) + { + user.RaiseMessage("- steamstore: {0}", Uri.EscapeUriString(module.resources.steamstore.ToString())); + } } - // Compute the CKAN filename. - string file_uri_hash = NetFileCache.CreateURLHash(module.download); - string file_name = CkanModule.StandardName(module.identifier, module.version); - - user.RaiseMessage("\r\nFilename: {0}", file_uri_hash + "-" + file_name); + if (!module.IsDLC) + { + // Compute the CKAN filename. + string file_uri_hash = NetFileCache.CreateURLHash(module.download); + string file_name = CkanModule.StandardName(module.identifier, module.version); + + user.RaiseMessage("\r\nFilename: {0}", file_uri_hash + "-" + file_name); + } return Exit.OK; } diff --git a/Cmdline/Action/Upgrade.cs b/Cmdline/Action/Upgrade.cs index 91cb23b95f..d6cf45f3f1 100644 --- a/Cmdline/Action/Upgrade.cs +++ b/Cmdline/Action/Upgrade.cs @@ -1,6 +1,8 @@ +using System; +using System.Linq; using System.Collections.Generic; -using CKAN.Versioning; using log4net; +using CKAN.Versioning; namespace CKAN.CmdLine { @@ -108,7 +110,7 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) // This may be an unindexed mod. If so, // skip rather than crash. See KSP-CKAN/CKAN#841. - if (latest == null) + if (latest == null || latest.IsDLC) { continue; } @@ -150,6 +152,19 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) User.RaiseMessage(kraken.ToString()); return Exit.ERROR; } + catch (ModuleIsDLCKraken kraken) + { + User.RaiseMessage($"CKAN can't upgrade expansion '{kraken.module.name}' for you."); + var res = kraken?.module?.resources; + var storePagesMsg = new Uri[] { res?.store, res?.steamstore } + .Where(u => u != null) + .Aggregate("", (a, b) => $"{a}\r\n- {b}"); + if (!string.IsNullOrEmpty(storePagesMsg)) + { + User.RaiseMessage($"To upgrade this expansion, download any updates from the store page from which you purchased it:\r\n{storePagesMsg}"); + } + return Exit.ERROR; + } User.RaiseMessage("\r\nDone!\r\n"); return Exit.OK; diff --git a/Core/DLC/IDlcDetector.cs b/Core/DLC/IDlcDetector.cs index 170529cfa8..92589a17fc 100644 --- a/Core/DLC/IDlcDetector.cs +++ b/Core/DLC/IDlcDetector.cs @@ -37,19 +37,19 @@ public interface IDlcDetector /// /// /// - bool IsInstalled (KSP ksp, out string identifier, out UnmanagedModuleVersion version); + bool IsInstalled(KSP ksp, out string identifier, out UnmanagedModuleVersion version); /// /// Path to the DLC directory relative to GameDir. /// E.g. GameData/SquadExpansion/Serenity /// /// The relative path as string. - string InstallPath (); + string InstallPath(); /// /// Determines whether the DLC is allowed to be installed (or faked) /// on the specified base version (i.e. the game version of the KSP instance.) /// - bool AllowedOnBaseVersion (KspVersion baseVersion); + bool AllowedOnBaseVersion(KspVersion baseVersion); } } diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 97c07ccceb..26bfb78504 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -285,7 +285,7 @@ public IEnumerable GetModuleContentsList(CkanModule module) /// private void Install(CkanModule module, bool autoInstalled, Registry registry, string filename = null) { - CheckMetapackageInstallationKraken(module); + CheckKindInstallationKraken(module); ModuleVersion version = registry.InstalledVersion(module.identifier); @@ -332,11 +332,15 @@ private void Install(CkanModule module, bool autoInstalled, Registry registry, s /// Check if the given module is a metapackage: /// if it is, throws a BadCommandKraken. /// - private static void CheckMetapackageInstallationKraken(CkanModule module) + private static void CheckKindInstallationKraken(CkanModule module) { if (module.IsMetapackage) { - throw new BadCommandKraken("Metapackages can not be installed!"); + throw new BadCommandKraken("Metapackages cannot be installed!"); + } + if (module.IsDLC) + { + throw new BadCommandKraken("DLC cannot be installed!"); } } @@ -348,7 +352,7 @@ private static void CheckMetapackageInstallationKraken(CkanModule module) /// private IEnumerable InstallModule(CkanModule module, string zip_filename, Registry registry) { - CheckMetapackageInstallationKraken(module); + CheckKindInstallationKraken(module); using (ZipFile zipfile = new ZipFile(zip_filename)) { @@ -740,6 +744,14 @@ public void UninstallList( throw new ModNotInstalledKraken(mod); } + var instDlc = mods + .Select(ident => registry_manager.registry.InstalledModule(ident)) + .FirstOrDefault(m => m.Module.IsDLC); + if (instDlc != null) + { + throw new ModuleIsDLCKraken(instDlc.Module); + } + // Find all the things which need uninstalling. IEnumerable revdep = mods .Union(registry_manager.registry.FindReverseDependencies( @@ -1261,14 +1273,14 @@ out Dictionary> supporters if (!registry.IsInstalled(provider.identifier) && !toInstall.Any(m => m.identifier == provider.identifier) && dependersIndex.TryGetValue(provider, out List dependers) - && CanInstall(RelationshipResolver.DependsOnlyOpts(), - toInstall.ToList().Concat(new List() { provider }).ToList(), registry)) + && (provider.IsDLC || CanInstall(RelationshipResolver.DependsOnlyOpts(), + toInstall.ToList().Concat(new List() { provider }).ToList(), registry))) { dependersIndex.Remove(provider); recommendations.Add( provider, new Tuple>( - providers.Count <= 1 || provider.identifier == (rel as ModuleRelationshipDescriptor)?.name, + !provider.IsDLC && (providers.Count <= 1 || provider.identifier == (rel as ModuleRelationshipDescriptor)?.name), dependers) ); } @@ -1288,8 +1300,8 @@ out Dictionary> supporters if (!registry.IsInstalled(provider.identifier) && !toInstall.Any(m => m.identifier == provider.identifier) && dependersIndex.TryGetValue(provider, out List dependers) - && CanInstall(RelationshipResolver.DependsOnlyOpts(), - toInstall.ToList().Concat(new List() { provider }).ToList(), registry)) + && (provider.IsDLC || CanInstall(RelationshipResolver.DependsOnlyOpts(), + toInstall.ToList().Concat(new List() { provider }).ToList(), registry))) { dependersIndex.Remove(provider); suggestions.Add(provider, dependers); diff --git a/Core/Registry/CompatibilitySorter.cs b/Core/Registry/CompatibilitySorter.cs index 43ae86f6df..51e2356647 100644 --- a/Core/Registry/CompatibilitySorter.cs +++ b/Core/Registry/CompatibilitySorter.cs @@ -25,7 +25,7 @@ public CompatibilitySorter( Dictionary available, Dictionary> providers, HashSet dlls, - Dictionary dlc + IDictionary dlc ) { CompatibleVersions = crit; @@ -63,7 +63,7 @@ public readonly SortedDictionary Incompatible private readonly Stack Investigating = new Stack(); private readonly HashSet dlls; - private readonly Dictionary dlc; + private readonly IDictionary dlc; /// /// Filter the provides mapping by compatibility @@ -78,9 +78,10 @@ private Dictionary> CompatibleProviders(KspVers var compat = new Dictionary>(); foreach (var kvp in providers) { - // Find providing modules that are compatible with crit + // Find providing non-DLC modules that are compatible with crit var compatAvail = kvp.Value.Where(avm => avm.AllAvailable().Any(ckm => + !ckm.IsDLC && ckm.ProvidesList.Contains(kvp.Key) && ckm.IsCompatibleKSP(crit)) ).ToHashSet(); // Add compatible providers to mapping, if any diff --git a/Core/Registry/IRegistryQuerier.cs b/Core/Registry/IRegistryQuerier.cs index 9b9acd4da1..70f51f6928 100644 --- a/Core/Registry/IRegistryQuerier.cs +++ b/Core/Registry/IRegistryQuerier.cs @@ -11,7 +11,7 @@ public interface IRegistryQuerier { IEnumerable InstalledModules { get; } IEnumerable InstalledDlls { get; } - IDictionary InstalledDlc { get; } + IDictionary InstalledDlc { get; } int? DownloadCount(string identifier); diff --git a/Core/Registry/InstalledModule.cs b/Core/Registry/InstalledModule.cs index 8d075fd77c..adce3e6bf9 100644 --- a/Core/Registry/InstalledModule.cs +++ b/Core/Registry/InstalledModule.cs @@ -116,7 +116,13 @@ public DateTime InstallTime public bool AutoInstalled { get { return auto_installed; } - set { auto_installed = value; } + set { + if (Module.IsDLC) + { + throw new ModuleIsDLCKraken(Module); + } + auto_installed = value; + } } #endregion diff --git a/Core/Registry/Registry.cs b/Core/Registry/Registry.cs index fcc8438408..bdd28f486a 100644 --- a/Core/Registry/Registry.cs +++ b/Core/Registry/Registry.cs @@ -70,15 +70,6 @@ public void SetDownloadCounts(SortedDictionary counts) private Dictionary> providers = new Dictionary>(); - /// - /// A map between module identifiers and versions for official DLC that are installed. - /// - /// - /// This shouldn't have a as detection at runtime should be fast. - /// - private readonly Dictionary _installedDlcModules = - new Dictionary(); - [JsonIgnore] private string transaction_backup; /// @@ -108,9 +99,16 @@ [JsonIgnore] public IEnumerable InstalledDlls get { return installed_dlls.Keys; } } - [JsonIgnore] public IDictionary InstalledDlc + /// + /// A map between module identifiers and versions for official DLC that are installed. + /// + [JsonIgnore] public IDictionary InstalledDlc { - get { return _installedDlcModules; } + get { + return installed_modules.Values + .Where(im => im.Module.IsDLC) + .ToDictionary(im => im.Module.identifier, im => im.Module.version); + } } /// @@ -853,12 +851,51 @@ public void ClearDlls() public void RegisterDlc(string identifier, UnmanagedModuleVersion version) { - _installedDlcModules[identifier] = version; + if (available_modules.TryGetValue(identifier, out AvailableModule avail)) + { + CkanModule dlcModule = avail.ByVersion(version); + if (dlcModule != null) + { + installed_modules.Add( + dlcModule.identifier, + new InstalledModule(null, dlcModule, new string[] { }, false) + ); + } + } + else + { + // Don't have the real thing, make a fake one + installed_modules.Add( + identifier, + new InstalledModule( + null, + new CkanModule() + { + spec_version = new ModuleVersion("v1.28"), + identifier = identifier, + name = identifier, + @abstract = "An official expansion pack for KSP", + author = new List() { "SQUAD" }, + version = version, + kind = "dlc", + license = new List() { new License("restricted") }, + }, + new string[] { }, + false + ) + ); + } } public void ClearDlc() { - _installedDlcModules.Clear(); + var installedDlcs = installed_modules.Values + .Where(instMod => instMod.Module.IsDLC) + .ToList(); + foreach (var instMod in installedDlcs) + { + installed_modules.Remove(instMod.identifier); + } } /// @@ -884,17 +921,12 @@ public Dictionary Installed(bool withProvides = true) } // Index our installed modules (which may overwrite the installed DLLs and provides) + // (Includes DLCs) foreach (var modinfo in installed_modules) { installed[modinfo.Key] = modinfo.Value.Module.version; } - // Index our detected DLC (which overwrites everything) - foreach (var i in _installedDlcModules) - { - installed[i.Key] = i.Value; - } - return installed; } @@ -949,13 +981,8 @@ public ModuleVersion InstalledVersion(string modIdentifier, bool with_provides=t { InstalledModule installedModule; - // If it's in our DLC registry, return that - if (_installedDlcModules.ContainsKey(modIdentifier)) - { - return _installedDlcModules[modIdentifier]; - } - // If it's genuinely installed, return the details we have. + // (Includes DLCs) if (installed_modules.TryGetValue(modIdentifier, out installedModule)) { return installedModule.Module.version; @@ -1014,13 +1041,13 @@ public string FileOwner(string file) public void CheckSanity() { IEnumerable installed = from pair in installed_modules select pair.Value.Module; - SanityChecker.EnforceConsistency(installed, installed_dlls.Keys, _installedDlcModules); + SanityChecker.EnforceConsistency(installed, installed_dlls.Keys, InstalledDlc); } public List GetSanityErrors() { var installed = from pair in installed_modules select pair.Value.Module; - return SanityChecker.ConsistencyErrors(installed, installed_dlls.Keys, _installedDlcModules).ToList(); + return SanityChecker.ConsistencyErrors(installed, installed_dlls.Keys, InstalledDlc).ToList(); } /// @@ -1038,7 +1065,7 @@ internal static IEnumerable FindReverseDependencies( IEnumerable modulesToInstall, IEnumerable origInstalled, IEnumerable dlls, - IDictionary dlc + IDictionary dlc ) { modulesToRemove = modulesToRemove.Memoize(); @@ -1109,7 +1136,7 @@ public IEnumerable FindReverseDependencies( ) { var installed = new HashSet(installed_modules.Values.Select(x => x.Module)); - return FindReverseDependencies(modulesToRemove, modulesToInstall, installed, new HashSet(installed_dlls.Keys), _installedDlcModules); + return FindReverseDependencies(modulesToRemove, modulesToInstall, installed, new HashSet(installed_dlls.Keys), InstalledDlc); } /// @@ -1123,9 +1150,9 @@ public IEnumerable FindReverseDependencies( /// Sequence of removable auto-installed modules, if any /// private static IEnumerable FindRemovableAutoInstalled( - IEnumerable installedModules, - IEnumerable dlls, - IDictionary dlc + IEnumerable installedModules, + IEnumerable dlls, + IDictionary dlc ) { // ToList ensures that the collection isn't modified while the enumeration operation is executing @@ -1229,7 +1256,7 @@ public void SetCompatibleVersion(KspVersionCriteria versCrit) { sorter = new CompatibilitySorter( versCrit, available_modules, providers, - InstalledDlls.ToHashSet(), _installedDlcModules + InstalledDlls.ToHashSet(), InstalledDlc ); } } diff --git a/Core/Registry/RegistryManager.cs b/Core/Registry/RegistryManager.cs index 82b4b25fad..f88b19eed9 100644 --- a/Core/Registry/RegistryManager.cs +++ b/Core/Registry/RegistryManager.cs @@ -478,8 +478,8 @@ public CkanModule GenerateModpack(bool recommends = false, bool with_versions = /// public bool ScanDlc() { - var dlc = new Dictionary(registry.InstalledDlc); - UnmanagedModuleVersion foundVer; + var dlc = new Dictionary(registry.InstalledDlc); + ModuleVersion foundVer; bool changed = false; registry.ClearDlc(); diff --git a/Core/Relationships/RelationshipResolver.cs b/Core/Relationships/RelationshipResolver.cs index 2327b60c01..ed66724b32 100644 --- a/Core/Relationships/RelationshipResolver.cs +++ b/Core/Relationships/RelationshipResolver.cs @@ -508,6 +508,10 @@ private void Add(CkanModule module, SelectionReason reason) { if (module.IsMetapackage) return; + if (module.IsDLC) + { + throw new ModuleIsDLCKraken(module); + } log.DebugFormat("Adding {0} {1}", module.identifier, module.version); @@ -552,7 +556,10 @@ private void Add(CkanModule module, SelectionReason reason) /// If it has dependencies compatible for the current version private bool MightBeInstallable(CkanModule module, List compatible = null) { - if (module.depends == null) return true; + if (module.IsDLC) + return false; + if (module.depends == null) + return true; if (compatible == null) { compatible = new List(); diff --git a/Core/Relationships/SanityChecker.cs b/Core/Relationships/SanityChecker.cs index fdee41a29d..62b1e102f9 100644 --- a/Core/Relationships/SanityChecker.cs +++ b/Core/Relationships/SanityChecker.cs @@ -20,7 +20,7 @@ public static class SanityChecker public static ICollection ConsistencyErrors( IEnumerable modules, IEnumerable dlls, - IDictionary dlc + IDictionary dlc ) { List> unmetDepends; @@ -48,7 +48,7 @@ IDictionary dlc public static void EnforceConsistency( IEnumerable modules, IEnumerable dlls = null, - IDictionary dlc = null + IDictionary dlc = null ) { List> unmetDepends; @@ -65,7 +65,7 @@ public static void EnforceConsistency( public static bool IsConsistent( IEnumerable modules, IEnumerable dlls = null, - IDictionary dlc = null + IDictionary dlc = null ) { List> unmetDepends; @@ -76,7 +76,7 @@ public static bool IsConsistent( private static bool CheckConsistency( IEnumerable modules, IEnumerable dlls, - IDictionary dlc, + IDictionary dlc, out List> UnmetDepends, out List> Conflicts ) @@ -101,7 +101,7 @@ out List> Conflicts public static List> FindUnsatisfiedDepends( IEnumerable modules, HashSet dlls, - IDictionary dlc + IDictionary dlc ) { var unsat = new List>(); @@ -135,7 +135,7 @@ IDictionary dlc public static List> FindConflicting( IEnumerable modules, HashSet dlls, - IDictionary dlc + IDictionary dlc ) { var confl = new List>(); diff --git a/Core/Types/CkanModule.cs b/Core/Types/CkanModule.cs index 545ae93948..cbccea6e15 100644 --- a/Core/Types/CkanModule.cs +++ b/Core/Types/CkanModule.cs @@ -19,7 +19,7 @@ public abstract class RelationshipDescriptor : IEquatable modules, HashSet dlls, - IDictionary dlc + IDictionary dlc ); public abstract bool WithinBounds(CkanModule otherModule); @@ -101,7 +101,7 @@ public bool WithinBounds(ModuleVersion other) public override bool MatchesAny( IEnumerable modules, HashSet dlls, - IDictionary dlc + IDictionary dlc ) { modules = modules?.AsCollection(); @@ -225,7 +225,7 @@ public override bool WithinBounds(CkanModule otherModule) public override bool MatchesAny( IEnumerable modules, HashSet dlls, - IDictionary dlc + IDictionary dlc ) { return any_of?.Any(r => r.MatchesAny(modules, dlls, dlc)) @@ -306,6 +306,14 @@ public class ResourcesDescriptor [JsonProperty("metanetkan", Order = 9, NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonOldResourceUrlConverter))] public Uri metanetkan; + + [JsonProperty("store", Order = 10, NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(JsonOldResourceUrlConverter))] + public Uri store; + + [JsonProperty("steamstore", Order = 11, NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(JsonOldResourceUrlConverter))] + public Uri steamstore; } public class DownloadHashesDescriptor @@ -344,16 +352,44 @@ public class CkanModule : IEquatable private static readonly ILog log = LogManager.GetLogger(typeof (CkanModule)); - private static readonly string[] required_fields = - { - "spec_version", - "name", - "abstract", - "identifier", - "download", - "license", - "version" - }; + 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. @@ -365,6 +401,7 @@ public class CkanModule : IEquatable public string description; // Package type: in spec v1.6 can be either "package" or "metapackage" + // In spec v1.28, "dlc" [JsonProperty("kind", Order = 29, NullValueHandling = NullValueHandling.Ignore)] public string kind; @@ -566,7 +603,7 @@ public CkanModule(string json, IGameComparator comparator) // Check everything in the spec is defined. // TODO: This *can* and *should* be done with JSON attributes! - foreach (string field in required_fields) + foreach (string field in required_fields[kind ?? "package"]) { object value = null; if (GetType().GetField(field) != null) @@ -581,9 +618,6 @@ public CkanModule(string json, IGameComparator comparator) if (value == null) { - // Metapackages are allowed to have no download field - if (field == "download" && IsMetapackage) continue; - string error = String.Format("{0} missing required field {1}", identifier, field); throw new BadMetadataKraken(null, error); @@ -818,7 +852,18 @@ public bool DoesProvide(string identifier) public bool IsMetapackage { - get { return (!string.IsNullOrEmpty(this.kind) && this.kind == "metapackage"); } + get + { + return this.kind == "metapackage"; + } + } + + public bool IsDLC + { + get + { + return this.kind == "dlc"; + } } protected bool Equals(CkanModule other) diff --git a/Core/Types/Kraken.cs b/Core/Types/Kraken.cs index f63d495a7a..cabb5e442e 100644 --- a/Core/Types/Kraken.cs +++ b/Core/Types/Kraken.cs @@ -567,4 +567,19 @@ public InstanceNameTakenKraken(string name, string reason = null) this.instName = name; } } + + public class ModuleIsDLCKraken : Kraken + { + /// + /// The DLC module that can't be operated upon + /// + public readonly CkanModule module; + + public ModuleIsDLCKraken(CkanModule module, string reason = null) + : base(reason) + { + this.module = module; + } + } + } diff --git a/GUI/Controls/AllModVersions.cs b/GUI/Controls/AllModVersions.cs index ea864c0e8c..136fe07567 100644 --- a/GUI/Controls/AllModVersions.cs +++ b/GUI/Controls/AllModVersions.cs @@ -106,6 +106,8 @@ public GUIMod SelectedModule } } VersionsListView.Items.Clear(); + // Only show checkboxes for non-DLC modules + VersionsListView.CheckBoxes = !value.ToModule().IsDLC; KSP currentInstance = Main.Instance.Manager.CurrentInstance; IRegistryQuerier registry = RegistryManager.Instance(currentInstance).registry; diff --git a/GUI/Controls/ChooseRecommendedMods.cs b/GUI/Controls/ChooseRecommendedMods.cs index 949579265b..16449e7363 100644 --- a/GUI/Controls/ChooseRecommendedMods.cs +++ b/GUI/Controls/ChooseRecommendedMods.cs @@ -65,24 +65,35 @@ private void RecommendedModsListView_SelectedIndexChanged(object sender, EventAr } } - private void RecommendedModsListView_ItemChecked(object sender, EventArgs e) + private void RecommendedModsListView_ItemChecked(object sender, ItemCheckedEventArgs e) { - var conflicts = FindConflicts(); - 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)) + var module = e.Item.Tag as CkanModule; + if (module?.IsDLC ?? false) { - item.BackColor = conflicts.ContainsKey(item.Tag as CkanModule) - ? Color.LightCoral - : Color.Empty; + if (e.Item.Checked) + { + e.Item.Checked = false; + } } - RecommendedModsContinueButton.Enabled = !conflicts.Any(); - if (OnConflictFound != null) + else { - OnConflictFound(conflicts.Any() ? conflicts.First().Value : ""); + var conflicts = FindConflicts(); + 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)) + { + item.BackColor = conflicts.ContainsKey(item.Tag as CkanModule) + ? Color.LightCoral + : Color.Empty; + } + RecommendedModsContinueButton.Enabled = !conflicts.Any(); + if (OnConflictFound != null) + { + OnConflictFound(conflicts.Any() ? conflicts.First().Value : ""); + } } } @@ -138,9 +149,10 @@ private ListViewItem getRecSugItem(NetModuleCache cache, CkanModule module, stri { return new ListViewItem(new string[] { - cache.IsMaybeCachedZip(module) - ? string.Format(Properties.Resources.MainChangesetCached, module.name, module.version) - : string.Format(Properties.Resources.MainChangesetHostSize, module.name, module.version, module.download.Host ?? "", CkanModule.FmtSize(module.download_size)), + module.IsDLC ? module.name + : cache.IsMaybeCachedZip(module) + ? string.Format(Properties.Resources.MainChangesetCached, module.name, module.version) + : string.Format(Properties.Resources.MainChangesetHostSize, module.name, module.version, module.download?.Host ?? "", CkanModule.FmtSize(module.download_size)), descrip, module.@abstract }) diff --git a/GUI/Controls/ModInfo.Designer.cs b/GUI/Controls/ModInfo.Designer.cs index 2f501718dd..9ee34fc7c4 100644 --- a/GUI/Controls/ModInfo.Designer.cs +++ b/GUI/Controls/ModInfo.Designer.cs @@ -45,8 +45,6 @@ private void InitializeComponent() this.ReplacementTextBox = new TransparentTextBox(); this.KSPCompatibilityLabel = new System.Windows.Forms.Label(); this.ReleaseLabel = new System.Windows.Forms.Label(); - this.GitHubLabel = new System.Windows.Forms.Label(); - this.HomePageLabel = new System.Windows.Forms.Label(); this.AuthorLabel = new System.Windows.Forms.Label(); this.LicenseLabel = new System.Windows.Forms.Label(); this.MetadataModuleVersionTextBox = new TransparentTextBox(); @@ -54,9 +52,7 @@ private void InitializeComponent() this.MetadataModuleAuthorTextBox = new TransparentTextBox(); this.VersionLabel = new System.Windows.Forms.Label(); this.MetadataModuleReleaseStatusTextBox = new TransparentTextBox(); - this.MetadataModuleHomePageLinkLabel = new System.Windows.Forms.LinkLabel(); this.MetadataModuleKSPCompatibilityTextBox = new TransparentTextBox(); - this.MetadataModuleGitHubLinkLabel = new System.Windows.Forms.LinkLabel(); this.RelationshipTabPage = new System.Windows.Forms.TabPage(); this.DependsGraphTree = new System.Windows.Forms.TreeView(); this.LegendDependsImage = new System.Windows.Forms.PictureBox(); @@ -214,26 +210,22 @@ private void InitializeComponent() this.MetaDataLowerLayoutPanel.Controls.Add(this.VersionLabel, 0, 0); this.MetaDataLowerLayoutPanel.Controls.Add(this.LicenseLabel, 0, 1); this.MetaDataLowerLayoutPanel.Controls.Add(this.AuthorLabel, 0, 2); - this.MetaDataLowerLayoutPanel.Controls.Add(this.HomePageLabel, 0, 3); - this.MetaDataLowerLayoutPanel.Controls.Add(this.GitHubLabel, 0, 4); - this.MetaDataLowerLayoutPanel.Controls.Add(this.ReleaseLabel, 0, 5); - this.MetaDataLowerLayoutPanel.Controls.Add(this.KSPCompatibilityLabel, 0, 6); - this.MetaDataLowerLayoutPanel.Controls.Add(this.IdentifierLabel, 0, 7); - this.MetaDataLowerLayoutPanel.Controls.Add(this.ReplacementLabel, 0, 8); + this.MetaDataLowerLayoutPanel.Controls.Add(this.ReleaseLabel, 0, 3); + this.MetaDataLowerLayoutPanel.Controls.Add(this.KSPCompatibilityLabel, 0, 4); + this.MetaDataLowerLayoutPanel.Controls.Add(this.IdentifierLabel, 0, 5); + this.MetaDataLowerLayoutPanel.Controls.Add(this.ReplacementLabel, 0, 6); this.MetaDataLowerLayoutPanel.Controls.Add(this.MetadataModuleVersionTextBox, 1, 0); this.MetaDataLowerLayoutPanel.Controls.Add(this.MetadataModuleLicenseTextBox, 1, 1); this.MetaDataLowerLayoutPanel.Controls.Add(this.MetadataModuleAuthorTextBox, 1, 2); - this.MetaDataLowerLayoutPanel.Controls.Add(this.MetadataModuleHomePageLinkLabel, 1, 3); - this.MetaDataLowerLayoutPanel.Controls.Add(this.MetadataModuleGitHubLinkLabel, 1, 4); - this.MetaDataLowerLayoutPanel.Controls.Add(this.MetadataModuleReleaseStatusTextBox, 1, 5); - this.MetaDataLowerLayoutPanel.Controls.Add(this.MetadataModuleKSPCompatibilityTextBox, 1, 6); - this.MetaDataLowerLayoutPanel.Controls.Add(this.MetadataIdentifierTextBox, 1, 7); - this.MetaDataLowerLayoutPanel.Controls.Add(this.ReplacementTextBox, 1, 8); + this.MetaDataLowerLayoutPanel.Controls.Add(this.MetadataModuleReleaseStatusTextBox, 1, 3); + this.MetaDataLowerLayoutPanel.Controls.Add(this.MetadataModuleKSPCompatibilityTextBox, 1, 4); + this.MetaDataLowerLayoutPanel.Controls.Add(this.MetadataIdentifierTextBox, 1, 5); + this.MetaDataLowerLayoutPanel.Controls.Add(this.ReplacementTextBox, 1, 6); this.MetaDataLowerLayoutPanel.Dock = System.Windows.Forms.DockStyle.Fill; this.MetaDataLowerLayoutPanel.Location = new System.Drawing.Point(0, 0); this.MetaDataLowerLayoutPanel.Name = "MetaDataLowerLayoutPanel"; this.MetaDataLowerLayoutPanel.Padding = new System.Windows.Forms.Padding(0, 8, 0, 0); - this.MetaDataLowerLayoutPanel.RowCount = 9; + this.MetaDataLowerLayoutPanel.RowCount = 7; this.MetaDataLowerLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.MetaDataLowerLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.MetaDataLowerLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); @@ -241,8 +233,6 @@ private void InitializeComponent() this.MetaDataLowerLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.MetaDataLowerLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.MetaDataLowerLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); - this.MetaDataLowerLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); - this.MetaDataLowerLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 15F)); this.MetaDataLowerLayoutPanel.Size = new System.Drawing.Size(346, 255); this.MetaDataLowerLayoutPanel.AutoSize = true; this.MetaDataLowerLayoutPanel.AutoScroll = true; @@ -323,28 +313,6 @@ private void InitializeComponent() this.ReleaseLabel.TabIndex = 12; resources.ApplyResources(this.ReleaseLabel, "ReleaseLabel"); // - // GitHubLabel - // - this.GitHubLabel.AutoSize = true; - this.GitHubLabel.Dock = System.Windows.Forms.DockStyle.Fill; - this.GitHubLabel.ForeColor = System.Drawing.SystemColors.GrayText; - this.GitHubLabel.Location = new System.Drawing.Point(3, 120); - this.GitHubLabel.Name = "GitHubLabel"; - this.GitHubLabel.Size = new System.Drawing.Size(84, 30); - this.GitHubLabel.TabIndex = 10; - resources.ApplyResources(this.GitHubLabel, "GitHubLabel"); - // - // HomePageLabel - // - this.HomePageLabel.AutoSize = true; - this.HomePageLabel.Dock = System.Windows.Forms.DockStyle.Fill; - this.HomePageLabel.ForeColor = System.Drawing.SystemColors.GrayText; - this.HomePageLabel.Location = new System.Drawing.Point(3, 90); - this.HomePageLabel.Name = "HomePageLabel"; - this.HomePageLabel.Size = new System.Drawing.Size(84, 30); - this.HomePageLabel.TabIndex = 7; - resources.ApplyResources(this.HomePageLabel, "HomePageLabel"); - // // AuthorLabel // this.AuthorLabel.AutoSize = true; @@ -434,17 +402,6 @@ private void InitializeComponent() this.MetadataModuleReleaseStatusTextBox.BorderStyle = System.Windows.Forms.BorderStyle.None; resources.ApplyResources(this.MetadataModuleReleaseStatusTextBox, "MetadataModuleReleaseStatusTextBox"); // - // MetadataModuleHomePageLinkLabel - // - this.MetadataModuleHomePageLinkLabel.AutoSize = true; - this.MetadataModuleHomePageLinkLabel.Location = new System.Drawing.Point(93, 90); - this.MetadataModuleHomePageLinkLabel.Name = "MetadataModuleHomePageLinkLabel"; - this.MetadataModuleHomePageLinkLabel.Size = new System.Drawing.Size(250, 30); - this.MetadataModuleHomePageLinkLabel.TabIndex = 25; - this.MetadataModuleHomePageLinkLabel.TabStop = true; - this.MetadataModuleHomePageLinkLabel.Text = "linkLabel1"; - this.MetadataModuleHomePageLinkLabel.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.LinkLabel_LinkClicked); - // // MetadataModuleKSPCompatibilityTextBox // this.MetadataModuleKSPCompatibilityTextBox.AutoSize = true; @@ -459,17 +416,6 @@ private void InitializeComponent() this.MetadataModuleKSPCompatibilityTextBox.BorderStyle = System.Windows.Forms.BorderStyle.None; resources.ApplyResources(this.MetadataModuleKSPCompatibilityTextBox, "MetadataModuleKSPCompatibilityTextBox"); // - // MetadataModuleGitHubLinkLabel - // - this.MetadataModuleGitHubLinkLabel.AutoSize = true; - this.MetadataModuleGitHubLinkLabel.Location = new System.Drawing.Point(93, 120); - this.MetadataModuleGitHubLinkLabel.Name = "MetadataModuleGitHubLinkLabel"; - this.MetadataModuleGitHubLinkLabel.Size = new System.Drawing.Size(250, 30); - this.MetadataModuleGitHubLinkLabel.TabIndex = 26; - this.MetadataModuleGitHubLinkLabel.TabStop = true; - this.MetadataModuleGitHubLinkLabel.Text = "linkLabel2"; - this.MetadataModuleGitHubLinkLabel.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.LinkLabel_LinkClicked); - // // RelationshipTabPage // this.RelationshipTabPage.BackColor = System.Drawing.SystemColors.Control; @@ -708,8 +654,6 @@ private void InitializeComponent() private TransparentTextBox ReplacementTextBox; private System.Windows.Forms.Label KSPCompatibilityLabel; private System.Windows.Forms.Label ReleaseLabel; - private System.Windows.Forms.Label GitHubLabel; - private System.Windows.Forms.Label HomePageLabel; private System.Windows.Forms.Label AuthorLabel; private System.Windows.Forms.Label LicenseLabel; private TransparentTextBox MetadataModuleVersionTextBox; @@ -717,9 +661,7 @@ private void InitializeComponent() private TransparentTextBox MetadataModuleAuthorTextBox; private System.Windows.Forms.Label VersionLabel; private TransparentTextBox MetadataModuleReleaseStatusTextBox; - private System.Windows.Forms.LinkLabel MetadataModuleHomePageLinkLabel; private TransparentTextBox MetadataModuleKSPCompatibilityTextBox; - private System.Windows.Forms.LinkLabel MetadataModuleGitHubLinkLabel; private System.Windows.Forms.TabPage RelationshipTabPage; private System.Windows.Forms.TreeView DependsGraphTree; private System.Windows.Forms.PictureBox LegendDependsImage; diff --git a/GUI/Controls/ModInfo.cs b/GUI/Controls/ModInfo.cs index 2c72f8663d..27d01e1cb4 100644 --- a/GUI/Controls/ModInfo.cs +++ b/GUI/Controls/ModInfo.cs @@ -21,10 +21,12 @@ public partial class ModInfo : UserControl { private GUIMod selectedModule; private CkanModule currentModContentsModule; + private readonly int staticRowCount; public ModInfo() { InitializeComponent(); + staticRowCount = MetaDataLowerLayoutPanel.RowCount; DependsGraphTree.BeforeExpand += BeforeExpand; } @@ -136,12 +138,75 @@ private void UpdateModInfo(GUIMod gui_module) Util.Invoke(MetadataModuleAuthorTextBox, () => MetadataModuleAuthorTextBox.Text = gui_module.Authors); Util.Invoke(MetadataIdentifierTextBox, () => MetadataIdentifierTextBox.Text = gui_module.Identifier); - // If we have a homepage provided, use that; otherwise use the spacedock page, curse page or the github repo so that users have somewhere to get more info than just the abstract. - Util.Invoke(MetadataModuleHomePageLinkLabel, () => MetadataModuleHomePageLinkLabel.Text = gui_module.Homepage.ToString()); - Util.Invoke(MetadataModuleGitHubLinkLabel, () => MetadataModuleGitHubLinkLabel.Text = module.resources?.repository?.ToString() ?? Properties.Resources.ModInfoNSlashA); Util.Invoke(MetadataModuleReleaseStatusTextBox, () => MetadataModuleReleaseStatusTextBox.Text = module.release_status?.ToString() ?? Properties.Resources.ModInfoNSlashA); Util.Invoke(MetadataModuleKSPCompatibilityTextBox, () => MetadataModuleKSPCompatibilityTextBox.Text = gui_module.KSPCompatibilityLong); Util.Invoke(ReplacementTextBox, () => ReplacementTextBox.Text = gui_module.ToModule()?.replaced_by?.ToString() ?? Properties.Resources.ModInfoNSlashA); + + Util.Invoke(MetaDataLowerLayoutPanel, () => + { + ClearResourceLinks(); + var res = module.resources; + if (res != null) + { + AddResourceLink(Properties.Resources.ModInfoHomepageLabel, res.homepage); + AddResourceLink(Properties.Resources.ModInfoSpaceDockLabel, res.spacedock); + AddResourceLink(Properties.Resources.ModInfoCurseLabel, res.curse); + AddResourceLink(Properties.Resources.ModInfoRepositoryLabel, res.repository); + AddResourceLink(Properties.Resources.ModInfoBugTrackerLabel, res.bugtracker); + AddResourceLink(Properties.Resources.ModInfoContinuousIntegrationLabel, res.ci); + AddResourceLink(Properties.Resources.ModInfoLicenseLabel, res.license); + AddResourceLink(Properties.Resources.ModInfoManualLabel, res.manual); + AddResourceLink(Properties.Resources.ModInfoMetanetkanLabel, res.metanetkan); + AddResourceLink(Properties.Resources.ModInfoStoreLabel, res.store); + AddResourceLink(Properties.Resources.ModInfoSteamStoreLabel, res.steamstore); + } + }); + } + + private void ClearResourceLinks() + { + for (int row = MetaDataLowerLayoutPanel.RowCount - 1; row >= staticRowCount; --row) + { + RemovePanelControl(MetaDataLowerLayoutPanel, 0, row); + RemovePanelControl(MetaDataLowerLayoutPanel, 1, row); + MetaDataLowerLayoutPanel.RowStyles.RemoveAt(row); + } + MetaDataLowerLayoutPanel.RowCount = staticRowCount; + } + + private static void RemovePanelControl(TableLayoutPanel panel, int col, int row) + { + var ctl = panel.GetControlFromPosition(col, row); + if (ctl != null) + { + panel.Controls.Remove(ctl); + } + } + + private void AddResourceLink(string label, Uri link) + { + if (link != null) + { + Label lbl = new Label() + { + AutoSize = true, + Dock = DockStyle.Fill, + ForeColor = SystemColors.GrayText, + Text = label, + }; + LinkLabel llbl = new LinkLabel() + { + AutoSize = true, + TabStop = true, + Text = link.ToString(), + }; + llbl.LinkClicked += new LinkLabelLinkClickedEventHandler(LinkLabel_LinkClicked); + int row = MetaDataLowerLayoutPanel.RowCount; + MetaDataLowerLayoutPanel.Controls.Add(lbl, 0, row); + MetaDataLowerLayoutPanel.Controls.Add(llbl, 1, row); + MetaDataLowerLayoutPanel.RowCount = row + 1; + MetaDataLowerLayoutPanel.RowStyles.Add(new RowStyle(SizeType.Absolute, 30F)); + } } private ModuleLabelList ModuleLabels diff --git a/GUI/Controls/ModInfo.resx b/GUI/Controls/ModInfo.resx index 892b09bcae..223b78c7d6 100644 --- a/GUI/Controls/ModInfo.resx +++ b/GUI/Controls/ModInfo.resx @@ -125,8 +125,6 @@ - Max KSP ver.: Release status: - Source code: - Home page: Author: Licence: 0.0.0 diff --git a/GUI/Localization/de-DE/ModInfo.de-DE.resx b/GUI/Localization/de-DE/ModInfo.de-DE.resx index bf0f293d2a..5fe8b02d44 100644 --- a/GUI/Localization/de-DE/ModInfo.de-DE.resx +++ b/GUI/Localization/de-DE/ModInfo.de-DE.resx @@ -123,8 +123,6 @@ Ersetzt durch: Max. KSP-Version: Releasestatus: - Quellcode: - Homepage: Autor: Lizenz: N/A diff --git a/GUI/Main/Main.cs b/GUI/Main/Main.cs index 6d79dda8d7..fc1bc546ef 100644 --- a/GUI/Main/Main.cs +++ b/GUI/Main/Main.cs @@ -1038,47 +1038,60 @@ private void pluginsToolStripMenuItem_Click(object sender, EventArgs e) private void installFromckanToolStripMenuItem_Click(object sender, EventArgs e) { - OpenFileDialog open_file_dialog = new OpenFileDialog { Filter = Resources.CKANFileFilter }; + OpenFileDialog open_file_dialog = new OpenFileDialog() + { + Filter = Resources.CKANFileFilter, + Multiselect = true, + }; if (open_file_dialog.ShowDialog() == DialogResult.OK) { - var path = open_file_dialog.FileName; - CkanModule module; + // We'll need to make some registry changes to do this. + RegistryManager registry_manager = RegistryManager.Instance(CurrentInstance); - try - { - module = CkanModule.FromFile(path); - } - catch (Kraken kraken) + foreach (string path in open_file_dialog.FileNames) { - currentUser.RaiseError(kraken.InnerException == null - ? kraken.Message - : $"{kraken.Message}: {kraken.InnerException.Message}"); + CkanModule module; - return; - } - catch (Exception ex) - { - currentUser.RaiseError(ex.Message); - return; - } + try + { + module = CkanModule.FromFile(path); + } + catch (Kraken kraken) + { + currentUser.RaiseError(kraken.InnerException == null + ? kraken.Message + : $"{kraken.Message}: {kraken.InnerException.Message}"); - // We'll need to make some registry changes to do this. - RegistryManager registry_manager = RegistryManager.Instance(CurrentInstance); + continue; + } + catch (Exception ex) + { + currentUser.RaiseError(ex.Message); + continue; + } - // Don't add metapacakges to the registry - if (!module.IsMetapackage) - { - // Remove this version of the module in the registry, if it exists. - registry_manager.registry.RemoveAvailable(module); + // Don't add metapacakges to the registry + if (!module.IsMetapackage) + { + // Remove this version of the module in the registry, if it exists. + registry_manager.registry.RemoveAvailable(module); - // Sneakily add our version in... - registry_manager.registry.AddAvailable(module); - } + // Sneakily add our version in... + registry_manager.registry.AddAvailable(module); + } - menuStrip1.Enabled = false; + if (module.IsDLC) + { + currentUser.RaiseError(Properties.Resources.MainCantInstallDLC, module); + continue; + } + + menuStrip1.Enabled = false; - InstallModuleDriver(registry_manager.registry, module); + InstallModuleDriver(registry_manager.registry, module); + } + registry_manager.Save(true); } } diff --git a/GUI/Main/MainInstall.cs b/GUI/Main/MainInstall.cs index 4ce9676ff1..b7224ab1ae 100644 --- a/GUI/Main/MainInstall.cs +++ b/GUI/Main/MainInstall.cs @@ -328,6 +328,13 @@ out Dictionary> supporters } throw; } + catch (ModuleIsDLCKraken kraken) + { + string msg = string.Format(Properties.Resources.MainInstallCantInstallDLC, kraken.module.name); + currentUser.RaiseMessage(msg); + currentUser.RaiseError(msg); + return; + } } } diff --git a/GUI/Main/MainModList.cs b/GUI/Main/MainModList.cs index 911f958154..daca76793f 100644 --- a/GUI/Main/MainModList.cs +++ b/GUI/Main/MainModList.cs @@ -188,16 +188,19 @@ private void _UpdateModsList(IEnumerable mc, Dictionary var gui_mods = new HashSet(); gui_mods.UnionWith( registry.InstalledModules + .Where(instMod => !instMod.Module.IsDLC) .Select(instMod => new GUIMod(instMod, registry, versionCriteria)) ); Wait.AddLogMessage(Properties.Resources.MainModListLoadingAvailable); gui_mods.UnionWith( registry.CompatibleModules(versionCriteria) + .Where(m => !m.IsDLC) .Select(m => new GUIMod(m, registry, versionCriteria)) ); Wait.AddLogMessage(Properties.Resources.MainModListLoadingIncompatible); gui_mods.UnionWith( registry.IncompatibleModules(versionCriteria) + .Where(m => !m.IsDLC) .Select(m => new GUIMod(m, registry, versionCriteria, true)) ); diff --git a/GUI/Model/GUIMod.cs b/GUI/Model/GUIMod.cs index c0668f97c3..ea551177cc 100644 --- a/GUI/Model/GUIMod.cs +++ b/GUI/Model/GUIMod.cs @@ -84,7 +84,6 @@ private void OnPropertyChanged([CallerMemberName] string name = null) public string Abstract { get; private set; } public string Description { get; private set; } - public string Homepage { get; private set; } public string Identifier { get; private set; } public bool IsInstallChecked { get; set; } public bool IsUpgradeChecked { get; private set; } @@ -168,15 +167,6 @@ public GUIMod(CkanModule mod, IRegistryQuerier registry, KspVersionCriteria curr HasReplacement = registry.GetReplacement(mod, current_ksp_version) != null; DownloadSize = mod.download_size == 0 ? Properties.Resources.GUIModNSlashA : CkanModule.FmtSize(mod.download_size); - if (mod.resources != null) - { - Homepage = mod.resources.homepage?.ToString() - ?? mod.resources.spacedock?.ToString() - ?? mod.resources.curse?.ToString() - ?? mod.resources.repository?.ToString() - ?? Properties.Resources.GUIModNSlashA; - } - // Get the Searchables. SearchableName = mod.SearchableName; SearchableAbstract = mod.SearchableAbstract; @@ -254,9 +244,6 @@ public GUIMod(string identifier, IRegistryQuerier registry, KspVersionCriteria c LatestVersion = "-"; } - // If we have a homepage provided, use that; otherwise use the spacedock page, curse page or the github repo so that users have somewhere to get more info than just the abstract. - Homepage = Properties.Resources.GUIModNSlashA; - SearchableIdentifier = CkanModule.nonAlphaNums.Replace(Identifier, ""); } diff --git a/GUI/Model/ModChange.cs b/GUI/Model/ModChange.cs index 928281580a..72126c1829 100644 --- a/GUI/Model/ModChange.cs +++ b/GUI/Model/ModChange.cs @@ -58,7 +58,7 @@ public override string ToString() { return $"{ChangeType} {Mod} ({Reason})"; } - + protected string modNameAndStatus(CkanModule m) { return m.IsMetapackage @@ -68,7 +68,7 @@ protected string modNameAndStatus(CkanModule m) : string.Format(Properties.Resources.MainChangesetHostSize, m.name, m.version, m.download.Host ?? "", CkanModule.FmtSize(m.download_size)); } - + public virtual string NameAndStatus { get @@ -104,7 +104,7 @@ public ModUpgrade(CkanModule mod, GUIModChangeType changeType, SelectionReason r { this.targetMod = targetMod; } - + public override string NameAndStatus { get @@ -112,7 +112,7 @@ public override string NameAndStatus return modNameAndStatus(targetMod); } } - + public override string Description { get @@ -120,10 +120,10 @@ public override string Description return string.Format( Properties.Resources.MainChangesetUpdateSelected, targetMod.version - ); + ); } } - private readonly CkanModule targetMod; + private readonly CkanModule targetMod; } } diff --git a/GUI/Properties/Resources.Designer.cs b/GUI/Properties/Resources.Designer.cs index 09518e50ac..f1305f8d22 100644 --- a/GUI/Properties/Resources.Designer.cs +++ b/GUI/Properties/Resources.Designer.cs @@ -430,6 +430,9 @@ internal static string MainNotFound { internal static string MainReinstallConfirm { get { return (string)(ResourceManager.GetObject("MainReinstallConfirm", resourceCulture)); } } + internal static string MainCantInstallDLC { + get { return (string)(ResourceManager.GetObject("MainCantInstallDLC", resourceCulture)); } + } internal static string AllModVersionsInstallPrompt { get { return (string)(ResourceManager.GetObject("AllModVersionsInstallPrompt", resourceCulture)); } @@ -539,6 +542,9 @@ internal static string MainInstallFailed { internal static string MainInstallProvidedBy { get { return (string)(ResourceManager.GetObject("MainInstallProvidedBy", resourceCulture)); } } + internal static string MainInstallCantInstallDLC { + get { return (string)(ResourceManager.GetObject("MainInstallCantInstallDLC", resourceCulture)); } + } internal static string ModInfoNSlashA { get { return (string)(ResourceManager.GetObject("ModInfoNSlashA", resourceCulture)); } @@ -555,6 +561,39 @@ internal static string ModInfoNotCached { internal static string ModInfoCached { get { return (string)(ResourceManager.GetObject("ModInfoCached", resourceCulture)); } } + internal static string ModInfoHomepageLabel { + get { return (string)(ResourceManager.GetObject("ModInfoHomepageLabel", resourceCulture)); } + } + internal static string ModInfoSpaceDockLabel { + get { return (string)(ResourceManager.GetObject("ModInfoSpaceDockLabel", resourceCulture)); } + } + internal static string ModInfoCurseLabel { + get { return (string)(ResourceManager.GetObject("ModInfoCurseLabel", resourceCulture)); } + } + internal static string ModInfoRepositoryLabel { + get { return (string)(ResourceManager.GetObject("ModInfoRepositoryLabel", resourceCulture)); } + } + internal static string ModInfoBugTrackerLabel { + get { return (string)(ResourceManager.GetObject("ModInfoBugTrackerLabel", resourceCulture)); } + } + internal static string ModInfoContinuousIntegrationLabel { + get { return (string)(ResourceManager.GetObject("ModInfoContinuousIntegrationLabel", resourceCulture)); } + } + internal static string ModInfoLicenseLabel { + get { return (string)(ResourceManager.GetObject("ModInfoLicenseLabel", resourceCulture)); } + } + internal static string ModInfoManualLabel { + get { return (string)(ResourceManager.GetObject("ModInfoManualLabel", resourceCulture)); } + } + internal static string ModInfoMetanetkanLabel { + get { return (string)(ResourceManager.GetObject("ModInfoMetanetkanLabel", resourceCulture)); } + } + internal static string ModInfoStoreLabel { + get { return (string)(ResourceManager.GetObject("ModInfoStoreLabel", resourceCulture)); } + } + internal static string ModInfoSteamStoreLabel { + get { return (string)(ResourceManager.GetObject("ModInfoSteamStoreLabel", resourceCulture)); } + } internal static string MainModListWaitTitle { get { return (string)(ResourceManager.GetObject("MainModListWaitTitle", resourceCulture)); } diff --git a/GUI/Properties/Resources.de-DE.resx b/GUI/Properties/Resources.de-DE.resx index b1cc9bb61e..db09ae68a6 100644 --- a/GUI/Properties/Resources.de-DE.resx +++ b/GUI/Properties/Resources.de-DE.resx @@ -253,6 +253,8 @@ Wenn du ein Fehler mit dem CKAN Client vermutest: https://github.com/KSP-CKAN/CK {0} (nicht indiziert) Diese Mod ist nicht im Cache, klicke auf 'Download' um eine Inhaltsübersicht zu erhalten. Modul ist im Cache, Vorschau verfügbar + Homepage: + Lizenz: Module laden Lade Registry... Lade installierte Module... diff --git a/GUI/Properties/Resources.en-US.resx b/GUI/Properties/Resources.en-US.resx index a5cc6a1661..20792929c3 100644 --- a/GUI/Properties/Resources.en-US.resx +++ b/GUI/Properties/Resources.en-US.resx @@ -120,4 +120,5 @@ Favorites The license for this modpack + License: diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx index dcef0d2825..c3e71b05af 100644 --- a/GUI/Properties/Resources.resx +++ b/GUI/Properties/Resources.resx @@ -202,6 +202,7 @@ Tab-separated values (*.tsv) Not found. Do you want to reinstall {0}? + CKAN can't install expansion {0}! {0} is not supported on your current game version and may not work at all. If you have any problems with it, you should NOT ask its maintainers for help. Do you really want to install it? @@ -269,11 +270,23 @@ If you suspect a metadata problem: https://github.com/KSP-CKAN/NetKAN/issues/new If you suspect a bug in the client: https://github.com/KSP-CKAN/CKAN/issues/new/choose Installation failed! Module {0} is provided by more than one available module, please choose one of the following mods: + CKAN can't install expansion '{0}' for you. N/A {0} (virtual) {0} (not indexed) This mod is not in the cache, click 'Download' to preview contents Module is cached, preview available + Home page: + SpaceDock: + Curse: + Repository: + Bug tracker: + Continuous integration: + Licence: + Manual: + Metanetkan: + Store: + Steam store: Loading modules Loading registry... Loading installed modules... diff --git a/Netkan/CKAN-netkan.csproj b/Netkan/CKAN-netkan.csproj index 54bd68b854..b68c4c8914 100644 --- a/Netkan/CKAN-netkan.csproj +++ b/Netkan/CKAN-netkan.csproj @@ -152,6 +152,7 @@ + diff --git a/Netkan/Validators/CkanValidator.cs b/Netkan/Validators/CkanValidator.cs index bb8db1ce75..c5a6f688c2 100644 --- a/Netkan/Validators/CkanValidator.cs +++ b/Netkan/Validators/CkanValidator.cs @@ -15,7 +15,8 @@ public CkanValidator(IHttpService downloader, IModuleService moduleService) new IsCkanModuleValidator(), new InstallsFilesValidator(downloader, moduleService), new MatchesKnownGameVersionsValidator(), - new ObeysCKANSchemaValidator() + new ObeysCKANSchemaValidator(), + new KindValidator(), }; } diff --git a/Netkan/Validators/InstallsFilesValidator.cs b/Netkan/Validators/InstallsFilesValidator.cs index a03b346eeb..0770e2e8b0 100644 --- a/Netkan/Validators/InstallsFilesValidator.cs +++ b/Netkan/Validators/InstallsFilesValidator.cs @@ -18,25 +18,28 @@ public InstallsFilesValidator(IHttpService http, IModuleService moduleService) public void Validate(Metadata metadata) { var mod = CkanModule.FromJson(metadata.Json().ToString()); - var file = _http.DownloadPackage(metadata.Download, metadata.Identifier, metadata.RemoteTimestamp); - - // Make sure this would actually generate an install. - if (!_moduleService.HasInstallableFiles(mod, file)) - { - throw new Kraken(string.Format( - "Module contains no files matching: {0}", - mod.DescribeInstallStanzas() - )); - } - - // Make sure no paths include GameData other than at the start - var gamedatas = _moduleService.FileDestinations(mod, file) - .Where(p => p.StartsWith("GameData") && p.LastIndexOf("/GameData/") > 0) - .ToList(); - if (gamedatas.Any()) + if (!mod.IsDLC) { - var badPaths = string.Join("\r\n", gamedatas); - throw new Kraken($"GameData directory found within GameData:\r\n{badPaths}"); + var file = _http.DownloadPackage(metadata.Download, metadata.Identifier, metadata.RemoteTimestamp); + + // Make sure this would actually generate an install. + if (!_moduleService.HasInstallableFiles(mod, file)) + { + throw new Kraken(string.Format( + "Module contains no files matching: {0}", + mod.DescribeInstallStanzas() + )); + } + + // Make sure no paths include GameData other than at the start + var gamedatas = _moduleService.FileDestinations(mod, file) + .Where(p => p.StartsWith("GameData") && p.LastIndexOf("/GameData/") > 0) + .ToList(); + if (gamedatas.Any()) + { + var badPaths = string.Join("\r\n", gamedatas); + throw new Kraken($"GameData directory found within GameData:\r\n{badPaths}"); + } } } } diff --git a/Netkan/Validators/KindValidator.cs b/Netkan/Validators/KindValidator.cs new file mode 100644 index 0000000000..440eec3ece --- /dev/null +++ b/Netkan/Validators/KindValidator.cs @@ -0,0 +1,25 @@ +using CKAN.NetKAN.Model; +using CKAN.Versioning; + +namespace CKAN.NetKAN.Validators +{ + internal sealed class KindValidator : IValidator + { + public void Validate(Metadata metadata) + { + var json = metadata.Json(); + var kind = json["kind"]; + if (metadata.SpecVersion < v1p6 && (string)kind == "metapackage") + { + throw new Kraken($"spec_version 1.6+ required for kind 'metpackage'"); + } + if (metadata.SpecVersion < v1p28 && (string)kind == "dlc") + { + throw new Kraken($"spec_version 1.28+ required for kind 'dlc'"); + } + } + + private static readonly ModuleVersion v1p6 = new ModuleVersion("v1.6"); + private static readonly ModuleVersion v1p28 = new ModuleVersion("v1.28"); + } +} diff --git a/Netkan/Validators/KrefDownloadMutexValidator.cs b/Netkan/Validators/KrefDownloadMutexValidator.cs index d941bcff0a..c7032ba5c8 100644 --- a/Netkan/Validators/KrefDownloadMutexValidator.cs +++ b/Netkan/Validators/KrefDownloadMutexValidator.cs @@ -11,7 +11,7 @@ public void Validate(Metadata metadata) { throw new Kraken($"{metadata.Identifier} has a $kref and a download field, this is likely incorrect"); } - if (!json.ContainsKey("download") && !json.ContainsKey("$kref")) + if ((string)json["kind"] != "dlc" && !json.ContainsKey("download") && !json.ContainsKey("$kref")) { throw new Kraken($"{metadata.Identifier} has no $kref field, this is required when no download url is specified"); } diff --git a/Spec.md b/Spec.md index 791a9cc206..454b851634 100644 --- a/Spec.md +++ b/Spec.md @@ -71,6 +71,7 @@ and the ### Example CKAN file +```json { "spec_version" : 1, "name" : "Advanced Jet Engine (AJE)", @@ -108,6 +109,7 @@ and the { "name" : "HotRockets" } ] } +``` ### Metadata description @@ -160,7 +162,8 @@ with any version and the `.dll` suffix removed. ##### download A fully formed URL, indicating where a machine may download the -described version of the mod. Note: This field is not required if the `kind` is `metapackage`. +described version of the mod. Note: This field is not required if the `kind` is `metapackage` +or `dlc`. ##### license @@ -187,9 +190,11 @@ A single license (**v1.0**) , or list of licenses (**v1.8**) may be provided. Th are both valid, the first describing a mod released under the BSD license, the second under the *user's choice* of BSD-2-clause or GPL-2.0 licenses. +```json "license" : "BSD-2-clause" "license" : [ "BSD-2-clause", "GPL-2.0" ] +``` If different assets in the mod have different licenses, the *most restrictive* license should be specified, which may be `restricted`. @@ -320,12 +325,14 @@ and install that with a target of `GameData`. A typical install directive only has `file` and `install_to` sections: +```json "install" : [ { "file" : "GameData/ExampleMod", "install_to" : "GameData" } ] +``` ##### comment @@ -443,11 +450,13 @@ are not installed at the same time. At its most basic, this is an array of objects, each being a name and identifier: +```json "depends" : [ { "name" : "ModuleManager" }, { "name" : "RealFuels" }, { "name" : "RealSolarSystem" } ] +``` Each relationship is an array of entries, each entry *must* have a `name`. @@ -455,11 +464,13 @@ have a `name`. The optional fields `min_version`, `max_version`, and `version`, may more precisely describe which versions are needed: +```json "depends" : [ { "name" : "ModuleManager", "min_version" : "2.1.5" }, { "name" : "RealSolarSystem", "min_version" : "7.3" }, { "name" : "RealFuels" } ] +``` It is an error to mix `version` (which specifies an exact version) with either `min_version` or `max_version` in the same object. @@ -477,6 +488,7 @@ satisfied if **any** of the specified modules are installed. It is intended for situations in which a module supports multiple ways of providing functionality, which are not in themselves mutually compatible enough to use the `"provides"` property. +```json "depends": [ { "any_of": [ @@ -487,6 +499,7 @@ which are not in themselves mutually compatible enough to use the `"provides"` p ] } ] +``` ##### depends @@ -549,24 +562,30 @@ are described. Unless specified otherwise, these are URLs: - `curse` : (**v1.20**) The mod on Curse. - `manual` : The mod's manual, if it exists. - `metanetkan` : (**v1.27**) URL of the module's remote hosted netkan +- `store`: (**v1.28**) URL where you can purchase a DLC +- `steamstore`: (**v1.28**) URL where you can purchase a DLC on Steam Example resources: +```json "resources" : { "homepage" : "https://tinyurl.com/DogeCoinFlag", "bugtracker" : "https://github.com/pjf/DogeCoinFlag/issues", "repository" : "https://github.com/pjf/DogeCoinFlag", - "ci" : "https://ksp.sarbian.com/jenkins/DogecoinFlag" - "spacedock" : "https://spacedock.info/mod/269/Dogecoin%20Flag" + "ci" : "https://ksp.sarbian.com/jenkins/DogecoinFlag", + "spacedock" : "https://spacedock.info/mod/269/Dogecoin%20Flag", "curse" : "https://kerbal.curseforge.com/projects/220221" } +``` While all currently defined resources are URLs, future revisions of the spec may provide for more complex types. It is permissible to have fields prefixed with an `x_`. These are considered custom use fields, and will be ignored. For example: +```json "x_twitter" : "https://twitter.com/pjf" +``` #### Special use fields @@ -575,7 +594,11 @@ Typical mods *should not* include these special use fields. ##### kind -Specifies the type of package the .ckan file delivers. This field defaults to `package`, the other option (and presently the only time the field is explicitly declared) is `metapackage`. Metapackages allow for a distributable .ckan file that has relationships to other mods while having no `download` of its own. **v1.6** +Specifies the type of package the .ckan file represents. Allowed values: + +- `package` or empty - the default, a normal installable module +- `metapackage` - a distributable .ckan file that has relationships to other mods while having no `download` of its own. **v1.6** +- `dlc` - A paid expansion from SQUAD, which CKAN can detect but not install. Also has no `download`. **v1.28** ##### provides @@ -584,7 +607,9 @@ is intended for use in modules which require one of a selection of texture downloads, or one of a selection of mods which provide equivalent functionality. For example: +```json "provides" : [ "RealSolarSystemTextures" ] +``` It is recommended that this field be used *sparingly*, as all mods with the same `provides` string are essentially declaring they can be used @@ -615,10 +640,12 @@ SHA1 and SHA256 calculated hashes of the resulting file downloaded. It is recommended that this field is only generated by automated tools (where it is encouraged), and not filled in by hand. +```json "download_hash": { "sha1": "1F4B3F21A77D4A302E3417A7C7A24A0B63740FC5", "sha256": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855" } +``` ##### download_content_type @@ -627,7 +654,9 @@ downloaded from the `download` URL. It is recommended that this field is only generated by automated tools (where it is encouraged), and not filled in by hand. +```json "download_content_type": "application/zip" +``` #### Extensions @@ -753,6 +782,7 @@ an `object` with the following fields: If any options are not present their default values are used. An example `.netkan` excerpt: + ```json { "$kref": "#/ckan/jenkins/https://jenkins.kspmods.example/job/AwesomeMod/", @@ -811,6 +841,7 @@ The following conditions apply: - Any fields specified in the metanetkan will override any fields in the target netkan file. An example `.netkan` including all required fields for a valid metanetkan: + ```json { "spec_version": 1, @@ -863,6 +894,7 @@ Note that an epoch can be added without this property or incremented automatical release. An example `.netkan` excerpt: + ```json { "x_netkan_epoch": 1 @@ -879,6 +911,7 @@ Typical mods should not use this property, to allow their version epochs to be a of order version. An example `.netkan` excerpt: + ```json { "x_netkan_allow_out_of_order": true @@ -894,6 +927,7 @@ A combination of `x_netkan_epoch` and `x_netkan_version_edit` should be used ins field *only* contains the actual version string. An example `.netkan` excerpt: + ```json { "x_netkan_force_v": true @@ -917,6 +951,7 @@ an `object` with the following fields: the default values for the `replace` and `strict` fields are used. An example `.netkan` excerpt: + ```json { "$kref": "#/ckan/jenkins/https://jenkins.kspmods.example/job/AwesomeMod/", diff --git a/Tests/Core/Relationships/SanityChecker.cs b/Tests/Core/Relationships/SanityChecker.cs index c74293fbb5..baf89e8474 100644 --- a/Tests/Core/Relationships/SanityChecker.cs +++ b/Tests/Core/Relationships/SanityChecker.cs @@ -98,7 +98,7 @@ public void FindUnsatisfiedDepends() { var mods = new List(); var dlls = CKAN.Extensions.EnumerableExtensions.ToHashSet(Enumerable.Empty()); - var dlc = new Dictionary(); + var dlc = new Dictionary(); Assert.IsEmpty(CKAN.SanityChecker.FindUnsatisfiedDepends(mods, dlls, dlc), "Empty list"); @@ -330,7 +330,7 @@ private static void TestDepends( List to_remove, List mods, List dlls, - Dictionary dlc, + Dictionary dlc, List expected, string message) { diff --git a/Tests/NetKAN/Validators/KindValidatorTests.cs b/Tests/NetKAN/Validators/KindValidatorTests.cs new file mode 100644 index 0000000000..bdfae1920f --- /dev/null +++ b/Tests/NetKAN/Validators/KindValidatorTests.cs @@ -0,0 +1,47 @@ +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using CKAN.NetKAN.Model; +using CKAN.NetKAN.Validators; + +namespace Tests.NetKAN.Validators +{ + [TestFixture] + public sealed class KindValidatorTests + { + [Test, + TestCase("1", @"""package"""), + TestCase("v1.2", @"""package"""), + TestCase("v1.6", @"""metapackage"""), + TestCase("v1.28", @"""dlc"""), + ] + public void Validate_GoodSpecVersionKind_DoesNotThrow(string spec_version, string kind) + { + Assert.DoesNotThrow(() => TryKind(spec_version, kind)); + } + + [Test, + TestCase("1", @"""metapackage"""), + TestCase("v1.5", @"""metapackage"""), + TestCase("1", @"""dlc"""), + TestCase("v1.4", @"""dlc"""), + TestCase("v1.17", @"""dlc"""), + ] + public void Validate_BadSpecVersionKind_Throws(string spec_version, string kind) + { + Assert.Throws(() => TryKind(spec_version, kind)); + } + + private void TryKind(string spec_version, string kind) + { + // Arrange + var json = new JObject(); + json["spec_version"] = spec_version; + json["identifier"] = "AwesomeMod"; + json["kind"] = JToken.Parse(kind); + + // Act + var val = new KindValidator(); + val.Validate(new Metadata(json)); + } + } +}