Skip to content

Commit

Permalink
Detect manually installed mods via cfg file :FOR[identifier] clauses
Browse files Browse the repository at this point in the history
  • Loading branch information
HebaruSan committed Mar 25, 2022
1 parent 46d260f commit c65336e
Show file tree
Hide file tree
Showing 13 changed files with 288 additions and 156 deletions.
48 changes: 9 additions & 39 deletions Core/GameInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -371,16 +371,19 @@ public bool Scan()
// GameData *twice*.
//
// The least evil is to walk it once, and filter it ourselves.
IEnumerable<string> 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);
Expand Down Expand Up @@ -419,39 +422,6 @@ public string ToAbsoluteGameDir(string path)
return CKANPathUtils.ToAbsolute(path, GameDir());
}

/// <summary>
/// https://xkcd.com/208/
/// This regex matches things like GameData/Foo/Foo.1.2.dll
/// </summary>
private static readonly Regex dllPattern = new Regex(
@"
^(?:.*/)? # Directories (ending with /)
(?<identifier>[^.]+) # Our DLL name, up until the first dot.
.*\.dll$ # Everything else, ending in dll
",
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled
);

/// <summary>
/// Find the identifier associated with a manually installed DLL
/// </summary>
/// <param name="relative_path">Path of the DLL relative to game root</param>
/// <returns>
/// Identifier if found otherwise null
/// </returns>
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}";
Expand Down
4 changes: 4 additions & 0 deletions Core/Games/IGame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
92 changes: 92 additions & 0 deletions Core/Games/KerbalSpaceProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -328,6 +332,94 @@ private string[] filterCmdLineArgs(string[] args, GameVersion installedVersion,
return args;
}

/// <summary>
/// https://xkcd.com/208/
/// This regex matches things like GameData/Foo/Foo.1.2.dll
/// </summary>
private static readonly Regex dllPattern = new Regex(
@"
^(?:.*/)? # Directories (ending with /)
(?<identifier>[^.]+) # Our DLL name, up until the first dot.
.*\.dll$ # Everything else, ending in dll
",
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled
);

/// <summary>
/// Find the identifier associated with a manually installed DLL
/// </summary>
/// <param name="relativePath">Path of the DLL relative to game root</param>
/// <returns>
/// Identifier if found otherwise null
/// </returns>
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<string> IdentifiersFromConfigNodes(IEnumerable<KSPConfigNode> nodes)
=> nodes
.Select(node => node.For)
.Where(ident => !string.IsNullOrEmpty(ident))
.Concat(nodes.SelectMany(node => IdentifiersFromConfigNodes(node.Children)))
.Distinct();

/// <summary>
/// Find the identifiers associated with a .cfg file string
/// using ModuleManager's :FOR[identifier] pattern
/// </summary>
/// <param name="cfgContents">Path of the .cfg file relative to game root</param>
/// <returns>
/// Array of identifiers, if any found
/// </returns>
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<string>();
},
success => IdentifiersFromConfigNodes(success.Value))
.ToArray();

/// <summary>
/// Find the identifiers associated with a manually installed .cfg file
/// using ModuleManager's :FOR[identifier] pattern
/// </summary>
/// <param name="absolutePath">Path of the .cfg file relative to game root</param>
/// <returns>
/// Array of identifiers, if any found
/// </returns>
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));
}
}
114 changes: 70 additions & 44 deletions Core/Games/KerbalSpaceProgram/ConfigParser/Parser.ConfigNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,49 +18,44 @@

#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;
public readonly KSPConfigProperty[] Properties;
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<string>? 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<KSPConfigProperty> props,
IEnumerable<KSPConfigNode> children)
{
Operator = op;
Name = name;
Filter = filter;
Filters = filters?.ToArray();
Needs = needs;
Has = has;
First = first;
Expand All @@ -75,50 +70,81 @@ 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<char, char> OpenBrace = Char('{').Between(Spaces());
public static readonly Parser<char, char> CloseBrace = Spaces().Right(Char('}'));
public static readonly Parser<char, char> OpenBrace = Char('{').Between(JunkBlock);
public static readonly Parser<char, char> CloseBrace = JunkBlock.Right(Char('}'));

// :BEFORE, :FOR, :AFTER, etc.
public static Parser<char, string> SimpleClause(string label)
=> Optional(Many1(LetterOrDigit()).Between(StringIgnoreCase($":{label}["),
Char(']'))
.AsString(),
null);
public static Parser<char, MMNodeSuffix> SimpleClause(string label)
=> KSPConfigParserPrimitives.Identifier
.Between(StringIgnoreCase($":{label}["),
Char(']'))
.Map(v => new MMNodeSuffix(label, v));

private static IEnumerable<T> FindByType<T>(IEnumerable<dynamic> suffixes) where T : class
=> suffixes.Where(s => s.GetType() == typeof(T))
.Select(s => (T)s);

private static string? FindSimpleSuffix(IEnumerable<dynamic> suffixes, string label)
=> FindByType<MMNodeSuffix>(suffixes).FirstOrDefault(s => s.Label == label)?.Value;

private static T? FindSuffix<T>(IEnumerable<dynamic> suffixes) where T : class
=> FindByType<T>(suffixes).FirstOrDefault();

// NODENAME { property = value ... }
// <Op><NodeType>([<NodeNameWithWildcards>])?(:HAS[<has block>])?(,<index-or-*>)?
public static readonly Parser<char, KSPConfigNode> ConfigNode =
Fix<char, KSPConfigNode>(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<MMNeedsAnd>(suffixes),
FindSuffix<MMHas>(suffixes),
FindSimpleSuffix(suffixes, "FIRST") != null,
FindSimpleSuffix(suffixes, "BEFORE"),
FindSimpleSuffix(suffixes, "FOR"),
FindSimpleSuffix(suffixes, "AFTER"),
FindSimpleSuffix(suffixes, "LAST"),
FindSimpleSuffix(suffixes, "FINAL") != null,
FindSuffix<MMIndex>(suffixes),
FindByType<KSPConfigProperty>(contents),
FindByType<KSPConfigNode>(contents)));

// Whole file (multiple config nodes)
public static readonly Parser<char, IEnumerable<KSPConfigNode>> ConfigFile =
Expand Down
9 changes: 5 additions & 4 deletions Core/Games/KerbalSpaceProgram/ConfigParser/Parser.Has.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion Core/Games/KerbalSpaceProgram/ConfigParser/Parser.Needs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public static partial class KSPConfigParser
{
public static readonly Parser<char, MMNeedsMod> 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<char, MMNeedsOr> NeedsOr =
Expand Down
Loading

0 comments on commit c65336e

Please sign in to comment.