Skip to content

World schemas

Zone metadata, map files, gravity constants, and spawn tables do not belong to one entity. World schemas let you declare this server-authoritative data once, generate typed loaders and client models, then choose whether each value goes to every client or only the session that needs it.

By default, a client receives one WorldUpdate frame per stored world type before entity snapshots. You can broadcast later changes, send a different value to one session, or exclude on-demand world types from the connect snapshot.

Create one .yaml file per world type in the directory set by world_schema in golem.yaml (default schemas/world/):

schemas/world/zone.yaml
world: Zone
fields:
name:
type: string
gravity:
type: float
max_enemies:
type: int32
  • world: PascalCase type name. Generated Go type is ZoneData; proto message is also ZoneData.
  • fields: Snake_case keys, same proto scalar types as entity schemas. Keys are sorted for stable wire tags.

After golem-bake, the go-server integration emits world_pb.go in your output directory with a ZoneData struct that has typed fields for everything you declared: Name string, Gravity float32, MaxEnemies int32. Populate and store in your server startup:

srv.World.Set(&synced.ZoneData{
Name: "Overworld",
Gravity: 9.8,
MaxEnemies: 50,
})

srv.World is always initialized. Clients receive the stored data on connect; call srv.PushWorldData("Zone") to re-broadcast after an in-game change.

Delivering different world data per session

Section titled “Delivering different world data per session”

For games where players occupy different maps or instances, exclude the map type from automatic connect snapshots and send the appropriate generated value after identifying the session:

srv := golem.NewServer(golem.ServerConfig{
Addr: ":8080",
WorldSnapshotExclude: []string{"Zone"},
})
srv.OnConnect(func(sess *golem.Session) {
zone := &synced.ZoneData{
Name: zoneNameForSession(sess.Data),
Gravity: 9.8,
MaxEnemies: 50,
}
if err := srv.SendWorldData(sess.ID, zone); err != nil {
log.Printf("send zone to session %d: %v", sess.ID, err)
}
})

SendWorldData serializes the supplied value directly. It does not replace the value in srv.World, so two sessions can receive different ZoneData values even though both use the same world: Zone schema. Clients process this targeted update through the same generated world manager callbacks used for connect snapshots and PushWorldData.

Use SendStoredWorldData(sess.ID, "Zone") when one session should receive the current value already held in srv.World. A missing stored name is a no-op, matching PushWorldData.

WorldSnapshotExclude matches the exact, case-sensitive name from world:, and only changes automatic connect delivery. Excluded values remain in srv.World, and broadcasts still work. Your game is responsible for sending an excluded type when a session needs it.

World updates use the reliable stream and each wrapped frame must fit within 32,000 bytes. For large Tiled or LDtk maps, generate map_url values and serve the map over HTTP instead of embedding tile_data.

Some game data is best thought of as a named registry of items: every item type in the game, every spell, every unit definition. Catalog world schemas give you that pattern: author uniform records, declare their shared type, and let golem-bake generate a typed loader and deliver the full catalog to every client on connect.

Catalogs can be written as YAML directly. Unity projects can instead use Golem Scribe to generate the custom type, world schema, and catalog rows from attributed ScriptableObjects.

Add a world schema file with source.format: catalog:

schemas/world/item_catalog.yaml
world: ItemCatalog
source:
format: catalog
file: data/items.yaml # path relative to your project root
type: Item # must match a custom type under types_schema (default schemas/types/)
key: id # optional: the field to use as map key; absent = list
  • type: references a custom type defined under types_schema (e.g. schemas/types/item.yaml by default). See Custom types & collections.
  • key: when set, the generated field is a dict<KeyType, Item> keyed by that field. When omitted, the generated field is a list<Item>. Either way, golem-bake checks at bake time that all key values are present and unique.

The file at data/items.yaml (the file: path) is a flat YAML sequence, one entry per item:

- id: 1
name: "Iron Sword"
damage: 10
- id: 2
name: "Fire Staff"
damage: 25

Field names must match the snake_case field names declared in your custom type. Missing fields default to zero values; extra fields are silently ignored.

After golem-bake, the go-server integration emits ItemCatalogData with a single Items field. Dict mode uses map[int32]Item, while list mode uses []Item. It also generates LoadItemCatalogData, which reads the catalog YAML and returns the populated value.

Wire it up at server startup:

d, err := synced.LoadItemCatalogData("data/items.yaml")
if err != nil {
log.Fatal(err)
}
srv.World.Set(d)

Clients receive ItemCatalogData as part of the initial world snapshot, before any entity state.

For dict catalogs (key: set), the generated WorldManager on the TypeScript client exposes a typed lookup method alongside the raw getter:

// Raw access: the full map sent by the server
worldManager.itemCatalog?.items // Record<number, Item>
// Generated lookup helper
worldManager.getItemCatalogById(1) // Item | undefined

For list catalogs (no key:), only the array getter is generated:

