Skip to content

Commit e4c9ae4

Browse files
committed
Expose LoadPlugins(string[]) for 3rd party tools
This is a temporary solution until metaplugins are available
1 parent 9cc8513 commit e4c9ae4

1 file changed

Lines changed: 148 additions & 77 deletions

File tree

BepInEx.Core/Bootstrap/BaseChainloader.cs

Lines changed: 148 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -175,14 +175,37 @@ protected virtual void InitializeLoggers()
175175
Logger.Sources.Add(new HarmonyLogSource());
176176
}
177177

178-
protected virtual IList<PluginInfo> DiscoverPlugins()
178+
/// <summary>
179+
/// Discovers all plugins in the plugin directory without loading them.
180+
/// </summary>
181+
/// <remarks>
182+
/// This is useful for discovering BepInEx plugin metadata.
183+
/// </remarks>
184+
/// <param name="path">Path from which to search the plugins.</param>
185+
/// <param name="cacheName">Cache name to use. If null, results are not cached.</param>
186+
/// <returns>List of discovered plugins and their metadata.</returns>
187+
protected IList<PluginInfo> DiscoverPluginsFrom(string path, string cacheName = "chainloader")
179188
{
180189
var pluginsToLoad =
181-
TypeLoader.FindPluginTypes(Paths.PluginPath, ToPluginInfo, HasBepinPlugins, "chainloader");
182-
190+
TypeLoader.FindPluginTypes(path, ToPluginInfo, HasBepinPlugins, cacheName);
183191
return pluginsToLoad.SelectMany(p => p.Value).ToList();
184192
}
185193

