Event schemas
Event schemas
Section titled “Event schemas”Server events are how the server tells clients that something happened: a chat message was sent, an explosion went off, a buff was applied. You define each event in a YAML file, run golem-bake, and get a typed EventBroadcaster on the server and an EventManager on the JS client—with optional FOI-filtered delivery built in.
Events are optional. If you don’t create any event schemas, no event code is generated.
File shape
Section titled “File shape”One .yaml file per event in the directory set by event_schema in golem.yaml (default schemas/events/).
Every event requires a target field that declares its delivery scope.
Global event
Section titled “Global event”A global event is broadcast to every connected client:
event: ChatMessage # PascalCase nametarget: global
fields: sender_name: string text: stringOn the server, EventBroadcaster.BroadcastChatMessage sends it to all connected sessions:
events := synced.NewEventBroadcaster(srv)
// inside OnMessage or OnTick:events.BroadcastChatMessage(playerName, strippedText)On the JS client, register a callback before connecting:
client.events.onChatMessage((e) => { chat.append(e.senderName, e.text);});Session-targeted event
Section titled “Session-targeted event”A session-targeted event is delivered to a single session. The generated method is SendXxx and takes a sessionID as its first argument:
event: LoginAcktarget: session
fields: ok: bool message: stringOn the server, pass the session ID you want to target — typically from OnConnect or a command handler:
events.SendLoginAck(session.ID, true, "welcome")On the JS client, session-targeted events are received exactly like global events — the server-side targeting is invisible to the receiver:
client.events.onLoginAck((e) => { if (e.ok) startGame(); else showError(e.message);});Entity-scoped event
Section titled “Entity-scoped event”When target is entity, entity_id is automatically added to the proto message as field 1. The event is broadcast to all clients, but the payload includes which entity triggered it:
event: BuffAppliedtarget: entityentity_type: Player # must match the "entity:" name in a schema file
fields: buff_id: int32The server method receives entityID as its first argument:
events.BroadcastBuffApplied(player.EntityID(), buffID)On the JS client, the event is dispatched as an instance method on the entity object. Override onBuffApplied in your SyncedPlayer subclass:
class MyPlayer extends SyncedPlayer { onBuffApplied(e: BuffAppliedEvent) { this.showBuffIcon(e.buffId); }}Because entity_id is in the payload, this is already the right entity — no lookup needed.
FOI-filtered event
Section titled “FOI-filtered event”Adding foi_only: true to an entity-scoped event restricts delivery to sessions that currently have the entity in their field of interest. This prevents clients from receiving events for entities they can’t see:
event: Explosiontarget: entityentity_type: Bombfoi_only: true # deliver only to sessions with this Bomb in their FOI
fields: damage: int32 radius: floatThe server API is identical to entity-scoped events:
events.BroadcastExplosion(bomb.EntityID(), damage, radius)The filtering is handled internally. When interest management is not enabled (CellSize == 0), foi_only: true degenerates to broadcast-all — every entity is effectively in every client’s interest set.
Thread-safety note: FOI-filtered broadcasts query the interest manager’s known sets. Call them from the game tick goroutine (inside OnTick or during tick-driven logic), not from OnMessage handlers, which run on a different goroutine.
event — PascalCase type name. The generated Go method is events.BroadcastXxx (global and entity targets) or events.SendXxx (session target); the generated TS callback is client.events.onXxx (global and session) or an instance method entity.onXxx (entity target). Must be unique across all files in the events directory—a duplicate or empty name is a bake error.
target — required. One of:
global— broadcast to all connected sessions; generatesBroadcastXxx()session— unicast to one session; generatesSendXxx(sessionID int64, ...)entity— broadcast with entity context; generatesBroadcastXxx(entityID int64, ...)
entity_type — required when target is entity. Must exactly match the entity: name in one of your entity schema files—a mismatch is a bake error. When set, entity_id is included in the proto message and user fields follow it. Not valid with target: global or target: session.
foi_only — optional boolean, default false. Only valid when target is entity. When true, delivery is restricted to sessions whose field of interest currently includes the target entity.
fields — optional map of snake_case field name to type. Accepts the bare scalar shorthand (buff_id: int32) or the explicit object form (buff_id: { type: int32 }). 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 (see Custom types & collections). An empty or absent fields block is valid. Note that list<T> and dict<K,V> collection types are not supported on event fields; use a flat custom type instead.
Wiring the broadcaster
Section titled “Wiring the broadcaster”Construct the broadcaster once and use it wherever you dispatch events:
events := synced.NewEventBroadcaster(srv)
router.OnChat(func(senderID int64, cmd *synced.ChatCommand) { name := lookupName(senderID) clean := stripHTML(cmd.Text) events.BroadcastChatMessage(name, clean)})The broadcaster is safe to hold as a package-level variable or pass around like any other struct — it holds nothing but a pointer to the server.
Client-side event handling
Section titled “Client-side event handling”Global and session events
Section titled “Global and session events”Both global and session events are handled via callbacks on client.events. The distinction is server-side only; the receiving client registers a callback the same way:
client.events.onChatMessage((e) => { console.log(e.senderName, e.text);});
client.events.onLoginAck((e) => { console.log(e.ok, e.message);});Register callbacks before calling client.connect() so no events are missed.
Entity-bound events
Section titled “Entity-bound events”Entity-bound events (with or without foi_only) are dispatched as optional instance methods on the Synced* entity class. Override the generated method stub in your subclass:
class MyBomb extends SyncedBomb { onExplosion(e: ExplosionEvent) { spawnFX(this.posX, this.posY, e.radius); }}When foi_only: true, the entity is guaranteed to be in the client’s known set when the method fires — you can safely access this.posX, this.posY, and any fields set from onSpawn. When foi_only: false, some clients may receive the event for an entity not currently in their interest set; handle the case where entity state is unavailable if needed.
After adding or changing events, run golem-bake to regenerate.