Files
Module-Rust/server/router/admin/npcs.controller.js
wtclaude fa16f0ad2e feat(rust): NPC profile and placement routes, the event picker, triggers and titles (runicnpc stage 4)
Admin: /admin/rust/npcs for profiles (create, change, delete, restore a
replaced one, push now) and each server's placements (list, add from a map
point, change, remove, rename, respawn). Public: the profiles a leaderboard
ranks by, one profile's ranking counted as the profile says (D247, D250), and
one player's kills by profile (D252). Player: your own kills by profile.

The Place NPCs step offers the site's profiles first, then Rust's own
(D243). rust.npc.died and rust.npc.health are triggers a phase can wait on.
A title rule can rank a profile's kills. Swagger fragment, engagement and
route manifests regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-30 04:34:43 -05:00

154 lines
4.9 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')
}
}
module.exports = {
describe,
create,
update,
remove,
restore,
push,
listPlacements,
addPlacement,
setPlacement,
removePlacement,
renamePlacement,
respawnPlacement,
}