67 lines
3.2 KiB
C#
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;
|
|
}
|
|
}
|