From 75c8ffc07a76e7e4d8e9acac1fcf5882b66e1ec3 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Thu, 22 Oct 2026 15:23:07 -0500 Subject: [PATCH] feat(docs): phase 15 - the Rust group, its reference pages checked against their sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PLAN.md §16 phase 15. A Rust group of fifteen pages after Ultima Online (D70): the module, installing the bridge (installer, Pterodactyl egg, by hand), connecting the site, verifying the bridge, linking Steam accounts, permissions and groups, mod config, who sees who is online, events, zones and rewards, optional plugins, the live map, troubleshooting, and three reference pages - rust-link's sidecar.toml, the plugin's RunicGateway.json, and the event kinds. checkReference.mjs gains three Rust sections rather than a new check: sidecar.toml keys and RUSTLINK_* overrides from Rust-Link config.rs, RunicGateway.json from the plugin's PluginConfig, and the event kinds in three classes against Module-Rust server/catalogue.js (D71). Every admin screen was walked on the walk site before it was described (D36). Core pages now hand off to the Rust group instead of linking out to the docs repository. Three defects the re-read found here are fixed: the UO /health sample's hard-coded protocol 4 (both games' samples now render platform.json through ), Authentication's missing Steam tab, and two pages saying the site cannot reach EventsEnabled - false on Rust. Installer v0.8.1 moved checkFacts red mid-phase; platform.json follows. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- PLAN.md | 77 ++++++++- scripts/checkReference.mjs | 126 +++++++++++++- src/config/sidebar.mjs | 38 +++++ .../docs/administration/authentication.mdx | 15 +- .../docs/docs/administration/events.mdx | 13 +- .../docs/administration/troubleshooting.mdx | 2 +- .../docs/architecture/events-architecture.mdx | 12 +- .../docs/docs/architecture/the-bridge.mdx | 2 +- .../getting-started/connect-a-game-server.mdx | 2 +- .../getting-started/install-a-game-module.mdx | 2 +- .../docs/getting-started/requirements.mdx | 3 +- .../verify-the-whole-stack.mdx | 3 +- src/content/docs/docs/index.mdx | 2 +- .../docs/docs/reference/installer-cli.mdx | 4 +- .../docs/docs/rust/connect-the-site.mdx | 108 ++++++++++++ src/content/docs/docs/rust/event-kinds.mdx | 106 ++++++++++++ .../docs/rust/events-zones-and-rewards.mdx | 104 +++++++++++ src/content/docs/docs/rust/index.mdx | 100 +++++++++++ .../docs/docs/rust/install-the-bridge.mdx | 161 ++++++++++++++++++ .../docs/docs/rust/linking-steam-accounts.mdx | 79 +++++++++ src/content/docs/docs/rust/mod-config.mdx | 76 +++++++++ .../docs/docs/rust/optional-plugins.mdx | 93 ++++++++++ .../docs/docs/rust/permissions-and-groups.mdx | 90 ++++++++++ .../docs/docs/rust/runicgateway-json.mdx | 70 ++++++++ src/content/docs/docs/rust/sidecar-toml.mdx | 83 +++++++++ src/content/docs/docs/rust/the-live-map.mdx | 65 +++++++ .../docs/docs/rust/troubleshooting.mdx | 60 +++++++ .../docs/docs/rust/verify-the-bridge.mdx | 101 +++++++++++ .../docs/docs/rust/who-sees-who-is-online.mdx | 85 +++++++++ .../docs/ultima-online/verify-the-bridge.mdx | 7 +- src/data/platform.json | 2 +- src/data/reference.mjs | 137 +++++++++++++++ 32 files changed, 1796 insertions(+), 32 deletions(-) create mode 100644 src/content/docs/docs/rust/connect-the-site.mdx create mode 100644 src/content/docs/docs/rust/event-kinds.mdx create mode 100644 src/content/docs/docs/rust/events-zones-and-rewards.mdx create mode 100644 src/content/docs/docs/rust/index.mdx create mode 100644 src/content/docs/docs/rust/install-the-bridge.mdx create mode 100644 src/content/docs/docs/rust/linking-steam-accounts.mdx create mode 100644 src/content/docs/docs/rust/mod-config.mdx create mode 100644 src/content/docs/docs/rust/optional-plugins.mdx create mode 100644 src/content/docs/docs/rust/permissions-and-groups.mdx create mode 100644 src/content/docs/docs/rust/runicgateway-json.mdx create mode 100644 src/content/docs/docs/rust/sidecar-toml.mdx create mode 100644 src/content/docs/docs/rust/the-live-map.mdx create mode 100644 src/content/docs/docs/rust/troubleshooting.mdx create mode 100644 src/content/docs/docs/rust/verify-the-bridge.mdx create mode 100644 src/content/docs/docs/rust/who-sees-who-is-online.mdx diff --git a/PLAN.md b/PLAN.md index d051438..0662524 100644 --- a/PLAN.md +++ b/PLAN.md @@ -237,7 +237,7 @@ Taken by the org lead (Colby Whitlock) on 2026-08-19. Recorded so they are not r **Decisions after D13 are recorded where they were taken**, in the section describing the phase that raised them, rather than appended here — a decision is only re-litigated when its reasoning is -somewhere other than the thing it decided. The count of record is **sixty-nine**: +somewhere other than the thing it decided. The count of record is **seventy-one**: | # | Where | What it settled | |---|---|---| @@ -258,6 +258,7 @@ somewhere other than the thing it decided. The count of record is **sixty-nine** | D66 | §16, "How phase 13 built it" | The hero and footer show the Module API only; a protocol and a bundle belong to a game | | D67 | §16, "One rule the phases need" | A screenshot belongs to its game: a capture tagged with a game may carry its words, beside the game's name | | D68–D69 | §16, "How phase 14 built it" | A game's real example on a core page is a tagged, labelled block; *Verify the whole stack* stays in Getting started, game-neutral (amending D60) | +| D70–D71 | §16, "How phase 15 built it" | The Rust group is fifteen pages, one per admin screen where it can be; the Rust event kinds are checked against the module's own classification | --- @@ -2094,6 +2095,80 @@ repositories): - **The installer's `--help` says "Which game's shard side"** for `--game`, and `--site-url` names "Admin → Shard (uo-link)" for both games. +### How phase 15 built it + +Two decisions, taken by the org lead on 2026-10-22 before any page was written. The count of +record is now **seventy-one**. + +**D70 — the Rust group is fifteen pages.** D60 named the topics and not the pages. The group +follows the Ultima Online group's shape: an overview, then the journey (*Install the bridge*, +*Connect the site*, *Verify the bridge*), then one page per admin screen where there is one +(*Permissions and groups*, *Mod config from the site*, *Who sees who is online*, *Events, zones +and rewards*), then *Linking Steam accounts*, *Optional plugins*, *The live map* and +*Troubleshooting*, and three reference pages (*sidecar.toml*, *RunicGateway.json*, *Event +kinds*). *Who sees who is online* and *Optional plugins* got pages of their own rather than +being folded into the overview and the events page, because each has a screen or a trap of its +own — RaidableBases must be restarted, never reloaded. The alternative was ten longer pages. The +question put to the org lead called this option "14 pages" while listing fifteen; the list is +what was chosen. RunicNPC's subgroup is phase 16's. + +**D71 — the Rust event kinds are checked against the module's classification.** The plan said the +protocol-14 event list would be checked "against its sources". There were two candidates: +`PROTOCOL.md`, whose kinds are spread across tables from §8.4 to §20.12, and `Module-Rust` +`server/catalogue.js`, which holds three lists in code — public, staff, and the presence subset +of public. The second was chosen, for D39's reason: the visibility ladder is checked against the +module that enforces it, and so is this. The module's own test already holds its lists to +`PROTOCOL.md`, so the chain reaches the spec without this repo parsing prose tables. The page +shows three classes, and each is compared separately, so a kind moving from staff to public +upstream fails here twice. + +**`checkReference.mjs` gained three Rust sections**, as the plan said, rather than a new check: +rust-link's `sidecar.toml` (parsed from its serde structs by the same function as uo-link's, now +shared) and the six `RUSTLINK_…` variables that override it; the bridge plugin's +`RunicGateway.json`, read from the public fields of `PluginConfig` in `RunicGateway.cs`; and the +event kinds (D71). The canonical documents gained `OPERATING.md` and `CARBON.md` under Rust. + +**Every admin screen was walked on the walk site first** (D36), signed in as the walk admin: +Rust servers (Test, Edit, Chat titles, Optional mods), Rust permissions (a player, a group's four +tabs), Rust mod config (the tree and `RunicGateway.json`'s read-only keys), Rust visibility, Rust +zone presets, the event editor's action list, a user's Rust section, and the public server page's +feed, map and leaderboard. Nothing was saved. The NPC screens are phase 16's. Writing from the +walk and the source corrected three of my own drafts before they shipped: a reward does **not** +need a linked account (rewards go to Steam ids), the bundle ships **three** helpers, not two, and +the Test button prints the raw status (`protocol-mismatch`), not a phrase. + +**What the re-read found in this repository**, fixed here: + +- ***Verify the bridge* (Ultima Online) showed `"protocol":4`** in its `/health` sample, a + hard-coded version four protocols stale. Both games' samples now render the protocol from + `platform.json` through Starlight's ``, which is how a version gets into a code block + without being typed. +- ***Authentication* said "four tabs"** and never mentioned Steam, which core shipped with Module + API 1.17.0. It now has five, and a *Steam* section the Rust pages link to. +- ***Scheduled events* and *Events architecture* said the site cannot reach `EventsEnabled`.** + True for Ultima Online, false for Rust: an administrator can edit `RunicGateway.json`, that switch + included, from Admin → Rust mod config. Both pages now say so, the first as two tagged examples + (D68). + +**`checkFacts` went red mid-phase, as designed.** Installer v0.8.1 shipped on 2026-10-22 — the +game-neutral `--help` that phase 14 reported, fixed in `installer` #43 alongside `website` #221 for +the Teams panel — so `platform.json` moved to it. Its only change is that help text, which no page +quotes. + +**Findings in other repositories**, reported rather than fixed: + +- **`Module-Rust` admin copy shows decision numbers.** A group's *Servers* tab reads "unless it is + shared (D189)", and the *not imported yet* chip's tooltip ends "(D198)". Neither means anything + to an operator. +- **`Module-Rust`'s servers page reads "Last pushed: 1 player hold one."** — the plural verb is + used for one. +- **`Rust-Plugins`' `PluginConfig.EventsEnabled` comment says it "gates taking a lease only"**. + That stopped being true at protocol 9, when it began gating every world write. +- **`docs` `rust-link/INTEGRATION.md` predates the installer**: it says there is no installer + support for Rust and shows protocol-1 samples. **`rust-link/INSTALL.md`'s by-hand step names two + helpers**; the bundle ships three. The org lead approved a separate `docs` PR for these: + `docs` #370. + --- [api]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_API.md diff --git a/scripts/checkReference.mjs b/scripts/checkReference.mjs index a248646..6e961e9 100644 --- a/scripts/checkReference.mjs +++ b/scripts/checkReference.mjs @@ -7,6 +7,10 @@ * this is the machinery that makes writing the NAMES down safe anyway — the same bargain * checkQuickstart.mjs struck for the quickstart, applied to six more sources. * + * Phase 15 (PLAN.md §16) added the Rust group's three: rust-link's `sidecar.toml` and its + * environment overrides, the bridge plugin's `RunicGateway.json`, and the event kinds with the + * class each is served under (D71). + * * Each enumeration in `src/data/reference.mjs` is compared against its authority, read from * the repository that owns it over the Gitea API — never from a working tree, per §1's * process rule. Every comparison is a SET comparison in both directions: @@ -39,6 +43,10 @@ import { bridgeCfg, visibilityLadder, canonicalDocs, + rustSidecarConfig, + rustSidecarEnv, + rustPluginConfig, + rustEventKinds, } from '../src/data/reference.mjs'; const ROOT = fileURLToPath(new URL('..', import.meta.url)); @@ -103,6 +111,45 @@ const cfgKeysOf = (text) => .filter(Boolean) .map((m) => m[1]); +/** + * A sidecar's TOML keys, parsed from its serde structs rather than from a sample file, + * because the sample is GENERATED by the binary on first run and no committed copy is + * authoritative. Each `pub name: T` inside a `struct XCfg` is one key, and the struct name + * gives the section. Both sidecars are written this way, so one parser serves both. + */ +function tomlKeysOf(configRs) { + const keys = []; + for (const m of configRs.matchAll(/struct\s+(\w+)Cfg\s*\{([\s\S]*?)\n\}/g)) { + const section = m[1].toLowerCase(); + for (const f of m[2].matchAll(/pub\s+(\w+)\s*:/g)) keys.push(`${section}.${f[1]}`); + } + return keys; +} + +/** The body of a C# class, by brace counting from its opening brace. */ +function classBody(source, name) { + const start = source.search(new RegExp(`class\\s+${name}\\b`)); + if (start < 0) return null; + const open = source.indexOf('{', start); + let depth = 0; + for (let i = open; i < source.length; i++) { + if (source[i] === '{') depth++; + else if (source[i] === '}' && --depth === 0) return source.slice(open + 1, i); + } + return null; +} + +/** + * The quoted strings inside `const NAME = Object.freeze([ … ])`, comments removed first — + * the module's comments are prose, and prose has apostrophes. + */ +function frozenListOf(source, name) { + const m = source.match(new RegExp(`const\\s+${name}\\s*=\\s*Object\\.freeze\\(\\[([\\s\\S]*?)\\]\\)`)); + if (!m) return null; + const code = m[1].replace(/\/\/.*$/gm, ''); + return [...code.matchAll(/'([^'\n]+)'/g)].map((x) => x[1]); +} + async function run() { if (!TOKEN) { console.error('checkReference: GITEA_TOKEN is not set. This check cannot run anonymously.'); @@ -122,13 +169,12 @@ async function run() { // Parsed from the serde structs rather than from a sample file, because the sample is // GENERATED by the binary on first run and no committed copy is authoritative. Each // `pub name: T` inside a `struct XCfg` is one key, and the struct name gives the section. - const configRs = await raw('link', 'sidecar/src/config.rs'); - const sidecarKeys = []; - for (const m of configRs.matchAll(/struct\s+(\w+)Cfg\s*\{([\s\S]*?)\n\}/g)) { - const section = m[1].toLowerCase(); - for (const f of m[2].matchAll(/pub\s+(\w+)\s*:/g)) sidecarKeys.push(`${section}.${f[1]}`); - } - compareSets('sidecar.toml', Object.keys(sidecarConfig), sidecarKeys, 'link main:sidecar/src/config.rs'); + compareSets( + 'sidecar.toml', + Object.keys(sidecarConfig), + tomlKeysOf(await raw('link', 'sidecar/src/config.rs')), + 'link main:sidecar/src/config.rs', + ); // ── 3. Installer commands ───────────────────────────────────────────────── const cliRs = await raw('installer', 'src/cli.rs'); @@ -175,6 +221,72 @@ async function run() { else fail(`canonical doc ${docPath}`, `not found in docs main (HTTP ${res.status})`); } + // ── 7. Rust: rust-link's sidecar.toml, and the variables that override it ── + // + // PLAN.md §16, phase 15. The same parser as uo-link's — both sidecars declare their file + // as serde structs — plus the environment overrides, which a Pterodactyl egg sets and an + // operator meets on the panel before they ever see the file. + const rustConfigRs = await raw('Rust-Link', 'sidecar/src/config.rs'); + compareSets( + 'rust-link sidecar.toml', + Object.keys(rustSidecarConfig), + tomlKeysOf(rustConfigRs), + 'Rust-Link main:sidecar/src/config.rs', + ); + compareSets( + 'rust-link environment variable', + Object.keys(rustSidecarEnv), + [...rustConfigRs.matchAll(/get\("(RUSTLINK_[A-Z_]+)"\)/g)].map((m) => m[1]), + 'Rust-Link main:sidecar/src/config.rs', + ); + for (const [envName, key] of Object.entries(rustSidecarEnv)) { + if (!(key in rustSidecarConfig)) fail(`rust-link environment variable ${envName}`, `says it overrides "${key}", which is not a key listed here`); + } + + // ── 8. Rust: the bridge plugin's RunicGateway.json ──────────────────────── + // + // The public fields of `PluginConfig`, which the framework serialises as the file. + const pluginCs = await raw('Rust-Plugins', 'overlay/oxide/plugins/RunicGateway.cs'); + const pluginBody = classBody(pluginCs, 'PluginConfig'); + if (!pluginBody) { + fail('RunicGateway.json', 'could not find class PluginConfig in Rust-Plugins main:overlay/oxide/plugins/RunicGateway.cs'); + } else { + compareSets( + 'RunicGateway.json', + Object.values(rustPluginConfig).flatMap((group) => Object.keys(group)), + [...pluginBody.matchAll(/public\s+[\w<>[\],]+\s+(\w+)\s*=/g)].map((m) => m[1]), + 'Rust-Plugins main:overlay/oxide/plugins/RunicGateway.cs PluginConfig', + ); + } + + // ── 9. Rust: the event kinds, and who may see each ──────────────────────── + // + // D71: checked against the module's own catalogue, because the module is what enforces + // the boundary (the same reasoning as check 5). Three comparisons, each a security + // statement: a kind listed public here must be public there, a presence kind must be one + // of the module's presence kinds, and a staff kind must be staff there. A kind moved from + // staff to public upstream fails this check twice over, which is the point. + const catalogue = await raw('Module-Rust', 'server/catalogue.js'); + const upPublic = frozenListOf(catalogue, 'PUBLIC_KINDS'); + const upStaff = frozenListOf(catalogue, 'STAFF_KINDS'); + const upPresence = frozenListOf(catalogue, 'PRESENCE_KINDS'); + if (!upPublic || !upStaff || !upPresence) { + fail('Rust event kinds', 'could not find PUBLIC_KINDS, STAFF_KINDS and PRESENCE_KINDS in Module-Rust main:server/catalogue.js'); + } else { + const hint = 'Module-Rust main:server/catalogue.js'; + compareSets('Rust presence kind', Object.keys(rustEventKinds.presence), upPresence, `${hint} PRESENCE_KINDS`); + compareSets( + 'Rust public kind', + Object.keys(rustEventKinds.public), + upPublic.filter((k) => !upPresence.includes(k)), + `${hint} PUBLIC_KINDS (less its presence kinds)`, + ); + compareSets('Rust staff kind', Object.keys(rustEventKinds.staff), upStaff, `${hint} STAFF_KINDS`); + for (const k of upPresence) { + if (!upPublic.includes(k)) fail(`Rust presence kind ${k}`, 'is a presence kind upstream but not a public one — the page explains presence as a narrowing of public, so re-read it'); + } + } + // ── Report ──────────────────────────────────────────────────────────────── if (failures.length === 0) { console.log(`checkReference: ${checked.length} enumeration check(s) passed against their sources.`); diff --git a/src/config/sidebar.mjs b/src/config/sidebar.mjs index 0fe21c5..1eeaca6 100644 --- a/src/config/sidebar.mjs +++ b/src/config/sidebar.mjs @@ -98,6 +98,26 @@ export const docsSidebar = [ { label: 'Shard event catalog', slug: 'docs/ultima-online/event-catalog' }, ], }, + { + label: 'Rust', + items: [ + { label: 'The Rust module', slug: 'docs/rust' }, + { label: 'Install the bridge', slug: 'docs/rust/install-the-bridge' }, + { label: 'Connect the site', slug: 'docs/rust/connect-the-site' }, + { label: 'Verify the bridge', slug: 'docs/rust/verify-the-bridge' }, + { label: 'Linking Steam accounts', slug: 'docs/rust/linking-steam-accounts' }, + { label: 'Permissions and groups', slug: 'docs/rust/permissions-and-groups' }, + { label: 'Mod config from the site', slug: 'docs/rust/mod-config' }, + { label: 'Who sees who is online', slug: 'docs/rust/who-sees-who-is-online' }, + { label: 'Events, zones and rewards', slug: 'docs/rust/events-zones-and-rewards' }, + { label: 'Optional plugins', slug: 'docs/rust/optional-plugins' }, + { label: 'The live map', slug: 'docs/rust/the-live-map' }, + { label: 'Troubleshooting', slug: 'docs/rust/troubleshooting' }, + { label: 'sidecar.toml', slug: 'docs/rust/sidecar-toml' }, + { label: 'RunicGateway.json', slug: 'docs/rust/runicgateway-json' }, + { label: 'Event kinds', slug: 'docs/rust/event-kinds' }, + ], + }, ]; /** @@ -174,4 +194,22 @@ export const plannedSidebar = { 'sidecar.toml', 'Shard event catalog', ], + // PLAN.md §16, D70 (phase 15). RunicNPC's subgroup lands in phase 16 (D61). + Rust: [ + 'The Rust module', + 'Install the bridge', + 'Connect the site', + 'Verify the bridge', + 'Linking Steam accounts', + 'Permissions and groups', + 'Mod config from the site', + 'Who sees who is online', + 'Events, zones and rewards', + 'Optional plugins', + 'The live map', + 'Troubleshooting', + 'sidecar.toml', + 'RunicGateway.json', + 'Event kinds', + ], }; diff --git a/src/content/docs/docs/administration/authentication.mdx b/src/content/docs/docs/administration/authentication.mdx index 1df190d..3be12c2 100644 --- a/src/content/docs/docs/administration/authentication.mdx +++ b/src/content/docs/docs/administration/authentication.mdx @@ -5,7 +5,7 @@ description: Local accounts and two-factor, SSO providers and the link-only poli import { Aside } from '@astrojs/starlight/components'; -**Admin → Authentication** has four tabs: Local Accounts, Google, Discord and Custom +**Admin → Authentication** has five tabs: Local Accounts, Google, Discord, Steam and Custom Providers. One session model sits behind all of them — a web cookie, a mobile bearer token and an SSO sign-in all produce the same session. @@ -35,6 +35,19 @@ account is still a stranger. Configuring Google here also unlocks **email delivery**, which reuses the same OAuth client — see [Notifications and email](/docs/administration/notifications-and-email/). +### Steam + +Steam signs people in with OpenID rather than OAuth, so it needs no client ID or secret — but +it does need a **Steam Web API key**, for the name and avatar Steam's sign-in does not carry, +and saving Steam as enabled without one is refused. It follows the same link-only policy, more +strictly: **Steam never creates an account**, whatever registration allows. A person may link +several Steam accounts, and each is listed on their account screen with its name, avatar and +Steam ID. + +A game module can also be told which Steam accounts a signed-in person has proved are theirs, +which is how a game that knows its players by Steam account can recognise a member. The Rust +module uses it — see [Linking Steam accounts](/docs/rust/linking-steam-accounts/). + ## Trusted devices A second factor that asks on every sign-in on the same laptop trains people to click diff --git a/src/content/docs/docs/administration/events.mdx b/src/content/docs/docs/administration/events.mdx index 89ecb36..7cc5327 100644 --- a/src/content/docs/docs/administration/events.mdx +++ b/src/content/docs/docs/administration/events.mdx @@ -141,8 +141,8 @@ restart puts every borrowed value back too. ## The game server has its own switches -They live in the bridge plugin's config on the game server's host, outside the site's reach, -and the site cannot turn them on. +They live in the bridge plugin's config on the game server's host, not in the site's +database, and no event can turn them on. **`EventsEnabled` is off by default**, in both games. Turning it on is consenting to the site changing and watching your world unattended. @@ -151,7 +151,14 @@ changing and watching your world unattended. `EventsEnabled` is a different switch from `AdminWriteEnabled`. Turning the admin plane on is consenting to staff moderation driven from a screen somebody is looking at; turning events on is consenting to unattended changes. One switch could not honestly express both. Every key -is in [`Bridge.cfg`](/docs/ultima-online/bridge-cfg/). +is in [`Bridge.cfg`](/docs/ultima-online/bridge-cfg/), which only the shard host can edit. + + + +`EventsEnabled` is in each server's [`RunicGateway.json`](/docs/rust/runicgateway-json/). An +administrator can edit it on the host, or from the site's +[Mod config](/docs/rust/mod-config/) screen, which reaches the game host's config files +through the bridge. Beside it sit the game's own ceilings — how much one call may place, how long something it diff --git a/src/content/docs/docs/administration/troubleshooting.mdx b/src/content/docs/docs/administration/troubleshooting.mdx index e2414b0..34a20a4 100644 --- a/src/content/docs/docs/administration/troubleshooting.mdx +++ b/src/content/docs/docs/administration/troubleshooting.mdx @@ -82,7 +82,7 @@ Two responses from a sidecar mean the same thing for every game: **`409`** is a mismatch (the sidecar refuses rather than mis-parsing), and **`401`** is the auth token. Each game's group in the sidebar has its own troubleshooting page with the exact commands: -[Ultima Online](/docs/ultima-online/troubleshooting/) today. +[Ultima Online](/docs/ultima-online/troubleshooting/) and [Rust](/docs/rust/troubleshooting/). ## Teams are missing diff --git a/src/content/docs/docs/architecture/events-architecture.mdx b/src/content/docs/docs/architecture/events-architecture.mdx index 2c32f05..d9eaf75 100644 --- a/src/content/docs/docs/architecture/events-architecture.mdx +++ b/src/content/docs/docs/architecture/events-architecture.mdx @@ -114,11 +114,13 @@ something must not take that path. are re-validated against the database on every request, so a demotion takes effect on the next click. -The game's switches are the layer the site cannot reach: they live in the bridge plugin's -own config on the game server's host — `EventsEnabled`, in both games — and are off out of -the box, and the -game's own ceilings **refuse rather than clamp** — because a silently shortened request leaves -the two halves disagreeing about what happened. +The game's switches are the layer outside the site's own database: they live in the bridge +plugin's own config on the game server's host — `EventsEnabled`, in both games — and are off +out of the box. No event and no role check can turn one on. On Rust an administrator can +still edit that file from the game module's config screen, which reaches the host's config +files through the bridge, so there it is a separate deliberate act rather than an unreachable +one. The game's own ceilings **refuse rather than clamp** — because a silently shortened +request leaves the two halves disagreeing about what happened.