Replication client
client.replication() is CrowdyCPP's native implementation of the public
Replication API. It owns the socket, the signing,
the verification, and the connection lifecycle; your game sees typed sends
and typed notification callbacks. This page describes behavior — for the
byte-level layouts, use the canonical references:
Wire formats and
HMAC.
Connect and server assignment
connect() performs the flow from
Authenticate and assign:
- Hold an app-scoped token (the 64-octet token doubles as the HMAC key).
- Call
serverWithLeastClientson the Game API. This has a side effect: it installs your UDP session server-side on the assigned replication server. - Wait for session-ready. Session install is asynchronous on the server,
so the client waits (default 1.5 s,
Config::sessionReadyWaitMs) before the state flips fromConnectingtoConnected. Traffic sent before the session is installed is silently dropped — this wait is not optional politeness. - Exchange signed UDP traffic with the returned host and client port
(IPv4 by default;
Config::preferIpv6opts into the IPv6 address).
Connection state (Idle → Connecting → Connected, with Reconnecting /
Failed / Closed) surfaces through a status callback and state().
Sends
All sends are HMAC-SHA256 signed per the HMAC guide,
thread-safe, and never throw — they return a crowdy::Result carrying the
message's sequence number:
sendActorUpdate— spatial actor statesendVoxelUpdate— voxel editssendAudio/sendText— spatial audio and text packetssendClientEvent— typed client events (eventType+ state bytes)sendGenericSpatial— generic spatial payloadssendSingleActorMessage— direct message to one actor (fire-and-forget, no echo; embed sender identity in the payload if the recipient needs it)sendChannelMessage— publish to a channel (membership +send_messagesenforced server-side)sendHeartbeat— idle keep-alive for your own actor (send every ~2 s while idle so presence never lapses)
Payload bytes are copied into a pooled send buffer during the call, so they need not outlive it.
Receive path
Inbound datagrams are processed on the network side and dispatched on your game thread:
- Bundle unpacking — a single datagram can carry multiple notifications; the client unpacks them.
- Per-notification HMAC verification — server-signed notifications are
verified in constant time and mismatches are dropped (counted in
stats().hmacFailures;Config::verifyNotificationsdisables this for benchmarking only). - Typed dispatch — a
Handlersstruct with one callback per notification kind (actor update, voxel update, audio, text, client/server event, generic spatial, single-actor message, channel message, generic error, status). - Error frames — server-reported failures arrive as generic-error notifications correlated by sequence number to the send that caused them.
Notification payloads are spans into the receive buffer — valid only during the callback. Copy anything you keep.
Threading modes
- Owned net thread (default). The SDK runs one receive/send thread and
hands you parsed notifications through a lock-free ring; you drain it with
poll()from your game thread. Callbacks always fire on the thread that callspoll()— never on the network thread. - Manual pump (
Config::manualPump). The SDK spawns no threads. You callpump()from your own network thread (or your tick) to do the socket work, andpoll()from the game thread to dispatch callbacks. This is the mode engine wrappers use.
Lifecycle: refresh, reconnect, watchdog
The connection manages its own health so gameplay code doesn't have to:
- Proactive token refresh. The app-scoped token expires; the client
refreshes it through the session provider ahead of expiry
(
Config::refreshLeadMs, default 5 minutes) and rotates the HMAC key. - Verified
COMMAND_RECONNECT. A server under load can ask the client to move. The command's HMAC is verified as server-originated, then the client re-assigns viaserverWithLeastClientswithin the grace period. See Operations. - Silent-drop watchdog (opt-in,
Config::watchdogSilenceMs). The protocol's documented failure mode is that auth failures are often silent drops — sends flow out, nothing comes back. If enabled and traffic has been received before, prolonged one-way traffic triggers reassignment. It is off by default because a lone player in an empty area legitimately receives nothing. See Troubleshooting.
Reconnects and drop counters are visible in stats() (datagrams sent and
received, HMAC failures, malformed frames, ring drops, reconnects).
AndWait correlation
UDP is best-effort and sequence numbers are correlation, not idempotency. For
request/confirm flows CrowdyCPP offers the same convenience CrowdyJS's
*AndWait methods provide:
sendActorUpdateAndWait/sendVoxelUpdateAndWait— wait (pumping and polling internally; other handlers still fire) until the send's self-echo notification arrives, a correlated error frame arrives, or the timeout elapses.sendTextAndWait— text (like audio and client events) has no echo; only errors correlate. A clean timeout therefore means accepted.waitForSequence— the low-level primitive: wait on any sequence + uuid.
The outcome reports acknowledged, the correlated error code if any, and the
server epoch-millis from the echo.
Errors
The replication layer never throws on the hot path: sends return
Result codes, server failures arrive as correlated error notifications, and
connection changes surface through the status callback. GraphQL-layer
operations (assignment, token mint/refresh) throw the structured exception
types shared with the rest of the SDK — branch on error.code() rather than
parsing messages.
Next
Most games should not drive Connection by hand — the
World session layer packages the send loop,
registries, and caches on top of it.