Command schemas
Command schemas
Section titled “Command schemas”Commands are how clients tell the server to do something: move a character, fire a weapon, cast a spell. You define each command in a YAML file, run golem-bake, and get a typed CommandRouter that decodes incoming messages and dispatches them to your handlers—with optional ownership checks built in.
Commands are optional. If your game is purely observational or you handle incoming messages yourself, you can skip this entirely.
File shape
Section titled “File shape”One .yaml file per command in the directory set by command_schema in golem.yaml (default schemas/commands/).
Session-scoped command
Section titled “Session-scoped command”A command that applies to the player’s session, not a specific entity:
command: ReadyUp # PascalCase name
fields: # optional: payload data team: int32The generated CommandRouter handler receives the sender’s session ID and a typed command struct:
router.OnReadyUp(func(senderID int64, cmd *synced.ReadyUpCommand) { // apply ready-up for that session _ = cmd.Team})Entity-targeted command
Section titled “Entity-targeted command”A command that acts on a specific entity owned by the sender. golem-bake validates that entity_type names a known entity schema:
command: Movetarget: entity # "entity" or "session" — defaults to "session"entity_type: Player # must match the "entity:" name in a schema file
fields: dx: float dy: floatThe generated handler receives the sender ID, the resolved entity, and the command struct. The CommandRouter checks ownership automatically—the handler only fires when the sender owns the entity:
router.OnMove(func(senderID int64, ent *synced.SyncedPlayer, cmd *synced.MoveCommand) { // only reached when senderID owns ent ent.SetPosition(ent.PositionX()+cmd.Dx, ent.PositionY()+cmd.Dy)})If the sender does not own the entity, Dispatch returns an error and the handler is not called.
Command with a custom type field
Section titled “Command with a custom type field”When a command needs to pass a composite value—an item to equip, a crafting recipe, a waypoint with several fields—you can use a custom type from your configured types directory (default schemas/types/) directly as the field type, rather than flattening everything into separate scalars.
command: UseItemtarget: entityentity_type: Player
fields: item: ItemThe generated handler receives a typed Go struct with the custom type’s fields directly accessible:
router.OnUseItem(func(senderID int64, ent *synced.SyncedPlayer, cmd *synced.UseItemCommand) { applyItem(ent, cmd.Item.Id, cmd.Item.Name, cmd.Item.Count)})On the JavaScript client, buildUseItemCommand accepts an Item object. The Item interface is exported from the generated output and can be imported from entities_pb.js:
import type { Item } from "./entities_pb.js";
client.send(buildUseItemCommand(entityId, { id: 42, name: "sword", count: 1 }));The import path is relative to wherever you place your generated code. See Custom types & collections for how to define the type itself.
command — PascalCase type name. Generated Go types are named XxxCommand; handler registration is router.OnXxx. Must be unique across all files in the commands directory—a duplicate or empty name is a bake error.
target — "entity" or "session". Defaults to "session" when omitted.
entity_type — required when target: entity. Must exactly match the entity: name in one of your entity schema files. A mismatch is a bake error.
fields — optional map of snake_case field name to type. Accepts the bare scalar shorthand (dx: float) or the explicit object form (dx: { type: float }). Type values can be proto scalars (double, float, int32, int64, bool, string, bytes, etc.) and custom type names defined under types_schema in golem.yaml (default schemas/types/; see Custom types & collections). An empty or absent fields block is valid—useful for commands that carry no payload beyond the intent itself. Note that list<T> and dict<K,V> collection types are not supported on command fields; use a flat custom type instead.
Wiring the router
Section titled “Wiring the router”Construct the router once and pass it to OnMessage:
router := synced.NewCommandRouter(srv)
router.OnMove(func(senderID int64, ent *synced.SyncedPlayer, cmd *synced.MoveCommand) { ent.SetPosition(ent.PositionX()+cmd.Dx, ent.PositionY()+cmd.Dy)})
srv.OnMessage(func(s *golem.Session, data []byte) { if err := router.Dispatch(s.ID, data); err != nil { log.Println("command:", err) }})See Client commands for the full server-side flow, JavaScript client for sending commands from the browser, and Go client for native Go clients.
After adding or changing commands, run golem-bake to regenerate.