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 Jan 25, 2022
1 parent 6a3eba7 commit fe804fe
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 17 deletions.
60 changes: 53 additions & 7 deletions Core/GameInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,14 +373,22 @@ public bool Scan()
// The least evil is to walk it once, and filter it ourselves.
IEnumerable<string> files = Directory
.EnumerateFiles(game.PrimaryModDirectory(this), "*", SearchOption.AllDirectories)
.Where(file => file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase))
.Where(file => file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase)
|| file.EndsWith(".cfg", StringComparison.CurrentCultureIgnoreCase))
.Select(CKANPathUtils.NormalizePath)
.Where(absPath => !game.StockFolders.Any(f =>
ToRelativeGameDir(absPath).StartsWith($"{f}/")));

foreach (string dll in files)
foreach (string file in files)
{
manager.registry.RegisterDll(this, dll);
if (file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase))
{
manager.registry.RegisterDll(this, file);
}
else if (file.EndsWith(".cfg", StringComparison.CurrentCultureIgnoreCase))
{
manager.registry.RegisterCfg(this, file);
}
}
var newDlls = manager.registry.InstalledDlls.ToHashSet();
bool dllChanged = !oldDlls.SetEquals(newDlls);
Expand Down Expand Up @@ -435,23 +443,61 @@ public string ToAbsoluteGameDir(string path)
/// <summary>
/// Find the identifier associated with a manually installed DLL
/// </summary>
/// <param name="relative_path">Path of the DLL relative to game root</param>
/// <param name="relativePath">Path of the DLL relative to game root</param>
/// <returns>
/// Identifier if found otherwise null
/// </returns>
public string DllPathToIdentifier(string relative_path)
public string DllPathToIdentifier(string relativePath)
{
if (!relative_path.StartsWith($"{game.PrimaryModDirectoryRelative}/", StringComparison.CurrentCultureIgnoreCase))
if (!relativePath.StartsWith($"{game.PrimaryModDirectoryRelative}/", StringComparison.CurrentCultureIgnoreCase))
{
// DLLs only live in the primary mod directory
return null;
}
Match match = dllPattern.Match(relative_path);
Match match = dllPattern.Match(relativePath);
return match.Success
? Identifier.Sanitize(match.Groups["identifier"].Value)
: null;
}

/// <summary>
/// Matches ModuleManager's :FOR[identifier] syntax in .cfg files
/// </summary>
private static readonly Regex cfgForClausePattern = new Regex(
@":FOR\[(?<identifier>[^\]]+)\]",
RegexOptions.Compiled);

/// <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>
public string[] CfgContentsToIdentifiers(string cfgContents)
{
return cfgForClausePattern
.Matches(cfgContents)
.Cast<Match>()
.Select(match => Identifier.Sanitize(match.Groups["identifier"].Value))
.Distinct()
.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>
public string[] CfgPathToIdentifiers(string absolutePath)
{
return CfgContentsToIdentifiers(File.ReadAllText(absolutePath));
}

public override string ToString()
{
return $"{game.ShortName} Install: {gameDir}";
Expand Down
65 changes: 58 additions & 7 deletions Core/Registry/Registry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,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 @@ -844,20 +847,21 @@ public void DeregisterModule(GameInstance ksp, string module)
///
/// Does nothing if the DLL is already part of an installed module.
/// </summary>
public void RegisterDll(GameInstance ksp, string absolute_path)
/// <param name="inst">The game instance containing the file</param>
/// <param name="absolute_path">Path to the .dll file</param>
public void RegisterDll(GameInstance inst, string absolute_path)
{
log.DebugFormat("Registering DLL {0}", absolute_path);
string relative_path = ksp.ToRelativeGameDir(absolute_path);
string relative_path = inst.ToRelativeGameDir(absolute_path);

string dllIdentifier = ksp.DllPathToIdentifier(relative_path);
string dllIdentifier = inst.DllPathToIdentifier(relative_path);
if (dllIdentifier == null)
{
log.WarnFormat("Attempted to index {0} which is not a DLL", relative_path);
log.WarnFormat("Not registering {0}, not a DLL", relative_path);
return;
}

string owner;
if (installed_files.TryGetValue(relative_path, out owner))
if (installed_files.TryGetValue(relative_path, out string owner))
{
log.InfoFormat(
"Not registering {0}, it belongs to {1}",
Expand All @@ -875,6 +879,53 @@ public void RegisterDll(GameInstance ksp, string absolute_path)
installed_dlls[dllIdentifier] = relative_path;
}

/// <summary>
/// Registers a .cfg file as installed, based on
/// ModuleManager's :FOR[identifier] syntax.
/// Provides support for manually installed modules without DLLs.
///
/// Does nothing if the file is already part of an installed module.
/// </summary>
/// <param name="inst">The game instance containing the file</param>
/// <param name="absolute_path">Path to the .cfg file</param>
public void RegisterCfg(GameInstance inst, string absolute_path)
{
log.DebugFormat("Registering cfg file {0}", absolute_path);
// This is faster than parsing the file
string relative_path = inst.ToRelativeGameDir(absolute_path);
if (installed_files.TryGetValue(relative_path, out string owner))
{
log.InfoFormat(
"Not registering {0}, it belongs to {1}",
relative_path,
owner
);
return;
}
var cfgIdentifiers = inst.CfgPathToIdentifiers(absolute_path);
if (!cfgIdentifiers.Any())
{
log.WarnFormat("Not registering {0}, no :FOR[identifier] clause", relative_path);
return;
}
foreach (string cfgIdentifier in cfgIdentifiers)
{
// Don't overwrite so DLLs can take precedence
if (installed_dlls.TryGetValue(cfgIdentifier, out string registered_path))
{
log.InfoFormat(
"Not registering {0} as {1}, already registered to {2}",
relative_path, cfgIdentifier, registered_path);
}
else
{
log.InfoFormat("Registering {0} from {1}", cfgIdentifier, relative_path);
EnlistWithTransaction();
installed_dlls[cfgIdentifier] = relative_path;
}
}
}

/// <summary>
/// Clears knowledge of all DLLs from the registry.
/// </summary>
Expand Down
14 changes: 11 additions & 3 deletions Netkan/Validators/PluginsValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,17 @@ public void Validate(Metadata metadata)
.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.CfgContentsToIdentifiers(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");
Expand Down

0 comments on commit fe804fe

Please sign in to comment.