Godot Version
4.2.2 Mono
Question
Hi!
I am trying to make a mod system using reflection, but I am running into some issues.
I have my GoDot project called Game, a second .net project called ModLoader, and a third GoDot project called Mod.
ModLoader contains a public IMod interface. And a loader function which looks trough assemblies to find classes which implement this interface.
Game instances this ModLoader, and get a list of all available mods.
The Mod project has a bunch of resources and of course a class
public sealed class ModInfo : IMod
The reason the IMod interface and the loader are in a separate project are because I had issues when these where in the Game project. Most likely because GoDot does some CodeGeneration and this cause some discrepancies where it doesn’t recognize the interfaces properly.
My Loader looks as such:
public class ModLoader
{
private readonly Guid _targetGuid;
private readonly string _directory;
public ModLoader(Guid targetGuid, string directory)
{
_targetGuid = targetGuid;
_directory = directory;
}
public IEnumerable<LoadedMod> Load()
{
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
foreach (IMod mod in GetMods())
{
if (!mod.TargetGUID.Equals(_targetGuid))
{
yield return LoadedMod.LoadFailure(mod, $"targets incorrect GUID. Expected {_targetGuid}, but was {mod.TargetGUID}");
continue;
}
yield return LoadedMod.LoadSuccess(mod);
}
AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
}
private Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs args)
{
var result = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a =>
{
string sourceAssembly = args.Name.Split(',').FirstOrDefault();
string targetAssembly = a.FullName.Split(',').FirstOrDefault();
return sourceAssembly.Equals(targetAssembly, StringComparison.OrdinalIgnoreCase);
});
return result;
}
private IEnumerable<IMod> GetMods()
{
foreach (var dllFile in DLLFiles())
{
Assembly assembly = Assembly.LoadFrom(dllFile);
Type? type = GetMod(assembly);
if (type != null)
{
_logger?.Log(LogTypes.ModLoaderDebug, $"Creating IMod instance");
var result = (IMod?)Activator.CreateInstance(type);
if (result != null)
{
yield return result;
}
}
}
}
private Type? GetMod(Assembly assembly)
{
Type? result = null;
foreach (Type type in assembly.GetTypes())
{
if (typeof(IMod).IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract)
{
if (result != null)
{
return null;
}
result = type;
}
}
return result;
}
private IEnumerable<string> DLLFiles()
{
return Directory.GetFiles(_directory, "*.dll", SearchOption.AllDirectories);
}
}
Game calls the Load function, and gets a collection of mods in return.
GetMods() iterates trough a list of DLL files. This list contains the dll file from the Mod project.
It loads this assembly, and GetMod() iterates trough all types.
If a type is assignable from IMod, it is returned as an available mod.
Now this works in GoDot when running from the editor or visual studio. But when I export the game, and start the exported binary, it does not work anymore.
It can find the types i am expecting, but for some reason typeof(IMod).IsAssignableFrom(type);
returns false. When ignoring this and hardcasting it gives an InvalidCastException.
typeof(IMod).AssemblyQualifiedName
and type.AssemblyQualifiedName
are identical, even the versions are the same. So I see no reason this shouldn’t work.
Another thing to note is that I have made a custom Assembly Resolve function. This does some simple name checks to return an assembly. This is not entirely correct, but without it the ModLoader assembly could not be found.
But, when running the exported game, this Assembly Resolve function is not called at all. It now can find the assemblies by itself, but seemlingly there is still a mismatch.
I have messed around with different export settings, but no luck so far.
Maybe someone else has some tips to look into
Thanks!