Skip to content

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.

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.

A global event is broadcast to every connected client:

event: ChatMessage # PascalCase name
target: global
fields:
sender_name: string
text: string

On 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);
});

A session-targeted event is delivered to a single session. The generated method is SendXxx and takes a sessionID as its first argument:

event: LoginAck
target: session
fields:
ok: bool
message: string

On 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);
});

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: BuffApplied
target: entity
entity_type: Player # must match the "entity:" name in a schema file
fields:
buff_id: int32

The 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, so no lookup is needed.

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: Explosion
target: entity
entity_type: Bomb
foi_only: true # deliver only to sessions with this Bomb in their FOI
fields:
damage: int32
radius: float

The 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, since 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 results in a bake error.

target: required. One of:

  • global: broadcast to all connected sessions; generates BroadcastXxx()
  • session: unicast to one session; generates SendXxx(sessionID int64, ...)
  • entity: broadcast with entity context; generates BroadcastXxx(entityID int64, ...)

entity_type: required when target is entity. Must exactly match the entity: name in one of your entity schema files, and a mismatch results in 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.

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.

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 (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, so 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.