From c65336e25cabe25a20e4e6c6c7ec94a6afb725b4 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Tue, 25 Jan 2022 19:54:11 +0000 Subject: [PATCH] Detect manually installed mods via cfg file :FOR[identifier] clauses --- Core/GameInstance.cs | 48 ++------ Core/Games/IGame.cs | 4 + Core/Games/KerbalSpaceProgram.cs | 92 ++++++++++++++ .../ConfigParser/Parser.ConfigNode.cs | 114 +++++++++++------- .../ConfigParser/Parser.Has.cs | 9 +- .../ConfigParser/Parser.Needs.cs | 2 +- .../ConfigParser/Parser.Primitives.cs | 15 ++- Core/Registry/Registry.cs | 48 ++------ Netkan/Validators/PluginsValidator.cs | 18 ++- Tests/Core/GameInstance.cs | 2 +- .../KSPConfigParser.cs => ConfigParser.cs} | 79 +++++++++--- Tests/Core/Registry/Registry.cs | 5 +- .../Relationships/RelationshipResolver.cs | 8 +- 13 files changed, 288 insertions(+), 156 deletions(-) rename Tests/Core/Games/KerbalSpaceProgram/{ConfigParser/KSPConfigParser.cs => ConfigParser.cs} (89%) diff --git a/Core/GameInstance.cs b/Core/GameInstance.cs index 360662a739..2344c599e7 100644 --- a/Core/GameInstance.cs +++ b/Core/GameInstance.cs @@ -371,16 +371,19 @@ public bool Scan() // GameData *twice*. // // The least evil is to walk it once, and filter it ourselves. - IEnumerable files = Directory + var files = Directory .EnumerateFiles(game.PrimaryModDirectory(this), "*", SearchOption.AllDirectories) - .Where(file => file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase)) .Select(CKANPathUtils.NormalizePath) - .Where(absPath => !game.StockFolders.Any(f => - ToRelativeGameDir(absPath).StartsWith($"{f}/"))); + .Select(ToRelativeGameDir) + .Where(relPath => !game.StockFolders.Any(f => relPath.StartsWith($"{f}/")) + && manager.registry.FileOwner(relPath) == null); - foreach (string dll in files) + foreach (string relativePath in files) { - manager.registry.RegisterDll(this, dll); + foreach (var identifier in game.IdentifiersFromFileName(this, relativePath)) + { + manager.registry.RegisterFile(relativePath, identifier); + } } var newDlls = manager.registry.InstalledDlls.ToHashSet(); bool dllChanged = !oldDlls.SetEquals(newDlls); @@ -419,39 +422,6 @@ public string ToAbsoluteGameDir(string path) return CKANPathUtils.ToAbsolute(path, GameDir()); } - /// - /// https://xkcd.com/208/ - /// This regex matches things like GameData/Foo/Foo.1.2.dll - /// - private static readonly Regex dllPattern = new Regex( - @" - ^(?:.*/)? # Directories (ending with /) - (?[^.]+) # Our DLL name, up until the first dot. - .*\.dll$ # Everything else, ending in dll - ", - RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled - ); - - /// - /// Find the identifier associated with a manually installed DLL - /// - /// Path of the DLL relative to game root - /// - /// Identifier if found otherwise null - /// - public string DllPathToIdentifier(string relative_path) - { - if (!relative_path.StartsWith($"{game.PrimaryModDirectoryRelative}/", StringComparison.CurrentCultureIgnoreCase)) - { - // DLLs only live in the primary mod directory - return null; - } - Match match = dllPattern.Match(relative_path); - return match.Success - ? Identifier.Sanitize(match.Groups["identifier"].Value) - : null; - } - public override string ToString() { return $"{game.ShortName} Install: {gameDir}"; diff --git a/Core/Games/IGame.cs b/Core/Games/IGame.cs index 5cfb4e9027..0c307fdcde 100644 --- a/Core/Games/IGame.cs +++ b/Core/Games/IGame.cs @@ -34,6 +34,10 @@ public interface IGame string CompatibleVersionsFile { get; } string[] BuildIDFiles { get; } + // Manually installed file handling + string[] IdentifiersFromFileName(GameInstance inst, string absolutePath); + string[] IdentifiersFromFileContents(GameInstance inst, string relativePath, string contents); + // How to get metadata Uri DefaultRepositoryURL { get; } Uri RepositoryListURL { get; } diff --git a/Core/Games/KerbalSpaceProgram.cs b/Core/Games/KerbalSpaceProgram.cs index 4baf4bcb42..7b7e36527a 100644 --- a/Core/Games/KerbalSpaceProgram.cs +++ b/Core/Games/KerbalSpaceProgram.cs @@ -2,8 +2,12 @@ using System.Linq; using System.IO; using System.Collections.Generic; +using System.Text.RegularExpressions; + using Autofac; using log4net; +using ParsecSharp; + using CKAN.GameVersionProviders; using CKAN.Versioning; @@ -328,6 +332,94 @@ private string[] filterCmdLineArgs(string[] args, GameVersion installedVersion, return args; } + /// + /// https://xkcd.com/208/ + /// This regex matches things like GameData/Foo/Foo.1.2.dll + /// + private static readonly Regex dllPattern = new Regex( + @" + ^(?:.*/)? # Directories (ending with /) + (?[^.]+) # Our DLL name, up until the first dot. + .*\.dll$ # Everything else, ending in dll + ", + RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled + ); + + /// + /// Find the identifier associated with a manually installed DLL + /// + /// Path of the DLL relative to game root + /// + /// Identifier if found otherwise null + /// + private string[] DllPathToIdentifiers(string relativePath) + { + if (!relativePath.StartsWith($"{PrimaryModDirectoryRelative}/", + Platform.IsWindows + ? StringComparison.CurrentCultureIgnoreCase + : StringComparison.CurrentCulture)) + { + // DLLs only live in the primary mod directory + return new string[] { }; + } + Match match = dllPattern.Match(relativePath); + return match.Success + ? new string[] { Identifier.Sanitize(match.Groups["identifier"].Value) } + : new string[] { }; + } + + private IEnumerable IdentifiersFromConfigNodes(IEnumerable nodes) + => nodes + .Select(node => node.For) + .Where(ident => !string.IsNullOrEmpty(ident)) + .Concat(nodes.SelectMany(node => IdentifiersFromConfigNodes(node.Children))) + .Distinct(); + + /// + /// Find the identifiers associated with a .cfg file string + /// using ModuleManager's :FOR[identifier] pattern + /// + /// Path of the .cfg file relative to game root + /// + /// Array of identifiers, if any found + /// + private string[] CfgContentsToIdentifiers(GameInstance inst, string absolutePath, string cfgContents) + => KSPConfigParser.ConfigFile.Parse(cfgContents) + .CaseOf(failure => + { + log.InfoFormat("{0}:{1}:{2}: {3}", + inst.ToRelativeGameDir(absolutePath), + failure.State.Position.Line, + failure.State.Position.Column, + failure.Message); + return Enumerable.Empty(); + }, + success => IdentifiersFromConfigNodes(success.Value)) + .ToArray(); + + /// + /// Find the identifiers associated with a manually installed .cfg file + /// using ModuleManager's :FOR[identifier] pattern + /// + /// Path of the .cfg file relative to game root + /// + /// Array of identifiers, if any found + /// + private string[] CfgPathToIdentifiers(GameInstance inst, string absolutePath) + => CfgContentsToIdentifiers(inst, absolutePath, File.ReadAllText(absolutePath)); + + public string[] IdentifiersFromFileName(GameInstance inst, string relativePath) + => relativePath.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase) + ? DllPathToIdentifiers(relativePath) + : relativePath.EndsWith(".cfg", StringComparison.CurrentCultureIgnoreCase) + ? CfgPathToIdentifiers(inst, inst.ToAbsoluteGameDir(relativePath)) + : new string[] { }; + + public string[] IdentifiersFromFileContents(GameInstance inst, string relativePath, string contents) + => relativePath.EndsWith(".cfg", StringComparison.CurrentCultureIgnoreCase) + ? CfgContentsToIdentifiers(inst, relativePath, contents) + : new string[] { }; + private static readonly ILog log = LogManager.GetLogger(typeof(KerbalSpaceProgram)); } } diff --git a/Core/Games/KerbalSpaceProgram/ConfigParser/Parser.ConfigNode.cs b/Core/Games/KerbalSpaceProgram/ConfigParser/Parser.ConfigNode.cs index c4e9c588fc..79d3e4382e 100644 --- a/Core/Games/KerbalSpaceProgram/ConfigParser/Parser.ConfigNode.cs +++ b/Core/Games/KerbalSpaceProgram/ConfigParser/Parser.ConfigNode.cs @@ -18,18 +18,13 @@ #nullable enable -/* TODO: - Combining suffix clauses in different orders - IL-Repack problems with dependency -*/ - namespace CKAN { public class KSPConfigNode { public readonly MMOperator Operator; public readonly string Name; - public readonly string Filter; + public readonly string[]? Filters; public readonly MMNeedsAnd? Needs; public readonly MMHas? Has; public readonly MMIndex? Index; @@ -37,22 +32,22 @@ public class KSPConfigNode public readonly KSPConfigNode[] Children; public readonly bool First; - public readonly string Before; - public readonly string For; - public readonly string After; - public readonly string Last; + public readonly string? Before; + public readonly string? For; + public readonly string? After; + public readonly string? Last; public readonly bool Final; public KSPConfigNode(MMOperator op, string name, - string filter, + IEnumerable? filters, MMNeedsAnd? needs, MMHas? has, bool first, - string before, - string forMod, - string after, - string last, + string? before, + string? forMod, + string? after, + string? last, bool finalPass, MMIndex? index, IEnumerable props, @@ -60,7 +55,7 @@ public KSPConfigNode(MMOperator op, { Operator = op; Name = name; - Filter = filter; + Filters = filters?.ToArray(); Needs = needs; Has = has; First = first; @@ -75,17 +70,39 @@ public KSPConfigNode(MMOperator op, } } + public class MMNodeSuffix + { + public readonly string Label; + public readonly string Value; + + public MMNodeSuffix(string label, string value = "") + { + Label = label; + Value = value; + } + } + public static partial class KSPConfigParser { - public static readonly Parser OpenBrace = Char('{').Between(Spaces()); - public static readonly Parser CloseBrace = Spaces().Right(Char('}')); + public static readonly Parser OpenBrace = Char('{').Between(JunkBlock); + public static readonly Parser CloseBrace = JunkBlock.Right(Char('}')); // :BEFORE, :FOR, :AFTER, etc. - public static Parser SimpleClause(string label) - => Optional(Many1(LetterOrDigit()).Between(StringIgnoreCase($":{label}["), - Char(']')) - .AsString(), - null); + public static Parser SimpleClause(string label) + => KSPConfigParserPrimitives.Identifier + .Between(StringIgnoreCase($":{label}["), + Char(']')) + .Map(v => new MMNodeSuffix(label, v)); + + private static IEnumerable FindByType(IEnumerable suffixes) where T : class + => suffixes.Where(s => s.GetType() == typeof(T)) + .Select(s => (T)s); + + private static string? FindSimpleSuffix(IEnumerable suffixes, string label) + => FindByType(suffixes).FirstOrDefault(s => s.Label == label)?.Value; + + private static T? FindSuffix(IEnumerable suffixes) where T : class + => FindByType(suffixes).FirstOrDefault(); // NODENAME { property = value ... } // ([])?(:HAS[])?(,)? @@ -93,32 +110,41 @@ public static Parser SimpleClause(string label) Fix(configNode => from op in Operator from name in KSPConfigParserPrimitives.Identifier - from filter in KSPConfigParserPrimitives.Identifier - .Between(Char('['), Char(']')) + from filters in KSPConfigParserPrimitives.Identifier + .SeparatedBy(Char('|')) + .Between(Char('['), + Char(']')) .AsNullable() - from needs in NeedsClause.AsNullable() - from has in HasClause.AsNullable() - from first in Optional(StringIgnoreCase(":FIRST")) - from before in SimpleClause("BEFORE") - from forMod in SimpleClause("FOR") - from after in SimpleClause("AFTER") - from last in SimpleClause("LAST") - from finalPass in Optional(StringIgnoreCase(":FINAL")) - from index in Index.AsNullable() - from contents in (Property.AsDynamic() - | configNode.AsDynamic() - | Comment.AsDynamic()) + from suffixes in Many(HasClause!.AsDynamic() + | NeedsClause!.AsDynamic() + | Index!.AsDynamic() + | SimpleClause("FOR").AsDynamic() + | SimpleClause("BEFORE").AsDynamic() + | SimpleClause("AFTER").AsDynamic() + | SimpleClause("LAST").AsDynamic() + | StringIgnoreCase(":FIRST").Map(_ => new MMNodeSuffix("FIRST")).AsDynamic() + | StringIgnoreCase(":FINAL").Map(_ => new MMNodeSuffix("FINAL")).AsDynamic()) + from contents in (Property!.AsDynamic() + | configNode!.AsDynamic() + | Comment!.AsDynamic()) .SeparatedBy(AtLeastOneEOL) .Between(OpenBrace, CloseBrace) select new KSPConfigNode( - op, name, filter, needs, has, - first, before, forMod, after, last, finalPass, - index, - contents.Where(c => c.GetType() == typeof(KSPConfigProperty)) - .Select(c => c as KSPConfigProperty), - contents.Where(c => c.GetType() == typeof(KSPConfigNode)) - .Select(c => c as KSPConfigNode))); + op, name, filters, + + FindSuffix(suffixes), + FindSuffix(suffixes), + FindSimpleSuffix(suffixes, "FIRST") != null, + FindSimpleSuffix(suffixes, "BEFORE"), + FindSimpleSuffix(suffixes, "FOR"), + FindSimpleSuffix(suffixes, "AFTER"), + FindSimpleSuffix(suffixes, "LAST"), + FindSimpleSuffix(suffixes, "FINAL") != null, + FindSuffix(suffixes), + + FindByType(contents), + FindByType(contents))); // Whole file (multiple config nodes) public static readonly Parser> ConfigFile = diff --git a/Core/Games/KerbalSpaceProgram/ConfigParser/Parser.Has.cs b/Core/Games/KerbalSpaceProgram/ConfigParser/Parser.Has.cs index a607d862de..fe005a5d38 100644 --- a/Core/Games/KerbalSpaceProgram/ConfigParser/Parser.Has.cs +++ b/Core/Games/KerbalSpaceProgram/ConfigParser/Parser.Has.cs @@ -56,10 +56,11 @@ from hasType in Char('@').Map(_ => MMHasType.Node) | Char('!').Map(_ => MMHasType.NoNode) | Char('#').Map(_ => MMHasType.Property) | Char('~').Map(_ => MMHasType.NoProperty) - from hasKey in Many1(LetterOrDigit()).AsString() - from hasValue in Many1(LetterOrDigit()).Between(Char('['), Char(']')) - .AsString() - .AsNullable() + from hasKey in KSPConfigParserPrimitives.Identifier + from hasValue in Many1(NoneOf("]")).Between(Char('['), + Char(']')) + .AsString() + .AsNullable() from subHas in hasClause.AsNullable() select new MMHasPiece(hasType, hasKey, hasValue, subHas); diff --git a/Core/Games/KerbalSpaceProgram/ConfigParser/Parser.Needs.cs b/Core/Games/KerbalSpaceProgram/ConfigParser/Parser.Needs.cs index eb482e5984..4cca69925f 100644 --- a/Core/Games/KerbalSpaceProgram/ConfigParser/Parser.Needs.cs +++ b/Core/Games/KerbalSpaceProgram/ConfigParser/Parser.Needs.cs @@ -57,7 +57,7 @@ public static partial class KSPConfigParser { public static readonly Parser NeedsMod = from negated in Optional(Char('!')) - from name in Many1(LetterOrDigit()).AsString() + from name in Many1(LetterOrDigit() | OneOf("/_")).AsString() select new MMNeedsMod(name, negated); public static readonly Parser NeedsOr = diff --git a/Core/Games/KerbalSpaceProgram/ConfigParser/Parser.Primitives.cs b/Core/Games/KerbalSpaceProgram/ConfigParser/Parser.Primitives.cs index b566228e69..2b11c5a071 100644 --- a/Core/Games/KerbalSpaceProgram/ConfigParser/Parser.Primitives.cs +++ b/Core/Games/KerbalSpaceProgram/ConfigParser/Parser.Primitives.cs @@ -27,7 +27,11 @@ public enum MMOperator /// public static partial class KSPConfigParserPrimitives { - public static readonly Parser SpacesWithinLine = SkipMany(OneOf(" \t")); + // Some mods have Zero Width No-Break Space randomly pasted in their cfgs, + // and Char.IsWhiteSpace doesn't consider them whitespace. + public static readonly Parser BOM = Char('\ufeff'); + public static readonly Parser SpacesWithinLine = SkipMany(OneOf(" \t") | BOM); + public static readonly Parser SpacesOrBOM = SkipMany(WhiteSpace() | BOM); // // comment public static readonly Parser Comment = @@ -36,15 +40,16 @@ public static partial class KSPConfigParserPrimitives .AsString(); // Whitespace containing comments - public static readonly Parser JunkBlock = SkipMany(Comment | WhiteSpace().AsString()); + public static readonly Parser JunkBlock = SkipMany(Comment.Ignore() + | WhiteSpace().Ignore() + | BOM.Ignore()); public static readonly Parser AtLeastOneEOL = SpacesWithinLine.Left(EndOfLine()) .Left(JunkBlock); // [A-Za-z#][A-Za-z0-9#_-]* - public static readonly Parser Identifier = - (Letter() | OneOf("#?*")).Append(Many(OneOf("#-_?*") | LetterOrDigit())) - .AsString(); + public static readonly Parser Identifier = Many1(OneOf("#-_?*.") + | LetterOrDigit()).AsString(); // @+$-!% => enum public static readonly Parser Operator = diff --git a/Core/Registry/Registry.cs b/Core/Registry/Registry.cs index 2aa9d53fc0..8de0232968 100644 --- a/Core/Registry/Registry.cs +++ b/Core/Registry/Registry.cs @@ -105,7 +105,10 @@ [JsonIgnore] public IEnumerable InstalledDlls /// public string DllPath(string identifier) { - return installed_dlls.TryGetValue(identifier, out string path) ? path : null; + return (installed_dlls.TryGetValue(identifier, out string path) + && path.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase)) + ? path + : null; } /// @@ -620,7 +623,7 @@ public GameVersion LatestCompatibleKSP(string identifier) /// Return parameter for the highest game version public static void GetMinMaxVersions(IEnumerable modVersions, out ModuleVersion minMod, out ModuleVersion maxMod, - out GameVersion minKsp, out GameVersion maxKsp) + out GameVersion minKsp, out GameVersion maxKsp) { minMod = maxMod = null; minKsp = maxKsp = null; @@ -839,41 +842,14 @@ public void DeregisterModule(GameInstance ksp, string module) installed_modules.Remove(module); } - /// - /// Registers the given DLL as having been installed. This provides some support - /// for pre-CKAN modules. - /// - /// Does nothing if the DLL is already part of an installed module. - /// - public void RegisterDll(GameInstance ksp, string absolute_path) + public void RegisterFile(string relativePath, string identifier) { - log.DebugFormat("Registering DLL {0}", absolute_path); - string relative_path = ksp.ToRelativeGameDir(absolute_path); - - string dllIdentifier = ksp.DllPathToIdentifier(relative_path); - if (dllIdentifier == null) - { - log.WarnFormat("Attempted to index {0} which is not a DLL", relative_path); - return; - } - - string owner; - if (installed_files.TryGetValue(relative_path, out owner)) + if (!installed_dlls.ContainsKey(identifier)) { - log.InfoFormat( - "Not registering {0}, it belongs to {1}", - relative_path, - owner - ); - return; + EnlistWithTransaction(); + log.InfoFormat("Registering {0} from {1}", identifier, relativePath); + installed_dlls[identifier] = relativePath; } - - EnlistWithTransaction(); - - log.InfoFormat("Registering {0} from {1}", dllIdentifier, relative_path); - - // We're fine if we overwrite an existing key. - installed_dlls[dllIdentifier] = relative_path; } /// @@ -1048,9 +1024,9 @@ public CkanModule GetInstalledVersion(string mod_identifier) /// Returns the module which owns this file, or null if not known. /// Throws a PathErrorKraken if an absolute path is provided. /// - public string FileOwner(string file) + public string FileOwner(string relativePath) { - file = CKANPathUtils.NormalizePath(file); + var file = CKANPathUtils.NormalizePath(relativePath); if (Path.IsPathRooted(file)) { diff --git a/Netkan/Validators/PluginsValidator.cs b/Netkan/Validators/PluginsValidator.cs index c4e25f3dc1..64807af594 100644 --- a/Netkan/Validators/PluginsValidator.cs +++ b/Netkan/Validators/PluginsValidator.cs @@ -42,15 +42,25 @@ public void Validate(Metadata metadata) .OrderBy(f => f) .ToList(); var dllIdentifiers = dllPaths - .Select(p => inst.DllPathToIdentifier(p)) + .SelectMany(p => inst.game.IdentifiersFromFileName(inst, p)) .Where(ident => !string.IsNullOrEmpty(ident) && !identifiersToIgnore.Contains(ident)) .ToHashSet(); if (dllIdentifiers.Any() && !dllIdentifiers.Contains(metadata.Identifier)) { - Log.WarnFormat( - "No plugin matching the identifier, manual installations won't be detected: {0}", - string.Join(", ", dllPaths)); + // Check for :FOR[identifier] in .cfg files + var cfgIdentifiers = _moduleService + .GetConfigFiles(mod, zip, inst) + .SelectMany(cfg => inst.game.IdentifiersFromFileContents( + inst, cfg.destination, + new StreamReader(zip.GetInputStream(cfg.source)).ReadToEnd())) + .ToHashSet(); + if (!cfgIdentifiers.Contains(metadata.Identifier)) + { + Log.WarnFormat( + "No plugin or :FOR[] clause matching the identifier, manual installations won't be detected: {0}", + string.Join(", ", dllPaths)); + } } bool boundedCompatibility = json.ContainsKey("ksp_version") || json.ContainsKey("ksp_version_max"); diff --git a/Tests/Core/GameInstance.cs b/Tests/Core/GameInstance.cs index fb57d1c99e..dc57630026 100644 --- a/Tests/Core/GameInstance.cs +++ b/Tests/Core/GameInstance.cs @@ -99,7 +99,7 @@ public void ScanDlls() ksp.Scan(); - Assert.IsTrue(registry.IsInstalled("NewMod")); + Assert.IsTrue(registry.IsInstalled("NewMod"), "NewMod installed"); } [Test] diff --git a/Tests/Core/Games/KerbalSpaceProgram/ConfigParser/KSPConfigParser.cs b/Tests/Core/Games/KerbalSpaceProgram/ConfigParser.cs similarity index 89% rename from Tests/Core/Games/KerbalSpaceProgram/ConfigParser/KSPConfigParser.cs rename to Tests/Core/Games/KerbalSpaceProgram/ConfigParser.cs index a147186109..8832568065 100644 --- a/Tests/Core/Games/KerbalSpaceProgram/ConfigParser/KSPConfigParser.cs +++ b/Tests/Core/Games/KerbalSpaceProgram/ConfigParser.cs @@ -54,6 +54,12 @@ public void PropertyParse_Unpadded_Works() Assert.AreEqual("a", v.Name); Assert.AreEqual("b", v.Value); }); + Property.Parse("1=2.0") + .WillSucceed(v => + { + Assert.AreEqual("1", v.Name); + Assert.AreEqual("2.0", v.Value); + }); } [Test] @@ -149,14 +155,15 @@ public void MultiNodeMemberParse_BothTogether_Works() [Test] public void HasClauseParse_Operators_Works() { - HasClause.Parse(":HAS[@NODE,!NONODE,#PROP,~NOPROP]") + HasClause.Parse(":HAS[@NODE,!NONODE,#PROP,~NOPROP,#model[a/b/c/d]]") .WillSucceed(v => { - Assert.AreEqual(4, v.Pieces.Length); + Assert.AreEqual(5, v.Pieces.Length); Assert.AreEqual(MMHasType.Node, v.Pieces[0].HasType); Assert.AreEqual(MMHasType.NoNode, v.Pieces[1].HasType); Assert.AreEqual(MMHasType.Property, v.Pieces[2].HasType); Assert.AreEqual(MMHasType.NoProperty, v.Pieces[3].HasType); + Assert.AreEqual(MMHasType.Property, v.Pieces[4].HasType); }); } @@ -194,7 +201,7 @@ public void NeedsClauseParse_Complex_Satisfied() Assert.IsFalse(v.Satisfies("RealFuels", "ModularFuelSystem")); Assert.IsFalse(v.Satisfies("ModularFuelSystem")); }); - NeedsClause.Parse(":NEEDS[Mod1|Mod2,!Mod3|Mod4]") + NeedsClause.Parse(":NEEDS[Mod1|Mod2,!Mod3|Mod4|Mod_5]") .WillSucceed(v => { Assert.IsTrue(v.Satisfies("Mod1", "Mod3", "Mod4")); @@ -203,6 +210,16 @@ public void NeedsClauseParse_Complex_Satisfied() }); } + [Test] + public void SimpleClauseParse_Simple_Works() + { + SimpleClause("FOR").Parse(":FOR[Astrogator]") + .WillSucceed(v => + { + // + }); + } + [Test] public void ConfigNodeParse_NeedsClause_Works() { @@ -253,17 +270,19 @@ public void ConfigNodeParse_PatchOrdering_Works() NODE1:FIRST { } NODE2:BEFORE[AnotherMod] { } NODE3:FOR[ThisMod] { } - NODE4:AFTER[AnotherMod] { } - NODE5:LAST[AnotherMod] { } - NODE6:FINAL { } + NODE4:FOR[000_ThisMod] { } + NODE5:AFTER[AnotherMod] { } + NODE6:LAST[AnotherMod] { } + NODE7:FINAL { } ").WillSucceed(v => { Assert.IsTrue(v[0].First, "First node is FIRST"); - Assert.AreEqual("AnotherMod", v[1].Before); - Assert.AreEqual("ThisMod", v[2].For); - Assert.AreEqual("AnotherMod", v[3].After); - Assert.AreEqual("AnotherMod", v[4].Last); - Assert.IsTrue(v[5].Final, "Final node is FINAL"); + Assert.AreEqual("AnotherMod", v[1].Before); + Assert.AreEqual("ThisMod", v[2].For); + Assert.AreEqual("000_ThisMod", v[3].For); + Assert.AreEqual("AnotherMod", v[4].After); + Assert.AreEqual("AnotherMod", v[5].Last); + Assert.IsTrue(v[6].Final, "Final node is FINAL"); }); } @@ -341,6 +360,30 @@ public void ConfigNodeParse_ArrayIndex_Works() }); } + [Test] + public void ConfigNodeParse_MultipleNames_Works() + { + ConfigNode.Parse( +@"@PART[KA_Engine_125_02|KA_Engine_250_02|KA_Engine_625_02]:NEEDS[UmbraSpaceIndustries/KarbonitePlus] +{ + @MODULE[ModuleEngines*] + { + @atmosphereCurve + { + !key,* = nope + key = 0 10000 -17578.79 -17578.79 + key = 1 1500 -1210.658 -1210.658 + key = 4 0.001 0 0 + } + } +}" + ).WillSucceed(v => + { + Assert.AreEqual(1, v.Children.Length); + Assert.AreEqual(3, v.Filters.Length); + }); + } + [Test] public void ConfigFileParse_Empty_Works() { @@ -418,9 +461,12 @@ public void ConfigFileParse_MultipleNodes_Works() // Comment between nodes - NODENAME2 + Gradient { - propname2 = value2 + 0.0 = 0.38,0.40,0.44,1 + 0.2 = 0.08,0.08,0.08,1 + 0.4 = 0.01,0.01,0.01,1 + 1.0 = 0,0,0,1 }").WillSucceed(v => { Assert.AreEqual(2, v.Length, "Top level node count"); @@ -456,13 +502,13 @@ public void ConfigFileParse_NestedNode_Works() propname3 = value3 + // Comment at the end of a node } // Another top level comment ").WillSucceed(v => { - Assert.AreEqual(1, v.Length, "Top level node count"); Assert.AreEqual("NODENAME1", v[0].Name, "First node name"); Assert.AreEqual("propname1", v[0].Properties[0].Name, "First node property name"); Assert.AreEqual("value1", v[0].Properties[0].Value, "First node property value"); @@ -479,8 +525,7 @@ public static class ParsecSharpTestExtensions /// I don't know why their copy isn't public. /// public static void WillSucceed(this Result result, Action assert) - => result.CaseOf( - failure => Assert.Fail(failure.ToString()), - success => assert(success.Value)); + => result.CaseOf(failure => Assert.Fail(failure.ToString()), + success => assert(success.Value)); } } diff --git a/Tests/Core/Registry/Registry.cs b/Tests/Core/Registry/Registry.cs index cf7730a29f..e81863361b 100644 --- a/Tests/Core/Registry/Registry.cs +++ b/Tests/Core/Registry/Registry.cs @@ -236,8 +236,9 @@ public void HasUpdate_WithUpgradeableManuallyInstalledMod_ReturnsTrue() }"); registry.AddAvailable(mod); GameInstance gameInst = gameInstWrapper.KSP; - registry.RegisterDll(gameInst, Path.Combine( - gameInst.GameDir(), "GameData", $"{mod.identifier}.dll")); + registry.RegisterFile( + Path.Combine("GameData", $"{mod.identifier}.dll"), + mod.identifier); GameVersionCriteria crit = new GameVersionCriteria(mod.ksp_version); // Act diff --git a/Tests/Core/Relationships/RelationshipResolver.cs b/Tests/Core/Relationships/RelationshipResolver.cs index 10f73c5dc0..50f6ee7e2c 100644 --- a/Tests/Core/Relationships/RelationshipResolver.cs +++ b/Tests/Core/Relationships/RelationshipResolver.cs @@ -892,9 +892,11 @@ public void ReasonFor_WithTreeOfMods_GivesCorrectParents() [Test] public void AutodetectedCanSatisfyRelationships() { - using (var ksp = new DisposableKSP ()) + using (var ksp = new DisposableKSP()) { - registry.RegisterDll(ksp.KSP, Path.Combine(ksp.KSP.game.PrimaryModDirectory(ksp.KSP), "ModuleManager.dll")); + registry.RegisterFile( + Path.Combine(ksp.KSP.game.PrimaryModDirectoryRelative, "ModuleManager.dll"), + "ModuleManager"); var depends = new List(); depends.Add(new CKAN.ModuleRelationshipDescriptor { name = "ModuleManager" }); @@ -906,7 +908,7 @@ public void AutodetectedCanSatisfyRelationships() null, RelationshipResolver.DefaultOpts(), registry, - new GameVersionCriteria (GameVersion.Parse("1.0.0")) + new GameVersionCriteria(GameVersion.Parse("1.0.0")) ); } }