PluginFactory is a simple backend that lets game makers support end user-created mods and plugins provided as .NET assemblies. It's super fast, if you consider 26ns super fast.
Do you need plugins? You do if your game has these:
- Logging
- Tracing
- Units
- Behaviors
- Recipes
- Scripting
- Lots of drops
- Procedural Levels
- Procedural Characters
- Procedural Anything
Easily add the ability to mod your app or game by making any interface
pluggable.
Plugins expose constructors for pluggable classes one or more interfaces.
You ship a reference assembly with your public for modders to use.
Plugins can be loaded and unloaded to support runtime configuration changes.
You link the reference assembly into your project and write classes to implement the plugin interfaces. You package your plugin as a single .NET assembly with embedded or associated text and binary resources.
You'll get a plugin from the game or mod developer. It might be a .dll or a zip file. Just unzip that baby anywhere in your plugins/ folder and you're good to go.
The developer can easily enumerate, load and unload plugins using the Loader class. With the Plugin class, they can plugin resources and query its supported interfaces and types. Finally, they use a Factory to quickly and efficiently create new instances of those types.
This repository uses softlinks at the moment. Please turn them on. I'm in the process of getting rid of them. If softlinks are a problem you'll have to copy the reference assembly to the correct location manually for the Test.plugin project.
This project targets .NET 6.0 and C# 10 and explicit nullable.
Todo, probably a separate doc. See Usage
- .zip file support
- A Plugin that represents the game assembly (for resources mostly)
- Patch object for script patching
- Dependency system
- Plugin info/singleton types
- Multiple plugin paths for loader
- Better duplicate handling for loader
- Demo projects
- Basic logger - using just the factory class, creates loggers from the default execution context assemblies. Several builtin loggers are provided.
- Script player and plugin - an extremely simple scripting system dispatches script commands based on the first word in a line. A plugin is used to load some new scripting commands, which have access to the game infrastructure, including logging.
- Resources - the main app accesses plugin resources and plugin types have access to their own plugin resources as well as game resources (?).
Resources are associated with loaded plugins and equivalently loaded assemblies. They're not available until an assembly is loaded. However, you can store them off, unload the loader and its assemblies, and the stored resources will survive.
The resource compiler strips some extensions. Be wary of this if you are using extensions to differentiate between resources types, bitmap vs png, etc. Use a magic number based technique instead or enforce binary format standards. As far as I can tell, the compiled resource types, and whether they appear in TextKeys or BinKeys depends only on text (string) vs binary (byte[]).
I removed the .cs plumbing file used for the Resources. If your app is using a windows UI you should probably keep that.
string pluginPath = "plugins/";
using(Loader loader = new Loader(pluginPath, ".plugin")){
loader.Load();
foreach(Plugin pi in loader.Plugins) {
Console.WriteLine($"Loading plugin {pi.name}:{pi.version} ({pi.fileName})");
if(pi.Loaded) { // plugins can also be loaded on demand here
foreach(var s in pi.TextKeys) // strings and text files
Console.WriteLine($"Text: {s}={pi.Text(s)}");
foreach(var s in pi.BinKeys) // everything else
Console.WriteLine($"Bin: {s} ({pi.Bin(s)!.Length})\n{HexBytes(pi.Bin(s)!, 4, new[] { 2, 2 }, 4)}");
foreach(var s in pi.OtherKeys) // everything everything else
Console.WriteLine($"Othr: {s} Type {s.GetType()} String {s.ToString().Take(60)}");
}
}
var factory = new FooFactory(loader.GetPluginAssemblies());
foreach (string ctorName in factory.Ctors) {
ILogger newItem = factory.Create(ctorName, GetVerbosity(ctorName));
loggers.Add(newItem); // needs null check
}
}
using LoggerFactory = Factory<Verbosity, ILogger>;
public enum Verbosity { Debug, Info, Warning, Error };
public interface ILogger { void Log(string x); }
public class ConsoleLogger: ILogger {
public ConsoleLogger(Verbosity level) {}
public void Log(string x) {}
}
// Create a factory when your app starts up
var factory = new LoggerFactory();
// Create a specific logger
var result = factory.Create("ConsoleLogger", Verbosity.Info);
// Create all loggers
// Example of adding menu items for each class exposed by the plugin
foreach(var ctorName in factory.Ctors) {
loggers.Add(factory.Create(ctorName, GetVerbosity(ctorName)));
}
// Loading from serialization
foreach (var logger in config.loggers) ...
To be clear, you only create the factory once, at the start of the game or app. This was unclear to a reddit commentator, and to be fair, it is extremely slow if you create a new factory for every new object.
Use Assembly.Load() to activate plugins before creating the factory.
On an i5-5670K, underclocked, the benchmark shows an overhead of ~26ns. This was a best case; usually it runs 30-31ns. There's a couple of ways to bring that down by a few more ns but they add a little hassle.
Factory = 25.8ns, Alloc = 8.5ns, Total = 34.3ns YAY :)
We can of course naively reciprocate this to get a real big number - about 38 million in this case. If you are creating that many objects, hell, good for you, but I never will, at least from a plugin. So there's that.