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:
@@ -12,6 +12,10 @@
|
||||
// …and the announce leg, `rust.chat`, which says a published news post in the
|
||||
// chat of every server whose switch is on (D104).
|
||||
//
|
||||
// Phase 17 gave both a delivery — chat, or a popup through PopupNotifications
|
||||
// (D141, D142) — and a VOICE: the style of one permission group, which the
|
||||
// plugin says the line in with no player as its sender (D140).
|
||||
//
|
||||
// ── Who decides what ─────────────────────────────────────────────────────────
|
||||
//
|
||||
// The GAME counts: presence, kills, the score (D81, D99). The SITE picks the
|
||||
@@ -36,6 +40,7 @@ const servers = require('./model/servers/servers.model')
|
||||
const permDb = require('./model/permissions/permissions.db')
|
||||
const linksDb = require('./model/links/links.db')
|
||||
const emit = require('./engagement/emit')
|
||||
const voice = require('./model/permissions/voice')
|
||||
const { serverFor, transportError, pluginError, perServer, bounded } = require('./eventLeases')
|
||||
const { BUDGET_MS } = require('./eventWorld')
|
||||
|
||||
@@ -57,6 +62,9 @@ const MODES = ['everyone', 'top', 'minScore', 'random', 'topPercent']
|
||||
/** The fleet, in `rust.announce`'s `server` param (D105). */
|
||||
const EVERY_SERVER = '*'
|
||||
|
||||
/** Where a line goes (D141). Chat is the default and what every server can do. */
|
||||
const DELIVERIES = ['chat', 'popup']
|
||||
|
||||
/** The plugin's refusals a second attempt would repeat. */
|
||||
const PERMANENT = new Set([
|
||||
'events-disabled',
|
||||
@@ -68,6 +76,9 @@ const PERMANENT = new Set([
|
||||
'too-many',
|
||||
'too-long',
|
||||
'kits-missing',
|
||||
// Protocol 12: a popup on a server without PopupNotifications. Waiting does not
|
||||
// install it.
|
||||
'popup-unavailable',
|
||||
])
|
||||
|
||||
const BUDGETS = [
|
||||
@@ -620,6 +631,20 @@ const kitEntitle = {
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* The body of a chat line: its delivery, and the voice's format when the line
|
||||
* goes to chat and a voice is chosen (D140). A popup is not a chat line and
|
||||
* carries no format. Chat, the default, is left off the wire — the shape a
|
||||
* protocol-11 caller sent — so a line with nothing new looks exactly as before.
|
||||
*/
|
||||
function lineBody(base, delivery, format) {
|
||||
return {
|
||||
...base,
|
||||
...(delivery === 'popup' ? { delivery } : {}),
|
||||
...(delivery !== 'popup' && format ? { format } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Say one line on one server, and classify the answer. `repeat` is a success:
|
||||
* the plugin remembered the key, and the line was already said.
|
||||
@@ -645,6 +670,10 @@ const announce = {
|
||||
description: 'Which server, or * for every server (D105).' },
|
||||
{ name: 'message', type: 'string', required: true, example: 'The airfield brawl starts in five minutes!',
|
||||
description: `The line, up to ${MAX_CHAT} characters.` },
|
||||
// Optional, and the action stays version 1: a bump would stop every step
|
||||
// already written from dispatching until somebody re-saved it (§33.2).
|
||||
{ name: 'delivery', type: 'string', required: false, example: 'chat', source: 'rust.options.delivery',
|
||||
description: 'chat (the default), or popup — which needs PopupNotifications on the server, and is refused with a reason where it is missing (D141).' },
|
||||
],
|
||||
|
||||
// One per server reached. `*` is priced at the enabled servers when core asks,
|
||||
@@ -658,6 +687,10 @@ const announce = {
|
||||
return { ok: false, retry: false, error: `a chat line is at most ${MAX_CHAT} characters, and this one is ${message.length}` }
|
||||
}
|
||||
|
||||
const rawDelivery = params.delivery === undefined || params.delivery === null || params.delivery === '' ? 'chat' : params.delivery
|
||||
const delivery = oneOf(rawDelivery, DELIVERIES)
|
||||
if (!delivery) return { ok: false, retry: false, error: `a line is delivered to chat or to a popup, not to "${rawDelivery}"` }
|
||||
|
||||
const target = String(params.server || '').trim()
|
||||
let list
|
||||
if (target === EVERY_SERVER) {
|
||||
@@ -671,7 +704,8 @@ const announce = {
|
||||
|
||||
if (verify) return { ok: true }
|
||||
|
||||
const body = { key: idempotencyKey || `run:${runId}`, message, event: true }
|
||||
const format = delivery === 'chat' ? await voice.currentFormat() : null
|
||||
const body = lineBody({ key: idempotencyKey || `run:${runId}`, message, event: true }, delivery, format)
|
||||
const outcomes = await Promise.all(list.map(async (server) => ({ server, ...(await sayOn(server, body)) })))
|
||||
const name = (o) => o.server.name || o.server.id
|
||||
|
||||
@@ -730,8 +764,14 @@ const LEG = {
|
||||
|
||||
const list = (await servers.listForPolling()).filter((s) => s.announceNews)
|
||||
const key = chatKey(post, line)
|
||||
// Read once for the post, not once per server: every server says it in the
|
||||
// same voice (D140). Each server's own delivery decides chat or popup (D142).
|
||||
const format = list.some((s) => s.newsDelivery !== 'popup') ? await voice.currentFormat() : null
|
||||
const outcomes = await Promise.all(
|
||||
list.map(async (server) => ({ server: server.name || server.id, ...(await sayOn(server, { key, message: line })) })),
|
||||
list.map(async (server) => ({
|
||||
server: server.name || server.id,
|
||||
...(await sayOn(server, lineBody({ key, message: line }, server.newsDelivery, format))),
|
||||
})),
|
||||
)
|
||||
return { ok: true, outcomes }
|
||||
} catch (err) {
|
||||
@@ -823,6 +863,10 @@ const OPTION_SOURCES = [
|
||||
{ value: 'random', label: 'N drawn at random' },
|
||||
{ value: 'topPercent', label: 'The top X per cent (ties in)' },
|
||||
]),
|
||||
fixed('rust.options.delivery', 'Delivery', 'Where a line goes (D141).', [
|
||||
{ value: 'chat', label: 'Chat' },
|
||||
{ value: 'popup', label: 'A popup — needs PopupNotifications on the server' },
|
||||
]),
|
||||
{
|
||||
id: 'rust.options.chatservers',
|
||||
label: 'Chat servers',
|
||||
@@ -840,6 +884,7 @@ module.exports = {
|
||||
MAX_RECIPIENTS,
|
||||
MAX_CHAT,
|
||||
EVERY_SERVER,
|
||||
DELIVERIES,
|
||||
BUDGETS,
|
||||
ACTIONS,
|
||||
LEG,
|
||||
@@ -850,5 +895,6 @@ module.exports = {
|
||||
kitReward,
|
||||
chatLine,
|
||||
chatKey,
|
||||
lineBody,
|
||||
refParts,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user