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.

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.