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?
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:
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=184.108.40.206, 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:
Then, we compare the Assembly.FullName.Split(‘,’)  (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.
364 total views, 2 views today
Serialization has always been a thorn in my side. In VB6. in Python, in C#. The biggest annoyance is that most applications consist of somewhat complex object relationships and heirarchy’s, different race conditions about values that need to be initialized first, and who knows what else, all meticulously built during the course of your application, which you eventually need to save to persistent storage.
All applications can benefit from some form of persistence, even if it is only in the form of settings. As a result most Object Oriented languages have a way to allow you to “serialize” an object to a stream. In .NET, this is provided by way of the [Serializable] Attribute and the ISerializable interface, which allows you to customize the serialization somewhat.
What is interesting, at least in the case of .NET object serialization, is that you can choose one of a number of “Formatters” which will take the data acquired by the Serialization support framework and format it to one of any number of formats. Personally, I have only used The BinaryFormatter class, which will allow to write or read a serialized stream to or from a stream.
So, what use is serialization? Well, aside from the obvious ability to save the stream to a file, you can also use it in other ways. For example, since you can send the data across any stream, you could send it through a network stream, and on the other end another instance of your application would be able to rebuild the exact same object. Another usage I’ve found is for usage on the system clipboard; serialize what is being copied, plop it on the clipboard with your own format specifier, and then when you want to see if you can paste check if that specifier exists and when pasting deserialize that data from the clipboard, and you’ve got the objects that need to be pasted, which will be “new” objects separate from those copied (if they are still present).
Copying a object graph to stream, however, has a bit of boilerplate; you need to create the BinaryFormatter(), serialize, and the opposite in the other direction.
However, with generics, this task can be made a tad easier:
Two static methods that serve to input and output a object whose type implements ISerializable to and from a given stream. It also uses the GZipStream to compress and decompress that stream to try to save space.
Extending this directly to files is rather easy as well:
And huzzah! suddenly you can read and write objects to and from a file with ease.
One might even venture to create extension methods on the Stream class that provide this sort of functionality for input and output of any object supporting ISerializable, like the following, which assumes the previous routines are within a ‘ObjectStreaming’ class definition.
And there you have it, suddenly you can write objects directly to any stream, using methods of that stream Object, and that saved data is automatically GZipped for you as well.
This Does, however, present it’s own issues. If there is an error anywhere in your serialization code, using a “filter” type of stream, such as the GZipStream, may cause difficult to trace errors that cause exceptions to fire from the GZipStream itself, most notably “unexpected end of stream” type errors. You can usually trace these in Visual Studio by checking “thrown” on System.Runtime.Serialization.SerializationException in the Debug->Exceptions dialog, and allowing the data to save to a normal stream (that is, directly to a filestream or memory stream rather than by way of the GZipStream). This will allow you to determine where you made the mistake elsewhere. Typically, in my experience, it’s usually something as innocent as a misspelling, or even inconsistent capitalization.
2,681 total views, 26 views today