Files
runicgateway.com/src/data/reference.mjs
wtclaude 4426d3e339
All checks were successful
PR checks / checks (pull_request) Successful in 2m10s
feat(docs): phase 14 - game-neutral core docs, an Ultima Online group, and checkTerms
PLAN.md §16, D63/D64, plus D67-D69 taken 2026-10-22.

- Core docs use core's words: a Team, a game server, the sidecar, the bridge
  plugin, the game module. Detail that belonged to Ultima Online moved into
  its pages rather than being dropped.
- New "Ultima Online" sidebar group (nine pages): an overview, Connect a
  ServUO shard, Verify the bridge, The shard connection, Client files,
  Troubleshooting, Bridge.cfg, sidecar.toml, Shard event catalog. Five moved
  URLs answer a real 301 (src/config/redirects.mjs, asserted by the served
  test).
- Getting started is game-neutral. "Connect your game server" keeps the old
  URL and hands off to each game; Rust links docs main:rust-link/INSTALL.md
  until phase 15. "Verify the whole stack" stays, neutral (D69).
- D68: GameExample.astro labels a game's real example on a core page with the
  game's name; checkTerms allows that game's words inside it.
- scripts/checkTerms.mjs, the twelfth check: 16 game words, docs pages and
  both sidebar trees, one allowlisted phrase (Discord's "Guild (server) ID").
  Wired into verify and pr-checks.yml.
- canonicalDocs grouped by game; the Rust group carries four documents.
- D67: the screenshot rule from #32 is adopted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-22 08:54:37 -05:00

280 lines
13 KiB
JavaScript

/**
* The Reference section's enumerations.
*
* §1 says a Reference page is "a navigable summary plus a link to the canonical document —
* never a re-specification". This file is the line between those two things, and it is
* worth being explicit about where it falls:
*
* * The NAMES are here — every environment variable, every config key, every command,
* every event kind. A reference section that cannot answer "what variables are there?"
* without a click-through is a link farm.
* * The SEMANTICS are not. One terse line each, saying what a thing is FOR. Shapes,
* defaults that matter, interactions, and every "why" stay in the canonical document.
*
* Everything below is checked against its source by `scripts/checkReference.mjs`, in both
* directions — a name that disappears upstream fails, and a name that appears upstream and
* is missing here fails too. That is the whole reason it is safe to write names down at
* all: the enumeration cannot rot into fiction without turning the build red.
*
* Descriptions are NOT checked, and cannot be. They are the part a human has to keep
* honest, which is why they are kept short enough to re-read.
*/
/** `website` root `.env.example` — the file a Compose deployment actually reads. */
export const envVars = {
IMAGE_TAG: 'Which published image tag to run',
NODE_ENV: 'production or development — several refusals are production-only',
PORT: 'The port the app listens on',
INTERNAL_PORT: 'The internal-only listener, for the bot channel',
UPLOAD_DIR: 'Where uploads are written',
LOG_LEVEL: 'Console log level',
FILE_LOG_LEVEL: 'File log level, set separately',
LOG_TO_FILE: 'Whether to write a log file at all',
LOG_DIR: 'Directory for the log file',
LOG_FILE: 'Log file name',
BRAND_NAME: 'Site name — branding is data, not a build',
BRAND_SHORT_NAME: 'Short form, for tight spaces',
BRAND_TAGLINE: 'One line under the name',
BRAND_DESCRIPTION: 'Meta description',
BRAND_CONTACT_EMAIL: 'Published contact address',
BRAND_URL: 'Canonical public URL',
BRAND_ACCENT_COLOR: 'Accent colour',
BRAND_LOGO: 'Logo path',
BRAND_HERO: 'Hero image path',
BRAND_FAVICON: 'Favicon path',
DB_HOST: 'Database host',
DB_PORT: 'Database port',
DB_NAME: 'Database name',
DB_USER: 'Database user',
DB_PASSWORD: 'Database password',
DB_ROOT_PASSWORD: "The database container's root password",
JWT_SECRET: 'Signs session tokens. Rotating it logs everyone out',
SECRET_ENC_KEY:
'Encrypts secrets at rest. Required in production, and rotating it ORPHANS every stored secret',
JWT_EXPIRES_IN: 'Session lifetime',
COOKIE_SECURE: 'auto decides Secure per request, so HTTPS and LAN HTTP both work',
COOKIE_NAME: 'Session cookie name. Changing it invalidates existing sessions',
TRUST_PROXY: 'Needed behind a reverse proxy for secure cookies, real IPs and rate limiting',
DEBUG_TRUST_PROXY: 'Diagnostic for the above',
TOTP_CHALLENGE_TTL: 'How long a pending 2FA challenge is valid',
ADMIN_USERNAME: 'First admin, created only when no users exist',
ADMIN_PASSWORD: 'First admin password. Set it before the first boot, not after',
CLIENT_ORIGIN: 'Dev only — the Vite origin allowed through CORS',
BOT_INTERNAL_URL: 'Where the Discord bot listens',
BOT_INTERNAL_KEY:
'Authenticates the site↔bot channel. Required in production EVEN IF you run no bot',
NTFY_BASE_URL: 'Push notification relay base URL',
};
/** `link/sidecar/src/config.rs` → the TOML the sidecar writes on first run. */
export const sidecarConfig = {
'shard.bind': 'Loopback address the game plugin dials out to',
'web.bind': 'Address the website reaches the sidecar on',
'web.auth_token': 'Shared secret the website must present. Generated on first run if blank',
'store.path': "The sidecar's own durable store",
};
/** `installer` — `src/cli.rs`'s `Command`. */
export const installerCommands = {
Install: "Set up a game server's side: deploy the bridge plugin, install the sidecar, register its service",
Doctor: 'Diagnose an existing install',
Update: 'Move to a newer bundle',
Uninstall: 'Remove what install put there',
};
/** `servuo-plugins/overlay/Config/Bridge.cfg` — the plugin's config, grouped for reading. */
export const bridgeCfg = {
Connection: {
Host: 'Sidecar address the shard dials out to',
Port: 'Sidecar port',
QueueCap: 'Bounded queue depth. Full means drop-oldest — never block the game',
PublicConnectAddress: 'Address players connect to, published to the site',
LinkUrl: 'Where in-game account linking sends a player',
},
Sweeps: {
StatSweepSeconds: 'Character stat sweep interval',
DecaySweepSeconds: 'House decay sweep',
EconomySweepSeconds: 'Economy totals sweep',
ChampSweepSeconds: 'Champion spawn sweep',
PageSweepSeconds: 'Staff page sweep',
GuildSweepSeconds: 'Guild roster sweep',
CitySweepSeconds: 'City / governor sweep',
PresenceSweepSeconds: 'Who is online',
HousingSweepSeconds: 'Housing sweep',
},
Guilds: {
GuildRosterMembersPerLine: 'Frame cap — a roster is split rather than sent oversized',
GuildRosterGuildsPerTick: 'How many guilds are swept per tick',
},
Points: {
PointsSweepSeconds: 'Points sweep interval',
PointsLeaderboardEnabled: 'Publish a leaderboard at all',
PointsTopN: 'Leaderboard length',
PointsSystems: 'Which point systems to include',
PointsProfileEnabled: 'Show points on a character profile',
PointsProfileRank: 'Show rank as well as total',
},
Market: {
MarketEnabled: 'Publish player vendor listings',
MarketSweepSeconds: 'Market sweep interval',
MarketSweepBatch: 'Vendors per sweep',
MarketMaxListings: 'Cap on listings published',
},
Ruleset: {
RulesetEnabled: 'Publish the shard ruleset',
RulesetIncludeSchedule: 'Include the event schedule with it',
},
'Town crier': {
TownCrierMaxLines: 'Lines per notice',
TownCrierMaxLineLength: 'Characters per line',
TownCrierMaxActive: 'Concurrent notices',
TownCrierMaxDurationSec: 'Longest a notice may run',
},
News: {
NewsMaxTitleLength: 'Title cap',
NewsMaxBodyLength: 'Body cap',
NewsMaxExternal: 'How many site posts are carried in-game',
NewsAnnounceDurationSec: 'How long an announcement shows',
},
'Admin commands': {
AdminWriteEnabled: 'Whether the site may write to the game at all. Off by default',
AdminAccessFloor: 'Minimum in-game access level for admin actions',
AdminBroadcastMaxLength: 'Broadcast cap',
AdminReasonMaxLength: 'Reason field cap',
AdminBanMaxDurationSec: 'Longest ban the site may set',
},
'Scheduled events': {
EventsEnabled:
'Whether the website may run scheduled events against this world at all. Off by ' +
'default, and deliberately a separate switch from AdminWriteEnabled',
EventsSweepSeconds: 'How often expired gates are collected and lost objects pruned',
EventsMinSaveIntervalSec:
'Shortest gap between world saves, counted from the last save by anyone. A save ' +
'asked for too soon is refused rather than queued',
},
'Event caps': {
EventsMaxCreatures: 'Creatures one call may spawn',
EventsMaxBosses: 'Enhanced "boss" variants one call may spawn',
EventsMaxNpcs: 'Oracle NPCs one call may place',
EventsMaxDecor: 'Decoration items one call may place',
EventsMaxGateMinutes: 'Longest a temporary gate may stand',
EventsMaxOwnedPerRun: 'Objects one run may own across every verb — the runaway bound',
EventsMaxSpread: 'How far from the chosen spot things may be scattered',
EventsMaxBossMultiplier: 'How much harder than normal a boss may be made',
EventsMaxGrantPerRun: 'How many characters one item grant may reach',
EventsMaxGrantStack: 'How large one granted stack may be',
},
'The oracle NPC': {
EventsOracleMaxLines: 'Keyword lines it will answer to',
EventsOracleGreetRange: 'How close a player must be to be greeted',
EventsOracleSpeechRange: 'How close a player must be to be heard',
EventsOracleGreetCooldownSec: 'How often it greets the same player',
EventsOracleAnswerCooldownSec: 'How often it answers the same player',
},
Leases: {
LeaseMaxDurationSec:
'Longest lease this shard will hold, whatever the site asks for. A longer request ' +
'is refused rather than shortened',
LeaseGraceSec: 'How long a restored lease stays listed, so a late teardown still gets a verdict',
},
Participation: {
ParticipationSweepSeconds: "How often everyone standing in a run's area is credited",
ParticipationKillWeight: 'What one kill inside the area is worth against one minute in it',
ParticipationMaxRuns: 'Runs counted at once',
ParticipationMaxMembers: 'Members counted per run',
ParticipationMaxRadius: 'Widest area an event may declare',
ParticipationGraceSec: "How long a closed run's tally stays readable",
ParticipationSnapshotChunk: "Members resolved per yield of the game's core thread",
},
Accounts: {
SignupMode: 'How game accounts may be created',
AccountCreateEnabled: 'Allow creation at all',
RequireIpForCreate: 'Require a real client IP',
AccountNameMaxLength: 'Account name cap',
AccountPasswordMaxLength: 'Account password cap',
},
'Client assets': {
AssetsEnabled:
'Whether the website may read the UO client files on this host \u2014 art, animations, ' +
'the string table \u2014 over the link at all. Its own switch, because it is its own consent',
AssetBatchBytes:
'Byte budget for one reply page, inside the 1 MiB line cap the sidecar accepts',
AssetBodyBatch:
'Bodies one catalogue request may name. Counted in items rather than bytes, because ' +
'what it bounds is building and deleting that many real mobiles on the core thread. ' +
'A larger request is refused, never truncated',
AssetFetchKeys:
'How many keys one fetch may name. The byte budget above still decides where a page is cut',
AssetScanMs: 'How long a catalogue scan may run before the page it has is returned',
AssetPlayerDirection:
'Which of the five directions a player body renders as. 0 is head-on, facing the viewer',
AssetCreatureDirection:
'The same for everything else. 1 is the front three-quarter \u2014 a wolf seen head-on ' +
'is a dark blob',
},
'Spawn files': {
TreeEnabled:
'Whether the shard configuration itself \u2014 spawn files, regions, locations, champion ' +
'spawns, decoration \u2014 may cross the bridge. A third switch for a third consent: ' +
'this is the work of the operator rather than the game client. Off means the spawn ' +
'atlas needs a shared filesystem again',
TreeChunkBytes: 'How large a slice of one file may be before it is compressed and sent',
},
};
/**
* The five-rung visibility ladder, from `module-uo`'s `server/utils/shardVisibility.js`.
*
* This one is a SECURITY boundary, not a convenience filter, which is why it is enumerated
* rather than described: a reader needs to see the whole ladder at once to reason about it.
*/
export const visibilityLadder = ['anonymous', 'logged_in', 'player', 'staff', 'admin'];
/**
* Canonical documents, by the question each answers. Checked to still exist in `docs`.
*
* Grouped by whose documents they are (PLAN.md §16, D63): `core` describes the platform in
* core's words, and each game's group may use that game's. The keys of the outer object are
* `core` plus keys of `platform.json`'s `games`, which is what lets the page label a game's
* group with the game's name and `checkTerms.mjs` hold each group to its own vocabulary.
*/
export const canonicalDocs = {
core: {
'website/ARCHITECTURE.md': 'How the website fits together — the canonical diagram',
'website/BACKEND_DESIGN.md': 'The API, schema and security contract',
'website/MODULE_SYSTEM.md': 'Why the module system is shaped this way',
'website/MODULE_API.md': 'Everything a module may do — the contract',
'website/TEAMS.md': 'Teams as a platform primitive',
'website/EVENTS.md': 'The Event System — leases, the ledger, and the module seam',
'website/THEMING_AND_NAV.md': 'Admin-configurable theme, assets and navigation',
'website/TRUSTED_DEVICES_MFA.md': 'Trusted devices and the second factor',
'installer/INSTALL.md': 'The operator guide for setting a game server up',
'installer/PLAN.md': "The installer's design of record",
'modules/rust-dryrun.md': 'The second game module as first designed on paper, to test that the contract generalises — since built',
'android/PLAN.md': 'The Android app',
},
uo: {
'link/PLAN.md': 'The sidecar design of record, the data catalog and the wire protocol',
'link/INTEGRATION.md': 'Integrating with the sidecar',
'link/v8.md': 'The current protocol, and its cross-repository obligations',
'link/SHARD_PREREQS.md': 'What a shard host needs before any of this works',
'link/ADMIN_CONTROLS.md': 'What the site may command the game to do',
'website/SHARD_VISIBILITY.md': 'The audience ladder, for administrators',
'modules/uo/API.md': "The module's own API, including its audience rules",
},
rust: {
'rust-link/INSTALL.md': 'Installing the Rust bridge — the installer, the Pterodactyl egg, or by hand',
'rust-link/INTEGRATION.md': 'Integrating with the sidecar',
'rust-link/PROTOCOL.md': 'The wire protocol between the bridge plugin and the sidecar',
'modules/rust/PLAN.md': 'The Rust module design of record',
},
};