feat(rust): RaidableBases — feed, raids map layer, Warlord, Spawn a raid base, profiles (PLAN_REDESIGNS §11.4)
All checks were successful
PR Checks / server-tests (pull_request) Successful in 37s
PR Checks / client-build (pull_request) Successful in 24s
PR Checks / frozen-manifest (pull_request) Successful in 49s

PLAN_REDESIGNS §11.4 (D202-D208, D386-D391), protocol 14. Spec: docs
rust-link/PROTOCOL.md §20.12.

- raid.started, raid.completed, raid.purchased and raid.locked public,
  raid.ended staff's; raidableBases.js reads each row per viewer (raiders'
  and a buyer's names only for the presence audience, D376/D390; Steam ids,
  position and a run's key only for staff). Feed lines in the World filter.
- The step rust.raidbase.spawn, "Spawn a raid base" (ledgered, budget
  rust.raids): difficulty, profile and base from option sources listing each
  server's own names (D386), a spot as a world call's location with a height
  adjustment or an exact height, and PvP/NPCs/lock as on/off/blank. A base
  the raids helper made is a world resource, so eventWorld's revert and
  reconcile serve it and world.expired marks it expired; one it did not make
  records nothing and the run log says it despawns on its own timer. Refused
  at verify for a server that said it cannot; a dry run with a spot returns
  the spot check as detail.warnings (MODULE_API 1.16.0, D391). rb-not-ready
  retries in ten minutes.
- Warlord: raids_completed, title category raidscompleted, the leaderboard's
  Raid bases column (sort=raidsCompleted).
- The map's raids layer (D114), public by default with its own audience; the
  run's key is dropped before any viewer gets it.
- Server settings' line for RaidableBases; its profiles in Mod configuration
  under a virtual path (D205).
- Swagger fragment, bundle and README.

Tests: server 684/684 (raidableBases.test.js new, every name in it invented);
client 99/99, build and externals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-16 15:18:52 -05:00
parent 49d9e42a23
commit 481837e937
34 changed files with 1996 additions and 38 deletions

166
server/raidableBases.js Normal file
View File

