using System.Reflection; using AcDream.Plugin.Abstractions; using AcDream.Plugin.Abstractions.Rendering; namespace AcDream.Core.Plugins; public static class PluginLoader { /// /// Load a plugin DLL from into a collectible /// , resolve the gameplay /// and/or render-pack entry points declared by its manifest, and invoke only /// the facilities this host supplied. Any failure is returned as a failed /// rather than thrown. /// A returned partial plugin/context remains caller-owned; this method never /// requests unload because the caller must close host and render-pack /// registrations first. /// public static LoadedPlugin Load( string pluginDirectory, PluginManifest manifest, IPluginHost host, IRenderPackRegistry? renderPacks = null) { ArgumentException.ThrowIfNullOrWhiteSpace(pluginDirectory); ArgumentNullException.ThrowIfNull(manifest); ArgumentNullException.ThrowIfNull(host); // Refuse a contract we cannot honour BEFORE loading any code from it. // Checking after the fact is not equivalent: the assembly is already in // a collectible context, and the mismatch surfaces as a type-load or // missing-member failure from inside the plugin, which reads like the // plugin is broken rather than built for a different host. if (!PluginApi.IsSupported(manifest.ApiVersion)) return new LoadedPlugin( manifest, Plugin: null, LoadContext: null, Error: new PluginApiVersionException( $"plugin '{manifest.Id}' declares apiVersion {manifest.ApiVersion}, " + $"but this build supports {PluginApi.MinimumSupported}" + $"..{PluginApi.Current}")); var dllPath = Path.Combine(pluginDirectory, manifest.EntryDll); if (!File.Exists(dllPath)) return new LoadedPlugin( manifest, Plugin: null, LoadContext: null, Error: new FileNotFoundException($"entry dll not found: {dllPath}", dllPath)); PluginAssemblyLoadContext? alc = null; IAcDreamPlugin? instance = null; IRenderPackPlugin? renderPackInstance = null; try { alc = new PluginAssemblyLoadContext(pluginDirectory, dllPath); var asm = alc.LoadFromAssemblyPath(dllPath); IEnumerable types; try { types = asm.GetTypes(); } catch (ReflectionTypeLoadException rtle) { types = rtle.Types.OfType(); } Type[] concreteTypes = types .Where(static type => !type.IsAbstract && !type.IsInterface) .ToArray(); Type? pluginType = manifest.Declares(PluginKind.Gameplay) ? concreteTypes.FirstOrDefault( static type => typeof(IAcDreamPlugin).IsAssignableFrom(type)) : null; bool registerRenderPack = manifest.Declares(PluginKind.RenderPack) && renderPacks is not null; CountingRenderPackRegistry? countedRenderPacks = registerRenderPack ? new CountingRenderPackRegistry(renderPacks!) : null; Type? renderPackType = null; if (registerRenderPack) { Type[] renderPackTypes = concreteTypes .Where(static type => typeof(IRenderPackPlugin).IsAssignableFrom(type)) .ToArray(); if (renderPackTypes.Length != 1) { return new LoadedPlugin( manifest, Plugin: null, LoadContext: alc, Error: new InvalidOperationException( $"render-pack entry DLL '{manifest.EntryDll}' must contain exactly " + "one IRenderPackPlugin implementation; found " + renderPackTypes.Length)); } renderPackType = renderPackTypes[0]; if (!renderPackType.IsVisible || renderPackType.GetConstructor(Type.EmptyTypes) is null) { return new LoadedPlugin( manifest, Plugin: null, LoadContext: alc, Error: new InvalidOperationException( $"render-pack entry type '{renderPackType.FullName}' must be public, " + "non-abstract, and expose a public parameterless constructor")); } } if (manifest.Declares(PluginKind.Gameplay) && pluginType is null) { return new LoadedPlugin( manifest, Plugin: null, LoadContext: alc, Error: new InvalidOperationException( $"no IAcDreamPlugin implementation found in {manifest.EntryDll}")); } if (registerRenderPack && renderPackType is null) { return new LoadedPlugin( manifest, Plugin: null, LoadContext: alc, Error: new InvalidOperationException( $"no IRenderPackPlugin implementation found in {manifest.EntryDll}")); } if (pluginType is null && renderPackType is null) { return new LoadedPlugin( manifest, Plugin: null, LoadContext: alc, Error: new InvalidOperationException( "the host did not supply any facility declared by this plugin")); } object? sharedInstance = null; if (pluginType is not null) { sharedInstance = Activator.CreateInstance(pluginType); instance = (IAcDreamPlugin?)sharedInstance ?? throw new InvalidOperationException( $"could not construct IAcDreamPlugin {pluginType.FullName}"); instance.Initialize(host); } if (renderPackType is not null) { object renderObject = ReferenceEquals(renderPackType, pluginType) ? sharedInstance! : Activator.CreateInstance(renderPackType) ?? throw new InvalidOperationException( $"could not construct IRenderPackPlugin {renderPackType.FullName}"); renderPackInstance = (IRenderPackPlugin)renderObject; renderPackInstance.Register(countedRenderPacks!); if (countedRenderPacks!.RegistrationCount == 0) { throw new InvalidOperationException( $"render-pack entry point '{renderPackType.FullName}' registered no packs"); } } return new LoadedPlugin( manifest, instance, alc, Error: null, renderPackInstance); } catch (Exception ex) { // The caller owns rollback for a partial instance/context. In // particular, Initialize may already have attached host callbacks; // the per-plugin host scope must remove those registrations before // Disable or any collectible unload request can run. return new LoadedPlugin( manifest, Plugin: instance, LoadContext: alc, Error: ex, RenderPackPlugin: renderPackInstance); } } private sealed class CountingRenderPackRegistry(IRenderPackRegistry inner) : IRenderPackRegistry { private int _registrationCount; internal int RegistrationCount => Volatile.Read(ref _registrationCount); public IDisposable Register( RenderPackDescriptor descriptor, IRenderPackAssets assets) { IDisposable registration = inner.Register(descriptor, assets) ?? throw new InvalidOperationException( "The render-pack registry returned a null registration handle."); Interlocked.Increment(ref _registrationCount); return new CountedRegistration(this, registration); } private sealed class CountedRegistration( CountingRenderPackRegistry owner, IDisposable inner) : IDisposable { private CountingRenderPackRegistry? _owner = owner; private IDisposable? _inner = inner; public void Dispose() { CountingRenderPackRegistry? activeOwner = Interlocked.Exchange(ref _owner, null); if (activeOwner is null) return; Interlocked.Decrement(ref activeOwner._registrationCount); Interlocked.Exchange(ref _inner, null)?.Dispose(); } } } }