Skip to content

Custom types & collections

Sometimes a single scalar (a health float, a name string) isn’t enough. A player’s inventory might be a list of items, or a buff system might track active effects keyed by name. Custom types let you define reusable composite shapes and then reference them in entity schemas as lists or dictionaries, or as flat fields in command schemas.

Create a .yaml file in the directory set by types_schema in golem.yaml (default: schemas/types/). Each file defines one named type:

schemas/types/item.yaml
type: Item
fields:
id: { tag: 1, type: int32 }
name: { tag: 2, type: string }
count: { tag: 3, type: int32 }
  • type: PascalCase name used to reference this type in entity schemas and command schemas.
  • fields: a map of snake_case field names. Each field requires an explicit type (proto scalar: int32, string, double, bool, etc.) and a tag.
  • tag: a required positive integer, unique within the type. It maps directly to the Protobuf field number and must stay stable. See Tags below.

Custom types are global, so any entity schema or command schema can reference them.

Once you have custom types defined, use list<T> and dict<K,V> as the type value for any entity field:

schemas/entities/player.yaml
entity: Player
vars:
inventory: { tag: 1, type: list<Item>, sync: tick }
active_buffs: { tag: 2, type: "dict<string, Item>", sync: tick }
score: { tag: 3, type: int32, sync: tick }
  • list<T>: an ordered list of T. T can be any proto scalar (int32, string, etc.) or a custom type.
  • dict<K, V>: a map keyed by K (any proto scalar) to values of type V (proto scalar or custom type). Spaces around the comma are optional.

Run golem-bake after adding or editing custom types or collection fields.

You can also use a custom type directly as a field type on a command, which helps when a command payload is too structured for individual scalars:

schemas/commands/use_item.yaml
command: UseItem
target: entity
entity_type: Player
fields:
item: Item

The same Item type that entity code uses carries over to command payloads, so command handlers work with the same server-side struct, and every client you generate exposes the matching type (a TypeScript interface, a Go struct, a C# class). No extra types are generated per usage site. See Command schemas for a full walkthrough.

For each custom type, bake emits a matching type in every target you generate: a Go struct, a TypeScript interface, and a C# class. The same definition is reused whether you reference the type in an entity schema or a command schema, with no extra code generated per usage site.

On the Go server, the custom type becomes a plain struct, and any collection field that uses it gets typed getters and setters:

type Item struct {
Id int32
Name string
Count int32
}
// On SyncedPlayer, for an `inventory` field typed `list<Item>`:
func (e *SyncedPlayer) Inventory() []Item { ... }
func (e *SyncedPlayer) SetInventory(v []Item) { ... }

Passing a non-nil empty slice ([]Item{}) to a setter clears the collection on the next delta. Passing nil leaves the field unchanged. Each generated client exposes the same field in its own idiom.

Collections use replace-whole semantics: when any element changes, the entire collection is sent in the delta, not individual element diffs. For each tick, each collection field is either:

  • Absent: nothing changed; the client keeps what it has.
  • Present (possibly empty): the new authoritative value replaces the previous one entirely. An empty collection here means the field was explicitly cleared.

This makes reasoning straightforward: every delta you receive for a collection is the current complete value of that field.

Every field in a custom type must have a tag, a positive integer that maps directly to a Protobuf field number. Tags must be unique within their type:

fields:
id: { tag: 1, type: int32 } # required; must be unique within this type
name: { tag: 2, type: string }

You can number freely from 1, leave gaps, and add new fields at any unused tag. The only constraint is that you never reassign an existing tag to a different field, since that would corrupt any stored data that uses this type. Missing or duplicate tags result in a bake error.

  • Custom type names must be PascalCase and unique across all files in your configured types directory (default schemas/types/).
  • Every field must have a tag integer ≥ 1, unique within the type. Missing or duplicate tags result in a bake error.
  • Field types in custom types must be proto scalars. Nesting custom types inside other custom types is not supported.
  • list<T> and dict<K,V> fields are only valid for entity vars, not for command fields. Flat custom types (e.g. type: Item) are valid on command fields. For world schemas, collections are generated automatically by catalog sources. See World schemas.
  • If a collection field references a custom type that doesn’t exist in your types directory, golem-bake fails with an error.
  • Collection fields participate in sync like any other tick field: they are tracked per-tick and sent only when dirty.

After edits, run golem-bake to regenerate all outputs.