194+
/// <summary>
195+
/// Discovers plugins to load.
196+
/// </summary>
197+
/// <returns>List of plugins to be loaded.</returns>
198+
protected virtual IList<PluginInfo> DiscoverPlugins()
199+
{
200+
return DiscoverPluginsFrom(Paths.PluginPath);
201+
}
202+
203+
/// <summary>
204+
/// Preprocess the plugins and modify the load order.
205+
/// </summary>
206+
/// <remarks>Some plugins may be skipped if they cannot be loaded (wrong metadata, etc).</remarks>
207+
/// <param name="plugins">Plugins to process.</param>
208+
/// <returns>List of plugins to load in the correct load order.</returns>
186209
protected virtual IList<PluginInfo> ModifyLoadOrder(IList<PluginInfo> plugins)
187210
{
188211
// We use a sorted dictionary to ensure consistent load order
@@ -192,6 +215,13 @@ protected virtual IList<PluginInfo> ModifyLoadOrder(IList<PluginInfo> plugins)
192215

193216
foreach (var pluginInfoGroup in plugins.GroupBy(info => info.Metadata.GUID))
194217
{
218+
if (Plugins.TryGetValue(pluginInfoGroup.Key, out var loadedPlugin))
219+
{
220+
Logger.Log(LogLevel.Warning,
221+
$"Skipping [{pluginInfoGroup.Key}] because a plugin with a similar GUID ([{loadedPlugin}]) has been already loaded.");
222+
continue;
223+
}
224+
195225
PluginInfo loadedVersion = null;
196226
foreach (var pluginInfo in pluginInfoGroup.OrderByDescending(x => x.Metadata.Version))
197227
{
@@ -225,14 +255,18 @@ protected virtual IList<PluginInfo> ModifyLoadOrder(IList<PluginInfo> plugins)
225255

226256
foreach (var pluginInfo in pluginsByGuid.Values.ToList())
227257
if (pluginInfo.Incompatibilities.Any(incompatibility =>
228-
pluginsByGuid.ContainsKey(incompatibility.IncompatibilityGUID))
258+
pluginsByGuid.ContainsKey(incompatibility.IncompatibilityGUID)
259+
|| Plugins.ContainsKey(incompatibility.IncompatibilityGUID))
229260
)
230261
{
231262
pluginsByGuid.Remove(pluginInfo.Metadata.GUID);
232263
dependencyDict.Remove(pluginInfo.Metadata.GUID);
233264

234-
var incompatiblePlugins = pluginInfo.Incompatibilities.Select(x => x.IncompatibilityGUID)
235-
.Where(x => pluginsByGuid.ContainsKey(x)).ToArray();
265+
var incompatiblePluginsNew = pluginInfo.Incompatibilities.Select(x => x.IncompatibilityGUID)
266+
.Where(x => pluginsByGuid.ContainsKey(x));
267+
var incompatiblePluginsExisting = pluginInfo.Incompatibilities.Select(x => x.IncompatibilityGUID)
268+
.Where(x => Plugins.ContainsKey(x));
269+
var incompatiblePlugins = incompatiblePluginsNew.Concat(incompatiblePluginsExisting).ToArray();
236270
var message =
237271
$@"Could not load [{pluginInfo}] because it is incompatible with: {string.Join(", ", incompatiblePlugins)}";
238272
DependencyErrors.Add(message);
@@ -246,6 +280,8 @@ protected virtual IList<PluginInfo> ModifyLoadOrder(IList<PluginInfo> plugins)
246280
Logger.Log(LogLevel.Warning, message);
247281
}
248282

283+
// We don't add already loaded plugins to the dependency graph as they are already loaded
284+
249285
var emptyDependencies = new string[0];
250286

251287
// Sort plugins by their dependencies.
@@ -259,107 +295,142 @@ protected virtual IList<PluginInfo> ModifyLoadOrder(IList<PluginInfo> plugins)
259295
return sortedPlugins.Where(pluginsByGuid.ContainsKey).Select(x => pluginsByGuid[x]).ToList();
260296
}
261297

298+
/// <summary>
299+
/// Run the chainloader and load all plugins from the plugins folder.
300+
/// </summary>
262301
public virtual void Execute()
263302
{
264303
try
265304
{
266305
var plugins = DiscoverPlugins();
267-
268306
Logger.Log(LogLevel.Info, $"{plugins.Count} plugin{(plugins.Count == 1 ? "" : "s")} to load");
307+
LoadPlugins(plugins);
308+
}
309+
catch (Exception ex)
310+
{
311+
try
312+
{
313+
ConsoleManager.CreateConsole();
314+
}
315+
catch { }
269316

270-
var sortedPlugins = ModifyLoadOrder(plugins);
317+
Logger.Log(LogLevel.Error, $"Error occurred loading plugins: {ex}");
318+
}
271319

272-
var invalidPlugins = new HashSet<string>();
273-
var processedPlugins = new Dictionary<string, SemanticVersioning.Version>();
274-
var loadedAssemblies = new Dictionary<string, Assembly>();
320+
Logger.Log(LogLevel.Message, "Chainloader startup complete");
321+
}
275322

276-
foreach (var plugin in sortedPlugins)
277-
{
278-
var dependsOnInvalidPlugin = false;
279-
var missingDependencies = new List<BepInDependency>();
280-
foreach (var dependency in plugin.Dependencies)
281-
{
282-
static bool IsHardDependency(BepInDependency dep) =>
283-
(dep.Flags & BepInDependency.DependencyFlags.HardDependency) != 0;
284-
285-
// If the dependency wasn't already processed, it's missing altogether
286-
var dependencyExists =
287-
processedPlugins.TryGetValue(dependency.DependencyGUID, out var pluginVersion);
288-
if (!dependencyExists || dependency.VersionRange != null &&
289-
!dependency.VersionRange.IsSatisfied(pluginVersion))
290-
{
291-
// If the dependency is hard, collect it into a list to show
292-
if (IsHardDependency(dependency))
293-
missingDependencies.Add(dependency);
294-
continue;
295-
}
296-
297-
// If the dependency is a hard and is invalid (e.g. has missing dependencies), report that to the user
298-
if (invalidPlugins.Contains(dependency.DependencyGUID) && IsHardDependency(dependency))
299-
{
300-
dependsOnInvalidPlugin = true;
301-
break;
302-
}
303-
}
323+
private IList<PluginInfo> LoadPlugins(IList<PluginInfo> plugins)
324+
{
325+
var sortedPlugins = ModifyLoadOrder(plugins);
304326

305-
processedPlugins.Add(plugin.Metadata.GUID, plugin.Metadata.Version);
327+
var invalidPlugins = new HashSet<string>();
328+
var processedPlugins = new Dictionary<string, SemanticVersioning.Version>();
329+
var loadedAssemblies = new Dictionary<string, Assembly>();
330+
var loadedPlugins = new List<PluginInfo>();
306331

307-
if (dependsOnInvalidPlugin)
332+
foreach (var plugin in sortedPlugins)
333+
{
334+
var dependsOnInvalidPlugin = false;
335+
var missingDependencies = new List<BepInDependency>();
336+
foreach (var dependency in plugin.Dependencies)
337+
{
338+
static bool IsHardDependency(BepInDependency dep) =>
339+
(dep.Flags & BepInDependency.DependencyFlags.HardDependency) != 0;
340+
341+
// If the dependency wasn't already processed, it's missing altogether
342+
var dependencyExists =
343+
processedPlugins.TryGetValue(dependency.DependencyGUID, out var pluginVersion);
344+
// Alternatively, if the dependency hasn't been loaded before, it's missing too
345+
if (!dependencyExists)
308346
{
309-
var message =
310-
$"Skipping [{plugin}] because it has a dependency that was not loaded. See previous errors for details.";
311-
DependencyErrors.Add(message);
312-
Logger.Log(LogLevel.Warning, message);
313-
continue;
347+
dependencyExists = Plugins.TryGetValue(dependency.DependencyGUID, out var pluginInfo);
348+
pluginVersion = pluginInfo?.Metadata.Version;
314349
}
315350

316-
if (missingDependencies.Count != 0)
351+
if (!dependencyExists || dependency.VersionRange != null &&
352+
!dependency.VersionRange.IsSatisfied(pluginVersion))
317353
{
318-
var message = $@"Could not load [{plugin}] because it has missing dependencies: {
319-
string.Join(", ", missingDependencies.Select(s => s.VersionRange == null ? s.DependencyGUID : $"{s.DependencyGUID} ({s.VersionRange})").ToArray())
320-
}";
321-
DependencyErrors.Add(message);
322-
Logger.Log(LogLevel.Error, message);
323-
324-
invalidPlugins.Add(plugin.Metadata.GUID);
354+
// If the dependency is hard, collect it into a list to show
355+
if (IsHardDependency(dependency))
356+
missingDependencies.Add(dependency);
325357
continue;
326358
}
327359

328-
try
360+
// If the dependency is a hard and is invalid (e.g. has missing dependencies), report that to the user
361+
if (invalidPlugins.Contains(dependency.DependencyGUID) && IsHardDependency(dependency))
329362
{
330-
Logger.Log(LogLevel.Info, $"Loading [{plugin}]");
363+
dependsOnInvalidPlugin = true;
364+
break;
365+
}
366+
}
331367

332-
if (!loadedAssemblies.TryGetValue(plugin.Location, out var ass))
333-
loadedAssemblies[plugin.Location] = ass = Assembly.LoadFile(plugin.Location);
368+
processedPlugins.Add(plugin.Metadata.GUID, plugin.Metadata.Version);
334369

335-
Plugins[plugin.Metadata.GUID] = plugin;
336-
TryRunModuleCtor(plugin, ass);
337-
plugin.Instance = LoadPlugin(plugin, ass);
370+
if (dependsOnInvalidPlugin)
371+
{
372+
var message =
373+
$"Skipping [{plugin}] because it has a dependency that was not loaded. See previous errors for details.";
374+
DependencyErrors.Add(message);
375+
Logger.Log(LogLevel.Warning, message);
376+
continue;
377+
}
338378

339-
//_plugins.Add((TPlugin)plugin.Instance);
340-
}
341-
catch (Exception ex)
342-
{
343-
invalidPlugins.Add(plugin.Metadata.GUID);
344-
Plugins.Remove(plugin.Metadata.GUID);
379+
if (missingDependencies.Count != 0)
380+
{
381+
var message = $@"Could not load [{plugin}] because it has missing dependencies: {
382+
string.Join(", ", missingDependencies.Select(s => s.VersionRange == null ? s.DependencyGUID : $"{s.DependencyGUID} ({s.VersionRange})").ToArray())
383+
}";
384+
DependencyErrors.Add(message);
385+
Logger.Log(LogLevel.Error, message);
345386

346-
Logger.Log(LogLevel.Error,
347-
$"Error loading [{plugin}]: {(ex is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : ex.ToString())}");
348-
}
387+
invalidPlugins.Add(plugin.Metadata.GUID);
388+
continue;
349389
}
350-
}
351-
catch (Exception ex)
352-
{
390+
353391
try
354392
{
355-
ConsoleManager.CreateConsole();
393+
Logger.Log(LogLevel.Info, $"Loading [{plugin}]");
394+
395+
if (!loadedAssemblies.TryGetValue(plugin.Location, out var ass))
396+
loadedAssemblies[plugin.Location] = ass = Assembly.LoadFile(plugin.Location);
397+
398+
Plugins[plugin.Metadata.GUID] = plugin;
399+
TryRunModuleCtor(plugin, ass);
400+
plugin.Instance = LoadPlugin(plugin, ass);
401+
loadedPlugins.Add(plugin);
402+
403+
//_plugins.Add((TPlugin)plugin.Instance);
356404
}
357-
catch { }
405+
catch (Exception ex)
406+
{
407+
invalidPlugins.Add(plugin.Metadata.GUID);
408+
Plugins.Remove(plugin.Metadata.GUID);
358409

359-
Logger.Log(LogLevel.Error, $"Error occurred starting the game: {ex}");
410+
Logger.Log(LogLevel.Error,
411+
$"Error loading [{plugin}]: {(ex is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : ex.ToString())}");
412+
}
360413
}
361414

362-
Logger.Log(LogLevel.Message, "Chainloader startup complete");
415+
return loadedPlugins;
416+
}
417+
418+
/// <summary>
419+
/// Detects and loads all plugins in the specified directories.
420+
/// </summary>
421+
/// <remarks>
422+
/// It is better to collect all paths at once and use a single call to LoadPlugins than multiple calls.
423+
/// This allows to run proper dependency resolving and to load all plugins in one go.
424+
/// </remarks>
425+
/// <param name="pluginsPaths">Directories to search the plugins from.</param>
426+
/// <returns>List of loaded plugin infos.</returns>
427+
public IList<PluginInfo> LoadPlugins(params string[] pluginsPaths)
428+
{
429+
// TODO: This is a temporary solution for 3rd party loaders. Instead, this should be done via metaplugins.
430+
var plugins = new List<PluginInfo>();
431+
foreach (var pluginsPath in pluginsPaths)
432+
plugins.AddRange(DiscoverPluginsFrom(pluginsPath));
433+
return LoadPlugins(plugins);
363434
}
364435

365436
private static void TryRunModuleCtor(PluginInfo plugin, Assembly assembly)

0 commit comments

Comments
 (0)