> For the complete documentation index, see [llms.txt](https://kinematicsoup.gitbook.io/reactor/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://kinematicsoup.gitbook.io/reactor/tutorials/custom_property_types.md).

# Custom Types in Properties

## Summary

Entity, room and player properties hold a `ksMultiType`, which covers primitives, vectors, colors, quaternions, strings, and arrays of each. Real games quickly need something structured instead: an inventory, a loadout, a stats block, a build order.

This tutorial shows how to put your own type in a property by implementing `ksISerializable`, how to read it back, and the one rule about it that catches everyone: a packed property is a snapshot, not a live reference.

### Requirements

* [Reactor Requirements](/reactor/overview.md#requirements)
* Completion of [Networked Properties and RPCs](/reactor/tutorials/properties_and_rpcs.md)

## What ksMultiType Already Covers

Before writing a custom type, check whether you need one. `ksMultiType` natively stores `byte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `float`, `double`, `char`, `bool`, `string`, `ksVector2`, `ksVector3`, `ksVector2Int`, `ksVector3Int`, `ksColor`, `ksQuaternion`, and an array form of each.

A custom type earns its place when a value only makes sense as a group. Three parallel float arrays that must stay index-aligned are a custom type waiting to happen. A single cooldown timer is not.

## Implementing ksISerializable

The interface is two methods, and they must mirror each other exactly: whatever `Serialize` writes, `Deserialize` reads back in the same order and the same widths.

Put the type in your common assembly so both sides can use it. See [How Server Code Is Compiled](/reactor/architecture/server-code-and-assemblies.md) for where that is.

*Loadout.cs*

```c#
using KS.Reactor;

public class WeaponSlot : ksISerializable
{
    public string Weapon;
    public ushort Ammo;

    // Required. ReadSerializableArray<T> constrains T to new(), and declaring any other
    // constructor removes the implicit parameterless one.
    public WeaponSlot() { }

    public WeaponSlot(string weapon, ushort ammo)
    {
        Weapon = weapon;
        Ammo = ammo;
    }

    public void Serialize(ksStreamBuffer output)
    {
        output.Write(Weapon);
        output.Write(Ammo);
    }

    public void Deserialize(ksStreamBuffer input)
    {
        Weapon = input.ReadString();
        Ammo = input.ReadUShort();
    }
}

public class Loadout : ksISerializable
{
    public string Name;
    public byte Level;
    public ksVector3 SpawnOffset;
    public WeaponSlot[] Slots;

    public void Serialize(ksStreamBuffer output)
    {
        output.Write(Name);
        output.Write(Level);
        output.Write(SpawnOffset.X);
        output.Write(SpawnOffset.Y);
        output.Write(SpawnOffset.Z);

        // Nested custom types compose. The array form writes its own length.
        output.WriteSerializableArray(Slots);
    }

    public void Deserialize(ksStreamBuffer input)
    {
        Name = input.ReadString();
        Level = input.ReadByte();
        float x = input.ReadFloat();
        float y = input.ReadFloat();
        float z = input.ReadFloat();
        SpawnOffset = new ksVector3(x, y, z);
        Slots = input.ReadSerializableArray<WeaponSlot>();
    }
}
```

Pick the narrowest type that fits. `Ammo` is a `ushort` rather than an `int` because a magazine count never exceeds 65535, and that choice costs two bytes every time the property changes.

> *`ksStreamBuffer` has matched pairs for every primitive, plus `WriteEncodedValue` and `ReadEncodedInt` and friends for 7 bit encoded integers, which are smaller for values that are usually small. There is no automatic schema. If `Serialize` and `Deserialize` disagree, you get garbage rather than an error, so keep them adjacent and edit them together.*

## Storing and Reading

Pack with `ksMultiType.FromSerializable`, unpack with `ToSerializable<T>`.

*Server*

```c#
public class seAvatar : ksServerEntityScript
{
    private Loadout m_loadout;

    public override void Initialize()
    {
        m_loadout = new Loadout();
        m_loadout.Name = "Scout";
        m_loadout.Level = 3;
        m_loadout.SpawnOffset = new ksVector3(0.5f, 1.5f, -0.25f);
        m_loadout.Slots = new WeaponSlot[]
        {
            new WeaponSlot("rifle", 30),
            new WeaponSlot("pistol", 12)
        };
        Properties[Prop.LOADOUT] = ksMultiType.FromSerializable(m_loadout);
    }
}
```

*Client*

```c#
public class ceLoadout : ksEntityScript
{
    public override void Initialize()
    {
        Entity.OnPropertyChange[Prop.LOADOUT] += OnLoadoutChange;
        Apply(Properties[Prop.LOADOUT]);
    }

    public override void Detached()
    {
        Entity.OnPropertyChange[Prop.LOADOUT] -= OnLoadoutChange;
    }

    private void OnLoadoutChange(ksMultiType oldValue, ksMultiType newValue)
    {
        Apply(newValue);
    }

    private void Apply(ksMultiType value)
    {
        // ksMultiType is a value type, so there is no null to test for. A property that has
        // never been set reports Types.NULL.
        if (value.Type == ksMultiType.Types.NULL)
        {
            return;
        }
        Loadout loadout = value.ToSerializable<Loadout>();
        // Drive your UI from loadout here.
    }
}
```

As with any property, read the current value in `Initialize` as well as handling the change event. A client that sees the entity for the first time receives the value as initial state, not as a change, so a handler alone would miss it.

Arrays of a custom type have their own pair, which avoids wrapping the array in a container type:

```c#
Properties[Prop.SLOTS] = ksMultiType.FromSerializableArray(slots);
WeaponSlot[] slots = Properties[Prop.SLOTS].ToSerializableArray<WeaponSlot>();
```

## A Packed Property Is a Snapshot

This is the rule to internalise. `FromSerializable` runs your `Serialize` immediately and stores the resulting bytes. The property does not hold a reference to your object. Changing the object afterwards changes nothing that anyone else will ever see.

```c#
Loadout loadout = new Loadout();
loadout.Slots = new WeaponSlot[] { new WeaponSlot("rifle", 30) };
Properties[Prop.LOADOUT] = ksMultiType.FromSerializable(loadout);

loadout.Slots[0].Ammo--;      // Local object only. Nothing is sent.

Properties[Prop.LOADOUT] = ksMultiType.FromSerializable(loadout);   // Now it is sent.
```

Confirming this directly, packing a loadout, mutating the source, then unpacking:

```
restored   = Loadout(Scout L3 off=(0.5, 1.5, -0.25) [rifle:30, pistol:12])
source now = Loadout(MUTATED L3 off=(0.5, 1.5, -0.25) [rifle:999, pistol:12])
```

The restored copy kept the values as they were at the moment of packing. Unpacking likewise produces a fresh object each call, so `ToSerializable<T>()` twice gives you two unrelated instances, and writing to either changes nothing.

The practical consequence is that every change means re-serializing and reassigning the whole value. Structure your data so that things which change at different rates live in different properties. A loadout that changes on respawn and an ammo count that changes every shot should not share one property, or you will resend the weapon names on every trigger pull.

## What You Give Up

A custom type is opaque to everything above the byte level, which costs you three things.

**No field level delta.** The property is replaced as a unit. Changing one number resends the entire payload.

**No prediction.** The predictors interpolate typed values, and their predicted property types are the numeric and vector kinds listed in [Motion Prediction](/reactor/examples/predictors.md). A packed custom type is not among them, so it arrives in discrete steps on sync frames. Anything that needs to move smoothly should be a float or a vector property in its own right, even when it is conceptually part of a larger structure.

**No inspector support.** A packed property is bytes, so tooling cannot show you the fields.

## Versioning

Your serialized format is a wire format. A client built against one version of `Deserialize` and a server built against another will not error, they will misread. If the type will outlive a single release, spend a byte on a version tag:

```c#
public void Serialize(ksStreamBuffer output)
{
    output.Write((byte)2);
    output.Write(Name);
    output.Write(Level);           // added in version 2
    output.WriteSerializableArray(Slots);
}

public void Deserialize(ksStreamBuffer input)
{
    byte version = input.ReadByte();
    Name = input.ReadString();
    Level = version >= 2 ? input.ReadByte() : (byte)1;
    Slots = input.ReadSerializableArray<WeaponSlot>();
}
```

## Common Problems

| Symptom                                                                        | Cause                                                                                                              |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
| Values come back as garbage, or reads run off the end of the buffer            | `Serialize` and `Deserialize` disagree on order or width. They are not checked against each other.                 |
| Changes to the object never reach clients                                      | The property was not reassigned. Mutating the source object after packing does nothing.                            |
| `ReadSerializableArray<T>` will not compile                                    | `T` needs a public parameterless constructor. Declaring any other constructor removes the implicit one.            |
| The value is empty on a client that joined late                                | `Initialize` handles the change event but does not read the current property value.                                |
| `Cannot convert null to 'ksMultiType' because it is a non-nullable value type` | `ksMultiType` is a struct. There is no null to guard against. Test `value.Type == ksMultiType.Types.NULL` instead. |
| The property is not among the predictable types                                | Correct, custom types cannot be predicted. Split out anything that must interpolate.                               |
| Bandwidth is worse than expected                                               | The whole payload resends on every change. Split fields that change at different rates into separate properties.   |

## Where to Go Next

* [Networked Properties and RPCs](/reactor/tutorials/properties_and_rpcs.md): the property basics this builds on.
* [How Server Code Is Compiled](/reactor/architecture/server-code-and-assemblies.md): where a shared type has to live for both sides to use it.
* [Automatic Client Data Syncing](/reactor/architecture/data-syncing.md): when property values are sent.
