Add comprehensive wiki documentation for RconDataBridge, covering installation, configuration, protocol details, operation references, and internal architecture.

This commit is contained in:
2026-08-27 17:27:11 +02:00
parent 807bd9f4ef
commit 9b8e11f260
7 changed files with 829 additions and 0 deletions

225
wiki/Protocol-Reference.md Normal file
View File

@@ -0,0 +1,225 @@
# 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 <name> <value>`, 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_<ID>` | 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 <base64>`.
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.