worldManager.itemCatalog?.items // Item[]

Use a catalog when your data is authored as a list of uniform records and you want golem-bake to manage the loader and wire format automatically. Use plain fields: when your world data is a single struct with named scalar fields: zone gravity, map dimensions, configuration constants.

You can mix both approaches: a catalog world type for items, a plain world type for zone config, and a Tiled source for map data.

If your team uses Tiled (.tmj files) or LDtk (.ldtk files), you can link a world schema directly to a map file. The source: block in the world YAML tells golem-bake to generate a LoadXxxData() helper that reads and parses the file at server startup.

schemas/world/level1.yaml
world: Level1
source:
format: tiled # "tiled" or "ldtk"
file: maps/level1.tmj # path relative to your project root
extract: # custom map properties → typed proto fields (optional)
- name: gravity
type: float
- name: spawn_count
type: int32
  • When source: is present, fields: is optional, and extracted fields are merged with any explicitly declared ones.
  • Fields are sorted alphabetically for stable wire tags. A synthetic bytes tile_data field is appended last; it carries the raw file contents so clients can pass them to their own Tiled/LDtk renderer.
  • The same proto scalar types used in entity schemas apply in extract:.

golem-bake emits a LoadLevel1Data(path string) (*Level1Data, error) helper alongside the struct. Call it at startup:

d, err := synced.LoadLevel1Data("maps/level1.tmj")
if err != nil {
log.Fatal(err)
}
srv.World.Set(d)

The loader reads the file once, populates extracted fields from the map’s custom properties (gravity, spawn_count in the example above), and stores the raw bytes in TileData. Everything is ready for serialization through the normal WorldUpdate path.

Tiled properties are extracted from the map’s top-level custom properties. LDtk properties are extracted from the first level’s field instances.

schemas/world/dungeon.yaml
world: Dungeon
source:
format: ldtk
file: maps/dungeon.ldtk
extract:
- name: floor_count
type: int32
- name: theme
type: string

The LoadDungeonData(path string) loader calls ldtk.Parse internally, reads floor_count and theme from the first level’s field instances, and stores the raw .ldtk JSON in TileData.

Embedding raw map bytes in the WorldUpdate proto works well for small maps. For larger files, set url_prefix in the source block instead of omitting it: golem-bake then generates a string map_url field (the URL prefix plus the filename) rather than bytes tile_data. Clients use that URL to fetch the file out-of-band over HTTP.

Set MapDir in ServerConfig (or mount MapFileHandler on your own mux) to have the server serve those files. See Map file serving for the full wiring.

The golem/tiled and golem/ldtk packages are also available directly for server-side game logic (collision grids, pathfinding, spawn point detection), independent of any codegen or client delivery:

import "github.com/demiurgos-hub/golem-engine/golem/tiled"
m, err := tiled.Load("maps/level1.tmj")
if err != nil {
log.Fatal(err)
}
// Collision layer: tiles layer named "Collision"
collision := m.LayerByName("Collision")
// Custom properties on the map itself
if p, ok := m.PropertyByName("gravity"); ok {
gravity = p.FloatValue()
}
import "github.com/demiurgos-hub/golem-engine/golem/ldtk"
proj, err := ldtk.Load("maps/dungeon.ldtk")
if err != nil {
log.Fatal(err)
}
// All levels across single- and multi-world projects
for _, level := range proj.AllLevels() {
entities := level.LayerByIdentifier("Entities")
// entities.EntityInstances: spawn points, triggers, etc.
}

Both packages let you navigate the map structure with typed helpers. For Tiled maps, m.LayerByName(name) returns the first top-level layer matching that name (or nil), and m.PropertyByName(name) looks up a named custom property and gives you FloatValue(), IntValue(), StringValue(), or BoolValue() accessors. For LDtk projects, proj.AllLevels() returns every level regardless of single- or multi-world mode, proj.LevelByIdentifier(id) finds one level by its LDtk identifier, level.LayerByIdentifier(id) finds a layer instance, and level.FieldByIdentifier(id) returns a field instance with the same typed value accessors.

  • Each world type name must be unique across files in your world_schema directory (default schemas/world/); duplicates result in a bake error.
  • Field keys are sorted alphabetically for stable wire tags. Adding a field shifts tags for fields that sort after it, so treat changes with the same care as any proto field addition.
  • tile_data and map_url are reserved field names when source: is set for Tiled/LDtk; declaring them explicitly results in a bake error.
  • source.format must be "tiled", "ldtk", or "catalog"; any other value results in a bake error.
  • source.file must not be empty when source: is present.
  • Catalog sources require source.type to be set; it must match a type defined under types_schema (default schemas/types/).
  • If source.key is set on a catalog, golem-bake validates that every entry in the catalog file has that field and that all key values are unique.
  • The world schema directory is optional. If it does not exist, bake skips world generation without error.

After any schema change, run golem-bake to regenerate outputs.