@@ -0,0 +1,166 @@
// ── RaidableBases (protocol 14, PLAN_REDESIGNS §11.4) ───────────────────────
//
// nivex's RaidableBases pastes a stocked, guarded building for players to raid.
// Its bases reach this module as five kinds: `raid.started`, `raid.completed`,
// `raid.purchased` and `raid.locked` (public), and `raid.ended` (staff's). A base
// completed is *Warlord*'s count, from the tally's `raidsCompleted` (D202). A
// run's step spawns one (`eventRaidableBases.js`).
//
// **Every name is the plugin's (D386).** Difficulty names and levels, profile
// names and base names arrive as the installed plugin's own strings; nothing
// here lists them, and an operator's own difficulty is shown as written.
//
// ── Who may read what ──────────────────────────────────────────────────────
//
// As a world event is read (`worldEvents.js`, D353, D376, D390): the base, its
// difficulty, whether it is PvP, and its grid are public; the raiders' and a
// buyer's names show only to the operator's presence audience, so with presence
// left at staff a signed-out visitor reads "A Hard raid base was raided at G7";
// Steam ids, the position and a run's key are staff's. How
// many raided it is a count, and names nobody.
const { gridOf } = require('./worldEvents')
const KINDS = Object.freeze(['raid.started', 'raid.completed', 'raid.ended', 'raid.purchased', 'raid.locked'])
const STEAM_ID = /^\d{17}$/
function isKind(kind) {
return KINDS.includes(kind)
}
/**
* One `raid.*` frame for one viewer: `{ staff, presence, geometry }`. Passes
* the envelope, `level`, `difficulty`, `base`, `pvp` and `despawnAt`, adds `grid`; a
* completion adds `raiderCount`, and `names` (with the lock holder's
* `ownerName`) for the presence audience; a purchase or a lock adds `name` for
* the presence audience. Staff also get `x`, `z`, the run's `id`,
* `runId` and `key`, and every Steam id. Any other frame is returned as is.
*/
function shapeFor(frame, { staff = false, presence = false, geometry = null } = {}) {
if (!frame || !isKind(frame.kind)) return frame
const out = {}
for (const key of ['kind', 'type', 't', 'serverId', 'wipeId']) {
if (frame[key] !== undefined) out[key] = frame[key]
}
const level = Number(frame.level)
if (Number.isInteger(level)) out.level = level
if (typeof frame.difficulty === 'string' && frame.difficulty) out.difficulty = frame.difficulty
if (typeof frame.pvp === 'boolean') out.pvp = frame.pvp
if (Number.isFinite(Number(frame.despawnAt)) && Number(frame.despawnAt) > 0) out.despawnAt = Number(frame.despawnAt)
// The base's file name is the operator's, as the difficulty is, and the map shows it too.
if (typeof frame.base === 'string' && frame.base) out.base = frame.base
const grid = gridOf(geometry, frame.x, frame.z)
if (grid) out.grid = grid
const named = presence || staff
if (frame.kind === 'raid.completed') {
const raiders = Array.isArray(frame.raiders) ? frame.raiders.filter((r) => r && typeof r === 'object') : []
const count = Number(frame.raiderCount)
out.raiderCount = Number.isInteger(count) && count >= 0 ? count : raiders.length
if (named) {
out.names = raiders.map((r) => (typeof r.name === 'string' ? r.name : '')).filter(Boolean)
if (typeof frame.ownerName === 'string' && frame.ownerName) out.ownerName = frame.ownerName
}
if (staff) {
out.raiders = raiders
.filter((r) => STEAM_ID.test(String(r.steamId || '')))
.map((r) => ({ steamId: String(r.steamId), ...(typeof r.name === 'string' ? { name: r.name } : {}) }))
if (STEAM_ID.test(String(frame.ownerId || ''))) out.ownerId = String(frame.ownerId)
}
}
if (frame.kind === 'raid.purchased' || frame.kind === 'raid.locked') {
if (named && typeof frame.name === 'string' && frame.name) out.name = frame.name
if (staff && STEAM_ID.test(String(frame.steamId || ''))) out.steamId = String(frame.steamId)
}
if (staff) {
for (const key of ['x', 'z', 'id', 'runId', 'key', 'radius']) {
if (frame[key] !== undefined) out[key] = frame[key]
}
}
return out
}
/** A JSON column as an object, or null. */
function parse(value) {
if (value === null || value === undefined) return null
if (typeof value !== 'string') return value
try {
return JSON.parse(value)
} catch {
return null
}
}
/** One profile as the helper reports it, or null when it is not one. */
function profileOf(p) {
if (!p || typeof p !== 'object' || typeof p.name !== 'string' || !p.name) return null
return {
name: p.name,
difficulty: typeof p.difficulty === 'string' ? p.difficulty : null,
level: Number.isInteger(Number(p.level)) ? Number(p.level) : null,
enabled: p.enabled === true,
pvp: p.pvp === true,
bases: Array.isArray(p.bases) ? p.bases.filter((b) => typeof b === 'string' && b) : [],
seabed: p.seabed === true,
float: p.float === true,
}
}
/**
* A server's last report about RaidableBases and its console, from
* `servers.db.rbReportFor`: `{ raidableBases, consolePlugins }`, each null
* where the plugin never said (no state yet, or a plugin before this build).
* `raidableBases` is `{ loaded, version, premium, spawnCall, copyPaste,
* difficulties, helper, gridReady, profiles }` — `helper.state` is `missing`,
* `patched`, `unsupported` or `no-raidablebases`; `profiles` is null without a
* patched helper, which is the only thing that can read them.
*/
function reportOf(row) {
const rb = row ? parse(row.raidableBases) : null
const probe = row ? parse(row.console) : null
let raidableBases = null
if (rb && typeof rb === 'object') {
const helper = rb.helper && typeof rb.helper === 'object' ? rb.helper : {}
raidableBases = {
loaded: rb.loaded === true,
version: typeof rb.version === 'string' ? rb.version : null,
premium: rb.premium === true,
spawnCall: rb.spawnCall === true,
copyPaste: rb.copyPaste && typeof rb.copyPaste === 'object' ? { loaded: rb.copyPaste.loaded === true } : null,
difficulties: Array.isArray(rb.difficulties)
? rb.difficulties
.filter((d) => d && typeof d.name === 'string' && d.name && Number.isInteger(Number(d.level)))
.map((d) => ({ name: d.name, level: Number(d.level) }))
: [],
helper: {
state: typeof helper.state === 'string' ? helper.state : 'missing',
version: typeof helper.version === 'string' ? helper.version : null,
reason: typeof helper.reason === 'string' ? helper.reason : null,
},
gridReady: typeof rb.gridReady === 'boolean' ? rb.gridReady : null,
profiles: Array.isArray(rb.profiles) ? rb.profiles.map(profileOf).filter(Boolean) : null,
}
}
return {
raidableBases,
consolePlugins: probe && typeof probe === 'object' && typeof probe.plugins === 'boolean' ? probe.plugins : null,
}
}
/** Whether the report says the raids helper is installed and patched. */
function helperPatched(report) {
const rb = report && report.raidableBases
return Boolean(rb && rb.loaded && rb.helper && rb.helper.state === 'patched')
}
module.exports = { KINDS, isKind, shapeFor, reportOf, profileOf, helperPatched }