feat(rust): chat titles, BetterChat group styles, the voice and popups (phase 17)

PLAN.md §33, D134-D143. Protocol 12.

- Chat titles (D135-D137): per-server rules (stat, top N, text, colour)
  that rank the current wipe, and a mode (first | all | up to N). Worked
  out once in model/titles and read three ways: pushed whole to the game by
  a new titleSync loop (on change, restart or wipe), and on every
  leaderboard row as `titles`. Admin: PUT /servers/:id/titles.
- Group styles (D138, D139): a site group may carry all twelve BetterChat
  fields (rust_perm_group_chat). They ride perm.sync with `expect` from the
  pushed ledger, which gains a value column; a field changed in game is a
  `chat-field` drift row with the game's value, adopted into the style or
  put back. A withdrawn style is one `chat-group` retirement, never for
  `default`, cleared from the ledger only once BetterChat removed it.
- The voice (D140): one fleet setting naming a styled group; news and
  rust.announce chat lines carry its format and the plugin says them with
  no sender. Admin: GET/PUT /voice.
- Popups (D141, D142): rust.announce gains `delivery` (still version 1,
  from rust.options.delivery); each server gains news_delivery beside the
  news switch; `popup-unavailable` is not retried.
- GET /servers/:id/integrations reads, live, which optional mods a server
  has loaded. README lists BetterChat and PopupNotifications as optional.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-25 17:48:22 -05:00
parent fb5a581a94
commit 1b70cef5be
43 changed files with 3371 additions and 72 deletions

View File

@@ -19,6 +19,11 @@
-- it knows this module registered, because it is the side that knows which
-- registrant owned what.
-- Phase 17. `rust_perm_group_chat` before `rust_perm_groups`, which it
-- references; the rest of this phase is columns, which go with their tables.
DROP TABLE IF EXISTS rust_perm_group_chat;
DROP TABLE IF EXISTS rust_title_rules;
-- Phase 14.
DROP TABLE IF EXISTS rust_map_overrides;
DROP TABLE IF EXISTS rust_map_images;

View File

@@ -927,3 +927,65 @@ ALTER TABLE rust_servers ADD COLUMN IF NOT EXISTS wipe_time CHAR(5) NULL;
ALTER TABLE rust_servers ADD COLUMN IF NOT EXISTS wipe_tz VARCHAR(64) NULL;
ALTER TABLE rust_servers ADD COLUMN IF NOT EXISTS wipe_anchor DATE NULL;
ALTER TABLE rust_servers ADD COLUMN IF NOT EXISTS wipe_once_at DATETIME NULL;
-- ── The optional mods (phase 17, protocol 12) ─────────────────────────────
--
-- Chat titles (D135): an operator's rules, per server, in their own order. A
-- rule ranks the CURRENT wipe by one stat and gives its top N a title. The
-- titles themselves are not stored: they are computed from the standings each
-- time they are pushed or shown, so nothing rolls them over at a wipe and
-- nothing can go stale (`model/titles`).
--
-- stat `kills` · `npckills` · `playtime`
-- top_n 1–10
-- text at most 24 characters, with `[ ] < > { }` stripped on save so an
-- operator's title can carry no markup of its own (§33.4)
-- color `#rrggbb`
-- position the operator's order, which is also precedence (D136)
CREATE TABLE IF NOT EXISTS rust_title_rules (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
server_id VARCHAR(64) NOT NULL,
position INT NOT NULL DEFAULT 0,
stat VARCHAR(16) NOT NULL,
top_n TINYINT NOT NULL DEFAULT 1,
text VARCHAR(24) NOT NULL,
color CHAR(7) NOT NULL DEFAULT '#ffaa55',
KEY idx_rust_title_rules_server (server_id, position),
CONSTRAINT fk_rust_title_rules_server
FOREIGN KEY (server_id) REFERENCES rust_servers (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- How many titles one player shows (D136): `first` the first rule they meet,
-- `all`, or `upto` `title_max` of them. A word this build does not know reads
-- as `first`, the fewest.
ALTER TABLE rust_servers ADD COLUMN IF NOT EXISTS title_mode VARCHAR(8) NOT NULL DEFAULT 'first';
ALTER TABLE rust_servers ADD COLUMN IF NOT EXISTS title_max TINYINT NOT NULL DEFAULT 2;
-- Where a news post goes on this server when `announce_news` is on (D142):
-- `chat` or `popup`. A popup needs PopupNotifications, which is optional
-- (D141), and a server without it refuses with a reason.
ALTER TABLE rust_servers ADD COLUMN IF NOT EXISTS news_delivery VARCHAR(8) NOT NULL DEFAULT 'chat';
-- A group's BetterChat style (D138): all twelve fields or none, one row each,
-- as the text BetterChat's own `chat group set` takes. A style is authored with
-- the group and dies with it; the push, the drift and the removal (D139) are the
-- permission mirror's, like everything else about a group.
CREATE TABLE IF NOT EXISTS rust_perm_group_chat (
group_name VARCHAR(64) NOT NULL,
field VARCHAR(32) NOT NULL,
value VARCHAR(255) NOT NULL,
PRIMARY KEY (group_name, field),
CONSTRAINT fk_rust_perm_group_chat_group
FOREIGN KEY (group_name) REFERENCES rust_perm_groups (name) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- What a pushed style field HELD when it landed (kind `chat-field`, subject the
-- group, object the field). Every other ledger row is a fact with no value — a
-- grant is held or it is not — and a field is not: the value is what tells a
-- field this site set from one somebody changed by hand (§33.2). NULL on every
-- other kind.
ALTER TABLE rust_perm_pushed ADD COLUMN IF NOT EXISTS value VARCHAR(255) NULL;
-- The value a changed field holds in the game, for a `chat-field` drift row.
-- NULL on every other kind: a foreign grant is its own description.
ALTER TABLE rust_perm_drift ADD COLUMN IF NOT EXISTS detail VARCHAR(255) NULL;