This commit is contained in:
Kurt
2025-05-31 22:51:57 -05:00
3 changed files with 130 additions and 49 deletions

View File

@@ -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<IPlugin>(PluginPath, Settings.Startup.PluginLoadMethod));
Plugins.AddRange(PluginLoader.LoadPlugins<IPlugin>(PluginPath, Settings.Startup.PluginLoadMerged));
}
catch (InvalidCastException c)
{

View File

@@ -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;
/// <summary>
/// Provides functionality to load plugins from assemblies at runtime.
/// </summary>
public static class PluginLoader
{
public static IEnumerable<T> LoadPlugins<T>(string pluginPath, PluginLoadSetting loadSetting) where T : class
/// <summary>
/// Loads plugin assemblies from the given directory using the provided load setting.
/// </summary>
/// <param name="pluginPath">The directory path to search for plugin assemblies.</param>
/// <param name="loadMerged">The plugin load setting to use.</param>
/// <returns>A PluginLoadResult containing contexts and assemblies.</returns>
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<T>(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;
}
/// <summary>
/// Loads plugins of the specified type from the given directory using the provided load setting.
/// </summary>
/// <typeparam name="T">The type of plugin to load.</typeparam>
/// <param name="pluginPath">The directory path to search for plugin assemblies.</param>
/// <param name="loadMerged">The plugin load setting to use.</param>
/// <returns>An enumerable of loaded plugin instances of type <typeparamref name="T"/>.</returns>
public static IEnumerable<T> LoadPlugins<T>(string pluginPath, bool loadMerged) where T : class
{
var result = LoadPluginAssemblies(pluginPath, loadMerged);
var pluginTypes = GetPluginsOfType<T>(result.GetAssemblies());
return LoadPlugins<T>(pluginTypes);
}
/// <summary>
/// Loads plugin instances of the specified type from the given plugin types.
/// </summary>
/// <typeparam name="T">The type of plugin to load.</typeparam>
/// <param name="pluginTypes">The types of plugins to instantiate.</param>
/// <returns>An enumerable of loaded plugin instances of type <typeparamref name="T"/>.</returns>
private static IEnumerable<T> LoadPlugins<T>(IEnumerable<Type> pluginTypes) where T : class
{
foreach (var t in pluginTypes)
@@ -37,41 +82,24 @@ public static class PluginLoader
}
}
private static IEnumerable<Assembly> GetAssemblies(IEnumerable<string> 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<string, Assembly> 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;
/// <summary>
/// Gets all plugin types of the specified type from the given assemblies.
/// </summary>
/// <typeparam name="T">The type of plugin to search for.</typeparam>
/// <param name="assemblies">The assemblies to search for plugins.</param>
/// <returns>An enumerable of plugin types.</returns>
private static IEnumerable<Type> GetPluginsOfType<T>(IEnumerable<Assembly> assemblies)
{
var pluginType = typeof(T);
return assemblies.SelectMany(z => GetPluginTypes(z, pluginType));
}
/// <summary>
/// Gets all types from the specified assembly that match the given plugin type.
/// </summary>
/// <param name="z">The assembly to search.</param>
/// <param name="plugin">The plugin type to match.</param>
/// <returns>An enumerable of matching types.</returns>
private static IEnumerable<Type> GetPluginTypes(Assembly z, Type plugin)
{
try
@@ -101,6 +129,12 @@ private static IEnumerable<Type> GetPluginTypes(Assembly z, Type plugin)
}
}
/// <summary>
/// Determines whether the specified type is a valid plugin type.
/// </summary>
/// <param name="type">The type to check.</param>
/// <param name="plugin">The plugin type to match.</param>
/// <returns><c>true</c> if the type is a valid plugin type; otherwise, <c>false</c>.</returns>
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);
}
}
/// <summary>
/// Encapsulates the result of loading plugins, including their contexts and assemblies.
/// </summary>
public class PluginLoadResult
{
public List<PluginLoadContext> Contexts { get; } = new();
public List<Assembly> Assemblies { get; } = new();
/// <summary>
/// Returns all loaded assemblies for downstream use.
/// </summary>
public IEnumerable<Assembly> GetAssemblies() => Assemblies;
}
/// <summary>
/// Custom AssemblyLoadContext for loading plugin assemblies in isolation.
/// </summary>
public class PluginLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver Resolver;
/// <summary>
/// Initializes a new instance of the <see cref="PluginLoadContext"/> class.
/// </summary>
/// <param name="pluginPath">The path to the plugin assembly.</param>
public PluginLoadContext(string pluginPath) : base(isCollectible: true)
{
Resolver = new AssemblyDependencyResolver(pluginPath);
}
/// <summary>
/// Loads the main plugin assembly from the specified path. Delegates framework assemblies to the default context.
/// </summary>
/// <param name="assemblyName">The assembly name to load.</param>
/// <returns>The loaded assembly, or null if not the main plugin assembly.</returns>
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;
}
}
}

View File

@@ -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<string> 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.")]