# Protocol Reference This page describes how an external tool, such as an RCON client, a bot, or a dashboard backend, talks to RconDataBridge: connecting, the option map, the request/response cycle, idempotency, rate limiting, and error codes. For the specific operations you can call, see [Operations Reference](Operations-Reference). ## 1. Connecting RconDataBridge has no transport of its own; it rides entirely on the server's existing RCON connection (the standard Source RCON protocol, the same one any RCON tool already speaks). Point your RCON client at the server's configured `RCONPort` and `RCONPassword` as usual. Only two RCON commands are involved: - `changeoption `, used to write an option (only ever used for `RconDataBridge_Request`) - `showoptions`, used to read every option's current value (used to read everything else) There is no dedicated bridge port and no separate authentication step. RCON access is the access control. ## 2. Option map | Option | Direction | Contents | |-----------------------------------|------------------|-------------------------------------------------------------------------------------------| | `RconDataBridge_ProtocolVersion` | Server to client | `{version, ops}`, the bridge protocol version and the current allowlisted operation names | | `RconDataBridge_WorldStats` | Server to client | Cached world telemetry, refreshed roughly once a minute | | `RconDataBridge_PlayerStats_` | Server to client | Cached snapshot for one player, created lazily on first query | | `RconDataBridge_Request` | Client to server | Your request mailbox, a single slot rather than a queue | | `RconDataBridge_Response` | Server to client | The response to the most recently processed request | | `RconDataBridge_LastResponse` | Server to client | Millisecond epoch timestamp of that response | Every value in this table is base64 encoded, in both directions, with no exceptions. Decode after reading, encode before writing. `RconDataBridge_LastResponse` is base64 of a plain decimal string, for example `MA==` decodes to `"0"`, not JSON; decode it the same way, then parse it as an integer. Option names use underscores, never dots. Project Zomboid's own `changeoption` command validates the option name argument against `\w+` (word characters only) before it ever reaches option lookup, so a dotted name is silently rejected at the command parsing stage, before the bridge sees it at all. ### Why base64 everywhere? `changeoption` strips every literal double-quote character from every argument as part of its own command line parsing. This happens unconditionally, so no quoting strategy avoids it, and raw JSON can never survive a `changeoption` call intact. Base64's alphabet contains no quote characters, so it survives the round trip untouched. This is a hard requirement for the client to server `Request` channel specifically. It's not strictly required for the server to client options; those are written directly into the option store rather than going through `changeoption`'s command parser, so raw JSON would technically survive there too. They're base64 encoded anyway, deliberately, so a client only ever needs one rule, "everything is base64," instead of a special case for one channel. ## 3. The request/response cycle There is no push notification and no long poll here; this is a single mailbox slot, not a queue. The cycle is: 1. Build a request envelope (see section 4). 2. Encode it as compact JSON, with no whitespace, see the note in section 4, then base64. 3. Send `changeoption RconDataBridge_Request `. 4. Poll `showoptions`, base64 decode `RconDataBridge_LastResponse`, and wait until it changes from whatever it was before your write, or just wait a fixed short delay and check that `RconDataBridge_Response`'s `id` field matches the id you sent. 5. Base64 decode `RconDataBridge_Response` and parse it as JSON. The server checks for a new request roughly once a second by default; see the Poll Interval setting in [Configuration](Configuration). There is no engine event for "an option changed," so this is a genuine poll, not a callback. Responses have consistently shown up within a few seconds of the request being written in practice, but this isn't a guaranteed bound; budget a few seconds of latency and don't assume sub-second turnaround. Because it's a single mailbox, two requests in flight at once will race: if you send a second request before the first one's response has been read, you may only ever see the second response. Send one request, wait for its response, matched by `id`, then send the next. This bridge is not designed for concurrent callers. ## 4. Request envelope ```json {"v":1,"id":"unique-request-id","op":"get_player","args":{"id":"alice"}} ``` | Field | Type | Required | Notes | |--------|--------|----------|------------------------------------------------------------------------------| | `v` | number | yes | Must equal the current protocol version (`1`) | | `id` | string | yes | 1 to 128 characters. Your correlation id, see [idempotency](#6-idempotency) | | `op` | string | yes | One of the operations in [Operations Reference](Operations-Reference) | | `args` | object | no | Operation specific arguments; omit or use `{}` for operations that take none | The JSON must be compact, with no whitespace anywhere. This isn't a style preference: RCON's command line parser only treats a double-quoted span as one argument, so a payload containing a literal space gets split into multiple arguments and fails to parse as a single value. Since the whole thing is base64 encoded regardless, this only matters for how you serialize the JSON before encoding it; just don't pretty-print it. Size limit: the decoded JSON must be 4096 bytes or smaller, or the request is rejected with `PAYLOAD_TOO_LARGE` (see section 7) before it's even parsed. ## 5. Response envelope Success: ```json {"v":1,"id":"unique-request-id","ok":true,"data":{}} ``` Failure: ```json {"v":1,"id":"unique-request-id","ok":false,"error":{"code":"PLAYER_NOT_FOUND","message":"No registered player matched the supplied id."}} ``` `id` always echoes the request's `id`, letting you confirm you're looking at the response to your request and not a stale one from before you polled. `data`'s shape depends on the operation; see [Operations Reference](Operations-Reference). A response isn't strictly guaranteed, but you're very unlikely to see the gap in practice. If an operation's handler throws an unexpected error, a bug rather than a normal error condition, you get back `INTERNAL_ERROR` like any other error response; it doesn't go missing. Implement a client-side timeout anyway, see the latency note in section 3, rather than waiting indefinitely for a reply. ## 6. Idempotency Repeating the same `id` within the configured idempotency window (60 seconds by default, see [Configuration](Configuration)) replays the original cached response. The operation does not run again, and any different `args` you send along with the repeat are ignored. This exists for safe retries: you sent a request, didn't see a response in time, and want to resend without double-triggering a side-effecting operation, such as `save_server`; use the same `id` for that. For routine, repeated polling, such as checking world stats every few minutes, generate a fresh `id` per logical request: a UUID, an incrementing counter, or anything else that won't collide. Reusing a fixed id like `"poll"` for every call means you'll only ever see the very first response for the whole idempotency window, then a fresh one, then the same pattern again, which isn't what you want for periodic sampling. After the window expires, an id is no longer considered a retry; reusing it triggers a completely fresh execution, not an error. ## 7. Rate limiting and errors The bridge allows a limited number of requests per rolling time window (10 requests per 10 seconds by default, admin-tunable, see [Configuration](Configuration)), globally rather than per RCON connection, since RCON requests carry no distinguishable caller identity. Exceeding it returns `RATE_LIMITED` rather than queuing or blocking. | Error code | Meaning | |---------------------|----------------------------------------------------------------------------------------------------------| | `SCHEMA_ERROR` | Malformed base64 or JSON, or the envelope is missing or misshaped | | `PAYLOAD_TOO_LARGE` | Decoded request exceeds the size limit (4096 bytes) | | `OP_NOT_ALLOWED` | `op` isn't a recognized operation, see [Operations Reference](Operations-Reference) for the current list | | `RATE_LIMITED` | Too many requests in the current rate-limit window | | `PLAYER_NOT_FOUND` | `get_player` found no snapshot for the given id | | `SAVE_FAILED` | `save_server` triggered but the underlying save call failed | | `INTERNAL_ERROR` | An operation's handler threw an unexpected error, a bug rather than a normal error condition | A rejected request, any error above, is still logged server-side and still publishes an error response. The only case with no response at all is the unhandled internal error edge case described in section 5. ## 8. `RconDataBridge_ProtocolVersion` structure ```json {"version": 1, "ops": ["get_audit_log", "get_bridge_status", "get_player", "get_world_stats", "list_players", "save_server"]} ``` Check `version` against the protocol version you were built against before assuming anything else in this reference still applies. Check `ops` before calling an operation you're not certain is still registered on this build. This array is generated live from the server's actual allowlist, so it never lags a build that added or removed an operation, even if this wiki page temporarily does. ## 9. Client implementation checklist - Generate a fresh, unique `id` per logical request (section 6); don't hardcode one. - Serialize compact JSON, with no whitespace, before base64 encoding (section 4). - Decode every `RconDataBridge_*` value you read, including `LastResponse` (section 2); nothing comes back as raw JSON. - Match responses to requests by `id`, not just by "the response changed." A response could still be sitting there from a previous call if you poll too early. - Implement a timeout of a few seconds rather than polling forever; some failure modes produce no response at all (section 5). - Don't fire a second request before you've read the first one's response. There's one mailbox slot, not a queue (section 3). - Treat unknown or extra response fields as forward-compatible; don't fail parsing on fields not documented here. - Accumulate multi-packet RCON responses. A large `showoptions` output, this mod's options plus every built-in server option, can exceed a single RCON packet. Project Zomboid's RCON framing chunks large responses across multiple packets sharing the same request id, with no explicit end-of-response marker, so a client that resolves on the first packet will intermittently see truncated or missing option values that are, in fact, present server-side. Accumulate every packet for a given request id, using a short quiet-read timeout or a trailer-command trick such as sending a cheap follow-up command and reading until its response shows up, before treating a response as complete. This is a general RCON client concern, not specific to this mod, but it's the most common cause of "options intermittently missing" reports.