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

169
wiki/Architecture.md Normal file
View File

@@ -0,0 +1,169 @@
# Architecture
This page covers how RconDataBridge is built internally: the module map,
the boot sequence, the security model, and the Project Zomboid engine
quirks the mod works around. It's aimed at anyone reading or modifying the
mod's own Lua code, rather than consuming the protocol from outside; for
that, see [Protocol Reference](Protocol-Reference) and [Operations
Reference](Operations-Reference).
## Boot sequence
On `Events.OnServerStarted`, bootstrap code runs, in order:
1. Run internal self-tests (JSON and base64 codecs), if enabled.
2. Register the base RCON options (`ProtocolVersion`, `WorldStats`,
`Request`, `Response`, `LastResponse`).
3. Register every operation into the operation registry.
4. Publish the initial `RconDataBridge_ProtocolVersion` value.
5. Start the request pipeline and the world and player telemetry pollers.
`Events.OnServerStarted` is known to fire more than once per boot in
Project Zomboid, so every step above guards against running twice: option
registration checks for an existing option first and wraps the underlying
engine call in a protected call as a second line of defense, and each
poller's `init()` function is guarded by a local flag, so its event
listeners are only ever added once.
## Request flow
Once booted, a single request travels through the modules like this:
```text
RCON client
|
| changeoption RconDataBridge_Request <base64 JSON>
v
RconDataBridge_Request option (single mailbox slot)
|
| polled every N ticks (Poll Interval setting)
v
Request pipeline
|-- base64 decode, size check ----> reject: SCHEMA_ERROR / PAYLOAD_TOO_LARGE
|-- JSON decode, envelope check --> reject: SCHEMA_ERROR
|-- idempotency check ------------> known id: replay the cached response
|-- rate limit check -------------> reject: RATE_LIMITED
v
Operation registry (allowlist, exact-name lookup)
|-- unknown op -------------------> reject: OP_NOT_ALLOWED
v
Operation handler (Ops/RDB_Op*.lua)
|-- handler throws ---------------> reject: INTERNAL_ERROR
v
Response envelope { ok, data } or { ok: false, error }
|
| every outcome above, success or rejection, is also written to the audit log
v
RconDataBridge_Response / RconDataBridge_LastResponse
|
| showoptions, base64 decode
v
RCON client
```
See [Protocol Reference](Protocol-Reference) for what each error code
means and how a client should consume this cycle.
## Module responsibilities
### Options registry
A thin wrapper over Project Zomboid's server options store. Registering an
option needs both an "add option" call and a separate "add to public
options" call, or it never shows up in `showoptions`, since that command
reads a different internal list than the one a plain add populates.
### Request pipeline
Polls `RconDataBridge_Request` on a configurable tick interval, since
there's no "option changed" event. Decodes base64 to JSON, validates the
envelope, checks idempotency and the rate limit, dispatches through the
operation registry, publishes the response, and always logs the outcome to
the audit trail, including rejections.
### Operation registry
The allowlist; Dispatch is an exact-name lookup into a table, nothing more;
new operations are added one file per operation.
### Security
Payload size cap (4096 bytes decoded), envelope schema validation, and a
global, not per-connection, sliding-window rate limit, since RCON requests
carry no caller identity to limit by individually.
### Idempotency
Replays the cached response for a repeated request `id` within a
configurable window, protecting side-effecting operations, such as
`save_server`, from retries. After the window expires, the same `id` runs
fresh.
### Config
A facade over admin-tunable settings. A small set of values, such as the
maximum payload size, are fixed constants; everything else is read live
from Sandbox Options. See [Configuration](Configuration).
### World stats
Refreshed roughly once an in-game minute from the engine's game clock,
cached in memory, and mirrored directly to `RconDataBridge_WorldStats`, so
`get_world_stats` and the option always agree, and reading the option skips
a request/response round trip entirely.
### Player snapshot
The source of truth for player data, both online and offline, persisted so
it survives a restart, since the engine has no API for offline player data
on its own.
### JSON and base64 codecs
Self-contained shared codecs with no external dependencies. Each runs its
own self-test at boot when the debug self-test setting is enabled.
## Security model
A handful of rules define the mod's security boundary and are treated as
hard constraints rather than style preferences:
- **No new transport, port, or authentication layer.** RCON access is the
only access control, by design.
- **No dynamic operation dispatch.** The operation registry only ever does
an exact-name table lookup; an RCON request can never cause arbitrary
Lua code to run.
- **All operations are explicitly allowlisted.** An unrecognized `op` value
returns `OP_NOT_ALLOWED` rather than being interpreted in any way.
- **Every remote action is logged**, with its request id, operation,
result, and timestamp, including rejected requests, through the audit
log described above.
- **Requests are validated and bounded** on payload size, envelope schema,
and rate, before an operation handler ever runs.
## Engine constraints the mod works around
A number of behaviours in the code exist specifically to work around gaps
or quirks in the Project Zomboid server engine, rather than being
arbitrary design choices. The straightforward ones:
| Constraint | Why | How it's handled |
|-------------------------------------------------|-----------------------------|------------------------------------------------------|
| No "option changed" event | Doesn't exist in the engine | Tick-throttled polling in the request pipeline |
| No connect or disconnect event | Doesn't exist in the engine | Diffing the online roster against the last known set |
| No offline-player registry API | Doesn't exist in the engine | Player snapshots are persisted independently |
| `OnServerStarted` fires more than once per boot | Engine behavior | Every initializer guards on a local flag |
As a special client facing quirk:
### RCON responses can span multiple packets
Confirmed from the server's own RCON response handling: a large response,
such as a `showoptions` call once this mod's options are added to the
roughly 140 built-in ones, gets chunked across multiple packets with no
explicit end-of-response marker.
This is a client-side concern rather than a mod-side one. Any RCON client
must accumulate every packet sharing a request id rather than resolving on
the first one it receives. See the client checklist in [Protocol
Reference](Protocol-Reference#9-client-implementation-checklist).

83
wiki/Configuration.md Normal file
View File

@@ -0,0 +1,83 @@
# Configuration
RconDataBridge exposes its admin-tunable settings through Project Zomboid's
native Sandbox Options system, the same "New Game, Sandbox Settings" screen
also reachable in-game via the server admin panel on an already running
server. This is a separate mechanism from the RCON `ServerOptions`
transport the protocol itself uses (see [Protocol
Reference](Protocol-Reference)); Sandbox Options is purely for these
runtime knobs.
All values live under the RconDataBridge Sandbox Options page, and are
applied live: an admin can change them at runtime with no server restart
required. There is, however, no "value changed" event for Sandbox Options
either, so a changed value takes effect the next time the relevant code
path reads it, such as the next tick for poll cadence settings, rather than
instantaneously.
## Options
### Debug Self-Tests
Boolean, default `true`.
Runs the mod's internal JSON and base64 codec self-tests and logs the
result on every server boot. Diagnostic only; doesn't affect protocol
behavior. Safe to turn off once you trust the install.
### Poll Interval (ticks)
Integer, default `30`, range 1 to 300.
How many server ticks pass between checks of the request mailbox
(`RconDataBridge_Request`) and the online player roster. A lower value
means more responsiveness to incoming requests and player connect or
disconnect changes, at the cost of doing that check more often.
### Idempotency Window (seconds)
Integer, default `60`, range 1 to 3600.
How long a repeated request `id` replays its cached response instead of
re-running the operation. Protects side-effecting operations, such as
triggering a save, from duplicate execution on client retries. See
[Protocol Reference, Idempotency](Protocol-Reference#6-idempotency).
### Rate Limit: Max Requests
Integer, default `10`, range 1 to 1000.
Maximum number of bridge requests allowed within one rate-limit window; see
the next setting. This limit is global, not per connection, since RCON
requests carry no distinguishable caller identity, so it's the only way to
bound total load.
### Rate Limit: Window (seconds)
Integer, default `10`, range 1 to 3600.
Length of the rolling rate-limit window, in seconds. Combined with the
setting above, the default is 10 requests per rolling 10 seconds.
A running server's current values for the rate-limit pair, reflecting any
admin change and not just the defaults, are also readable at any time via
the `get_bridge_status` operation. This is useful for a client that wants
to self-tune its polling instead of hardcoding assumptions. See [Operations
Reference](Operations-Reference#get_bridge_status).
## What's not configurable here, and why
Two protocol-level values are intentionally not exposed as Sandbox
Options, even though they look like they could be:
- **Max request payload size** (4096 bytes decoded) and the underlying RCON
option's max length are hardcoded. They're sized against the fixed
capacity of the RCON option storing the request, after base64 expansion.
An admin-set larger value here, with no matching change to that
underlying capacity, could silently truncate a request instead of
cleanly rejecting it with an error. Since that failure mode is worse than
simply always rejecting payloads over the fixed limit, it isn't offered
as a tunable.
- **Protocol identifiers**, meaning the protocol version number, the fixed
`RconDataBridge_*` option names, and the standard error codes, are
fixed, documented, client-facing constants, not server-tunable behavior.

46
wiki/Home.md Normal file
View File

@@ -0,0 +1,46 @@
# RconDataBridge
**RconDataBridge** is a server-side [Project Zomboid](https://projectzomboid.com/)
(Build 42) mod that exposes structured world and player telemetry, plus a
small allowlisted RPC surface, over the server's existing RCON connection.
There is no new port and no new authentication layer to configure; it uses
the RCON connection your server already has.
It rides entirely on two vanilla RCON commands, `changeoption` and
`showoptions`, which every RCON client or library already speaks.
There is no client-side mod component. "Client" throughout this wiki means
an external RCON tool, bot, or dashboard backend, not in-game Project
Zomboid UI.
## Why?
Vanilla RCON gives you console commands and raw `showoptions` server
settings, but nothing structured and nothing queryable, and no safe way to
trigger a handful of specific server actions without giving a bot full
console access. RconDataBridge adds a thin, versioned, allowlisted JSON
protocol on top of that same connection.
## Wiki contents
- **[Installation](Installation)**: getting the mod onto a server and
confirming it's running.
- **[Configuration](Configuration)**: the admin-tunable Sandbox Options,
such as rate limits and poll cadence.
- **[Protocol Reference](Protocol-Reference)**: how the RCON transport
works, covering the option map, base64 encoding, the request/response
cycle, idempotency, rate limiting, and error codes.
- **[Operations Reference](Operations-Reference)**: every operation, such
as `get_player` and `list_players`, with its arguments, response shape, and errors.
- **[Architecture](Architecture)**: how the mod is built internally,
covering module responsibilities, the boot sequence, and the engine
quirks it works around.
## Requirements and License
- A Project Zomboid dedicated server, Build 42.0.0 or newer, per the mod
manifest's `versionMin`.
- RCON enabled on the server, meaning `RCONPort` and `RCONPassword` are set
in the server's `.ini`. This mod adds no transport of its own, so if RCON
isn't reachable, neither is this mod.
- Licensed under AGPLv3.

71
wiki/Installation.md Normal file
View File

@@ -0,0 +1,71 @@
# Installation
RconDataBridge is a server-side-only mod. There's no client component to
install; players don't need it, and it does nothing to the game client. It
only needs to run on the dedicated server process itself.
## Requirements
- A Project Zomboid dedicated server, Build 42.0.0 or newer.
- RCON enabled on that server, meaning `RCONPort` and `RCONPassword` are set
in the server's `.ini` file. RconDataBridge has no transport of its own;
every interaction happens over the RCON connection the server already
exposes, so if RCON isn't reachable, this mod isn't reachable either.
## Getting the mod onto the server
Install it like any other Project Zomboid mod:
1. Copy, or Steam Workshop subscribe to, the mod so that its folder,
containing `mod.info`, `poster.png`, and the `common/` tree, ends up
under the server's `mods` search path (typically the server's
`Zomboid/mods/` directory, or the Workshop cache if installed that way).
2. Add the mod to the server config, either through the in-game or admin
server settings screen, or by editing the server's `.ini` directly: add
`RconDataBridge` to the `Mods=` list, and, if installed via Steam
Workshop, the corresponding Workshop item id to `WorkshopItems=`.
3. Restart the server. Lua mods in Project Zomboid have no hot reload, so
any install or update requires a full server restart to take effect.
## Verifying it's running
Once the server is back up, connect with any RCON client and run:
```text
showoptions
```
You should see, among the roughly 140 built-in server options, a handful of
`RconDataBridge_*` entries:
- `RconDataBridge_ProtocolVersion`
- `RconDataBridge_WorldStats`
- `RconDataBridge_Request`
- `RconDataBridge_Response`
- `RconDataBridge_LastResponse`
All of their values are base64 encoded; see [Protocol
Reference](Protocol-Reference) before trying to read them by eye.
If the server has a very large `showoptions` output, be aware that Project
Zomboid's RCON implementation chunks large responses across multiple
packets with no explicit end-of-response marker. A client that stops
reading after the first packet can appear to be missing these options
intermittently, even though the mod registered them correctly; that's a
client-side accumulation issue, not a sign the mod isn't running. If in
doubt, check the server's own log for a `[RconDataBridge] bootstrap
complete` line, which is printed once boot finishes successfully.
`RconDataBridge_PlayerStats_<id>` options are not present at boot; they're
created lazily, the first time a `get_player` request is made for that
specific player (see [Operations Reference](Operations-Reference)). Don't
expect to see them until then.
## Configuring it
Everything tunable, such as rate limits, poll cadence, and the idempotency
window, is exposed through Project Zomboid's own Sandbox Options system,
under an `RconDataBridge` page reachable either when creating a new save
("Sandbox Settings"), or from the in-game admin panel on an already running
server. See [Configuration](Configuration) for the full list and what each
value does.

View File

@@ -0,0 +1,227 @@
# Operations Reference
This page describes every operation RconDataBridge currently registers,
along with the shared data structures they return. For how to actually
send a request and read a response, see [Protocol
Reference](Protocol-Reference).
Operation names are flat, verb-first `snake_case`, such as `get_player`.
Query `RconDataBridge_ProtocolVersion`'s `ops` array for the authoritative,
currently registered list; this page may lag a build that added or removed
an operation.
Each operation below is documented with an EmmyLua-style stub: a compact
comment block giving its arguments, return shape, and possible error
codes. This is documentation notation, not literal Lua you can call; an
operation is invoked by sending its name and an `args` object over the
wire protocol described in [Protocol Reference](Protocol-Reference), not
by calling a function directly.
Response examples are shown as JSON5, with aligned fields and comments,
purely for readability on this page. The actual wire format is always
strict, compact JSON, no comments, no trailing commas, no whitespace, see
[Protocol Reference § 4](Protocol-Reference#4-request-envelope).
## `get_world_stats`
```lua
--- World telemetry. Not player-related; see get_player / list_players.
--- @return WorldStats
fn get_world_stats()
```
```json5
{
"worldAgeHours": 6.6,
"worldAgeDays": 0.27,
"nightsSurvived": 0,
"date": {
"year": 1993,
"month": 6, // 0-indexed: 0 = January ... 11 = December
"day": 8,
"hour": 13,
"minute": 37
},
"isNight": false,
"isRaining": true,
"timeMultiplier": 4.8, // current game-speed multiplier
"generatedAt": 1787749440066 // ms epoch, when this snapshot was cached
}
```
This is the cached value, refreshed roughly once a minute
in the background. The same data is also mirrored directly
to `RconDataBridge_WorldStats`, so you can read it there
without spending a request/response round trip at all.
The cache only advances while the world clock is actually running.
On a server configured with `PauseEmpty=true`, the game clock, and
therefore this data, freezes while no players are connected; this
reflects the world's real state rather than a stale bridge.
## `get_player`
```lua
--- Returns a player snapshot (see "Player snapshot structure" below).
--- @param id string Exact, case-sensitive username.
--- @return PlayerSnapshot
--- @error PLAYER_NOT_FOUND No snapshot exists yet for that id, meaning
--- either the username never connected to this server, or it hasn't
--- been observed by the bridge yet.
fn get_player(id)
```
A successful lookup also publishes or refreshes
`RconDataBridge_PlayerStats_<id>`, described below,
so subsequent checks on that specific player can skip
the request/response cycle entirely.
## `list_players`
```lua
--- Lists every player the bridge has ever seen, online and offline.
--- @param limit number? Page size. Default 50, capped at 200. A
--- non-numeric or absent value falls back to the default rather than
--- erroring. Optional.
--- @param cursor string? The last `id` from a previous page's results;
--- omit for the first page. Optional.
--- @return { players: PlayerSnapshot[], nextCursor: string? }
fn list_players(limit, cursor)
```
```json5
{
"players": [ /* PlayerSnapshot objects, sorted by id */ ],
"nextCursor": "bob" // absent, not null, once you've reached the last page
}
```
`players` includes every player the bridge has ever seen,
online and offline, not just currently connected ones.
## `save_server`
```lua
--- Triggers a world save. Confirms the save was triggered, not that it
--- completed; there's no separate completion signal.
--- @return { triggered: true }
--- @error SAVE_FAILED The underlying save call threw.
fn save_server()
```
## `get_bridge_status`
```lua
--- Bridge health and introspection: uptime, current rate-limit window
--- usage, and the live runtime configuration. Useful for a client to
--- self-tune, for example backing off polling when the rate-limit
--- window is nearly full, instead of guessing at hardcoded defaults.
--- @return BridgeStatus
fn get_bridge_status()
```
```json5
{
"version": 1,
"bootedAt": 1787740000000, // ms epoch, server's last boot time
"uptimeSeconds": 9440.2,
"paused": false, // true when PauseEmpty=true and 0 players are online
"rateLimit": {
"currentWindowRequests": 3,
"maxRequests": 10,
"windowSeconds": 10
},
"config": {
"maxPayloadBytes": 4096,
"rateLimitMaxRequests": 10, // mirrors the live Sandbox Options value
"rateLimitWindowSeconds": 10 // mirrors the live Sandbox Options value
}
}
```
`paused` doesn't affect this operation itself; the bridge keeps
responding either way. It does tell you the world clock is frozen, so
`get_world_stats` and `RconDataBridge_WorldStats` won't advance until a
player joins. `config` mirrors the bridge's current settings, not just the
defaults, see [Configuration](Configuration): if an admin changed them at
runtime, this reflects the live values.
## `get_audit_log`
```lua
--- Read-only tail of the bridge's own audit trail, the same record of
--- every processed and rejected request that's written to the server
--- console and an internal ring buffer.
--- @param limit number? Max entries to return. Default 50, capped at
--- 200. A non-numeric or absent value falls back to the default
--- rather than erroring. Optional.
--- @return { entries: AuditEntry[] }
fn get_audit_log(limit)
```
```json5
{
"entries": [
{
"id": "ex1",
"op": "get_world_stats",
"ok": true,
"ts": 1787749440066 // ms epoch
},
{
"id": "ex2",
"op": "get_player",
"ok": false,
"code": "PLAYER_NOT_FOUND", // present only when ok is false
"ts": 1787749430012
}
]
}
```
Entries are returned newest first. There's no cursor or pagination beyond
`limit`; this is a log tail, not a stable dataset to page through. The
underlying store holds at most the 500 most recent entries; `limit`
can't retrieve more history than that.
## Player snapshot structure
Returned by `get_player`, embedded in `list_players`'s `players`
array, and published to `RconDataBridge_PlayerStats_<id>`:
```json5
{
"id": "alice",
"online": false,
"characterId": 42, // live session id; not stable across reconnects
"lastSeenAt": 1787486000123, // ms epoch: last refresh online, or disconnect/death time offline
"stats": {
"hoursSurvived": 17.4, // frozen at the last known value once offline
"zombiesKilled": 83 // frozen at the last known value once offline
}
}
```
Only these fields are ever collected: no position, no Steam ID,
and no online ID beyond `characterId`.
Refresh triggers are a periodic sweep of the online roster, roughly once a
second by default, player death, and connect/disconnect detection, which is
also polled, since there's no connect or disconnect event available in the
engine.
## `RconDataBridge_PlayerStats_<ID>`, the read-only side channel
This option exists purely as a convenience cache, not a source of truth;
the request/response cycle via `get_player` always works and is
authoritative. Two things to know before relying on it:
- **It's lazy.** The option for a given player is only created the first
time someone calls `get_player` for that id, from any client, including
the one reading it now, or a previous session. Don't assume it exists
for a player you haven't explicitly queried at least once.
- **`<ID>` is sanitized**, not the raw username: any character outside
`[A-Za-z0-9_]` in the username is replaced with `_` to form a valid RCON
option name. The `id` field inside the JSON payload is still the real,
unsanitized username; use that as the source of truth if you need to
disambiguate.

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.

8
wiki/_Sidebar.md Normal file
View File

@@ -0,0 +1,8 @@
### RconDataBridge
* [Home](Home)
* [Installation](Installation)
* [Configuration](Configuration)
* [Protocol Reference](Protocol-Reference)
* [Operations Reference](Operations-Reference)
* [Architecture](Architecture)