using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
namespace PKHeX.WinForms;
///
/// Provides functionality to load plugins from assemblies at runtime.
///
public static class PluginLoader
{
///
/// Loads plugin assemblies from the given directory using the provided load setting.
///
/// The directory path to search for plugin assemblies.
/// The plugin load setting to use.
/// A PluginLoadResult containing contexts and assemblies.
public static PluginLoadResult LoadPluginAssemblies(string pluginPath, bool loadMerged)
{
var result = new PluginLoadResult();
var dllFileNames = !Directory.Exists(pluginPath)
? []
: Directory.EnumerateFiles(pluginPath, "*.dll", SearchOption.AllDirectories);
foreach (var file in dllFileNames)
{
try
{
var context = new PluginLoadContext(file);
var asm = context.LoadFromAssemblyPath(file);
result.Contexts.Add(context);
result.Assemblies.Add(asm);
}
catch (Exception ex)
{
Debug.WriteLine($"Unable to load plugin from file: {file}");
Debug.WriteLine(ex.Message);
}
}
if (loadMerged)
result.Assemblies.Add(Assembly.GetExecutingAssembly());
return result;
}
///
/// Loads plugins of the specified type from the given directory using the provided load setting.
///
/// The type of plugin to load.
/// The directory path to search for plugin assemblies.
/// The plugin load setting to use.
/// An enumerable of loaded plugin instances of type .
public static IEnumerable LoadPlugins(string pluginPath, bool loadMerged) where T : class
{
var result = LoadPluginAssemblies(pluginPath, loadMerged);
var pluginTypes = GetPluginsOfType(result.GetAssemblies());
return LoadPlugins(pluginTypes);
}
///
/// Loads plugin instances of the specified type from the given plugin types.
///
/// The type of plugin to load.
/// The types of plugins to instantiate.
/// An enumerable of loaded plugin instances of type .
private static IEnumerable LoadPlugins(IEnumerable pluginTypes) where T : class
{
foreach (var t in pluginTypes)
{
T? activate;
try { activate = (T?)Activator.CreateInstance(t); }
catch (Exception ex)
{
Debug.WriteLine($"Unable to load plugin [{t.Name}]: {t.FullName}");
Debug.WriteLine(ex.Message);
continue;
}
if (activate is not null)
yield return activate;
}
}
///
/// Gets all plugin types of the specified type from the given assemblies.
///
/// The type of plugin to search for.
/// The assemblies to search for plugins.
/// An enumerable of plugin types.
private static IEnumerable GetPluginsOfType(IEnumerable assemblies)
{
var pluginType = typeof(T);
return assemblies.SelectMany(z => GetPluginTypes(z, pluginType));
}
///
/// Gets all types from the specified assembly that match the given plugin type.
///
/// The assembly to search.
/// The plugin type to match.
/// An enumerable of matching types.
private static IEnumerable GetPluginTypes(Assembly z, Type plugin)
{
try
{
// Handle Costura merged plugin dll's; need to Attach for them to correctly retrieve their dependencies.
var assemblyLoaderType = z.GetType("Costura.AssemblyLoader", false);
var attachMethod = assemblyLoaderType?.GetMethod("Attach", BindingFlags.Static | BindingFlags.Public);
attachMethod?.Invoke(null, []);
var types = z.GetExportedTypes();
return types.Where(type => IsTypePlugin(type, plugin));
}
// User plugins can be out of date, with mismatching API surfaces.
catch (Exception ex)
{
Debug.WriteLine($"Unable to load plugin [{plugin.FullName}]: {z.FullName}");
Debug.WriteLine(ex.Message);
if (ex is not ReflectionTypeLoadException rtle)
return [];
foreach (var le in rtle.LoaderExceptions)
{
if (le is not null)
Debug.WriteLine(le.Message);
}
return [];
}
}
///
/// Determines whether the specified type is a valid plugin type.
///
/// The type to check.
/// The plugin type to match.
/// if the type is a valid plugin type; otherwise, .
private static bool IsTypePlugin(Type type, Type plugin)
{
if (type.IsInterface || type.IsAbstract)
return false;
return plugin.IsAssignableFrom(type);
}
}
///
/// Encapsulates the result of loading plugins, including their contexts and assemblies.
///
public class PluginLoadResult
{
public List Contexts { get; } = new();
public List Assemblies { get; } = new();
///
/// Returns all loaded assemblies for downstream use.
///
public IEnumerable GetAssemblies() => Assemblies;
}
///
/// Custom AssemblyLoadContext for loading plugin assemblies in isolation.
///
public class PluginLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver Resolver;
///
/// Initializes a new instance of the class.
///
/// The path to the plugin assembly.
public PluginLoadContext(string pluginPath) : base(isCollectible: true)
{
Resolver = new AssemblyDependencyResolver(pluginPath);
}
///
/// Loads the main plugin assembly from the specified path. Delegates framework assemblies to the default context.
///
/// The assembly name to load.
/// The loaded assembly, or null if not the main plugin assembly.
protected override Assembly? Load(AssemblyName assemblyName)
{
// Try to resolve plugin-local dependencies
var assemblyPath = Resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
return LoadFromAssemblyPath(assemblyPath);
// Fallback: try to resolve from the default context (main app/shared dependencies)
try
{
return Default.LoadFromAssemblyName(assemblyName);
}
catch
{
// Not found in default context
return null;
}
}
}