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 28, 2022
1 parent e3e92d4 commit e9be017
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 81 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[] { };
}

public static 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 static 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 static 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));
}
}
48 changes: 12 additions & 36 deletions Core/Registry/Registry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ [JsonIgnore] public IEnumerable<string> InstalledDlls
/// </summary>
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;
}

/// <summary>
Expand Down Expand Up @@ -620,7 +623,7 @@ public GameVersion LatestCompatibleKSP(string identifier)
/// <param name="maxKsp">Return parameter for the highest game version</param>
public static void GetMinMaxVersions(IEnumerable<CkanModule> 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;
Expand Down Expand Up @@ -839,41 +842,14 @@ public void DeregisterModule(GameInstance ksp, string module)
installed_modules.Remove(module);
}

/// <summary>
/// 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.
/// </summary>
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;
}

/// <summary>
Expand Down Expand Up @@ -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.
/// </summary>
public string FileOwner(string file)
public string FileOwner(string relativePath)
{
file = CKANPathUtils.NormalizePath(file);
var file = CKANPathUtils.NormalizePath(relativePath);

if (Path.IsPathRooted(file))
{
Expand Down
2 changes: 1 addition & 1 deletion Tests/Core/GameInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public void ScanDlls()

ksp.Scan();

Assert.IsTrue(registry.IsInstalled("NewMod"));
Assert.IsTrue(registry.IsInstalled("NewMod"), "NewMod installed");
}

[Test]
Expand Down
5 changes: 3 additions & 2 deletions Tests/Core/Registry/Registry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions Tests/Core/Relationships/RelationshipResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CKAN.RelationshipDescriptor>();
depends.Add(new CKAN.ModuleRelationshipDescriptor { name = "ModuleManager" });
Expand All @@ -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"))
);
}
}
Expand Down

0 comments on commit e9be017

Please sign in to comment.