I've been working on the scripting system some more. I was going to rework CSharpScript's script class loading behavior to make it work more like the assembly based provider does things, but i discovered that there is no way to directly access the generated assembly's Reflection instance. I've requested that this be added:
https://github.com/dotnet/roslyn/issues/32200Once this is possible the assembly and CSharpScript providers should behave largely the same way. There won't be any need to return the type of the script class, and a callback in the script info instance will allow the user to resolve the correct class. This will make it much easier to implement per-entity custom classes by simply finding the class defined in a script that inherits from the right base class as shown in bonbon's image.
That callback should just do this:
private static Type FindEntityClass(CSharpScriptInfo info, Assembly assembly)
{
var scriptClasses = assembly.GetTypes().Where(t => t.IsClass && typeof(TScript).IsAssignableFrom(t)).ToList();
if (scriptClasses.Count == 0)
{
//No classes found that extend from the required type
throw some exception;
}
if (scriptClasses.Count > 1)
{
//More than one candidate class found
throw some exception;
}
return scriptClasses[0];
}
Where
TScript
is the type of the entity that it needs to inherit from.
The same thing will be possible using the assembly provider, which makes it easier to use both.
With this in place the game should have a simple scripting system that lets you load scripts once and then create instances of types when you need them like this:
//The script system should cache the loaded script so repeated calls return the same script instance
var script = Context.ScriptSystem.Load("my_script_file.csx");
//The script itself doesn't contain any object instances, these are created on demand only
var entity = script?.CreateEntity(Context.EntityList, typeof(Item));
It may be easier to rework the scripting system to only provide an abstraction for the provider and letting the user get the type information using the resulting
Assembly
, but this would restrict the supported languages to whatever can produce an
Assembly
instance.
In that case i can remove the type parameter from the scripting system itself, since the only purpose is to make it easier to access individual script object instances and it's better to let users keep track of them. Perhaps splitting the system up into providers and type instance factories would allow for both, i'll need to look into it further.
I'm also going to make sure that scripts can load other scripts using
#load
, which CSharpScript already supports but which requires me to provide a
SourceReferenceResolver
. This should let you split scripts up just like you can with Angelscript. That in turn means that this resolver will need to understand how the SteamPipe filesystem works. That shouldn't be a problem since the resolver class API is pretty easy to use, and should just pass through to the filesystem class i've already created for SharpLife.
Edit: i just discovered that there is an existing framework that does pretty much the same thing called Managed Extensibility Framework:
https://docs.microsoft.com/en-us/dotnet/framework/mef/It looks like it can do pretty much everything that this system can currently do, albeit without CSharpScript support. I'm going to look into it some more to see if it can do what i'm doing with my own system.
Edit: MEF can do everything that my system can do, and for the use case i have in mind for it each script assembly would need its own CompositionContainer which eliminates the problem with adding assemblies later on.
That leaves only one problem which is memory usage. I'll have to look into how much it uses for small assemblies like scripts. I read some reports about high memory usage due to dependent assemblies being loaded, but CSharpScript requires explicit references to all dependent assemblies so they have to be loaded already. Indirect dependencies might be a bigger issue but that remains to be seen. The biggest issue i need to look into is how much memory a CompositionContainer needs on its own.
Edit: Looks like an empty container is just 320 bytes in total, though that may not be 100% accurate. Looks like it's worth using like this then:
Assembly and CSharpScript based plugins are all part of one container, exporting their main script implementation using MEF. Required interfaces can be exported from the game and imported by plugins using MEF, allowing for dependency injection on both sides.
CSharpScript based map-specific scripts get their own container, with scripts being cached along with their container based on file modification time to allow easy reloading. This reduces memory usage by loading scripts only once per server lifetime. Since a compiled script is just an assembly it can't be unloaded, just like a regular assembly. Otherwise it'd leak tons of memory by way of redundant compilation and assembly generation.
Scripts that are reloaded, either due to a newer version existing on disk or by forcing a reload (cheat command) would throw out the cached version and reload it entirely. The old version would continue to be in memory and objects created from it will continue to exist and be referenced as before, so a complete refresh is needed to oust old versions (reloading the map should do it since it's a map specific script).
I'm going to look into reworking the scripting system to support this, so most of my current provider code will probably end up being thrown out. The script validation code and API configuration types are still needed so most of it will still see use. This will make it impossible to use non-CLR based languages for plugins or scripts, but i doubt that's a big deal since C# and VB are more than enough to support the required use cases.