07 — Events¶
Custom events are the object-less sibling of an RPC — a small tagged message you send to other
peers, where the handler learns who sent it. Where lattice_rpc always names a netid, an event
can be global (netid 0) or optionally object-scoped, and its callback carries the originating
sender peer id.
Verified against: reference/include/lattice/lattice.h:656-684;
reference/tests/two_process.cpp:73-74,111-116,206-217,296-305.
Sending an event¶
lattice_result lattice_send_event(lattice_runner* r, uint16_t event_id,
lattice_event_target target, lattice_netid netid,
uint64_t peer, const uint8_t* payload,
uint32_t len, int reliable);
Verified against: reference/include/lattice/lattice.h:680-684.
event_id— auint16_ttag delivered verbatim to the receiver'son_event.target— alattice_event_target(below). For a client, the target is implicitly the authority.netid—0for a global / session-scoped event, or a live object's netid for an object-scoped one (used forOWNER/ALL_BUT_OWNERrouting exactly like an RPC).peer— the destination connection id forLATTICE_EVENT_PEER(ignored otherwise).payload/len— arbitrary serialized bytes, delivered byte-exact; may benullptr/0.reliable— non-zero rides the reliable-ordered channel (exactly-once, ordered); zero is best-effort.
The two-process driver sends a global HELLO event from the client and a global WELCOME event from
the server, alongside the RPCs — note netid = 0 and peer = 0 for a global send:
static const uint16_t EVT_HELLO = 0x10; // client -> server (object-less)
static const uint16_t EVT_WELCOME = 0x20; // server -> client (object-less)
// CLIENT: HELLO to the authority
lattice_send_event(r, EVT_HELLO, LATTICE_EVENT_SERVER, /*netid=*/0, /*peer=*/0,
(const uint8_t*)HELLO_MSG, (uint32_t)std::strlen(HELLO_MSG), /*reliable=*/1);
// SERVER: WELCOME to everyone
lattice_send_event(r, EVT_WELCOME, LATTICE_EVENT_ALL, /*netid=*/0, /*peer=*/0,
(const uint8_t*)WELCOME_MSG, (uint32_t)std::strlen(WELCOME_MSG), /*reliable=*/1);
Verified against: reference/tests/two_process.cpp:73-74,296-297 (client HELLO → SERVER),
:216-217 (server WELCOME → ALL).
Event targets¶
lattice_event_target mirrors the RPC routing policy, plus a PEER target for a directed send:
| Target | Value | Delivers to |
|---|---|---|
LATTICE_EVENT_SERVER |
0 | The authority (a client's events implicitly go here) |
LATTICE_EVENT_OWNER |
1 | The (optional) object's owner |
LATTICE_EVENT_ALL |
2 | Every peer |
LATTICE_EVENT_ALL_BUT_OWNER |
3 | Everyone except the owner |
LATTICE_EVENT_PEER |
4 | One specific connection (via the peer argument) |
Verified against: reference/include/lattice/lattice.h:123-129. The values deliberately mirror
lattice_rpc_target so the routing policy is one decision, not two.
Verified against: reference/include/lattice/lattice.h:118-121.
A directed reply to one connection — LATTICE_EVENT_PEER
To answer exactly one peer (e.g. a join handshake: the authority assigns a newcomer its slot and
replies to just that connection), pass LATTICE_EVENT_PEER and put the destination connection id
in the peer argument. You learn that id from the sender of the event they sent you (below).
Verified against: reference/include/lattice/lattice.h:128 (LATTICE_EVENT_PEER — "send to
one specific connection"), :533 (the peer argument).
Receiving an event — and the sender¶
Install on_event. Its signature is the key difference from on_rpc — it carries the sender:
void (*on_event)(void* user_data, uint16_t event_id, lattice_netid netid,
uint64_t sender, const uint8_t* payload, uint32_t len);
event_id— the tag that was sent.netid—0for a global event, or the owning object's netid for an object-scoped one.sender— the originating peer id (0means the local authority).payload/len— the byte-exact blob; the pointer is owned by the core and valid only for the duration of the call.
Verified against: reference/include/lattice/lattice.h:302-312.
static void cb_event(void* u, uint16_t event_id, lattice_netid, uint64_t sender,
const uint8_t* p, uint32_t len) {
Capture* c = (Capture*)u;
c->got_event = true;
c->event_id = event_id;
c->event_sender = sender; // <-- who sent it (0 == authority)
c->event_payload.assign((const char*)p, len); // copy it out before the call returns
}
Verified against: reference/tests/two_process.cpp:111-116.
This is why events exist alongside RPCs
on_rpc gives you (id, rpc_id, payload); on_event gives you (event_id, netid, sender,
payload). For a host-authoritative design where the server must know who asked for
something — to map a peer to its assigned role and validate the request — the sender is
essential, and only events carry it. This is the pattern chapter 08 builds on.
Verified against: reference/include/lattice/lattice.h:300-301 (on_rpc: no sender) vs.
:311-312 (on_event: has sender).
Reliability¶
Like RPCs, the final reliable argument selects the channel: non-zero → reliable-ordered
(exactly-once, ordered); zero → best-effort. Both drivers send their events reliably so the exchange
survives a lost datagram.
Verified against: reference/include/lattice/lattice.h:673-674;
reference/tests/two_process.cpp:247-249,328-330 (all reliable=1).
Is the event channel real? Yes — and verified¶
The event channel is proven by the two-process UDP driver and the conformance harness: a HELLO
event (client → server) and a WELCOME event (server → all) are sent and asserted to arrive with the
right id and payload, in both directions, over a real localhost UDP socket — ending in
SERVER: ALL CHECKS PASSED / CLIENT: ALL CHECKS PASSED.
Verified against: reference/tests/two_process.cpp:206-217,296-317 (asserted event exchange);
reference/README.md (PART B).
Encoding payloads: the BitWriter / BitReader¶
Events (and RPCs, and the manual serialization path of chapter 05) carry raw bytes, so you encode and decode the payload yourself. For a byte or two the drivers just use a C string. For anything structured, the core ships a typed bit-packing writer/reader that both ends use to produce byte-identical wire data.
lattice_bitwriter* lattice_bw_create(void);
void lattice_bw_destroy(lattice_bitwriter* w);
void lattice_bw_reset(lattice_bitwriter* w);
uint32_t lattice_bw_byte_count(const lattice_bitwriter* w);
const uint8_t* lattice_bw_data(const lattice_bitwriter* w); /* the flushed byte buffer */
void lattice_bw_write_bool(lattice_bitwriter* w, int v);
void lattice_bw_write_int(lattice_bitwriter* w, int32_t v);
void lattice_bw_write_ranged_int(lattice_bitwriter* w, int32_t value, int32_t min, int32_t max);
void lattice_bw_write_long(lattice_bitwriter* w, int64_t v);
void lattice_bw_write_float(lattice_bitwriter* w, float v);
void lattice_bw_write_compressed_float(lattice_bitwriter* w, float value, lattice_float_quant q);
void lattice_bw_write_double(lattice_bitwriter* w, double v);
void lattice_bw_write_vector3(lattice_bitwriter* w, lattice_vec3 v, lattice_float_quant q);
void lattice_bw_write_quaternion(lattice_bitwriter* w, lattice_quat v, uint32_t bits_per_comp);
void lattice_bw_write_string(lattice_bitwriter* w, const char* s);
void lattice_bw_write_bytes(lattice_bitwriter* w, const uint8_t* p, uint32_t n);
/* … also write_enum, write_byte, write_ranged_long, write_vector2/4, write_bits … */
lattice_bitreader* lattice_br_create(const uint8_t* data, uint32_t byte_len);
void lattice_br_destroy(lattice_bitreader* r);
int lattice_br_read_bool(lattice_bitreader* r);
int32_t lattice_br_read_int(lattice_bitreader* r);
int32_t lattice_br_read_ranged_int(lattice_bitreader* r, int32_t min, int32_t max);
float lattice_br_read_float(lattice_bitreader* r);
lattice_vec3 lattice_br_read_vector3(lattice_bitreader* r, lattice_float_quant q);
uint32_t lattice_br_read_string(lattice_bitreader* r, char* out, uint32_t cap); /* returns length, NUL-terminates */
uint32_t lattice_br_read_bytes(lattice_bitreader* r, uint8_t* out, uint32_t cap); /* returns length */
/* … the mirror of every writer method … */
Verified against: reference/include/lattice/lattice.h:725-774.
lattice_bitwriter* w = lattice_bw_create();
lattice_bw_write_ranged_int(w, cell, 0, 8); // a board cell 0..8
lattice_bw_write_string(w, "gg"); // a chat line
lattice_send_event(r, MY_EVENT, LATTICE_EVENT_SERVER, /*netid=*/0, /*peer=*/0,
lattice_bw_data(w), lattice_bw_byte_count(w), /*reliable=*/1);
lattice_bw_destroy(w);
// on the receiving side, inside on_event(payload, len):
lattice_bitreader* rd = lattice_br_create(payload, len);
int cell = lattice_br_read_ranged_int(rd, 0, 8);
char text[64]; lattice_br_read_string(rd, text, sizeof(text));
lattice_br_destroy(rd);
Verified against: the writer/reader surface at reference/include/lattice/lattice.h:726-774; the
codec round-trips exactly (bool = 1 bit; ranged int in ceil(log2(range+1)) bits; compressed float
quantized; etc.) per reference/README.md (PART A) and reference/README.md (bit-packer
description).
Read fields back in the exact order and with the exact params you wrote them
The bit stream has no self-describing tags — a ranged_int written with (min,max) must be read
with the same (min,max), a compressed_float with the same lattice_float_quant, in the same
order. Reading with different parameters silently mis-decodes. Keep the encode/decode pair
together (as above) so they never drift.
Verified against: reference/include/lattice/lattice.h:737 / :761 (ranged read/write take
the same min/max); :604 / :628 (compressed float take the same q).
For small, fixed payloads a hand-rolled buffer is perfectly fine (an identity byte + UTF-8, say). Use the BitWriter/BitReader when the message has several typed fields or you want tight quantization.
Next: 08 — Authority, where the sender from this chapter powers
server-authoritative validation.