diff --git a/PKHeX.WinForms/MainWindow/Main.cs b/PKHeX.WinForms/MainWindow/Main.cs index b771a39d5..4e2f53c71 100644 --- a/PKHeX.WinForms/MainWindow/Main.cs +++ b/PKHeX.WinForms/MainWindow/Main.cs @@ -47,7 +47,7 @@ public Main() startup.ReadSettings(Settings.Startup); startup.ReadTemplateIfNoEntity(TemplatePath); - if (Settings.Startup.PluginLoadMethod != PluginLoadSetting.DontLoad) + if (Settings.Startup.PluginLoadEnable) FormLoadPlugins(); FormLoadInitialFiles(startup); @@ -274,7 +274,7 @@ private void FormLoadPlugins() #endif try { - Plugins.AddRange(PluginLoader.LoadPlugins(PluginPath, Settings.Startup.PluginLoadMethod)); + Plugins.AddRange(PluginLoader.LoadPlugins(PluginPath, Settings.Startup.PluginLoadMerged)); } catch (InvalidCastException c) { diff --git a/PKHeX.WinForms/MainWindow/PluginLoader.cs b/PKHeX.WinForms/MainWindow/PluginLoader.cs index 6c837bde0..17ead8787 100644 --- a/PKHeX.WinForms/MainWindow/PluginLoader.cs +++ b/PKHeX.WinForms/MainWindow/PluginLoader.cs @@ -4,22 +4,67 @@ using System.IO; using System.Linq; using System.Reflection; -using static PKHeX.WinForms.PluginLoadSetting; +using System.Runtime.Loader; namespace PKHeX.WinForms; +/// +/// Provides functionality to load plugins from assemblies at runtime. +/// public static class PluginLoader { - public static IEnumerable LoadPlugins(string pluginPath, PluginLoadSetting loadSetting) where T : class + /// + /// 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) - ? [] // Don't immediately return, as we may be loading plugins merged with this .exe + ? [] : Directory.EnumerateFiles(pluginPath, "*.dll", SearchOption.AllDirectories); - var assemblies = GetAssemblies(dllFileNames, loadSetting); - var pluginTypes = GetPluginsOfType(assemblies); + 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) @@ -37,41 +82,24 @@ public static class PluginLoader } } - private static IEnumerable GetAssemblies(IEnumerable dllFileNames, PluginLoadSetting loadSetting) - { - var loadMethod = GetPluginLoadMethod(loadSetting); - foreach (var file in dllFileNames) - { - Assembly x; - try { x = loadMethod(file); } - catch (Exception ex) - { - Debug.WriteLine($"Unable to load plugin from file: {file}"); - Debug.WriteLine(ex.Message); - continue; - } - yield return x; - } - if (loadSetting.IsMerged()) - yield return Assembly.GetExecutingAssembly(); // load merged too - } - - private static Func GetPluginLoadMethod(PluginLoadSetting pls) => pls switch - { - LoadFrom or LoadFromMerged => Assembly.LoadFrom, - LoadFile or LoadFileMerged => Assembly.LoadFile, - UnsafeLoadFrom or UnsafeMerged => Assembly.UnsafeLoadFrom, - _ => throw new IndexOutOfRangeException($"PluginLoadSetting: {pls} method not defined."), - }; - - public static bool IsMerged(this PluginLoadSetting loadSetting) => loadSetting is LoadFromMerged or LoadFileMerged or UnsafeMerged; - + /// + /// 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 @@ -101,6 +129,12 @@ private static IEnumerable GetPluginTypes(Assembly z, Type plugin) } } + /// + /// Determines whether the specified type is a valid plugin type. + /// + /// The type to check. + /// The plugin type to match. + /// true if the type is a valid plugin type; otherwise, false. private static bool IsTypePlugin(Type type, Type plugin) { if (type.IsInterface || type.IsAbstract) @@ -108,3 +142,58 @@ private static bool IsTypePlugin(Type type, Type plugin) 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; + } + } +} diff --git a/PKHeX.WinForms/Properties/PKHeXSettings.cs b/PKHeX.WinForms/Properties/PKHeXSettings.cs index 7fcbed663..ee2ca89ad 100644 --- a/PKHeX.WinForms/Properties/PKHeXSettings.cs +++ b/PKHeX.WinForms/Properties/PKHeXSettings.cs @@ -139,8 +139,11 @@ public sealed class StartupSettings : IStartupSettings [LocalizedDescription("Show the changelog when a new version of the program is run for the first time.")] public bool ShowChangelogOnUpdate { get; set; } = true; - [LocalizedDescription("Loads plugins from the plugins folder, assuming the folder exists. Try LoadFile to mitigate intermittent load failures.")] - public PluginLoadSetting PluginLoadMethod { get; set; } = PluginLoadSetting.LoadFrom; + [LocalizedDescription("Loads plugins from the plugins folder, assuming the folder exists.")] + public bool PluginLoadEnable { get; set; } = true; + + [LocalizedDescription("Loads any plugins that were merged into the main executable file.")] + public bool PluginLoadMerged { get; set; } [Browsable(false)] public List RecentlyLoaded { get; set; } = new(DefaultMaxRecent); @@ -203,17 +206,6 @@ public void LoadSaveFile(string path) } } -public enum PluginLoadSetting -{ - DontLoad, - LoadFrom, - LoadFile, - UnsafeLoadFrom, - LoadFromMerged, - LoadFileMerged, - UnsafeMerged, -} - public sealed class EntityConverterSettings { [LocalizedDescription("Allow PKM file conversion paths that are not possible via official methods. Individual properties will be copied sequentially.")]