Skip to content

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.

One .yaml file per command in the directory set by command_schema in golem.yaml (default schemas/commands/).

A command that applies to the player’s session, not a specific entity:

command: ReadyUp # PascalCase name
fields: # optional: payload data
team: int32

The 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
})

A command that acts on a specific entity owned by the sender. golem-bake validates that entity_type names a known entity schema:

command: Move
target: entity # "entity" or "session" — defaults to "session"
entity_type: Player # must match the "entity:" name in a schema file
fields:
dx: float
dy: float

The 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.

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: UseItem
target: entity
entity_type: Player
fields:
item: Item

The 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.

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.