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 XmlElement
s and XmlAttribute
s 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
, andb
define the default colour, an optionala
alpha value can also be included.snap
determines if any color should be allowed or if the slider to stick to certain predefined colors.