So, as mentioned in the previous post, I added a “sort” of scriptability to BASeBlock.
I made some tweaks, and refactored the code so it was a bit more abstracted; the original implementation was directly in the MultiTypeManager, but that didn’t really have anything to do with it, so I tweaked some of the parameters to a few methods there, added a new class with the static routines required for the needed functionality, etc. I also made it so that a “BASeBlock Script Group file” (.bbsg extension) could be used to both compile a set of files into an assembly, as well as include various other assemblies as required. Future additions will probably include the ability for each assembly to define a sort of “main” method, which can be called when the assembly is initialized.
However, once again, Serialization was the constant thorn in my side. I was able to mess about with a custom block written in a .cs script, and it even saved properly.
But the game encountered an exception when I tried to open that LevelSet; I forget the specifics, something to the tune of “failed to find assembly” type of error. What could I do?
SerializationBinder
What this basically meant was I was going to have to learn even more about the Serialization structure of .NET. Specifically, SerializationBinder’s. The concept was actually quite simple. You basically just derive a type from SerializationBinder, and use that as the .Binder property on a IFormatter class, overriding one method seems to be enough for the most part:
1 |
Type BindToType(string assemblyName, string typeName) |
Simple! it gives you an assembly Name, a Type Name, and you simply return the appropriate type. The reason the default implementation wasn’t working was certainly as a result of the assembly being loaded dynamically, since it wasn’t [i]really[/i] being referenced by the BASeBlock assembly, so the default implementation didn’t find the “plugin” class assembly or the appropriate type, so threw an exception.
The trick here is not to enumerate the referenced assemblies, but rather to use all loaded assemblies in the current AppDomain. The general consensus with regards to using CodeDOM and compiling things like this is to compile them to their own AppDomain; however, since the assemblies were being kept “alive” for the duration of the application, that wasn’t necessary, and in this case would have complicated things. Well, it would have complicated things more than they already were.
The “AssemblyName” parameter, however, was more akin to the FullName property of the System.Reflection.Assembly; for example, BASeBlock’s assembly would (for the current version) be passed in as “BASeBlock, Version=1.4.0.0, Culture=neutral, PublicKeyToken=null”. Since we are only interested in the actual name of the assembly, we can simply grab everything up to the first comma.
Armed with the Assembly’s base name, we can start enumerating all the loaded Assemblies and looking for a match:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public override Type BindToType(string assemblyName, string typeName) { try { string BaseAssemblyName = assemblyName.Split(',')[0]; Assembly[] Assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly loopassembly in Assemblies) { if (loopassembly.FullName.Split(',')[0].Equals(BaseAssemblyName,StringComparison.OrdinalIgnoreCase)) { return loopassembly.GetType(typeName); } } } catch (System.Exception exception) { throw exception; } return null; } |
Then, we compare the Assembly.FullName.Split(‘,’)[0] (the text before the first comma) to the modified string, BaseAssemblyName that was changed in the same manner. I decided to go string insensitive for no reason; mostly because the assembly names for the scripts are formed from the filenames and I wouldn’t want filename capitalization to prevent a script from serializing/deserializing properly. If we find a matching assembly, we return the result of a GetType() call to that assembly with the same typename passed in as a parameter to the method. The Formatter will than attempt to deserialize the data it has to that type as needed.
There are a few issues with this- for one thing, it doesn’t work with Generic types. At least, I assume it doesn’t; I assume I would need some special code to get the appropriate Type for a generic type given type arguments. I’ll cross that bridge when I come to it.
Right now, I’ve not actually tested this extensively. My main concern would be to test that it can serialize in one session, and deserialize in another. This concern is based on the fact that the two assemblies would in that case be literally distinct- in that it would have been compiled on two occasions. Assuming the Binder is enough to convince the serializer it can deserialize something, there shouldn’t be any issues. Once I decide to figure out how to add Generics support to this Binder, I’ll definitely write about it here.
Have something to say about this post? Comment!