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; } } }