Creating a custom mapper type
This is a step-by-step tutorial to adding a custom mapper type to the game.
Mapper types are basically values that can be edited using the block mapper (also applies
to entities, GenericDataHolder, etc.).
Custom mapper types are a somewhat advanced feature, but they allow for interesting possibilities by expanding what the block mapper can display.
An existing mod with the ability to run custom code is required to follow this tutorial. See Creating a mod and Custom Code for information on how to get to this point.
The three components of a mapper type
A custom mapper type is composed of three types: The underlying value that is edited (T
in the remainder of this document),
a class extending MCustom<T> (TMapper) and a class extending CustomSelector<T, TMapper>
(TSelector).
The basic idea is that T is the type we ultimately want to attach to an object and edit.
TMapper wraps this type and provides methods for (de)serializing it as well as behaviour
for the undo system and some other things.
TSelector is responsible for creating and handling the interface that is display in the
block mapper to actually edit the mapper type.
It is easiest to create a custom mapper type using an immutable value type (e.g. all C# primitive types) as underlying type, or structuring the remaining code in such a way that the underlying type is treated as immutable even if it is not. It is also possible to use mutable types but this requires more care and more code.
The remainder of this document will assume T is an immutable value type, for more information
on what is required for other cases, see the complete documentation.
The first step:
- Decide on a type to use for
T(or create one)
The TMapper class
- Create a class extending
MCustom<T>
By convention, these are named MSomething (e.g. MKey, MToggle, etc.) but this is not
required. In this document we'll call it TMapper.
public class TMapper : MCustom<T> {
}
- Create a constructor
There must be a constructor that calls the base constructor. By convention it has the
signature string displayName, string key, T defaultValue and calls the base constructor
with the same arguments, but it is also possible to receive additional parameters or omit
the default value to always pass the same one to the base constructor.
public TMapper(string displayName, string key, T defaultValue) : base(displayName, key, defaultValue)
{ }
- Create (de)serialization methods
The game stores mapper type values using the XData framework and thus there needs
to be a way of converting T from and to XData objects. This is achieved by overriding
the XData SerializeValue(T value) and T DeSerializeValue(XData data) methods.
One simple possibility is converting between T and a string (which is demonstrated below)
but depending on T there may be more appropriate ways (e.g. using other primitive types
or array types).
The XData object returned from SerializeValue should always have a key equivalent to the
SerializationKey property.
public XData SerializeValue(T value) {
return new XString(SerializationKey, value.ToString());
}
public T DeSerializeValue(XData data) {
return T.FromString((string) (XString) data);
}
There are other methods that can be overridden if necessary, but these are the only ones that must always be specified. For more information see the complete documentation.
The TSelector class
- Create a class extending
CustomSelector<T, TMapper>
public class TSelector : CustomSelector<T, TMapper>` {
}
The convention for naming these is <name of TMapper without the "M">Selector, e.g.
a ToggleSelector for MToggle etc. There is again no hard requirement to follow this
convention though.
- Creating the interface
Override the CreateInterface method and add code to create the actual UI for editing
the mapper type.
This is where creating a mapper type becomes somewhat tedious because the UI system used in the block mapper is not well-suited to creating it from code.
The UI consists of normal GameObjects that should all be beneath the Content object in
the hierarchy.
The Elements property contains some helper methods to create common objects and add common
behaviour that is consistent with the remainder of the game UI. The Materials property
gives access to materials commonly used in the block mapper. Depending on the exact UI,
these may be sufficient, otherwise it is necessary to create objects by hand.
When adding the behaviour to these elements that actually edits the underlying value, it is
important to ensure InvokeChanged is called on the TMapper instance (done automatically
when Value is set) and OnEdit is called on the selector.
- Updating the interface
UpdateInterface must also be overridden and is called whenever the underlying value changed
and should update the interface to display the new value.
For more information on creating the interface, see the complete documentation.
Registering the mapper type
As a last step, the mapper type needs to be registered by calling
CustomMapperTypes.AddMapperType<T, TMapper, TSelector>(). This should be done during
execution of OnLoad and before any attempt is made to add the mapper type to any object.
Now the mapper type can be added using the AddCustom(TMapper) method of any SaveableDataHolder
(block, entities, GenericDataHolder).