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:
2026-09-28 23:32:59 -05:00
parent afb16112b0
commit aea94b5d7a
18 changed files with 1150 additions and 65 deletions

View File

@@ -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,