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.
Basic world data (fields only)
Section titled “Basic world data (fields only)”Create one .yaml file per world type in the directory set by world_schema in golem.yaml (default schemas/world/):
world: Zone
fields: name: type: string gravity: type: float max_enemies: type: int32world: PascalCase type name. Generated Go type isZoneData; proto message is alsoZoneData.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.
Catalog data
Section titled “Catalog 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.
Declaring a catalog schema
Section titled “Declaring a catalog schema”Add a world schema file with source.format: catalog:
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 = listtype: references a custom type defined undertypes_schema(e.g.schemas/types/item.yamlby default). See Custom types & collections.key: when set, the generated field is adict<KeyType, Item>keyed by that field. When omitted, the generated field is alist<Item>. Either way,golem-bakechecks at bake time that all key values are present and unique.
The catalog data file
Section titled “The catalog data file”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: 25Field names must match the snake_case field names declared in your custom type. Missing fields default to zero values; extra fields are silently ignored.
What bake generates
Section titled “What bake generates”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.
Client access
Section titled “Client access”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 serverworldManager.itemCatalog?.items // Record<number, Item>
// Generated lookup helperworldManager.getItemCatalogById(1) // Item | undefinedFor list catalogs (no key:), only the array getter is generated:
worldManager.itemCatalog?.items // Item[]Catalog vs plain world fields
Section titled “Catalog vs plain world fields”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.
Tiled and LDtk integration
Section titled “Tiled and LDtk integration”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.
Declaring the source
Section titled “Declaring the source”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_datafield 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:.
Generated loader
Section titled “Generated loader”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.
LDtk example
Section titled “LDtk example”world: Dungeon
source: format: ldtk file: maps/dungeon.ldtk extract: - name: floor_count type: int32 - name: theme type: stringThe 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.
Serving map files over HTTP
Section titled “Serving map files over HTTP”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.
Server-side map use
Section titled “Server-side map use”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 itselfif 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 projectsfor _, 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_schemadirectory (defaultschemas/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_dataandmap_urlare reserved field names whensource:is set for Tiled/LDtk; declaring them explicitly results in a bake error.source.formatmust be"tiled","ldtk", or"catalog"; any other value results in a bake error.source.filemust not be empty whensource:is present.- Catalog sources require
source.typeto be set; it must match a type defined undertypes_schema(defaultschemas/types/). - If
source.keyis set on a catalog,golem-bakevalidates 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.