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
This commit is contained in:
@@ -137,8 +137,20 @@ async function respawnPlacement(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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,
|
||||
|
||||
@@ -41,6 +41,7 @@ const placementBody = [
|
||||
body('respawn').optional().isFloat({ min: 1, max: 86400 }).withMessage('respawn is 1 to 86400 seconds'),
|
||||
body('respawnMode').optional().isIn(['each', 'group']).withMessage('respawnMode is each or group'),
|
||||
body('movement').optional({ nullable: true }).isObject().withMessage('movement is { mode, radius }'),
|
||||
body('tether').optional({ nullable: true }).isString().matches(/^[A-Za-z0-9_.:-]{0,64}$/).withMessage('tether is a ZoneManager zone id'),
|
||||
]
|
||||
|
||||
npcsRouter.get(
|
||||
@@ -53,6 +54,20 @@ npcsRouter.get(
|
||||
npcs.describe,
|
||||
)
|
||||
|
||||
npcsRouter.put(
|
||||
'/factions',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Replace the NPC faction table'
|
||||
// #swagger.description = 'How factions treat each other (runicnpc stage 5, D254): one row per pair, both ways (D268), each `hostile`, `neutral` or `allied`. A pair that is not listed is neutral, and a faction is always allied to itself. `scientists` and `animals` are Rust’s own two; a pair of those two is refused, since RunicNPC does not change how Rust’s NPCs treat each other. Checked as RunicNPC checks it, stored as one table for the whole site, and pushed to every server with its profiles on the loop’s next tick. A profile with no faction settings still fights players only (D255).'
|
||||
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", required: ["factions"], properties: { factions: { type: "array", items: { $ref: "#/components/schemas/RustNpcFactionPair" } } } } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Saved', content: { "application/json": { schema: { type: "object", properties: { factions: { type: "array", items: { $ref: "#/components/schemas/RustNpcFactionPair" } } } } } } } */
|
||||
/* #swagger.responses[400] = { description: 'A pair RunicNPC would refuse, in its own words' } */
|
||||
requireRole('admin'),
|
||||
body('factions').isArray({ max: 400 }).withMessage('factions is a list of { a, b, relation }, at most 400'),
|
||||
validate,
|
||||
npcs.setFactions,
|
||||
)
|
||||
|
||||
npcsRouter.post(
|
||||
'/profiles',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
@@ -121,7 +136,7 @@ npcsRouter.post(
|
||||
'/servers/:id/push',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Push the NPC profiles to one server now'
|
||||
// #swagger.description = 'Rather than on the loop’s next tick. A server the site has never pushed to has its own profiles adopted first (D244). The answer says what happened: `pushed` (with the profiles and any RunicNPC refused), `absent` (no RunicNPC, or one older than API 3, and why) or `failed` (with the reason).'
|
||||
// #swagger.description = 'Rather than on the loop’s next tick. A server the site has never pushed to has its own profiles adopted first (D244). The answer says what happened: `pushed` (with the profiles and any RunicNPC refused), `absent` (no RunicNPC, or one older than API 4, and why) or `failed` (with the reason).'
|
||||
// #swagger.parameters['id'] = { in: 'path', required: true, description: 'The server’s id', schema: { type: 'string' } }
|
||||
/* #swagger.responses[200] = { description: 'What the push did' } */
|
||||
/* #swagger.responses[404] = { description: 'No such server' } */
|
||||
|
||||
Reference in New Issue
Block a user