Files
Module-Rust/server/router/admin/npcs.controller.js
wtclaude 11619a4dc6 feat(rust): NPC sides, the faction table, an event's escort, ally and tether (runicnpc stage 5)
RunicNPC stage 5 on the site (docs runicnpc/PLAN.md, D253-D272):

- profiles gain the guard role, faction, relations (its own exceptions),
  alertRadius, turrets, hurtByPlayers, hurtsPlayers and kitUse, checked in
  RunicNPC's order and words, and defaulting to "players only" (D255);
- the faction table: one site-wide table, one row per pair and both ways
  (D254, D268), stored in rust_npc_factions, edited on the NPC profiles page
  (PUT /admin/rust/npcs/factions), pushed to every server with its
  profiles and hashed with them, and a standalone server's own pairs
  adopted at its first push with the site's winning (D244, D251);
- the Place NPCs step takes escort (a Steam id or a {placeholder}), an ally
  (a clan from the new rust.options.clans source, or the team of a player)
  and tether (the zone this event made), for a RunicNPC profile only
  (D269, D270, D272);
- a placement may be held inside a zone (tether, D272);
- the bridge's RunicNPC API floor is 4.

Tests: 488 server, 66 client. routes.manifest.json regenerated against the
pinned core (one route added).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-01 07:58:13 -05:00

166 lines
5.4 KiB
JavaScript

// ── Admin · Rust · NPCs: the handlers (docs/runicnpc/PLAN.md stage 4) ──────
//
// Thin: the model decides, and a refusal it throws carries its own status and
// sentence. Every write is logged in core's activity log, a placement's too,
// because a placement puts armed NPCs in the world.
const core = require('../../core')
const npcs = require('../../model/npcs/npcs.model')
const npcSync = require('../../npcSync')
const log = core.logger('npcs')
const by = (req) => (req.user ? req.user.id : null)
/** A model refusal answers with its own status; anything else is ours to log. */
function fail(res, err, what) {
if (err instanceof npcs.NpcError) return res.status(err.status).json({ message: err.message })
log.error(`failed to ${what}`, { error: err.message })
return res.status(500).json({ message: `Failed to ${what}` })
}
async function describe(req, res) {
try {
res.json(await npcs.describe())
} catch (err) {
fail(res, err, 'read the NPC profiles')
}
}
async function create(req, res) {
try {
const profile = await npcs.create(req.body || {}, by(req))
await core.activity.log({ req, action: 'rust.npcs.profile.create', detail: { id: profile.id, name: profile.name } })
res.status(201).json(profile)
} catch (err) {
fail(res, err, 'save the NPC profile')
}
}
async function update(req, res) {
try {
const profile = await npcs.update(Number(req.params.pid), req.body || {})
await core.activity.log({ req, action: 'rust.npcs.profile.update', detail: { id: profile.id, name: profile.name } })
res.json(profile)
} catch (err) {
fail(res, err, 'save the NPC profile')
}
}
async function remove(req, res) {
try {
await npcs.remove(Number(req.params.pid))
await core.activity.log({ req, action: 'rust.npcs.profile.delete', detail: { id: Number(req.params.pid) } })
res.json({ ok: true })
} catch (err) {
fail(res, err, 'delete the NPC profile')
}
}
async function restore(req, res) {
try {
const profile = await npcs.restore(Number(req.params.pid))
await core.activity.log({ req, action: 'rust.npcs.profile.restore', detail: { id: profile.id, name: profile.name } })
res.json(profile)
} catch (err) {
fail(res, err, 'restore the NPC profile')
}
}
async function push(req, res) {
try {
const [result] = await npcSync.tick({ force: req.params.id })
if (!result) return res.status(404).json({ message: 'No such server, or it is switched off' })
await core.activity.log({ req, action: 'rust.npcs.push', detail: { server: req.params.id, outcome: result.outcome } })
res.json(result)
} catch (err) {
fail(res, err, 'push the NPC profiles')
}
}
async function listPlacements(req, res) {
try {
res.json(await npcs.listPlacements(req.params.id))
} catch (err) {
fail(res, err, 'read the placements')
}
}
async function addPlacement(req, res) {
try {
const placed = await npcs.addPlacement(req.params.id, req.body || {})
await core.activity.log({ req, action: 'rust.npcs.placement.add', detail: { server: req.params.id, id: placed.id, profile: req.body.profile, position: placed.position } })
res.status(201).json(placed)
} catch (err) {
fail(res, err, 'place the NPCs')
}
}
async function setPlacement(req, res) {
try {
const out = await npcs.setPlacement(req.params.id, req.params.placement, req.body || {})
await core.activity.log({ req, action: 'rust.npcs.placement.update', detail: { server: req.params.id, id: req.params.placement } })
res.json(out)
} catch (err) {
fail(res, err, 'change the placement')
}
}
async function removePlacement(req, res) {
try {
await npcs.removePlacement(req.params.id, req.params.placement)
await core.activity.log({ req, action: 'rust.npcs.placement.delete', detail: { server: req.params.id, id: req.params.placement } })
res.json({ ok: true })
} catch (err) {
fail(res, err, 'remove the placement')
}
}
async function renamePlacement(req, res) {
try {
const out = await npcs.renamePlacement(req.params.id, req.params.placement, req.body.to)
await core.activity.log({ req, action: 'rust.npcs.placement.rename', detail: { server: req.params.id, from: req.params.placement, to: req.body.to } })
res.json({ id: out.id || req.body.to, previous: req.params.placement })
} catch (err) {
fail(res, err, 'rename the placement')
}
}
async function respawnPlacement(req, res) {
try {
const out = await npcs.respawnPlacement(req.params.id, req.params.placement)
await core.activity.log({ req, action: 'rust.npcs.placement.respawn', detail: { server: req.params.id, id: req.params.placement } })
res.json({ id: req.params.placement, respawned: Number(out.respawned) || 0 })
} catch (err) {
fail(res, err, 'respawn the placement')
}
}
/** Stage 5 (D254, D268): replaces the site's faction table, pushed to every server with the profiles. */
async function setFactions(req, res) {
try {
const factions = await npcs.setFactions((req.body || {}).factions)
await core.activity.log({ req, action: 'rust.npcs.factions.set', detail: { pairs: factions.length } })
res.json({ factions })
} catch (err) {
fail(res, err, 'save the faction table')
}
}
module.exports = {
describe,
setFactions,
create,
update,
remove,
restore,
push,
listPlacements,
addPlacement,
setPlacement,
removePlacement,
renamePlacement,
respawnPlacement,
}