#nullable enable using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Loader; namespace ShrinkModFramework.Godot; public sealed class ShrinkGodotModLoader : IDisposable { private sealed record LoadedMod(AssemblyLoadContext LoadContext, WeakReference UnloadReference, ShrinkModContext Context, IReadOnlyList Instances); private readonly Dictionary _loaded = new(StringComparer.OrdinalIgnoreCase); public IReadOnlyCollection LoadedAssemblyPaths => _loaded.Keys; public IReadOnlyList Load(string assemblyPath) { assemblyPath = Path.GetFullPath(assemblyPath); if (_loaded.ContainsKey(assemblyPath)) throw new InvalidOperationException($"Mod assembly is already loaded: {assemblyPath}"); var loadContext = new AssemblyLoadContext($"ShrinkMod:{Path.GetFileNameWithoutExtension(assemblyPath)}", true); loadContext.Resolving += (_, name) => ResolveDependency(loadContext, Path.GetDirectoryName(assemblyPath)!, name); try { var assembly = loadContext.LoadFromAssemblyPath(assemblyPath); var entries = assembly.GetTypes().Where(type => !type.IsAbstract && typeof(IShrinkMod).IsAssignableFrom(type) && type.GetCustomAttribute() != null).OrderBy(type => type.GetCustomAttribute()!.LoadOrder).ToArray(); var context = new ShrinkModContext(Path.GetDirectoryName(assemblyPath)!); var instances = entries.Select(type => (IShrinkMod)(Activator.CreateInstance(type) ?? throw new InvalidOperationException($"Failed to create mod entry: {type.FullName}"))).ToArray(); foreach (var mod in instances) mod.OnConstruct(context); foreach (var mod in instances) mod.OnRegisterContent(context); foreach (var mod in instances) mod.OnInitialize(context); foreach (var mod in instances) mod.OnReady(context); _loaded[assemblyPath] = new LoadedMod(loadContext, new WeakReference(loadContext), context, instances); return instances; } catch { loadContext.Unload(); throw; } } public bool Unload(string assemblyPath) { assemblyPath = Path.GetFullPath(assemblyPath); if (!_loaded.Remove(assemblyPath, out var loaded)) return false; for (var index = loaded.Instances.Count - 1; index >= 0; index--) if (loaded.Instances[index] is IShrinkModUnload unload) unload.OnUnload(loaded.Context); loaded.LoadContext.Unload(); return true; } public void Dispose() { foreach (var path in _loaded.Keys.ToArray()) Unload(path); } private static Assembly? ResolveDependency(AssemblyLoadContext context, string directory, AssemblyName name) { var shared = AssemblyLoadContext.Default.Assemblies.FirstOrDefault(assembly => assembly.GetName().Name == name.Name); if (shared != null) return shared; var path = Path.Combine(directory, name.Name + ".dll"); return File.Exists(path) ? context.LoadFromAssemblyPath(path) : null; } }