Modding:Serialization (Saving/Loading): Difference between revisions

From Caves of Qud Wiki
Jump to navigation Jump to search
(add modding info box)
(expanded the article quite a bit)
Line 1: Line 1:
[[Category:Modding]]{{Modding Info}}
[[Category:Modding]]{{Modding Info}}
== What is Serialization? ==
== What is Serialization? ==
Serialization is how games and other programs convert data into a file format, so that it can be stored between sessions. In simpler terms, it's how a game saves its data. The process of loading data from a file is called "Deserialization".  
Serialization is how games and other programs convert data ''(specifically, C# class fields)'' into a file format, so that it can be stored between sessions. In simpler terms, it's how a game saves its data. The process of loading data from a file is called "Deserialization".  


There are many different ways data can be de/serialized, for example, Qud's xml files are a way of storing data in a formatted fashion. The advantage of this is that this data works independent of game versions, however, it also takes up much more storage space. This also makes the data human readable/editable, which is useful for modding, but could allow for save editing if used for game data.
There are many different ways data can be de/serialized, for example, Qud's xml files are a way of storing data in a formatted fashion. The advantage of this is that this data works independent of game versions, however, it also takes up much more storage space. This also makes the data human readable/editable, which is useful for modding, but could allow for save editing if used for game data.


These reasons are why Qud uses an unformatted save format. In this format, the game simply saves the raw data of in-game objects with only enough information necessary to load the data back into the game. This data takes up less space, and cannot easily be manipulated by players, but is not compatible between game/mod versions.
These reasons are why Qud uses an unformatted save format. In this format, the game simply saves the raw data of in-game objects with only enough information necessary to load the data back into the game. This data takes up less space, and cannot easily be manipulated by players, but is not compatible between game/mod versions.
== How Serialization Works for Qud ==
In many cases, you can simply include the the <code>[Serializable]</code> attribute at the top of your class definition, and Qud's game engine will take care of serializing all of your <code>public</code> class fields for you. Specifically, Qud is capable or correctly serializing all intrinsic types (such as <code>string</code>, <code>int</code>, etc.) as well as containers of those types (such as <code>List<string></code>). If your class only uses intrinsic types, the <code>[Serializable]</code> attribute should be all that you need. Qud will properly save and load your data without further effort. Note that <code>private</code> and <code>protected</code> class fields are not serialized by default. If you want the values associated with private or protected class fields to be retained through a save and load, you need to mark each individual private or protected field with a <code>[SerializeField]</code> attribute.
Here is an example of the basic concepts discussed above:<syntaxhighlight lang="c#">
namespace XRL.World.Parts
{
    [Serializable] //this attribute causes all public fields to be serialized automatically
    public class MyCoolModPart : IPart
    {
        public float CoolnessRatio;  //this field gets serialized!
        public int AbilityLevel;    //so does this field!
        private string CurrentValue; //because it's marked private, this field does NOT get serialized - the value is cleared out after a saved game is reloaded! Be careful with how you use this value.
        [SerializeField]
        private string SecretID;    //this field DOES get serialized even though it's private, because it's marked with the SerializeField attribute!
        public void MyMethod()
        {
            //...
        }
    }
</syntaxhighlight>Object fields are not serialized automatically. If your class introduces a new field for an object type or a container of objects, such as a GameObject field that stores a reference to some particular object, a List<LiquidVolume> that stores a list of liquid volumes, or a completely new type of object introduced by your mod, you must implement custom serialization for those object fields, or your object references will no longer be valid after the game is reloaded.
However, if your class extends a built-in class, such as <code>IPart</code>, object fields from the built-in class will already have the required custom serialization logic (typically implemented in their Save/Load or SaveData/LoadData functions). For example, IPart contains the field <code>public GameObject _ParentObject</code>, which stores a reference to the GameObject that the part is a component of. <code>IPart.Save()</code> contains the special logic necessary to serialize that GameObject when the game is saved, and <code>IPart.Load()</code> contains the logic required to deserialize that object so that it remains valid after a player reloads that save. This means that if your class extends IPart, you can safely rely on the fact that references to the parent object, such as <code>this.ParentObject</code>, will always be valid in your code.
If you can find a way to reference objects in your code without explicitly saving a reference to those objects in a new field, it's generally a good idea to avoid creating that field so you don't have to serialize it. For example, there's pretty much always a convenient way to retrieve a reference to the GameObject that your part is attached to, or a reference to the Player object, without saving those objects in your own discrete fields.


== Custom Serialization ==
== Custom Serialization ==
By default, Qud handles serialization of game objects by itself, so there's nothing you need to do to make it work. However, Qud's default serialization is not sufficient for certain classes, in those cases, you need to tell the game to let you serialize a field yourself. By putting the attribute <nowiki>[NonSerialized]</nowiki> above a field, it tells the game not to save that field. You can then add your own save behavior by overriding the save function.
As described above, Qud's default serialization is not sufficient for object fields introduced by your custom classes. In those cases, you need to tell the game to let you serialize a field yourself. By putting the attribute <nowiki>[NonSerialized]</nowiki> above a field, it tells the game not to save that field. You can then add your own custom save behavior.
 
If your class extends IPart or Effect, you should override the <code>SaveData</code> and <code>LoadData</code> methods to handle your custom serialization. These are the only virtual methods the game currently makes available for serialization. If you need to serialize data outside of a class that extends IPart or Effect, you will need to look at the <code>SerializationReader</code> and <code>SerializationWriter</code> classes and implement your own serialization logic.
 
Qud does provide some serialization helper functions. In particular, serializing GameObject fields or lists is particularly easy if you use the <code>WriteGameObject</code> or <code>WriteGameObjectList</code> functions. Here's an example straight from the game that shows how Qud serializes a character's inventory, which is stored as a list of GameObjects in the <code>Inventory</code> class:<syntaxhighlight lang="c#">
namespace XRL.World.Parts
{
[Serializable]
public class Inventory : IPart
{
public override void SaveData(SerializationWriter Writer)
{
Writer.WriteGameObjectList(this.Objects);
base.SaveData(Writer);
}
 
//...
 
public override void LoadData(SerializationReader Reader)
{
Reader.ReadGameObjectList(this.Objects, null);
base.LoadData(Reader);
}
 
//...


A common use for this is serializing lists of objects, because Qud's serialization does not correctly handle object lists (lists of intrinsic types such as string or int usually do not require serialization).
[NonSerialized]
public List<GameObject> Objects = new List<GameObject>();


Below is an example segment of code that shows how to serialize your lists.
//...
<syntaxhighlight lang="csharp">
}
}
</syntaxhighlight>Below is an additional example segment of code with comments that shows how you might serialize a custom list.<syntaxhighlight lang="csharp">
...
...
         // The non-serialized attribute signals not to save this list when SaveData is called.
         // The non-serialized attribute signals not to save this list when SaveData is called.

Revision as of 18:48, 5 July 2019

This page is about modding. See the modding overview for an abstract on modding.
This page is about modding. See the modding overview for an abstract on modding.

What is Serialization?

Serialization is how games and other programs convert data (specifically, C# class fields) into a file format, so that it can be stored between sessions. In simpler terms, it's how a game saves its data. The process of loading data from a file is called "Deserialization".

There are many different ways data can be de/serialized, for example, Qud's xml files are a way of storing data in a formatted fashion. The advantage of this is that this data works independent of game versions, however, it also takes up much more storage space. This also makes the data human readable/editable, which is useful for modding, but could allow for save editing if used for game data.

These reasons are why Qud uses an unformatted save format. In this format, the game simply saves the raw data of in-game objects with only enough information necessary to load the data back into the game. This data takes up less space, and cannot easily be manipulated by players, but is not compatible between game/mod versions.

How Serialization Works for Qud

In many cases, you can simply include the the [Serializable] attribute at the top of your class definition, and Qud's game engine will take care of serializing all of your public class fields for you. Specifically, Qud is capable or correctly serializing all intrinsic types (such as string, int, etc.) as well as containers of those types (such as List<string>). If your class only uses intrinsic types, the [Serializable] attribute should be all that you need. Qud will properly save and load your data without further effort. Note that private and protected class fields are not serialized by default. If you want the values associated with private or protected class fields to be retained through a save and load, you need to mark each individual private or protected field with a [SerializeField] attribute.

Here is an example of the basic concepts discussed above:

namespace XRL.World.Parts
{
    [Serializable] //this attribute causes all public fields to be serialized automatically
    public class MyCoolModPart : IPart
    {
        public float CoolnessRatio;  //this field gets serialized!
        public int AbilityLevel;     //so does this field!
        private string CurrentValue; //because it's marked private, this field does NOT get serialized - the value is cleared out after a saved game is reloaded! Be careful with how you use this value.
        [SerializeField]
        private string SecretID;     //this field DOES get serialized even though it's private, because it's marked with the SerializeField attribute!

        public void MyMethod()
        {
            //...
        }
    }

Object fields are not serialized automatically. If your class introduces a new field for an object type or a container of objects, such as a GameObject field that stores a reference to some particular object, a List<LiquidVolume> that stores a list of liquid volumes, or a completely new type of object introduced by your mod, you must implement custom serialization for those object fields, or your object references will no longer be valid after the game is reloaded.

However, if your class extends a built-in class, such as IPart, object fields from the built-in class will already have the required custom serialization logic (typically implemented in their Save/Load or SaveData/LoadData functions). For example, IPart contains the field public GameObject _ParentObject, which stores a reference to the GameObject that the part is a component of. IPart.Save() contains the special logic necessary to serialize that GameObject when the game is saved, and IPart.Load() contains the logic required to deserialize that object so that it remains valid after a player reloads that save. This means that if your class extends IPart, you can safely rely on the fact that references to the parent object, such as this.ParentObject, will always be valid in your code.

If you can find a way to reference objects in your code without explicitly saving a reference to those objects in a new field, it's generally a good idea to avoid creating that field so you don't have to serialize it. For example, there's pretty much always a convenient way to retrieve a reference to the GameObject that your part is attached to, or a reference to the Player object, without saving those objects in your own discrete fields.

Custom Serialization

As described above, Qud's default serialization is not sufficient for object fields introduced by your custom classes. In those cases, you need to tell the game to let you serialize a field yourself. By putting the attribute [NonSerialized] above a field, it tells the game not to save that field. You can then add your own custom save behavior.

If your class extends IPart or Effect, you should override the SaveData and LoadData methods to handle your custom serialization. These are the only virtual methods the game currently makes available for serialization. If you need to serialize data outside of a class that extends IPart or Effect, you will need to look at the SerializationReader and SerializationWriter classes and implement your own serialization logic.

Qud does provide some serialization helper functions. In particular, serializing GameObject fields or lists is particularly easy if you use the WriteGameObject or WriteGameObjectList functions. Here's an example straight from the game that shows how Qud serializes a character's inventory, which is stored as a list of GameObjects in the Inventory class:

namespace XRL.World.Parts
{
	[Serializable]
	public class Inventory : IPart
	{
		public override void SaveData(SerializationWriter Writer)
		{
			Writer.WriteGameObjectList(this.Objects);
			base.SaveData(Writer);
		}

		//...

		public override void LoadData(SerializationReader Reader)
		{
			Reader.ReadGameObjectList(this.Objects, null);
			base.LoadData(Reader);
		}

		//...

		[NonSerialized]
		public List<GameObject> Objects = new List<GameObject>();

		//...
	}
}

Below is an additional example segment of code with comments that shows how you might serialize a custom list.

...
        // The non-serialized attribute signals not to save this list when SaveData is called.
        [NonSerialized]
        public List<StandAbility> abilities = new List<StandAbility>();

        // SaveData is called when the game is ready to save this object, so we override it here.
        public override void SaveData(SerializationWriter Writer)
        {
            // We have to call base.SaveData to save all normally serialized fields on our class
            base.SaveData(Writer);
            // Writing out the number of items in this list lets us know how many items we need to read back in on Load
            Writer.Write(abilities.Count);
            foreach (StandAbility ability in abilities)
            {
                // Here, we call the save function on each ability because they are game objects
                ability.Save(Writer);
                // If our list was full of basic types, such as integers, instead, we would call Writer.Write, for example:
                // Writer.Write(someNumber)
            }
        }

        // Load data is called when loading the save game, we also need to override this
        public override void LoadData(SerializationReader Reader)
        {
            // Load our normal data
            base.LoadData(Reader);
            // Read the number we wrote earlier telling us how many items there were
            int arraySize = Reader.ReadInt32();
            for (int i = 0; i < arraySize; i++)
            {
                // Load returns a generic object, so we have to cast it to our object type before we add it to our list.
                abilities.Add((StandAbility)Load(Reader));
                // Similar to above, if we had a basic type in our list, we would instead use the Reader.Read function specific to our object type.
            }
        }
...

There may be other instances where you need custom serialization. The methods shown above can be applied to other situations as well.