// ── Admin · Rust · Servers — chat titles and the voice (phase 17) ───────── // // Two things the servers page gained with BetterChat and PopupNotifications // becoming optional (PLAN.md §33): // // • **A server's chat titles** (D135, D136): rules that rank the current wipe, // in the operator's order, and how many a player shows. Saved with their own // PUT, because they are not part of the server row — an operator retitling // "Top Killer" must not have to re-type a sidecar address. // • **The voice** (D140): the one styled permission group news and event lines // are said in. A fleet setting, so it sits once at the foot of the page. // // Neither needs BetterChat to save. Titles are held by the plugin until BetterChat // arrives, and the voice is said by our own plugin whether BetterChat is there or // not; the "What this server has" line says which is which. import { useState } from 'react' import { ErrorState, Loading, useAsync } from '../../core.js' import api from '../../api.js' import { contrastInk } from '../../lib/format.js' const STATS = [ { id: 'kills', label: 'Kills' }, { id: 'npckills', label: 'NPC kills' }, { id: 'playtime', label: 'Playtime' }, ] const MODES = [ { id: 'first', label: 'The first title they earn', hint: 'The rule highest in the list wins.' }, { id: 'all', label: 'Every title they earn' }, { id: 'upto', label: 'Up to a number of titles', hint: 'In list order.' }, ] const statLabel = (id) => (STATS.find((s) => s.id === id) || { label: id }).label /** One line summarising a server's titles, for its row on the list. */ export function titlesSummary(titles, push) { const rules = (titles && titles.rules) || [] if (!rules.length) return 'No chat titles.' const list = rules.map((r) => `${r.text} (top ${r.topN} ${statLabel(r.stat).toLowerCase()})`).join(', ') const shown = push ? ` Last pushed: ${push.count} player${push.count === 1 ? '' : 's'} hold one${push.betterChat ? '' : ' — BetterChat is not loaded, so they are not showing in game yet'}.` : '' return `Chat titles: ${list}.${shown}` } /** A title as the leaderboard will show it. */ function Chip({ text, color }) { return ( {text || '…'} ) } export function TitlesForm({ server, onSaved, onCancel }) { const start = server.titles || { mode: 'first', max: 2, rules: [] } const [mode, setMode] = useState(start.mode) const [max, setMax] = useState(start.max) const [rules, setRules] = useState(start.rules.map((r) => ({ ...r }))) const [busy, setBusy] = useState(false) const [error, setError] = useState('') const setRule = (i, key) => (e) => { const value = e.target.value setRules((list) => list.map((r, n) => (n === i ? { ...r, [key]: key === 'topN' ? Number(value) : value } : r))) } const move = (i, by) => setRules((list) => { const next = [...list] const [r] = next.splice(i, 1) next.splice(i + by, 0, r) return next }) const save = async (e) => { e.preventDefault() setBusy(true) setError('') try { await api.admin.saveTitles(server.id, { mode, max: Number(max), rules }) onSaved() } catch (err) { setError(err.message || 'That did not save.') } finally { setBusy(false) } } return (
) } /** What a server says it has loaded, as one sentence (§33.2 `integrations`). */ export function integrationsLine(r) { if (!r || !r.integrations) { return r && r.ok === false ? `The game could not be asked (${r.status}).` : 'This server’s plugin is older than protocol 12, so it cannot say which optional mods it has.' } const one = (name, x, without) => (x && x.loaded ? `${name} ${x.version || ''} is loaded`.trim() : `${name} is not loaded — ${without}`) return `${one('BetterChat', r.integrations.betterChat, 'titles and group styles wait for it')}. ${one('PopupNotifications', r.integrations.popupNotifications, 'a popup is refused, and chat still works')}.` } export function VoiceCard() { const [reloads, setReloads] = useState(0) const { data, error: loadError } = useAsync(() => api.admin.voice(), [reloads]) const [busy, setBusy] = useState(false) const [error, setError] = useState('') if (loadError) returnNews posts and event announcements said in game chat can wear the title and colours of one permission group that has a chat style. The line has no sender, so no player’s name appears. It works whether or not BetterChat is installed. Popups are plain text.
{data.voice && !current && (The group “{data.voice}” no longer has a chat style, so lines are said in plain chat until it has one again or another voice is chosen.
)} {current &&The line: {current.format}
No group has a chat style yet. Give one a style under Permissions.
)} {error &&{error}
}