Files
ShrinkModFramework/Godot~/ShrinkGodotModLoader.cs

67 lines
3.2 KiB
C#

#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<IShrinkMod> Instances);
private readonly Dictionary<string, LoadedMod> _loaded = new(StringComparer.OrdinalIgnoreCase);
public IReadOnlyCollection<string> LoadedAssemblyPaths => _loaded.Keys;
public IReadOnlyList<IShrinkMod> 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<ShrinkModAttribute>() != null).OrderBy(type => type.GetCustomAttribute<ShrinkModAttribute>()!.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;
}
}