diff --git a/wiki/Architecture.md b/wiki/Architecture.md new file mode 100644 index 0000000..233876d --- /dev/null +++ b/wiki/Architecture.md @@ -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 + 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). diff --git a/wiki/Configuration.md b/wiki/Configuration.md new file mode 100644 index 0000000..f44aa32 --- /dev/null +++ b/wiki/Configuration.md @@ -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. diff --git a/wiki/Home.md b/wiki/Home.md new file mode 100644 index 0000000..7a817d1 --- /dev/null +++ b/wiki/Home.md @@ -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. diff --git a/wiki/Installation.md b/wiki/Installation.md new file mode 100644 index 0000000..978c452 --- /dev/null +++ b/wiki/Installation.md @@ -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_` 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. diff --git a/wiki/Operations-Reference.md b/wiki/Operations-Reference.md new file mode 100644 index 0000000..d944d83 --- /dev/null +++ b/wiki/Operations-Reference.md @@ -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_`, 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_`: + +```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_`, 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. +- **`` 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. diff --git a/wiki/Protocol-Reference.md b/wiki/Protocol-Reference.md new file mode 100644 index 0000000..af2e260 --- /dev/null +++ b/wiki/Protocol-Reference.md @@ -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 `, 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. diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md new file mode 100644 index 0000000..8261ad3 --- /dev/null +++ b/wiki/_Sidebar.md @@ -0,0 +1,8 @@ +### RconDataBridge + +* [Home](Home) +* [Installation](Installation) +* [Configuration](Configuration) +* [Protocol Reference](Protocol-Reference) +* [Operations Reference](Operations-Reference) +* [Architecture](Architecture)