Show / Hide Table of Contents

Serialization

The mod loader needs to deserialize a variety of XML files to load mods.

In some cases, the API used to configure this deserialization may also be used by mods, e.g. when writing custom Block Modules.

This document describes how set up custom types to be deserialized by the system.

Basics

At its core, the mod loader uses the standard .NET XmlSerializer class/framework to implement deserialization, but with some added functionality.

Every type that can be deserialized should have either a [Serializable] or a [XmlRoot] attribute, and usually derive from Modding.Serialization.Element (this is automatically the case most of the time, e.g. a class inheriting BlockModule also inherits from Element).

Within a type that should be deserialized, every public member should at minimum have a [XmlIgnore], [XmlElement], or [XmlAttribute] attribute, depending on how it should be treated during deserialization.

The XmlElement, XmlAttribute, and XmlRoot attributes take an optional name parameter that can be used to specify what name the corresponding field/property/type should have in the XMl files. If the parameter is not included, the name is based on the C# name of the element.

Validation

The XmlSerializer and the mod loader deserialization system provide some logic to validate that the modder or user provided XML files deserialize into a valid object.

This not only includes the XML syntax checked XmlSerializer but also some additional features to verify that the resulting object contains valid state, according to what data is valid for a specific type.

Ideally, the valid states can also be described using attributes, though it is also possible to encode more complex validation logic manually.

Validation-related attributes

By default, all XmlElements and XmlAttributes are assumed to be required and deserialization will not succeed when one or more are missing.

To mark an element or attribute as optional, apply a [DefaultValue(x)] attribute to it. (The one in System, not in UnityEngine.)

Important: The value passed to DefaultValue is not important! It is not automatically assigned if the value is missing. It also doesn't have to type-check, it is perfectly valid to pass null even for non-nullable types.

There are a few ways of handling optional values: A default value can be assigned in the constructor, this will stay if the element/attribute is not present but is overwritten if it is present.

Alternatively, for a field called SomeField that should be deserialized, a field [XmlIgnore] public bool SomeFieldSpecified can be added to the type. This field will be true if the value was included in the XML file and false if it was not.

Lastly, when including elements/attributes that also extend Element, adding a [RequireToValidate] attribute will cause them to be automatically validated too.

Manual validation logic

To manually validate state of your type that cannot be encoded using the attributes described, override the bool Validate(string elemName) method in your class.

This method is called to validate the element after basic deserialization has occurred. It should return true if the object is valid, or false if it is not.

To run the default attribute-based checks in addition to your custom code, start the method with if (!base.Validate(elemName)) return false;.

Whenever possible, the MissingElement, MissingAttribute, and InvalidData methods should be used in an error case, this will ensure that error messages are printed and properly formatted, including line number and file name. These methods can be used like this:

if (<some condition>) {
    // The condition above told us that the "Foo" element is missing.
    return MissingElement(elemName, "Foo");
}

Lists and Arrays

Lists and arrays require special attributes to deserialize. For basic information about how to deserialize these types, please see the documentation for the normal XmlSerializer.

In addition, by default an error is thrown when a list is specified but empty. Lists that can be empty should be marked with the [CanBeEmpty] attribute.

Reloading

The serialization system also has features to facilitate the "reloading" of XML files at runtime, i.e. applying changes in the XML file directly to the corresponding objects in-game.

This can be seen in action with, for example, colliders and adding points of blocks, which are reloaded if the Debug element of the mod is set to true.

Reloading can be enabled for your custom deserializable types too, but only when the underlying mod loader feature loading the XML file containing your type supports it. This is the case for modules, for example.

To enable reloading, mark your type with a Modding.Serialization.Reloadable attribute.

Then, also add a Reloadable attribute to all fields and properties whose values should be dynamically replaced when a reload happens.

Lastly, make your type implement IReloadable. This interface requires two methods: void OnReload(IReloadable newObject) and void PreprocessForReloading().

In many cases, these can simply be kept empty, but sometimes it may be necessary to do some further processing in order to make reloading work correctly. This can be achieved using the two methods.

OnReload is called after the normal reloading has been performed and can be used if any additional values from the new object are needed that can't just be copied directly by the system.

PreprocessForReloading is called on the new object before the normal reloading process takes place. As the name suggests, it can be used to perform any preprocessing needed to correctly populate all values that should later be copied.

Common Elements

The mod loader contains some classes to deserialize values that are needed frequently.

Vector3

Unity's Vector3 class does unfortunately not (de)serialize correctly with the system. Use the Modding.Serialization.Vector3 class instead.

It has the same x, y and z components, but does not offer an additional functionality beyond that. It can however be cast to a Unity Vector3, and even supports implicit conversions, so a Modding.Serialization.Vector3 can be assigned to a field/variable of type UnityEngine.Vector3 and vice-versa without any explicit casts.

TransformValues

For cases where a position, rotation, and/or scale must be specified, instead of using 2 or more Vector3s, the TransformValues class can be used instead.

It's usage depends on what values are needed exactly, and whether any default values are present.

In the case that all 3 values are required, none are optional, just add the TransformValues field/property, an XmlElement attribute and a RequireToValidate attribute.

If there are any default values available, don't add the RequireToValidate attribute. Instead, override the Validate method in your type according to the instructions above.

Then, as custom validation logic, you can set default values on the object and ultimately call Check on it. An example is below:

protected override bool Validate(string elemName) {
    if (!base.Validate(elemName)) return false;

    SomeTransformValue
        .SetPositionDefault(new Vector3(0f, 0f, 0f))
        .SetRotationDefault(new Vector3(0f, 0f, 0f))
        .SetScaleDefault(new Vector3(0f, 0f, 0f));

    if (!SomeTransformValue.Check("SomeTransformValue")) return false;
}

It is possible to specify any combination of defaults, e.g. one for rotation and scale but none for position. The values without default will be treated as required to be specified by the user.

Additionally, the HasNoScale() method can also be called to specify that the object does not support a scale at all. In that case, a warning will be printed when a scale child element is included in the XML, but no error.

Lastly, there is a SetOnTransform(Transform t) method available to set all three (or, if HasNoScale was called, all two) values on a Transform. This will set the values as local coordinates, not world coordinates.

Direction

A simple enum that has possible values X, Y, and Z to avoid redefining this frequently.

Also provides two useful extension methods:

ToAxisVector returns a unit vector pointing in the direction given by the enum value.

GetAxisComponent takes an additional Vector3 and returns the component corresponding to the enum value.

MapperTypes

There are classes available for defining MKeys, MToggles, MSliders, MValues, and MColourSliders. These are for example used the ModuleMapperTypes system.

The key, displayName, and showInMapper attributes are present on all mapper types, with key and displayName always being required and showInMapper being optional (true by default).

  • <Key displayName="Name" key="key" default="G" />
  • <Slider displayName="Name" key="key" min="0.0" max="10.0" default="5.0" /> Additional optional attribute: unclamped, false by default. If unclamped is set to true, min and max only apply to the slider itself but it is possible to type in values outside of these bounds.
  • <Toggle displayName="Name" key="key" default="true" />
  • <Value displayName="Name" key="key" default="5.0" />
  • <ColourSlider displayName="Name" key="key" r="1.0" g="1.0" b="1.0" snap="false" /> r, g, and b define the default colour, an optional a alpha value can also be included. snap determines if any color should be allowed or if the slider to stick to certain predefined colors.
Back to top Generated by DocFX