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
166 lines
5.4 KiB
JavaScript
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,
|
|
}
|