PLAN.md §30 as approved, plus D119/D120 from the build. Server: - rust_map_images (one row per server: picture as MEDIUMBLOB, geometry, monuments, DERIVATION_VERSION) and rust_map_overrides; purge.sql pair. - mapImages.js: D110. The board poll notices a new boot/wipe/seed/size and asks map.info; a new key or hash from the free Rust+ cache (or a render kept on disk) is fetched in slices, checked against its SHA-256 and stored in one statement. One fetch per server, a backoff on failure, `stale` abandons a fetch that straddles a map change. Render now (D109) is admin-only and watched to completion. - mapLive.js: D111. One map.live per server per 5 s whoever asks; positions are held in memory only. - model/map: four layers (world, events public; players, bases staff), a fleet default plus per-server override (D114), the players layer capped by presence (D113), own dot and online first-party clan mates for a linked viewer (D115, D117, D118). A layer the viewer may not see is absent from the answer, never sent and hidden. - Routes: public /servers/:id/map, /map/image (immutable under its hash), /map/live; admin /servers/:id/map/fetch and /render; the Map card on the visibility PUT. Swagger fragment and frozen manifest regenerated. Client: - A Map tab: Leaflet over the picture in CRS.Simple, the game's own grid (labels only when a cell is wide enough to hold one), a legend that lists hidden layers with who can see them, polled every 10 s while visible. - D120: Leaflet is a lazy split chunk beside entry.js, not in it. release.yml copies every dist/*.js; checkExternals and build.test.js hold both ends. - The Map card on Admin -> Rust visibility, with Fetch again and Render now. Capability `map` declared for the Android app (phase 15). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
903 lines
47 KiB
SQL
903 lines
47 KiB
SQL
-- ── The schema fragment ───────────────────────────────────────────────────
|
||
--
|
||
-- Core replays this file on EVERY boot, statement by statement, immediately
|
||
-- after its own schema.sql and before it seeds defaults (MODULE_API.md §2.6).
|
||
--
|
||
-- There is no migration runner anywhere in this project. A module's schema is
|
||
-- not a sequence of changes to apply once — it is a statement of what the tables
|
||
-- should look like, written so that running it against a database that already
|
||
-- matches does nothing. Every CREATE carries IF NOT EXISTS; **changing a table
|
||
-- is an ALTER below the CREATE, never an edit to the CREATE**, because
|
||
-- `CREATE TABLE IF NOT EXISTS` does nothing at all when the table is already
|
||
-- there and an edited column would reach fresh installs only.
|
||
--
|
||
-- Every table here is prefixed `rust_`, which is this module's id and the only
|
||
-- prefix it may create under.
|
||
--
|
||
-- ── Four kinds of table, and the split between them is the whole design ───
|
||
--
|
||
-- CONFIGURATION `rust_servers` — rows an operator writes, from Admin → Rust.
|
||
-- OBSERVED STATE `rust_server_state`, `rust_presence` — what a sidecar last
|
||
-- reported, replaced rather than appended.
|
||
-- THE RECORD `rust_wipes`, `rust_players`, `rust_player_wipe_stats`,
|
||
-- `rust_gather_totals` — permanent, and the reason a wipe does
|
||
-- not erase a player's history.
|
||
-- THE WINDOW `rust_events` — recent detail, bounded by a sweep.
|
||
--
|
||
-- They are separate tables rather than columns on one because they have
|
||
-- different writers, different lifetimes and different audiences — and because
|
||
-- a purge of observed state while keeping the configuration is a thing an
|
||
-- operator will eventually want.
|
||
--
|
||
-- Teardown is `purge.sql`, which no boot ever runs.
|
||
|
||
|
||
-- ── The configured servers ────────────────────────────────────────────────
|
||
--
|
||
-- One row per Rust game server, and therefore one row per sidecar: the bridge is
|
||
-- one server to one sidecar, on that server's own host (R8). A community running
|
||
-- six servers has six rows here, each with its own base URL and its own token.
|
||
--
|
||
-- `id` is the operator's own slug and is what every URL under `/rust/servers/`
|
||
-- carries. It is deliberately NOT auto-increment: it appears in links people
|
||
-- share, and a row rebuilt after a mistake should be able to keep its address.
|
||
--
|
||
-- `sidecar_token_enc` holds the sidecar's shared secret **encrypted at rest**
|
||
-- through `ctx.secretBox` (MODULE_API.md §2.3), like every other secret this
|
||
-- platform stores. It is write-only in the API: the admin surface accepts a new
|
||
-- value and never returns the stored one, so a compromised admin session cannot
|
||
-- read back the credential that reaches the game host.
|
||
--
|
||
-- `protocol` records the wire version this row was configured against. It is
|
||
-- stored rather than assumed because a fleet is upgraded one host at a time, and
|
||
-- an operator needs to see WHICH server disagrees rather than that one does.
|
||
CREATE TABLE IF NOT EXISTS rust_servers (
|
||
id VARCHAR(64) NOT NULL PRIMARY KEY,
|
||
name VARCHAR(120) NOT NULL,
|
||
sidecar_base_url VARCHAR(255) NOT NULL,
|
||
sidecar_token_enc TEXT NULL,
|
||
protocol INT UNSIGNED NOT NULL DEFAULT 1,
|
||
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||
sort_order INT NOT NULL DEFAULT 0,
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
|
||
-- ── What each server last said about itself ───────────────────────────────
|
||
--
|
||
-- One row per configured server, replaced whole each time this module reads a
|
||
-- sidecar. It is the table that lets the site render while every game server is
|
||
-- off, which is the point of the sidecar holding a store at all.
|
||
--
|
||
-- `updated_at` carries no `ON UPDATE CURRENT_TIMESTAMP`, deliberately. That
|
||
-- clause fires only when an UPDATE actually CHANGES a value, so a writer sending
|
||
-- the same numbers back — which is exactly what a quiet server looks like —
|
||
-- would leave the timestamp frozen at the first write and the row would look
|
||
-- stale while nothing was wrong. The writer sets the column explicitly instead.
|
||
--
|
||
-- `boot_id` is the game process's own identity, not the sidecar's and not the
|
||
-- plugin's. It changes when the world started over and at no other time, which
|
||
-- is what makes it the thing to watch: a reconnect of either bridge component
|
||
-- loses nothing, and a game restart loses everything an event put in the world.
|
||
--
|
||
-- `raw` keeps the whole frame. This module indexes the columns it serves and
|
||
-- stores the rest verbatim, so a protocol version that adds a field needs no
|
||
-- migration here — the same dumb-forwarder property the sidecar has, one hop
|
||
-- further along.
|
||
CREATE TABLE IF NOT EXISTS rust_server_state (
|
||
server_id VARCHAR(64) NOT NULL PRIMARY KEY,
|
||
reachable TINYINT(1) NOT NULL DEFAULT 0,
|
||
online TINYINT(1) NOT NULL DEFAULT 0,
|
||
players INT UNSIGNED NOT NULL DEFAULT 0,
|
||
max_players INT UNSIGNED NOT NULL DEFAULT 0,
|
||
hostname VARCHAR(191) NULL,
|
||
level VARCHAR(120) NULL,
|
||
seed BIGINT NULL,
|
||
world_size INT UNSIGNED NULL,
|
||
boot_id VARCHAR(64) NULL,
|
||
save_created_at VARCHAR(32) NULL,
|
||
protocol INT UNSIGNED NULL,
|
||
raw LONGTEXT NULL,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_rust_server_state_server
|
||
FOREIGN KEY (server_id) REFERENCES rust_servers (id) ON DELETE CASCADE
|
||
);
|
||
|
||
|
||
-- ── The read path ─────────────────────────────────────────────────────────
|
||
--
|
||
-- Protocol 2 turned the bridge from a greeting into a catalogue, and these are
|
||
-- the tables that hold it. They divide on one line, and it is the line R12 drew:
|
||
--
|
||
-- PERMANENT `rust_wipes`, `rust_players`, `rust_player_wipe_stats`,
|
||
-- `rust_gather_totals` — a player's record, kept for ever. All-time
|
||
-- is a SUM across wipes rather than a second set of counters, so
|
||
-- there is no second number that can disagree with the first.
|
||
--
|
||
-- BOUNDED `rust_events` — the recent raw window the killfeed reads, pruned
|
||
-- on a sweep. It is detail, not record: losing last month's
|
||
-- individual deaths costs a scroll-back, losing last month's
|
||
-- totals costs a player their history.
|
||
--
|
||
-- DERIVED `rust_presence` — who is on right now, replaced wholesale from
|
||
-- the `players.online` board. Never a history, never appended.
|
||
--
|
||
-- The sidecar keeps its own bounded copy of the same events (default 14 days),
|
||
-- so shortening either window loses recent detail and neither loses a total.
|
||
|
||
|
||
-- ── Wipes ─────────────────────────────────────────────────────────────────
|
||
--
|
||
-- One row per (server, wipe). The id is the plugin's, derived from the save's
|
||
-- creation time and stamped on every frame (PROTOCOL.md §8.2) — this module
|
||
-- never derives one, because two derivations of one fact eventually disagree
|
||
-- about a boundary.
|
||
--
|
||
-- Rows appear by being MENTIONED: the first frame carrying a wipe id this module
|
||
-- has not seen creates it. There is no "start a wipe" call and there must not be
|
||
-- one, because the website is not present when a wipe happens — a wipe is a fact
|
||
-- about a world that was restarted while nobody was watching.
|
||
CREATE TABLE IF NOT EXISTS rust_wipes (
|
||
server_id VARCHAR(64) NOT NULL,
|
||
wipe_id VARCHAR(48) NOT NULL,
|
||
save_created_at VARCHAR(32) NULL,
|
||
first_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
last_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (server_id, wipe_id),
|
||
CONSTRAINT fk_rust_wipes_server
|
||
FOREIGN KEY (server_id) REFERENCES rust_servers (id) ON DELETE CASCADE
|
||
);
|
||
|
||
|
||
-- ── Players ───────────────────────────────────────────────────────────────
|
||
--
|
||
-- Identity, and deliberately nothing else. It is keyed on the Steam id alone
|
||
-- and carries no server: a player is the same person on all six of a community's
|
||
-- servers, and everything that is per-server lives in the stats table.
|
||
--
|
||
-- `user_id` is NOT here. Linking a Steam id to a website account is phase 6's
|
||
-- work (R1), and a column waiting for it would be a column every read has to
|
||
-- remember is always null.
|
||
CREATE TABLE IF NOT EXISTS rust_players (
|
||
steam_id VARCHAR(32) NOT NULL PRIMARY KEY,
|
||
name VARCHAR(191) NULL,
|
||
first_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
last_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
|
||
-- ── The permanent record ──────────────────────────────────────────────────
|
||
--
|
||
-- One row per player per wipe per server, and the only counters this module
|
||
-- keeps. R12's "per-wipe detail plus all-time rollups" is satisfied by SUMming
|
||
-- this rather than by maintaining a second all-time row, because two counters
|
||
-- for one fact drift the first time an ingest is replayed.
|
||
--
|
||
-- Every column is a COUNT that only ever goes up within a wipe, which is what
|
||
-- makes ingest idempotent-ish in the only way that matters: the cursor advances
|
||
-- only after the batch commits, so a crash re-reads a batch it has not counted.
|
||
--
|
||
-- `playtime_sec` comes from `sessionSec` on a disconnect, and a session whose
|
||
-- start this module never saw contributes NOTHING rather than zero — the plugin
|
||
-- omits the field, the ingest skips it, and the number stays honestly short
|
||
-- instead of quietly wrong.
|
||
CREATE TABLE IF NOT EXISTS rust_player_wipe_stats (
|
||
server_id VARCHAR(64) NOT NULL,
|
||
wipe_id VARCHAR(48) NOT NULL,
|
||
steam_id VARCHAR(32) NOT NULL,
|
||
kills INT UNSIGNED NOT NULL DEFAULT 0,
|
||
deaths INT UNSIGNED NOT NULL DEFAULT 0,
|
||
suicides INT UNSIGNED NOT NULL DEFAULT 0,
|
||
npc_kills INT UNSIGNED NOT NULL DEFAULT 0,
|
||
structures INT UNSIGNED NOT NULL DEFAULT 0,
|
||
sessions INT UNSIGNED NOT NULL DEFAULT 0,
|
||
playtime_sec BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||
last_seen DATETIME NULL,
|
||
PRIMARY KEY (server_id, wipe_id, steam_id),
|
||
KEY idx_rust_stats_kills (server_id, wipe_id, kills DESC),
|
||
KEY idx_rust_stats_player (steam_id)
|
||
);
|
||
|
||
|
||
-- ── What they gathered ────────────────────────────────────────────────────
|
||
--
|
||
-- A row per resource rather than a JSON blob on the stats row, for one reason:
|
||
-- the leaderboard question is "who gathered the most sulfur this wipe", and that
|
||
-- is an ORDER BY over a column in every SQL engine and a JSON function call in
|
||
-- exactly one. The resource name is the game's own shortname, unknown in advance
|
||
-- and not worth a lookup table.
|
||
CREATE TABLE IF NOT EXISTS rust_gather_totals (
|
||
server_id VARCHAR(64) NOT NULL,
|
||
wipe_id VARCHAR(48) NOT NULL,
|
||
steam_id VARCHAR(32) NOT NULL,
|
||
resource VARCHAR(64) NOT NULL,
|
||
amount BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||
PRIMARY KEY (server_id, wipe_id, steam_id, resource),
|
||
KEY idx_rust_gather_top (server_id, wipe_id, resource, amount DESC)
|
||
);
|
||
|
||
|
||
-- ── The recent raw window ─────────────────────────────────────────────────
|
||
--
|
||
-- Every ingested event, whole, for as long as the retention sweep keeps it. The
|
||
-- killfeed reads this; so does an admin looking at what happened.
|
||
--
|
||
-- `raw` holds the entire frame and the columns beside it are only what a query
|
||
-- needs to reach — the same rule the sidecar's own store follows, one hop along:
|
||
-- a protocol version that adds a field needs no migration here.
|
||
--
|
||
-- **`kind` is a security boundary, not a label.** Some kinds carry IP addresses
|
||
-- and player reports (PROTOCOL.md §8.4), and what makes them safe is that the
|
||
-- public read is filtered by an allowlist this module holds, default-deny. The
|
||
-- rows are stored either way, because an operator chasing ban evasion needs them.
|
||
CREATE TABLE IF NOT EXISTS rust_events (
|
||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||
server_id VARCHAR(64) NOT NULL,
|
||
wipe_id VARCHAR(48) NULL,
|
||
kind VARCHAR(64) NOT NULL,
|
||
t BIGINT NOT NULL,
|
||
steam_id VARCHAR(32) NULL,
|
||
raw LONGTEXT NOT NULL,
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
KEY idx_rust_events_server (server_id, id DESC),
|
||
KEY idx_rust_events_kind (server_id, kind, id DESC),
|
||
KEY idx_rust_events_wipe (server_id, wipe_id, id DESC),
|
||
KEY idx_rust_events_created (created_at)
|
||
);
|
||
|
||
|
||
-- ── Who is on right now ───────────────────────────────────────────────────
|
||
--
|
||
-- Replaced wholesale every time the `players.online` board arrives, which is on
|
||
-- every bridge connect and every 60 seconds. It is a BOARD, and the reason it is
|
||
-- its own table rather than rows in `rust_events` is that a board answers "now"
|
||
-- and an event answers "then"; storing a board as history is the mistake the
|
||
-- wire's `type` field exists to prevent, and it would be a shame to make it here
|
||
-- after the sidecar went to the trouble of not making it there.
|
||
CREATE TABLE IF NOT EXISTS rust_presence (
|
||
server_id VARCHAR(64) NOT NULL,
|
||
steam_id VARCHAR(32) NOT NULL,
|
||
name VARCHAR(191) NULL,
|
||
sleeping TINYINT(1) NOT NULL DEFAULT 0,
|
||
connected_at DATETIME NULL,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (server_id, steam_id)
|
||
);
|
||
|
||
|
||
-- ── The ingest cursor ─────────────────────────────────────────────────────
|
||
--
|
||
-- Where this module has read up to in each sidecar's feed. One row per server.
|
||
--
|
||
-- It is persisted rather than held in memory because the alternative is a module
|
||
-- that re-reads everything on every boot or nothing at all, and both are wrong in
|
||
-- a way that only shows up in production. The cursor advances **after** the batch
|
||
-- is written, never before: a crash mid-batch re-reads rows it has not counted,
|
||
-- which is the safe direction to be wrong in.
|
||
--
|
||
-- A NEW server starts at the sidecar's current end rather than at zero (see
|
||
-- `GET /feed` with no `since`). A module installed today against a sidecar that
|
||
-- has been running a month wants what happens next — replaying a fortnight of
|
||
-- deaths into stats whose wipes it never saw is not a catch-up, it is a
|
||
-- fabrication of history it was not present for.
|
||
CREATE TABLE IF NOT EXISTS rust_ingest_cursor (
|
||
server_id VARCHAR(64) NOT NULL PRIMARY KEY,
|
||
last_event_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||
events_seen BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_rust_cursor_server
|
||
FOREIGN KEY (server_id) REFERENCES rust_servers (id) ON DELETE CASCADE
|
||
);
|
||
|
||
|
||
-- ── Who owns which Steam account ──────────────────────────────────────────
|
||
--
|
||
-- R1's identity link, and the reason it is a table rather than a column on
|
||
-- `rust_players`: a link is a fact about a WEBSITE USER that happens to be keyed
|
||
-- by a Steam id, and it outlives every row this module writes about play. A
|
||
-- column here would be null for the overwhelming majority of players and would
|
||
-- be deleted by any sweep that pruned inactive ones.
|
||
--
|
||
-- **Keyed on `steam_id` alone, fleet-wide.** `rust_players` already made that
|
||
-- call in protocol 2 and it is the truth of the thing: a Steam account is one
|
||
-- person across every server an operator runs, where stats are per server and
|
||
-- per wipe. Linking on one server links for the fleet, because there is nothing
|
||
-- else it could honestly mean.
|
||
--
|
||
-- **One Steam id, at most one user** — that is what the primary key buys, and it
|
||
-- is load-bearing rather than tidy. Phase 7 makes the site the author of who may
|
||
-- do what in game and phase 13 makes it the thing that hands out loot; both are
|
||
-- grants against a Steam id, and both assume the question "whose is this?" has
|
||
-- exactly one answer.
|
||
--
|
||
-- The reverse is deliberately NOT constrained: one website user may hold several
|
||
-- Steam accounts. People have a second account, or a family shares a site login,
|
||
-- and refusing that would be inventing a rule the game does not have.
|
||
--
|
||
-- `ON DELETE CASCADE` from `users`: a deleted account's links go with it. The
|
||
-- alternative is a row naming a user id that resolves to nobody, which every
|
||
-- read would then have to defend against.
|
||
CREATE TABLE IF NOT EXISTS rust_account_links (
|
||
steam_id VARCHAR(32) NOT NULL PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
-- What the player was called in game when they linked. A display name, kept
|
||
-- so an operator reading the admin panel sees a person rather than a number;
|
||
-- never used to identify anybody, because a Rust name changes on a whim.
|
||
name VARCHAR(191) NULL,
|
||
-- Which server minted the code. Not part of the identity — the link is
|
||
-- fleet-wide — but an operator asking "where did this come from" has no other
|
||
-- way to find out, and a support conversation starts there.
|
||
server_id VARCHAR(64) NULL,
|
||
linked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_rust_links_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||
KEY idx_rust_links_user (user_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
|
||
|
||
-- ── Site-owned permissions (phase 7, R2) ──────────────────────────────────
|
||
--
|
||
-- The website is the author of record for who may do what in game, and the
|
||
-- framework's own permission store is an ENFORCEMENT CACHE. That is one
|
||
-- sentence with three consequences, and the tables below are shaped by them:
|
||
--
|
||
-- • Every third-party plugin honours a site grant with no adapter, because
|
||
-- they all already call `UserHasPermission`. Nothing here is read by the
|
||
-- game directly; it is pushed into the store the game already consults.
|
||
-- • A wipe stops being a data-loss event. The game forgets and the site does
|
||
-- not, so the next sync puts it all back.
|
||
-- • A hand edit is REPORTED, never silently overwritten (D31). Which means
|
||
-- the site has to be able to tell a grant it made from one somebody typed
|
||
-- at a console — and that is a fact only the site can hold, because the
|
||
-- store records who granted a permission nowhere.
|
||
--
|
||
-- ── A grant is against a WEBSITE USER (D28) ───────────────────────────────
|
||
--
|
||
-- Not against a Steam id, though a Steam id is what reaches the game. The site
|
||
-- authors privilege for a PERSON: phase 13's earned entitlements follow whoever
|
||
-- earned them, and an account unlinked from a person takes their privileges
|
||
-- with it. The Steam ids are resolved from `rust_account_links` at push time,
|
||
-- so a player who links a second account gets what they hold on both — which is
|
||
-- the honest reading of "this person may do this".
|
||
--
|
||
-- A user with no linked account is authored against perfectly well and simply
|
||
-- reaches nobody until they link. That is visible on the admin screen rather
|
||
-- than silent, because a grant that reaches nothing looks identical to a grant
|
||
-- that worked from every other angle.
|
||
--
|
||
-- ── Scope (D29) ───────────────────────────────────────────────────────────
|
||
--
|
||
-- Every authored row carries one: a server id, or `*` for the whole fleet. The
|
||
-- game stores permissions per server (each has its own store), an operator
|
||
-- running a modded server and a vanilla one will not want one set on both, and
|
||
-- a single-server community never has to think about it.
|
||
|
||
|
||
-- ── Groups ────────────────────────────────────────────────────────────────
|
||
--
|
||
-- Mirrored into the game as REAL groups (D30) rather than flattened into
|
||
-- per-player grants. Third-party plugins read group membership, BetterChat's
|
||
-- group API (R15, phase 17) has something to hang on, and an operator reading
|
||
-- `oxide.show groups` sees what the website shows.
|
||
--
|
||
-- The cost of that fidelity is written down in PLAN.md §12.2 rule 4 and does
|
||
-- not go away: **a player the store has never seen cannot be put in a group**,
|
||
-- while a direct grant to the same id works immediately. The sync reports those
|
||
-- members as pending and the membership lands on their first connection.
|
||
--
|
||
-- The name is the primary key, fleet-wide, even though the row carries a scope:
|
||
-- one `vip` on the site is one `vip` in the game, pushed to the servers its
|
||
-- scope names. Two groups of the same name with different scopes would be two
|
||
-- definitions of one name in every store that received both.
|
||
CREATE TABLE IF NOT EXISTS rust_perm_groups (
|
||
name VARCHAR(64) NOT NULL PRIMARY KEY,
|
||
title VARCHAR(120) NOT NULL DEFAULT '',
|
||
rank INT NOT NULL DEFAULT 0,
|
||
scope VARCHAR(64) NOT NULL DEFAULT '*',
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
|
||
-- What each group carries. A row per permission rather than a list on the group
|
||
-- for the ordinary reason: "which groups grant kits.vip" is the question an
|
||
-- operator asks when they are about to remove a plugin, and that is a WHERE
|
||
-- clause here and a scan of every row in the other shape.
|
||
CREATE TABLE IF NOT EXISTS rust_perm_group_permissions (
|
||
group_name VARCHAR(64) NOT NULL,
|
||
permission VARCHAR(128) NOT NULL,
|
||
PRIMARY KEY (group_name, permission),
|
||
CONSTRAINT fk_rust_perm_group_permissions_group
|
||
FOREIGN KEY (group_name) REFERENCES rust_perm_groups (name) ON DELETE CASCADE
|
||
);
|
||
|
||
|
||
-- Who is in each group — by website user, like every other authored row.
|
||
--
|
||
-- `added_by` is an admin's user id and deliberately carries NO foreign key: a
|
||
-- staff member's account being deleted must not delete the record of what they
|
||
-- did, and `ON DELETE SET NULL` would quietly rewrite history to "nobody".
|
||
-- The activity log is the audit trail; this column is a convenience beside it.
|
||
CREATE TABLE IF NOT EXISTS rust_perm_group_members (
|
||
group_name VARCHAR(64) NOT NULL,
|
||
user_id INT NOT NULL,
|
||
added_by INT NULL,
|
||
added_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (group_name, user_id),
|
||
KEY idx_rust_perm_members_user (user_id),
|
||
CONSTRAINT fk_rust_perm_members_group
|
||
FOREIGN KEY (group_name) REFERENCES rust_perm_groups (name) ON DELETE CASCADE,
|
||
CONSTRAINT fk_rust_perm_members_user
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
);
|
||
|
||
|
||
-- ── Direct grants ─────────────────────────────────────────────────────────
|
||
--
|
||
-- A permission held by one person, without a group. It is not a lesser version
|
||
-- of membership: it is the shape that reaches a player who has never connected
|
||
-- to that server, which is exactly what an entitlement earned on the website at
|
||
-- three in the morning has to do (R16).
|
||
--
|
||
-- `source` is why this table does not need changing in phase 13. Every later
|
||
-- author — an event action granting the right to redeem a kit, a lease handing
|
||
-- out a weekend group — writes a row here with its own source rather than a
|
||
-- store of its own, so there is one answer to "why does this player have this"
|
||
-- and one place the push reads.
|
||
CREATE TABLE IF NOT EXISTS rust_perm_grants (
|
||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
permission VARCHAR(128) NOT NULL,
|
||
scope VARCHAR(64) NOT NULL DEFAULT '*',
|
||
source VARCHAR(32) NOT NULL DEFAULT 'admin',
|
||
note VARCHAR(255) NULL,
|
||
granted_by INT NULL,
|
||
granted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
UNIQUE KEY uq_rust_perm_grant (user_id, permission, scope),
|
||
KEY idx_rust_perm_grant_user (user_id),
|
||
CONSTRAINT fk_rust_perm_grants_user
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
);
|
||
|
||
|
||
-- ── What this site has actually put in each game ──────────────────────────
|
||
--
|
||
-- The site's memory of its own authorship, one row per thing it has confirmed
|
||
-- into one server's store. It is the table that makes D31 possible at all.
|
||
--
|
||
-- Three sets, and every interesting question is the difference between two of
|
||
-- them:
|
||
--
|
||
-- desired − pushed what to apply
|
||
-- pushed − desired what to RETIRE, because the site put it there and has
|
||
-- since withdrawn it
|
||
-- present − desired drift: somebody else put it there
|
||
--
|
||
-- Without the middle row a withdrawn grant is indistinguishable from a hand
|
||
-- edit, and those two have opposite correct answers. Inferring it from absence
|
||
-- is the mistake this table exists to prevent.
|
||
--
|
||
-- It is keyed by Steam id rather than by user, because it records what is in the
|
||
-- GAME, and the game has never heard of a website account. Unlinking an account
|
||
-- therefore leaves its row here until the next sync retires it — which is the
|
||
-- correct behaviour and would be impossible to express keyed the other way.
|
||
CREATE TABLE IF NOT EXISTS rust_perm_pushed (
|
||
server_id VARCHAR(64) NOT NULL,
|
||
-- `grant` | `member` | `group-permission` | `group`
|
||
kind VARCHAR(24) NOT NULL,
|
||
-- a Steam id, or a group name
|
||
subject VARCHAR(64) NOT NULL,
|
||
-- a permission, a group name, or '' for the existence of a group
|
||
object VARCHAR(128) NOT NULL,
|
||
pushed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (server_id, kind, subject, object),
|
||
CONSTRAINT fk_rust_perm_pushed_server
|
||
FOREIGN KEY (server_id) REFERENCES rust_servers (id) ON DELETE CASCADE
|
||
);
|
||
|
||
|
||
-- ── Drift ─────────────────────────────────────────────────────────────────
|
||
--
|
||
-- What a sync found in a server's store that the site did not author, within
|
||
-- the namespace the site claims. Rows appear and disappear with the report:
|
||
-- this is the CURRENT difference, not a history of differences, and a hand edit
|
||
-- that somebody has since removed should stop being on the screen.
|
||
--
|
||
-- Nothing here is ever removed from the game by the sync itself. An operator
|
||
-- typing `oxide.grant` during an incident is drift, not an error, and the two
|
||
-- answers offered to them — adopt it, or revoke it — are both a person's
|
||
-- decision.
|
||
CREATE TABLE IF NOT EXISTS rust_perm_drift (
|
||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||
server_id VARCHAR(64) NOT NULL,
|
||
kind VARCHAR(24) NOT NULL,
|
||
subject VARCHAR(64) NOT NULL,
|
||
object VARCHAR(128) NOT NULL,
|
||
first_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
last_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
UNIQUE KEY uq_rust_perm_drift (server_id, kind, subject, object),
|
||
CONSTRAINT fk_rust_perm_drift_server
|
||
FOREIGN KEY (server_id) REFERENCES rust_servers (id) ON DELETE CASCADE
|
||
);
|
||
|
||
|
||
-- ── Removing something the site never put there ───────────────────────────
|
||
--
|
||
-- Revoking a drift row cannot go through `rust_perm_pushed`, because the whole
|
||
-- point of a drift row is that it was never pushed. It cannot go through the
|
||
-- authored tables either: a foreign grant often names a Steam id that belongs
|
||
-- to no website account at all, and there is no user to author it against.
|
||
--
|
||
-- So a revoke is its own instruction with its own lifetime: queued by a person,
|
||
-- carried in the next sync's retire list, and deleted once a report says the
|
||
-- game no longer has it. A server that is offline keeps the instruction until
|
||
-- it comes back, which is the behaviour an operator expects from a website that
|
||
-- claims to be the author of record.
|
||
CREATE TABLE IF NOT EXISTS rust_perm_revocations (
|
||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||
server_id VARCHAR(64) NOT NULL,
|
||
kind VARCHAR(24) NOT NULL,
|
||
subject VARCHAR(64) NOT NULL,
|
||
object VARCHAR(128) NOT NULL,
|
||
requested_by INT NULL,
|
||
requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
UNIQUE KEY uq_rust_perm_revocation (server_id, kind, subject, object),
|
||
CONSTRAINT fk_rust_perm_revocations_server
|
||
FOREIGN KEY (server_id) REFERENCES rust_servers (id) ON DELETE CASCADE
|
||
);
|
||
|
||
|
||
-- ── The state of the mirror, per server ───────────────────────────────────
|
||
--
|
||
-- One row per configured server: whether its store currently matches what the
|
||
-- site authors, when that was last true, and what the last report said.
|
||
--
|
||
-- `dirty` is how everything that should provoke a sync says so without knowing
|
||
-- anything about syncing: an admin writing a grant, a drift hook firing in the
|
||
-- game, a server reporting a new boot id or a new wipe. The loop owns WHEN, and
|
||
-- every other part of the module owns WHETHER.
|
||
--
|
||
-- `desired_hash` and `synced_hash` are the cheap half of that question. A loop
|
||
-- that pushed the whole set every tick would work and would also write to six
|
||
-- game servers every thirty seconds for ever; comparing a hash costs one query
|
||
-- and skips the round trip when nothing has changed. The periodic audit below
|
||
-- is what keeps that from being a way to never notice drift.
|
||
CREATE TABLE IF NOT EXISTS rust_perm_sync (
|
||
server_id VARCHAR(64) NOT NULL PRIMARY KEY,
|
||
-- `pending` | `ok` | `failed`
|
||
state VARCHAR(24) NOT NULL DEFAULT 'pending',
|
||
dirty TINYINT(1) NOT NULL DEFAULT 1,
|
||
desired_hash VARCHAR(64) NULL,
|
||
synced_hash VARCHAR(64) NULL,
|
||
boot_id VARCHAR(64) NULL,
|
||
wipe_id VARCHAR(48) NULL,
|
||
last_attempt_at DATETIME NULL,
|
||
last_ok_at DATETIME NULL,
|
||
report LONGTEXT NULL,
|
||
error VARCHAR(191) NULL,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_rust_perm_sync_server
|
||
FOREIGN KEY (server_id) REFERENCES rust_servers (id) ON DELETE CASCADE
|
||
);
|
||
|
||
|
||
-- ── What each server's plugins have registered ────────────────────────────
|
||
--
|
||
-- The option source the authoring form offers (D33), cached from the live read
|
||
-- so that opening the form is not six round trips to six game hosts.
|
||
--
|
||
-- It is a cache of a fact that changes when an operator loads a plugin, and it
|
||
-- is refreshed on every sync — which is also why a name that has stopped being
|
||
-- registered disappears from the form rather than lingering as a choice that
|
||
-- silently does nothing.
|
||
CREATE TABLE IF NOT EXISTS rust_perm_catalogue (
|
||
server_id VARCHAR(64) NOT NULL,
|
||
permission VARCHAR(128) NOT NULL,
|
||
seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (server_id, permission),
|
||
CONSTRAINT fk_rust_perm_catalogue_server
|
||
FOREIGN KEY (server_id) REFERENCES rust_servers (id) ON DELETE CASCADE
|
||
);
|
||
|
||
|
||
-- ── Changes to tables that already shipped ────────────────────────────────
|
||
--
|
||
-- An ALTER below the CREATE, never an edit to it: `CREATE TABLE IF NOT EXISTS`
|
||
-- does nothing against a database that already has the table, so an edited column
|
||
-- would reach fresh installs only — which is the worst possible distribution for
|
||
-- a schema change, because it works everywhere it is tested.
|
||
ALTER TABLE rust_server_state ADD COLUMN IF NOT EXISTS wipe_id VARCHAR(48) NULL;
|
||
|
||
-- Phase 4. `updated_at` is when THIS module last wrote the row, which is not the
|
||
-- same fact as when the server last said something — and the pages were reading
|
||
-- the first as if it were the second, so a server that had been down for three
|
||
-- days rendered "last reported just now" on every failed poll.
|
||
--
|
||
-- They are genuinely two facts and both are wanted: `updated_at` decides whether
|
||
-- the row is stale (a module that stopped polling must not leave a page claiming
|
||
-- a server is up), and `last_seen_at` is when a `server.hello` last arrived. Only
|
||
-- a successful refresh moves it.
|
||
ALTER TABLE rust_server_state ADD COLUMN IF NOT EXISTS last_seen_at DATETIME NULL;
|
||
|
||
|
||
-- ── Configuration written from the site (phase 7b, R18) ───────────────────
|
||
--
|
||
-- The audit trail for the most powerful thing this website can do to somebody's
|
||
-- game host: write a file on it. One row per save attempt, including the ones
|
||
-- that were refused and the ones the plugin rolled back — a write that did not
|
||
-- land is exactly the row an operator asking "why is ZoneManager down" needs to
|
||
-- find.
|
||
--
|
||
-- **No file bodies.** `changes` holds the fields that changed and their before
|
||
-- and after LITERALS, which is what a person reading this wants, and secrets are
|
||
-- redacted on the way in (`configEdit.redactChange`). D37 lets an admin read a
|
||
-- credential on the page they opened deliberately; this table is read by more
|
||
-- people, for longer, and usually by somebody who was not there.
|
||
--
|
||
-- The versions bracket the write: `version_before` is what the plugin said the
|
||
-- file was when it was read, `version_after` what it is now. They are the
|
||
-- plugin's own hashes, echoed — this module never computes one.
|
||
CREATE TABLE IF NOT EXISTS rust_config_writes (
|
||
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||
server_id VARCHAR(64) NOT NULL,
|
||
path VARCHAR(255) NOT NULL,
|
||
plugin VARCHAR(128) NULL,
|
||
-- What the admin asked us to reload. NULL is an honest value: a file whose
|
||
-- plugin is not loaded is written and not reloaded, and saying so is the
|
||
-- difference between "saved" and "in effect".
|
||
reload_target VARCHAR(128) NULL,
|
||
-- `form` or `raw`. Which tier an edit came through changes how it should be
|
||
-- read: a form edit is type-preserving and narrow, a raw edit replaced the
|
||
-- whole document.
|
||
tier VARCHAR(16) NOT NULL DEFAULT 'form',
|
||
user_id INT NULL,
|
||
-- `applied` | `rolled-back` | `refused` | `unreachable`
|
||
outcome VARCHAR(24) NOT NULL,
|
||
reloaded TINYINT(1) NOT NULL DEFAULT 0,
|
||
changes LONGTEXT NULL,
|
||
version_before VARCHAR(64) NULL,
|
||
version_after VARCHAR(64) NULL,
|
||
detail VARCHAR(500) NULL,
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_rust_config_writes_server
|
||
FOREIGN KEY (server_id) REFERENCES rust_servers (id) ON DELETE CASCADE,
|
||
-- A deleted account must not delete the record that they changed a setting.
|
||
-- The row stays and the name goes; the alternative is an audit trail that a
|
||
-- person can erase by closing their account.
|
||
CONSTRAINT fk_rust_config_writes_user
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL,
|
||
KEY idx_rust_config_writes_server (server_id, created_at)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
|
||
|
||
-- ── Who may see who is online (the presence fix, 2026-09-22) ──────────────
|
||
--
|
||
-- The org lead's rule: **nothing tells who is online by default.** The Online
|
||
-- list, the killfeed, chat and every other frame that says a named player was on
|
||
-- the server reach STAFF unless an operator deliberately widens them. A count is
|
||
-- not a name and stays public.
|
||
--
|
||
-- Two places, because the decision has two shapes:
|
||
--
|
||
-- • `rust_settings` holds the FLEET default — one row per key. A key/value
|
||
-- table rather than a column per setting, because phase 9's clan-roster
|
||
-- audience is the next key and a table that grows a column per setting grows
|
||
-- an ALTER per setting.
|
||
-- • `rust_servers.presence_audience` is an optional PER-SERVER override. NULL
|
||
-- means "inherit the fleet default", which is not the same as any audience —
|
||
-- an operator who later narrows the fleet must narrow every server that never
|
||
-- chose otherwise.
|
||
--
|
||
-- The stored value is a word (`staff` · `signed_in` · `public`) and an unknown
|
||
-- word reads as `staff` (`model/visibility`): a typo in a row must narrow, never
|
||
-- widen.
|
||
CREATE TABLE IF NOT EXISTS rust_settings (
|
||
setting_key VARCHAR(64) NOT NULL PRIMARY KEY,
|
||
value VARCHAR(255) NOT NULL,
|
||
updated_by INT NULL,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_rust_settings_user
|
||
FOREIGN KEY (updated_by) REFERENCES users (id) ON DELETE SET NULL
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
|
||
ALTER TABLE rust_servers ADD COLUMN IF NOT EXISTS presence_audience VARCHAR(16) NULL;
|
||
|
||
|
||
-- ── Clans (phase 9, protocol 6) ───────────────────────────────────────────
|
||
--
|
||
-- Rust's FIRST-PARTY clans, which this module answers core's Team questions
|
||
-- from (R5, PLAN.md §24). Three tables, and the split is the same one the rest
|
||
-- of this file makes: what a board said (`rust_clans`, `rust_clan_members`),
|
||
-- and what this module knows about the board itself (`rust_clan_boards`).
|
||
--
|
||
-- **`external_id` is the Team's identity, and it is NOT the game's clan id.**
|
||
-- It is `<serverId>:<clanId>:<createdMs>` (D52). The game keeps clans in
|
||
-- `clans.<version>.db` with the version hard-coded, so a game update that bumps
|
||
-- it starts a fresh file whose ids restart at 1. Keyed on the id alone, the new
|
||
-- clan #1 would inherit the old clan #1's Team — its forum, its members-only
|
||
-- history — and core would read the swap as a rename.
|
||
--
|
||
-- **A clan that leaves the board is marked gone, not deleted.** `gone_at` is set
|
||
-- only when a board that is COMPLETE for its server no longer carries it: a
|
||
-- board truncated at the game's 100-clan ceiling (D55) proves nothing about a
|
||
-- clan it does not list. A gone clan is not offered to core, which is what lets
|
||
-- core archive its Team.
|
||
CREATE TABLE IF NOT EXISTS rust_clans (
|
||
external_id VARCHAR(160) NOT NULL PRIMARY KEY,
|
||
server_id VARCHAR(64) NOT NULL,
|
||
clan_id BIGINT NOT NULL,
|
||
created_ms BIGINT NOT NULL,
|
||
name VARCHAR(191) NOT NULL,
|
||
-- `#rrggbb`, as the plugin spells it. Stored as sent rather than parsed, and
|
||
-- re-checked on the way out (`model/clans`), because it ends up in a style.
|
||
color VARCHAR(16) NULL,
|
||
score BIGINT NOT NULL DEFAULT 0,
|
||
member_count INT UNSIGNED NOT NULL DEFAULT 0,
|
||
max_members INT UNSIGNED NULL,
|
||
first_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
gone_at DATETIME NULL,
|
||
CONSTRAINT fk_rust_clans_server
|
||
FOREIGN KEY (server_id) REFERENCES rust_servers (id) ON DELETE CASCADE,
|
||
KEY idx_rust_clans_server (server_id, gone_at),
|
||
KEY idx_rust_clans_game_id (server_id, clan_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
|
||
-- One row per member per clan, replaced whole from each board.
|
||
--
|
||
-- `rank` is the role's rank, and **rank 1 is leader** — the game's own rule, and
|
||
-- several members may hold it. It is NULL when the member's role id matched no
|
||
-- role on the board: "not known" must never be read as "leads this clan".
|
||
--
|
||
-- There is deliberately no `last_seen`. The game has one; the plugin does not
|
||
-- send it, because when somebody was last on is presence (PLAN.md §23).
|
||
CREATE TABLE IF NOT EXISTS rust_clan_members (
|
||
external_id VARCHAR(160) NOT NULL,
|
||
steam_id VARCHAR(32) NOT NULL,
|
||
name VARCHAR(191) NULL,
|
||
role_rank INT NULL,
|
||
role_name VARCHAR(64) NULL,
|
||
joined_ms BIGINT NULL,
|
||
PRIMARY KEY (external_id, steam_id),
|
||
CONSTRAINT fk_rust_clan_members_clan
|
||
FOREIGN KEY (external_id) REFERENCES rust_clans (external_id) ON DELETE CASCADE,
|
||
KEY idx_rust_clan_members_steam (steam_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
|
||
-- What this module knows about each server's clan board, as opposed to what the
|
||
-- board said.
|
||
--
|
||
-- **Freshness is judged by THIS side's clock.** `board_t` is the plugin's own
|
||
-- timestamp on the board; `seen_at` is when this module first saw that value.
|
||
-- A board whose `t` stops advancing is a game that stopped talking, and the
|
||
-- age of `seen_at` is how long ago that was — comparing `board_t` to the
|
||
-- website's clock instead would let a game host whose clock runs ahead make a
|
||
-- stale board look current for as long as the skew lasts.
|
||
--
|
||
-- `umod_clans` is whether the optional uMod Clans plugin is loaded on that
|
||
-- server (D47): its clans are a separate system and never Teams, and the admin
|
||
-- page says so.
|
||
CREATE TABLE IF NOT EXISTS rust_clan_boards (
|
||
server_id VARCHAR(64) NOT NULL PRIMARY KEY,
|
||
board_t BIGINT NULL,
|
||
seen_at DATETIME NULL,
|
||
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||
supported TINYINT(1) NOT NULL DEFAULT 0,
|
||
truncated TINYINT(1) NOT NULL DEFAULT 0,
|
||
backend VARCHAR(64) NULL,
|
||
reason VARCHAR(255) NULL,
|
||
umod_clans TINYINT(1) NOT NULL DEFAULT 0,
|
||
clan_count INT UNSIGNED NOT NULL DEFAULT 0,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_rust_clan_boards_server
|
||
FOREIGN KEY (server_id) REFERENCES rust_servers (id) ON DELETE CASCADE
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
|
||
|
||
-- ── What an event granted (phase 13b, protocol 10) ────────────────────────
|
||
--
|
||
-- `rust.kit.entitle`'s ledger: one row per run, step, website user and
|
||
-- permission (PLAN.md §29, D84). It is a table of its own, and not rows in
|
||
-- `rust_perm_grants`, because that table is UNIQUE on (user, permission,
|
||
-- scope): an admin grant of the same kit would collide with an event's, and a
|
||
-- revert deleting "the" row would take the admin's grant with it. The push reads
|
||
-- the UNION of the two, so a permission held both ways survives either being
|
||
-- withdrawn.
|
||
--
|
||
-- The grant reaches every account the user has linked (D28), like any other.
|
||
-- The CREDIT does not: `steam_id` is the account that took part, and one win is
|
||
-- one extra use of the kit on that account (D103). `permission` is empty for a
|
||
-- kit anybody may redeem — the credit is then the whole reward.
|
||
--
|
||
-- `idem_key` is core's idempotency key for the step. It is what a revert has
|
||
-- when core lost the answer and holds no resource: without it the rows a lost
|
||
-- answer wrote would be a grant nothing could ever withdraw.
|
||
--
|
||
-- `server_id` is the kit's server and the only one the grant reaches (D102).
|
||
-- There is no foreign key to `rust_servers`: a server deleted mid-event must not
|
||
-- delete the ledger a revert needs to find.
|
||
CREATE TABLE IF NOT EXISTS rust_perm_run_grants (
|
||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||
run_id VARCHAR(64) NOT NULL,
|
||
step_id VARCHAR(64) NOT NULL,
|
||
idem_key VARCHAR(190) NOT NULL DEFAULT '',
|
||
user_id INT NOT NULL,
|
||
server_id VARCHAR(64) NOT NULL,
|
||
steam_id VARCHAR(32) NOT NULL,
|
||
permission VARCHAR(128) NOT NULL DEFAULT '',
|
||
kit VARCHAR(128) NOT NULL,
|
||
credit TINYINT(1) NOT NULL DEFAULT 1,
|
||
granted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
UNIQUE KEY uq_rust_perm_run_grant (run_id, step_id, user_id, permission),
|
||
KEY idx_rust_perm_run_grant_server (server_id),
|
||
KEY idx_rust_perm_run_grant_key (run_id, idem_key),
|
||
CONSTRAINT fk_rust_perm_run_grants_user
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
|
||
-- The news switch (D104): whether a published news post is also said in this
|
||
-- server's chat. Off, because core enqueues every registered leg for every
|
||
-- post, and without it the day this module updates every post would start
|
||
-- appearing in every server's chat.
|
||
ALTER TABLE rust_servers ADD COLUMN IF NOT EXISTS announce_news TINYINT(1) NOT NULL DEFAULT 0;
|
||
|
||
-- ── The map (phase 14, protocol 11) ───────────────────────────────────────
|
||
--
|
||
-- One row per server holding the picture of its CURRENT map (PLAN.md §30.2),
|
||
-- replaced whole when the map changes. The picture lives here and not in core's
|
||
-- `/uploads`, which core serves to anybody by URL and no purge would reach
|
||
-- (§30.5). One row per server bounds it at about a megabyte each.
|
||
--
|
||
-- `bytes` and `sha256` are NULL for a server whose game has no picture yet
|
||
-- (`source = 'none'`): the row still carries the geometry and the monuments, so
|
||
-- the page can draw the live layers on a plain background, and an admin's
|
||
-- render replaces it. `map_key` says WHICH map and `sha256` which picture of it.
|
||
--
|
||
-- `derivation` is `DERIVATION_VERSION` (R9) — how the geometry columns were
|
||
-- worked out from what the plugin said. A row whose number is older is
|
||
-- re-derived from a fresh `map.info` without fetching the bytes again.
|
||
--
|
||
-- `monuments` is JSON as the plugin sent it: kind, label, the `kind#n` token and
|
||
-- x/z. It changes only with the map, so it belongs to this row and not to the
|
||
-- live answer.
|
||
CREATE TABLE IF NOT EXISTS rust_map_images (
|
||
server_id VARCHAR(64) NOT NULL PRIMARY KEY,
|
||
map_key VARCHAR(160) NOT NULL,
|
||
sha256 CHAR(64) NULL,
|
||
source VARCHAR(16) NOT NULL,
|
||
width INT UNSIGNED NOT NULL,
|
||
height INT UNSIGNED NOT NULL,
|
||
ocean_margin INT UNSIGNED NOT NULL,
|
||
world_size INT UNSIGNED NOT NULL,
|
||
grid_cells INT UNSIGNED NOT NULL,
|
||
grid_cell_size DECIMAL(10,3) NOT NULL,
|
||
background VARCHAR(16) NULL,
|
||
derivation INT UNSIGNED NOT NULL,
|
||
monuments MEDIUMTEXT NOT NULL,
|
||
bytes MEDIUMBLOB NULL,
|
||
fetched_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_rust_map_images_server
|
||
FOREIGN KEY (server_id) REFERENCES rust_servers (id) ON DELETE CASCADE
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
|
||
-- Per-server overrides of the map's switches (D114): a layer's audience, or the
|
||
-- own-and-mates view. The fleet defaults are rows in `rust_settings`
|
||
-- (`map.layer.<layer>.audience`, `map.mates`); a server with no row here
|
||
-- inherits them. A table of its own rather than five more columns on
|
||
-- `rust_servers`, because the set of switches is the part most likely to grow.
|
||
--
|
||
-- `value` is a word (`staff` · `signed_in` · `public`, or `on` · `off`), and a
|
||
-- word this build does not recognise NARROWS (`model/map`).
|
||
CREATE TABLE IF NOT EXISTS rust_map_overrides (
|
||
server_id VARCHAR(64) NOT NULL,
|
||
setting VARCHAR(64) NOT NULL,
|
||
value VARCHAR(16) NOT NULL,
|
||
updated_by INT NULL,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (server_id, setting),
|
||
CONSTRAINT fk_rust_map_overrides_server
|
||
FOREIGN KEY (server_id) REFERENCES rust_servers (id) ON DELETE CASCADE,
|
||
CONSTRAINT fk_rust_map_overrides_user
|
||
FOREIGN KEY (updated_by) REFERENCES users (id) ON DELETE SET NULL
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|