feat(rust): chat titles rank twenty-three conditions, and admins name the categories (PLAN_REDESIGNS §5, D172-D175, D209)
- Schema: fourteen title columns on rust_player_wipe_stats (the two best_* distances move by GREATEST, the rest are sums), rust_weapon_kills (kills by weapon prefab name) and rust_title_categories (the admin's title per category). purge.sql drops the two tables. - Ingest: player.tally's new fields, in one statement per frame, none for an older plugin's frame. - Titles: 23 categories plus playtime, each naming where it is read from (a sum, a MAX, npc_kills - animal_kills in signed arithmetic, or a named list). The bow/melee/blade/revolver/wood/ore/plants lists are one file, applied when a title is read. "NPC kills" ranks human NPCs only (D209). - A rule's text may be empty, meaning the category's title: the rule's own, then the admin's, then the default. Typed-only-markup is still refused. A rename forgets every server's cached answer. - Admin API: GET /admin/rust/title-categories and PUT /admin/rust/title-categories/:stat (empty text resets); the server list returns them too. Swagger fragment and routes.manifest.json regenerated (63 routes, against the pinned core). - Screen: every category in the rule form, the category's title as a placeholder, and a Category titles section with Save and Reset. The weapon lists are unverified until the rig probe runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -26,15 +26,22 @@ const log = core.logger('admin')
|
||||
|
||||
async function listServers(req, res) {
|
||||
try {
|
||||
const [rows, settings] = await Promise.all([servers.listForAdmin(), titlesModel.settingsByServer()])
|
||||
const [rows, settings, categories] = await Promise.all([
|
||||
servers.listForAdmin(),
|
||||
titlesModel.settingsByServer(),
|
||||
titlesModel.categories(),
|
||||
])
|
||||
|
||||
// Phase 17: each server's chat titles, and what the last push of them found.
|
||||
// PLAN_REDESIGNS §5.5: the category titles too, since a rule with no text of
|
||||
// its own shows its category's, and the list says which that is.
|
||||
res.json({
|
||||
servers: rows.map((row) => ({
|
||||
...row,
|
||||
titles: settings.get(row.id) || { mode: 'first', max: 2, rules: [] },
|
||||
titlePush: titleSync.lastPush(row.id),
|
||||
})),
|
||||
titleCategories: categories,
|
||||
})
|
||||
} catch (err) {
|
||||
log.error('failed to read the server list', { error: err.message })
|
||||
@@ -262,6 +269,45 @@ async function putTitles(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
/** The title categories (D175): each one's default, the admin's own, and what shows. */
|
||||
async function getTitleCategories(req, res) {
|
||||
try {
|
||||
return res.json({ categories: await titlesModel.categories() })
|
||||
} catch (err) {
|
||||
log.error('failed to read the title categories', { error: err.message })
|
||||
return res.status(500).json({ message: 'Failed to read the title categories' })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename one category for the whole site (D175), or reset it with an empty
|
||||
* text. Every rule of that category without its own text shows the new title
|
||||
* on every server from the next push.
|
||||
*/
|
||||
async function putTitleCategory(req, res) {
|
||||
const { stat } = req.params
|
||||
|
||||
try {
|
||||
const checked = titles.validateCategory(stat, req.body.text)
|
||||
if (!checked.ok) {
|
||||
const missing = !titles.CATEGORY_IDS.includes(stat)
|
||||
return res.status(missing ? 404 : 400).json({ message: checked.error, errors: [checked.error] })
|
||||
}
|
||||
|
||||
await titlesModel.setCategory(stat, checked.text, req.user ? req.user.id : null)
|
||||
await core.activity.log({
|
||||
req,
|
||||
action: checked.text ? 'rust.titles.category.set' : 'rust.titles.category.reset',
|
||||
detail: { stat, text: checked.text || null },
|
||||
})
|
||||
|
||||
return res.json({ categories: await titlesModel.categories() })
|
||||
} catch (err) {
|
||||
log.error('failed to save a title category', { stat, error: err.message })
|
||||
return res.status(500).json({ message: 'Failed to save the category title' })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Which optional mods one server has loaded right now, read live from the game
|
||||
* (`integrations` on `server.status`). Live, because an operator installs
|
||||
@@ -320,6 +366,8 @@ module.exports = {
|
||||
fetchMap,
|
||||
renderMap,
|
||||
putTitles,
|
||||
getTitleCategories,
|
||||
putTitleCategory,
|
||||
integrations,
|
||||
getVoice,
|
||||
putVoice,
|
||||
|
||||
@@ -151,7 +151,7 @@ adminRustRouter.put(
|
||||
'/servers/:id/titles',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Set a server’s chat titles'
|
||||
// #swagger.description = 'Replaces the server’s title rules and how many titles a player shows. A rule ranks the CURRENT wipe by `kills`, `npckills` or `playtime` and gives its top N (1–10) a title; only a stat above zero counts, so a fresh wipe gives no titles. Rules are in precedence order, at most ten. `mode` is `first` (the first rule a player meets), `all`, or `upto` `max` (1–5). The titles are shown in game by BetterChat when it is loaded — they are pushed whether or not it is, and it shows them as soon as it is — and on the web and app leaderboards. A 400 carries one sentence per problem in `errors`.'
|
||||
// #swagger.description = 'Replaces the server’s title rules and how many titles a player shows. A rule ranks the CURRENT wipe by one of the title categories (`RustTitleSettings`) and gives its top N (1–10) a title; only a stat above zero counts, so a fresh wipe gives no titles. A rule’s `text` may be empty, meaning the category’s title (the admin’s, else the module’s default); `playtime` has no default and needs its own. A text that was typed but is only markup is refused. Rules are in precedence order, at most ten. `mode` is `first` (the first rule a player meets), `all`, or `upto` `max` (1–5). The titles are shown in game by BetterChat when it is loaded — they are pushed whether or not it is, and it shows them as soon as it is — and on the web and app leaderboards. A 400 carries one sentence per problem in `errors`.'
|
||||
// #swagger.parameters['id'] = { in: 'path', required: true, description: 'The server’s slug', schema: { type: 'string' } }
|
||||
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/RustTitleSettings" } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Saved; answers the server’s settings as stored' } */
|
||||
@@ -167,6 +167,33 @@ adminRustRouter.put(
|
||||
admin.putTitles,
|
||||
)
|
||||
|
||||
adminRustRouter.get(
|
||||
'/title-categories',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'The chat-title categories and their titles'
|
||||
// #swagger.description = 'Every category a title rule can rank that has a shipped title (PLAN_REDESIGNS §5, D172), with its label, the module’s default, the admin’s own title if they set one, and `current`, what a rule with no text of its own shows. The admin’s title is for the whole site (D175).'
|
||||
/* #swagger.responses[200] = { description: 'The categories', content: { "application/json": { schema: { type: 'object', properties: { categories: { type: 'array', items: { $ref: "#/components/schemas/RustTitleCategory" } } } } } } } */
|
||||
requireRole('admin'),
|
||||
admin.getTitleCategories,
|
||||
)
|
||||
|
||||
adminRustRouter.put(
|
||||
'/title-categories/:stat',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Rename a chat-title category, or reset it'
|
||||
// #swagger.description = 'Sets the site’s own title for one category (D175): at most 24 characters, markup characters taken out as a rule’s are. An empty `text` resets the category to the module’s default. Every rule of this category with no text of its own shows the new title on every server from the next push; a rule that carries its own text keeps it. Answers the whole list.'
|
||||
// #swagger.parameters['stat'] = { in: 'path', required: true, description: 'The category id, for example animalkills', schema: { type: 'string' } }
|
||||
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: 'object', properties: { text: { type: 'string', maxLength: 24, example: 'Hunter' } } } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Saved; answers every category' } */
|
||||
/* #swagger.responses[400] = { description: 'The title is too long, or only markup' } */
|
||||
/* #swagger.responses[404] = { description: 'No such category' } */
|
||||
requireRole('admin'),
|
||||
param('stat').isString().isLength({ min: 1, max: 16 }),
|
||||
body('text').optional({ nullable: true }).isString().withMessage('text is a string'),
|
||||
validate,
|
||||
admin.putTitleCategory,
|
||||
)
|
||||
|
||||
adminRustRouter.get(
|
||||
'/servers/:id/integrations',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
|
||||
Reference in New Issue
Block a user