/** * 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', }, }; /** * rust-link's `sidecar.toml` — `Rust-Link` `sidecar/src/config.rs`, parsed from its serde * structs the same way as uo-link's above. Each section is one `struct XCfg`. */ export const rustSidecarConfig = { 'game.bind': 'Loopback address the bridge plugin dials. Never move it off loopback', 'game.server_id': "A cross-check against the id the plugin announces. A plugin naming another server is refused before anything it sent is stored", 'web.bind': 'Address the website reaches the sidecar on', 'web.auth_token': 'Shared secret the website must present. Generated and saved on first run if blank', 'store.path': "The sidecar's own database. Relative paths resolve beside the config file", 'store.retain_days': 'Days of raw event history the sidecar keeps. 0 keeps everything', }; /** * The environment variables rust-link reads, each overriding one key above. Read from the * `get("RUSTLINK_…")` calls in the same file. An empty variable is ignored, which is what * lets a panel declare one and leave it blank. */ export const rustSidecarEnv = { RUSTLINK_GAME_BIND: 'game.bind', RUSTLINK_SERVER_ID: 'game.server_id', RUSTLINK_WEB_BIND: 'web.bind', RUSTLINK_WEB_TOKEN: 'web.auth_token', RUSTLINK_DB_PATH: 'store.path', RUSTLINK_RETAIN_DAYS: 'store.retain_days', }; /** * The bridge plugin's own config — `Rust-Plugins` `overlay/oxide/plugins/RunicGateway.cs`, * the public fields of its `PluginConfig` class. The framework writes it as * `oxide/config/RunicGateway.json` or `carbon/configs/RunicGateway.json`. * * `readOnlyFromSite` marks the three keys the site's mod config editor shows but will not * write: changing them from the website would cut the link carrying the change, or strand * every row the site holds for this server. */ export const rustPluginConfig = { Connection: { Host: { why: 'Where the sidecar listens. Loopback is the trust boundary', readOnlyFromSite: true }, Port: { why: "The sidecar's game port. Must equal its game.bind port", readOnlyFromSite: true }, QueueCap: { why: 'Lines that may wait for a wedged sidecar before the oldest is dropped' }, ServerId: { why: "This server's permanent identity on the website. Not the hostname, on purpose", readOnlyFromSite: true, }, }, Events: { EventsEnabled: { why: 'Whether the website may borrow values or change this world for events at all. Off by default', }, EventsMaxCrates: { why: 'Crates one event step may place. Lowerable, never raisable past the ceiling' }, EventsMaxNpcs: { why: 'NPCs one event step may place' }, EventsMaxRecipients: { why: 'People one reward step may name' }, EventsMaxTallies: { why: 'Participation tallies counting at once' }, EventsMaxChat: { why: 'Longest line an event or a news post may say in chat' }, EventsMaxWorldCalls: { why: 'World events one run may call: helicopters, cargo ships, airdrops' }, }, Map: { MapMaxSleepers: { why: 'Offline sleepers one live-map answer carries' }, MapMaxBases: { why: 'Tool cupboards and vending machines one live-map answer carries' }, }, }; /** * Every event kind the Rust bridge sends, through its current protocol, as `Module-Rust` * `server/catalogue.js` classifies them (PLAN.md §16, D71). The module is the side that * enforces the boundary, so it is what this list is checked against — the same reason the * UO ladder below is checked against the module and not against prose. * * - `public`: a signed-out visitor may see the kind. Some are filtered field by field * inside it — a name for the presence audience, a position for staff — and that is the * module's business, summarised on the page rather than restated here. * - `presence`: public kinds that say a NAMED player was on the server. Their audience is * an operator's choice on Admin → Rust visibility, staff only by default. A subset of * the module's public list, kept separate here because that is how a reader needs it. * - `staff`: admins and moderators, and nobody else. */ export const rustEventKinds = { public: { 'server.wipe': 'The map was wiped', 'server.initialized': 'The server came up', 'server.shutdown': 'The server went down', 'npc.boss.spawned': 'A RunicNPC boss appeared', 'world.event': "Rust's own world events: cargo, helicopters, Bradley, airdrops, the vendor", 'pvp.zone.created': 'Dynamic PVP opened a PvP zone', 'dt.started': 'A Dangerous Treasures event opened', 'dt.won': 'A Dangerous Treasures chest was won', 'gc.started': 'A guarded crate landed', 'gc.ended': 'A guarded crate was cleared', 'raid.started': 'A RaidableBases base appeared', 'raid.completed': 'A raid base was completed', 'raid.purchased': 'A raid base was bought', 'raid.locked': 'An admin locked a raid base to a player', }, presence: { 'player.connected': 'A player joined', 'player.disconnected': 'A player left', 'player.respawned': 'A player respawned', 'player.death': 'A death — the kill feed', 'player.chat': 'A line of in-game chat', 'player.tally': "A minute's gathering, kills and title counts, summed rather than sent per swing", 'npc.boss.died': 'A RunicNPC boss was killed, naming the killer', }, staff: { 'entity.destroyed': 'A building part was destroyed — it says where somebody lives', 'player.reported': "One player's report about another", 'player.banned': 'A ban, with the IP address', 'player.unbanned': 'An unban, with the IP address', 'player.login.attempt': 'A login attempt, with the IP address', 'player.approved': 'A login approved, with the IP address', 'account.link.requested': 'Somebody asked for a link code — never the code itself', 'account.unlinked': 'A Steam account was unlinked in game', 'perm.drift': 'A permission or group was changed in game, outside the site', 'config.outcome': "How a config save's reload ended, with the log tail when it failed", 'plugin.loaded': 'A plugin loaded, with the permissions it registers', 'plugin.unloaded': 'A plugin unloaded', 'lease.expired': 'A value an event borrowed was given back on its deadline', 'world.expired': 'Something an event placed ended on its deadline', 'clan.created': 'A clan was created', 'clan.disbanded': 'A clan was disbanded', 'clan.member.added': 'Somebody joined a clan', 'clan.member.left': 'Somebody left a clan', 'clan.member.kicked': 'Somebody was removed from a clan', 'npc.died': 'A RunicNPC NPC died, naming everyone who hurt it', 'npc.health': 'A RunicNPC NPC fell to a health threshold', 'npc.placement.changed': 'A RunicNPC placement changed', 'npc.boss.phase': "A boss changed phase — told in game only to the players near it", 'pvp.zone.deleted': 'A Dynamic PVP zone closed', 'dt.ended': 'A Dangerous Treasures event ended, won or not', 'gc.failed': 'A guarded crate timed out with its guards standing', 'raid.ended': 'A raid base despawned', }, }; /** * 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', 'modules/rust/OPERATING.md': "Running Oxide: uMod's operator pages, plus the notes on RaidableBases and Guarded Crate", 'modules/rust/CARBON.md': 'Where Carbon differs from Oxide, and nowhere else', }, };