Add comprehensive wiki documentation for RconDataBridge, covering installation, configuration, protocol details, operation references, and internal architecture.
This commit is contained in:
169
wiki/Architecture.md
Normal file
169
wiki/Architecture.md
Normal 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
83
wiki/Configuration.md
Normal 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
46
wiki/Home.md
Normal 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
71
wiki/Installation.md
Normal 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.
|
||||||
227
wiki/Operations-Reference.md
Normal file
227
wiki/Operations-Reference.md
Normal 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
225
wiki/Protocol-Reference.md
Normal 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
8
wiki/_Sidebar.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
### RconDataBridge
|
||||||
|
|
||||||
|
* [Home](Home)
|
||||||
|
* [Installation](Installation)
|
||||||
|
* [Configuration](Configuration)
|
||||||
|
* [Protocol Reference](Protocol-Reference)
|
||||||
|
* [Operations Reference](Operations-Reference)
|
||||||
|
* [Architecture](Architecture)
|
||||||
Reference in New Issue
Block a user