Skip to main content

Realtime events

A hub or spoke can show something to the players around a place in the world, or to the members of a channel, at once. Players' clients receive these events from the replication servers they're already connected to, as the same messages other players' actions produce. So a server-driven actor (an NPC, a mob) moves like a player's, and a server event arrives through the same parser as any other.

Use realtime events for what everyone nearby sees. Use topics for game state a client subscribes to through the gateway. Any hub or spoke can send them; no scope is needed.

From Rust​

use ckx_sdk::prelude::*;

fn boss_roars(ctx: &Ctx, at: ChunkPos, pose: &[u8], raid_channel: u64) -> Result<()> {
let boss = actor_id("boss-7");
// Its pose, rendered like a remote player, to players within 2 chunks.
ctx.emit_actor(&boss, at, 2, pose)?;
// A server event (type 94), decoded as `[u16 LE event type][state]`.
ctx.emit_spatial(at, 4, &boss, Spatial::Event { event_type: 94, state: br#"{"ability":"roar"}"# })?;
// And a line in the raid's channel, from the boss.
ctx.emit_channel(raid_channel, &boss, b"The ground shakes.")?;
Ok(())
}
CallPlayers who receive itArrives as
ctx.emit_actor(actor, at, distance, state)Within distance chunks of atACTOR_UPDATE_NOTIFICATION (130) from actor
ctx.emit_spatial(at, distance, actor, Spatial::Event { event_type, state })The sameSERVER_EVENT_NOTIFICATION (139), payload [u16 LE event_type][state]
Spatial::Voxel { x, y, z, voxel_type, state }The sameVOXEL_UPDATE_NOTIFICATION (133). This is an effect only; the durable write is ctx.world().set_voxels, which already shows the change.
Spatial::Generic { payload }The sameGENERIC_SPATIAL_1 (140)
Spatial::ActorMessage { payload }Only the actor named as actorSINGLE_ACTOR_MESSAGE (142)
ctx.emit_channel(channel, sender, payload)The channel's membersCHANNEL_MESSAGE_NOTIFICATION (18) from sender

The message layouts are in the replication API's wire formats. Server events use the reserved event types that CrowdyJS and CrowdyCPP already parse; see what clients already decode.

actor_id("…") makes a 32-byte actor id from a string. A player's actor is their 32-character UUID; a server-driven actor can use any 32 bytes, as long as it's stable.

What clients already decode​

Both SDKs ship codecs for one actor pose layout and a set of server event types, so a hub that sends these bytes needs no new client code. CrowdyJS exports enginePoseCodec, engineLanes() and the EVENT_* and FLAG_* constants from the package root; CrowdyCPP has them in crowdy/kit/wire.hpp.

An actor's state is a 48-byte little-endian pose (position, yaw and pitch, velocity, flags u8, held u8, updated_at f64 in milliseconds), optionally followed by a UTF-8 suffix. Flag bits 0 to 3 are reserved:

BitMeaning
0b0001 (FLAG_GROUNDED)grounded
0b0010 (FLAG_MOB)mob lane
0b0100 (FLAG_NPC)NPC or pet lane
0b1000 (FLAG_RESERVED3)the reserved lanes ghost:<courseId> (racing replays) and ball (possession), named by the suffix

A server event's state is JSON, and these event types are reserved:

TypeMeaning
77contact damage
90weather or season transition
91turn changed
92score or match summary
93match proposal (the matchmaking handoff)
94ability cast or impact
95movement violation
96control-point state
97race timing (checkpoint, lap, finish)
98zone change (shrinking circles, event areas)

Who receives what​

  • Only players in your app. An event carries its app, set by the host from the instance that sent it, and players connected to other apps never see it.
  • Spatial events go by proximity. Players within distance chunks of at receive the event (at most 8). There is no decay.
  • Channel messages go to the channel's members, wherever they are. Membership is managed as for any channel.
  • Blocks between players do not filter them. An event your code emits has no sending player, so it reaches everyone it is addressed to. When it carries one player's words to others, check players.blocked first, and never send one player's direct message to a minor as your own (Spatial::ActorMessage included).

Limits​

LimitValue
Payload1,024 bytes
Distance8 chunks
Rate1,000 events and 512 KiB a second, per app on each execution host, with bursts up to twice that

emit_* returns an error for a payload or distance over the limit. Events over the rate are dropped and counted, not queued.

Delivery is best effort, like all realtime traffic. A successful call means the host accepted the event, not that every client received it. The host batches events for up to 10 ms.

Usage​

Each app's events are metered per minute with its execution time: the events sent, their bytes, and the events dropped over the rate.

Coming from the legacy APIs​

Legacyck-exec
Compute emit_spatial(kind: "actor", …)ctx.emit_actor
emit_spatial(kind: "server_event", …)ctx.emit_spatial(…, Spatial::Event { … })
emit_spatial(kind: "text", …)Spatial::ActorMessage
emit_spatial(kind: "client_event", …)Not offered: a client event comes from a client. Send a server event instead.
emit_channel(channel_id, payload)ctx.emit_channel(channel, sender, payload), with a sender you choose
Game-model notifications effectsThe handler calls emit_* where the effect was declared
The per-module maxEgressMsgsPerMin budgetThe per-app rate above

Next: operations.