Files
Module-Rust/server/model/zones/zones.db.js
wtclaude 8361b19c24 feat(rust): zone presets, and the zone step's options, messages and dome (PLAN_REDESIGNS §3, D210)
Admin → Rust zone presets: named sets of ZoneManager flags and settings for
one server, several, or every server (D195), ticked in groups read from each
server's own ZoneManager list in its last hello (D211). A flag not on a
covered server is refused on save with the server named; two presets of one
name may not share a server.

rust.zone.open gains options (one line, NoBuild, radiation=10), enterMessage,
leaveMessage, delivery, dome and domeStack. The options field's dropdown is
rust.options.zone_presets, whose row VALUE is the preset's line, so picking
one copies it into the step (D210) and no published event changes when a
preset does. The line and the dome are checked against the server's hello
on save and in a dry run; bad-option and dome-unavailable are permanent.

Schema: rust_zone_presets, rust_zone_preset_servers. Swagger fragment and
routes.manifest regenerated against the pinned core f0e7d2a.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-29 05:25:44 -05:00

143 lines
5.1 KiB
JavaScript

// ── SQL for zone presets, and what each server said about its zone mods ──────
//
// The presets are the module's own (PLAN_REDESIGNS §3.1, D195). What a server
// has — its ZoneManager's flag list (D211), ZoneDomes and the domes helper (D194)
// — is read out of the hello each server's state row keeps whole (`raw`), so the
// preset form works while a server is off.
const core = require('../../core')
const PRESETS = 'rust_zone_presets'
const PRESET_SERVERS = 'rust_zone_preset_servers'
const SERVERS = 'rust_servers'
const STATE = 'rust_server_state'
/** The driver hands JSON_EXTRACT and TEXT back as strings. A bad value is no value. */
function json(value, fallback) {
if (value === null || value === undefined) return fallback
if (typeof value !== 'string') return value
try {
return JSON.parse(value)
} catch {
return fallback
}
}
function shape(row, serverIds) {
return {
id: Number(row.id),
name: row.name,
flags: json(row.flags, []),
settings: json(row.settings, {}),
allServers: Boolean(row.allServers),
servers: serverIds,
updatedAt: row.updatedAt ? new Date(row.updatedAt).toISOString() : null,
}
}
/** Every preset, with the servers each names, oldest first. */
async function listPresets() {
const [rows, links] = await Promise.all([
core.query(
`SELECT id, name, flags, settings, all_servers AS allServers, updated_at AS updatedAt
FROM ${PRESETS} ORDER BY name ASC, id ASC`,
),
core.query(`SELECT preset_id AS presetId, server_id AS serverId FROM ${PRESET_SERVERS} ORDER BY server_id ASC`),
])
const by = new Map()
for (const l of links) {
const id = Number(l.presetId)
if (!by.has(id)) by.set(id, [])
by.get(id).push(l.serverId)
}
return rows.map((r) => shape(r, by.get(Number(r.id)) || []))
}
async function getPreset(id) {
const rows = await core.query(
`SELECT id, name, flags, settings, all_servers AS allServers, updated_at AS updatedAt
FROM ${PRESETS} WHERE id = ?`,
[id],
)
if (!rows[0]) return null
const links = await core.query(`SELECT server_id AS serverId FROM ${PRESET_SERVERS} WHERE preset_id = ? ORDER BY server_id ASC`, [id])
return shape(rows[0], links.map((l) => l.serverId))
}
/**
* Writes one preset and replaces its server list. The model has checked every
* server exists first, so the inserts below have nothing left to refuse. The
* list is written in one statement, so a preset is never left half-listed.
*/
async function savePreset({ id = null, name, flags, settings, allServers, servers }, userId = null) {
let presetId = id
if (presetId) {
await core.query(
`UPDATE ${PRESETS} SET name = ?, flags = ?, settings = ?, all_servers = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
[name, JSON.stringify(flags), JSON.stringify(settings), allServers ? 1 : 0, presetId],
)
await core.query(`DELETE FROM ${PRESET_SERVERS} WHERE preset_id = ?`, [presetId])
} else {
const res = await core.query(
`INSERT INTO ${PRESETS} (name, flags, settings, all_servers, created_by) VALUES (?, ?, ?, ?, ?)`,
[name, JSON.stringify(flags), JSON.stringify(settings), allServers ? 1 : 0, userId],
)
presetId = Number(res.insertId)
}
if (!allServers && servers.length > 0) {
await core.query(
`INSERT INTO ${PRESET_SERVERS} (preset_id, server_id) VALUES ${servers.map(() => '(?, ?)').join(', ')}`,
servers.flatMap((serverId) => [presetId, serverId]),
)
}
return presetId
}
async function deletePreset(id) {
await core.query(`DELETE FROM ${PRESETS} WHERE id = ?`, [id])
}
/**
* Each configured server, in the operator's order, with what its last hello said
* about ZoneManager (`{ version, flags }`) and ZoneDomes (`{ loaded, version,
* helper, ready }`). Either is null for a server that never said: no ZoneManager,
* a plugin before protocol 13, or a server never reached.
*/
async function listZoneServers() {
const rows = await core.query(
`SELECT s.id, s.name, s.enabled,
JSON_EXTRACT(st.raw, '$.zoneManager') AS zoneManager,
JSON_EXTRACT(st.raw, '$.integrations.zoneDomes') AS zoneDomes
FROM ${SERVERS} s
LEFT JOIN ${STATE} st ON st.server_id = s.id
ORDER BY s.sort_order ASC, s.id ASC`,
)
return rows.map((r) => {
const zm = json(r.zoneManager, null)
const zd = json(r.zoneDomes, null)
return {
id: r.id,
name: r.name,
enabled: Boolean(r.enabled),
zoneManager:
zm && typeof zm === 'object'
? {
version: typeof zm.version === 'string' ? zm.version : null,
flags: Array.isArray(zm.flags) ? zm.flags.filter((f) => typeof f === 'string') : null,
}
: null,
zoneDomes:
zd && typeof zd === 'object'
? {
loaded: zd.loaded === true,
version: typeof zd.version === 'string' ? zd.version : null,
helper: zd.helper && typeof zd.helper.state === 'string' ? zd.helper : null,
ready: zd.ready === true,
}
: null,
}
})
}
module.exports = { PRESETS, PRESET_SERVERS, listPresets, getPreset, savePreset, deletePreset, listZoneServers }