Files
Module-Rust/server/commands/top.command.js
wtclaude 0670341198 feat(rust): slash commands and the next wipe, server half (phase 16)
Five read-only commands registered with api.registerSlashCommands:
/status, /wipe, /top, /online and /clan (D126). Every refusal is private,
and any answer narrower than public (online names, a clan roster) goes
to the caller alone (D127). No command asks a sidecar.

The next wipe (D128, D130): six nullable columns on rust_servers, a pure
nextWipe(row, now) with the zone arithmetic through Intl, computed on
every read. The public server shape gains nextWipe; the admin shape
gains the stored schedule; PUT /admin/rust/servers/:id takes the six
fields and writes them only when wipeRule is present.

server/commands joins ci/bundle.json, which checkBundle caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-25 13:29:59 -05:00

86 lines
3.3 KiB
JavaScript

// ── `/top [stat] [server] [alltime]` — the leaderboard's top ten (D126) ─────
//
// Public at every setting, as on the web: the leaderboard's NAMES are public, and
// only `lastSeen` sits behind presence (a tally refreshes it every minute a player
// is on, so it is the Online tab by another name). This answer never carries a
// `lastSeen`, and never a Steam id — `events.leaderboard` is asked with
// `presence: false`, so the field is not there to leak.
const c = require('./common')
const events = require('../model/events/events.model')
/** Ten rows: a summary a person reads, not a table they scroll (§32.4 reading 2). */
const LIMIT = 10
// `stat`'s choices are fixed, so they are a `choices` list rather than free
// text. The value is the leaderboard's own sort key.
const STATS = {
kills: { sort: 'kills', label: 'kills', value: (r) => r.kills },
deaths: { sort: 'deaths', label: 'deaths', value: (r) => r.deaths },
npckills: { sort: 'npcKills', label: 'NPC kills', value: (r) => r.npcKills },
playtime: { sort: 'playtime', label: 'playtime', value: (r) => r.playtimeSec, format: hours },
}
function hours(sec) {
const h = Math.floor(sec / 3600)
const m = Math.floor((sec % 3600) / 60)
return h ? `${h}h ${m}m` : `${m}m`
}
async function handler({ options }) {
const now = Date.now()
const stat = STATS[(options && options.stat) || 'kills'] || STATS.kills
// A leaderboard is one server's. With several and none named the command asks
// rather than choosing one, privately — the question is for the caller.
const picked = c.pickServer(await c.listServers(now), options && options.server)
const refusal = c.pickRefusal(picked, { needOne: true })
if (refusal) return refusal
const { server } = picked
// The current wipe unless asked for all-time. A server that has not reported
// a wipe yet has nothing to scope to, and all-time is the honest answer.
const allTime = Boolean(options && options.alltime) || !server.wipeId
const rows = await events.leaderboard({
serverId: server.id,
wipeId: allTime ? null : server.wipeId,
sort: stat.sort,
limit: LIMIT,
presence: false,
})
const scope = allTime ? 'all time' : 'this wipe'
const title = `${server.name} — top ${stat.label}, ${scope}`
if (!rows.length) return { title, url: c.serverUrl(server), text: `Nobody is on the board for ${scope} yet.` }
const format = stat.format || String
return {
title,
url: c.serverUrl(server),
text: rows.map((r, i) => `${i + 1}. ${r.name || 'Unknown player'} — ${format(stat.value(r))}`).join('\n'),
}
}
module.exports = {
name: 'top',
description: 'The top ten players on a Rust server by kills, deaths, NPC kills or playtime',
options: [
{
name: 'stat',
type: 'string',
description: 'What to rank by (kills when left out)',
required: false,
choices: [
{ name: 'Kills', value: 'kills' },
{ name: 'Deaths', value: 'deaths' },
{ name: 'NPC kills', value: 'npckills' },
{ name: 'Playtime', value: 'playtime' },
],
},
{ name: 'server', type: 'string', description: 'Server name or id (needed when there are several)', required: false },
{ name: 'alltime', type: 'boolean', description: 'Every wipe rather than the current one', required: false },
],
access: 'everyone',
handler,
}