06 — RPCs¶
A remote procedure call sends a one-shot message with a payload to other peers, routed by a target and delivered reliably or unreliably. Unlike replicated state (which converges to a value), an RPC is an event you handle once.
Verified against: reference/include/lattice/lattice.h:649-655;
reference/tests/two_process.cpp:71-77,202-217,294-305.
Sending an RPC¶
There is one entry point — a runner-level call scoped to an object by its lattice_netid:
lattice_result lattice_rpc(lattice_runner* r, lattice_netid id, uint16_t rpc_id,
lattice_rpc_target target, const uint8_t* payload,
uint32_t len, int reliable);
Verified against: reference/include/lattice/lattice.h:652-655.
id— thelattice_netidthe RPC is scoped to (drivesOWNER/ALL_BUT_OWNERrouting).rpc_id— auint16_tyou choose to identify the call (like an opcode).target— who receives it (below).payload/len— arbitrary bytes you encode yourself; passnullptr/0for none.reliable— non-zero guarantees delivery + ordering; zero is fire-and-forget.
The client half of the two-process driver fires a HELLO RPC to the server; the server replies with
a WELCOME RPC to everyone:
// tags the two processes agree on:
static const uint16_t RPC_HELLO = 0xA1; // client -> server
static const uint16_t RPC_WELCOME = 0xB2; // server -> client
// CLIENT: send HELLO to the authority, reliably
lattice_rpc(r, cap.last_spawn, RPC_HELLO, LATTICE_RPC_SERVER,
(const uint8_t*)HELLO_MSG, (uint32_t)std::strlen(HELLO_MSG), /*reliable=*/1);
// SERVER: broadcast WELCOME to all, reliably
lattice_rpc(r, id, RPC_WELCOME, LATTICE_RPC_ALL,
(const uint8_t*)WELCOME_MSG, (uint32_t)std::strlen(WELCOME_MSG), /*reliable=*/1);
Verified against: reference/tests/two_process.cpp:71-72,327-328 (client HELLO → SERVER),
:214-215 (server WELCOME → ALL).
The starter module shows the same round-trip on the loopback transport:
lattice_rpc(client, pid, RPC_HELLO, LATTICE_RPC_SERVER, (const uint8_t*)"hi", 2, /*reliable*/1);
// … pump …
lattice_rpc(server, pid, RPC_WELCOME, LATTICE_RPC_ALL, (const uint8_t*)"welcome", 7, /*reliable*/1);
Verified against: examples/starter-server-module/server_main.cpp:170-175.
Targets¶
lattice_rpc_target selects the routing:
| Target | Value | Delivers to |
|---|---|---|
LATTICE_RPC_SERVER |
0 | The authority (a client's RPC implicitly goes here) |
LATTICE_RPC_OWNER |
1 | The object's owner (input authority) |
LATTICE_RPC_ALL |
2 | Every peer |
LATTICE_RPC_ALL_BUT_OWNER |
3 | Everyone except the owner |
Verified against: reference/include/lattice/lattice.h:108-113.
Receiving an RPC¶
Install on_rpc on your lattice_callbacks. It hands you the netid the RPC was scoped to, the
rpc_id, and a payload pointer + len:
void (*on_rpc)(void* user_data, lattice_netid id, uint16_t rpc_id,
const uint8_t* payload, uint32_t len);
static void cb_rpc(void* u, lattice_netid, uint16_t rpc_id, const uint8_t* p, uint32_t len) {
Capture* c = (Capture*)u;
c->got_rpc = true;
c->rpc_id = rpc_id;
c->rpc_payload.assign((const char*)p, len); // COPY it out — the pointer is only valid in-call
}
Verified against: reference/tests/two_process.cpp:106-110.
The payload pointer is valid only during the callback — copy what you keep
Like every buffer the core hands a callback, the payload pointer is owned by the core and valid
only for the duration of the call. The driver assigns it into a std::string immediately.
If you stash the raw pointer and read it after the callback returns, it is a dangling read.
Verified against: reference/include/lattice/lattice.h:22-25 (buffers caller-owned,
callbacks synchronous); reference/tests/two_process.cpp:109.
The starter module dispatches on rpc_id:
static void on_rpc(void* user, lattice_netid id, uint16_t rpc_id, const uint8_t*, uint32_t) {
Context* ctx = (Context*)user;
if (rpc_id == RPC_HELLO) ctx->hellos_received++;
if (rpc_id == RPC_WELCOME) ctx->welcomes_received++;
}
Verified against: examples/starter-server-module/server_main.cpp:65-72.
Reliability¶
The final reliable argument maps straight to the core's reliable-vs-unreliable channels:
reliable = 1— guaranteed delivery, in order. Use for gameplay-critical, infrequent messages (a move, a purchase, "player died"). Both drivers send their HELLO/WELCOME reliably so a dropped localhost datagram can't lose them.reliable = 0— may be dropped or reordered; cheaper. Use for high-frequency, loss-tolerant hints where the latest wins.
Verified against: the reliable=1 argument in every RPC call at
reference/tests/two_process.cpp:214-215,294-295;
examples/starter-server-module/server_main.cpp:171,174.
Reliable messages are retried each tick until observed
The two-process driver resends its reliable HELLO/WELCOME each tick until it sees the reply,
which is how it survives a lost datagram on a real UDP socket. The reliable channel handles the
retransmission; your resend loop is just the driver's way of pumping until the exchange is
confirmed by observation.
Verified against: reference/tests/two_process.cpp:289-307 (client resends HELLO each tick
until it observes WELCOME).
Is the RPC path real? Yes — and verified¶
The RPC path is exercised end to end by both working drivers and the conformance harness: a client → server RPC and a server → ALL RPC are sent and asserted to arrive with the correct id and payload, over both the loopback transport and a real localhost UDP socket.
Verified against: reference/tests/two_process.cpp:202-217,294-307 (the asserted RPC exchange,
SERVER: ALL CHECKS PASSED / CLIENT: ALL CHECKS PASSED); reference/README.md (PART B: "a
client→server RPC and a server→ALL RPC are exchanged (payloads asserted)").
RPC vs. event: who sent it?¶
One design note that matters for server-authoritative games: on_rpc does not tell you which peer
sent the RPC — its signature carries the netid and rpc_id, but no sender. If your handler must know
who sent a message (to map a connection to a seat, validate a move came from the right player,
etc.), use a custom event instead — on_event carries the originating sender peer id. That is
the subject of chapter 07.
Verified against: reference/include/lattice/lattice.h:300-301 (on_rpc — no sender) vs. :311-312
(on_event — has sender).
| Use an RPC when… | Use an event when… |
|---|---|
| The handler doesn't need the sender's identity | The receiver must know who sent it (server-side validation) |
| It's naturally scoped to an object (netid) | It's session-global (no object) — pass netid 0 |
| A broadcast / owner-directed effect | A directed send to one specific connection (LATTICE_EVENT_PEER) |
Both ride the same reliable/unreliable channels and the same target routing.
Next: 07 — Events, the object-less message channel that carries the sender.