All checks were successful
PR checks / checks (pull_request) Successful in 2m10s
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
280 lines
13 KiB
JavaScript
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',
|
|
},
|
|
};
|