feat(rust): the kit weekend, linked rewards, reward messages and the kit warning (protocol 14)
PLAN_REDESIGNS §2, the Rust half (D164, D192, D332, D333):
- rust.template.kit_weekend (core registerEventTemplates, MODULE_API 1.13.0):
inputs kit/minutes/message/delivery; one phase advance after the weekend,
core.lease rust.group.permission default/<kit permission> + rust.kit.entitle
linked. Steps only where the kit can use them; validated by the real core.
- rust.kit.entitle: recipients 'linked' (every linked player who has played
on the server, no tally, refused past the bound), optional message ({kit})
and delivery, stored per row (rust_perm_run_grants.notice*).
- perm.sync notices: the untold set to the picked account, keyed
run:step:user, in the digest; the report's noticesTold marks them told.
- rust.options.grouppermissions notes a kit's permission rows via kits.uses
(cached a minute per server; a failure is no note).
- duration: 'minutes' on rust.zone.open and rust.participation.open; the
group-permission lease's value example is true.
- protocol 13 -> 14, coreApi ^1.13.0; ci/core-ref.json pinned to website#212's
head while it is open; engagement-triggers.json regenerated (coreApi line).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -100,7 +100,7 @@ function pluginError(data, fallback) {
|
||||
* of them to forget the drift check, which is the one thing §F says a lease must
|
||||
* not be allowed to skip.
|
||||
*/
|
||||
function lease({ id, label, description, type, min, max, family, targetLabel, source, example, wire }) {
|
||||
function lease({ id, label, description, type, min, max, family, targetLabel, source, example, valueExample, wire }) {
|
||||
async function resolve(target) {
|
||||
const { serverId, rest } = splitTarget(target)
|
||||
const found = await serverFor(serverId)
|
||||
@@ -130,6 +130,8 @@ function lease({ id, label, description, type, min, max, family, targetLabel, so
|
||||
type,
|
||||
...(min === undefined ? {} : { min }),
|
||||
...(max === undefined ? {} : { max }),
|
||||
// The VALUE's example (core MODULE_API 1.13.0), beside the target's below.
|
||||
...(valueExample === undefined ? {} : { example: valueExample }),
|
||||
maxDurationMs: MAX_LEASE_MS,
|
||||
target: { label: targetLabel, source, example },
|
||||
|
||||
@@ -322,6 +324,7 @@ const LEASES = [
|
||||
targetLabel: 'Which server, group and permission',
|
||||
source: 'rust.options.grouppermissions',
|
||||
example: 'main/default/kits.vip',
|
||||
valueExample: true,
|
||||
wire: (rest) => {
|
||||
const slash = rest.lastIndexOf('/')
|
||||
if (slash <= 0 || slash >= rest.length - 1) {
|
||||
@@ -371,6 +374,80 @@ async function familyOptions(family, sourceId) {
|
||||
return bounded(rows, sourceId)
|
||||
}
|
||||
|
||||
// ── The kit warning (PLAN_REDESIGNS.md §2.6, protocol 14) ────────────────────
|
||||
//
|
||||
// "Kit vip: 2 uses, 24 h cooldown — 9 of 41 linked players have used it up."
|
||||
// Shown under a group-permission target that is a kit's permission, which is
|
||||
// what a kit weekend borrows: it tells the author that the weekend's extra use
|
||||
// (step 2) is what lets those players take the kit again.
|
||||
|
||||
/** How long one server's kit counts are reused. The editor asks while typing. */
|
||||
const KIT_NOTE_TTL_MS = 60_000
|
||||
|
||||
/** The most kits one server's notes ask about. One `kits.uses` each. */
|
||||
const KIT_NOTE_MAX_KITS = 25
|
||||
|
||||
const kitNoteCache = new Map()
|
||||
|
||||
/** "24 h", "90 min", "45 s". */
|
||||
function durationWords(seconds) {
|
||||
const s = Math.max(0, Math.round(Number(seconds) || 0))
|
||||
if (s >= 3600 && s % 3600 === 0) return `${s / 3600} h`
|
||||
if (s >= 60) return `${Math.round(s / 60)} min`
|
||||
return `${s} s`
|
||||
}
|
||||
|
||||
/** One kit's note, from its `kits.list` row and its `kits.uses` answer. */
|
||||
function kitNote(kit, uses) {
|
||||
const limit = Number(kit.max) > 0 ? `${kit.max} use${Number(kit.max) === 1 ? '' : 's'}` : 'no use limit'
|
||||
const cooldown = Number(kit.cooldown) > 0 ? `, ${durationWords(kit.cooldown)} cooldown` : ''
|
||||
const asked = Number(uses.asked) || 0
|
||||
if (!asked) return `Kit ${kit.name}: ${limit}${cooldown} — no linked player has played on this server yet.`
|
||||
const used = Number(kit.max) > 0
|
||||
? `${Number(uses.exhausted) || 0} of ${asked} linked players have used it up`
|
||||
: `${asked} linked players`
|
||||
const waiting = Number(uses.onCooldown) > 0 ? `, ${uses.onCooldown} are on its cooldown` : ''
|
||||
return `Kit ${kit.name}: ${limit}${cooldown} — ${used}${waiting}. A kit weekend's extra use is what lets them take it again.`
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission → note, for the kits on one server that a permission gates.
|
||||
* Cached a minute per server; anything that fails is simply no note — a warning
|
||||
* is advice, and its absence must never break the field it advises.
|
||||
*/
|
||||
async function kitNotes(server) {
|
||||
const cached = kitNoteCache.get(server.id)
|
||||
if (cached && Date.now() - cached.at < KIT_NOTE_TTL_MS) return cached.notes
|
||||
|
||||
const notes = new Map()
|
||||
try {
|
||||
const listed = await client.kits(server)
|
||||
const gated = ((listed.ok && listed.data && listed.data.kits) || [])
|
||||
.filter((k) => k && k.permission)
|
||||
.slice(0, KIT_NOTE_MAX_KITS)
|
||||
|
||||
if (gated.length) {
|
||||
// eslint-disable-next-line global-require
|
||||
const linksDb = require('./model/links/links.db')
|
||||
const steamIds = (await linksDb.linkedOnServer(server.id)).map((r) => String(r.steamId))
|
||||
|
||||
for (const kit of gated) {
|
||||
const uses = await client.kitsUses(server, kit.name, steamIds)
|
||||
if (!uses.ok || !uses.data || uses.data.kind !== 'kits.uses') continue
|
||||
const permission = String(kit.permission).toLowerCase()
|
||||
// Two kits behind one permission: both are named, one after the other.
|
||||
const note = kitNote(kit, uses.data)
|
||||
notes.set(permission, notes.has(permission) ? `${notes.get(permission)} ${note}` : note)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn('kit notes could not be read', { server: server.id, message: err.message })
|
||||
}
|
||||
|
||||
kitNoteCache.set(server.id, { at: Date.now(), notes })
|
||||
return notes
|
||||
}
|
||||
|
||||
const OPTION_SOURCES = [
|
||||
{
|
||||
id: 'rust.options.servers',
|
||||
@@ -411,13 +488,19 @@ const OPTION_SOURCES = [
|
||||
for (const { server, result } of answers) {
|
||||
const data = result.data || {}
|
||||
const perms = (data.permissions || []).map((p) => String(p).toLowerCase())
|
||||
// The kit warning (PLAN_REDESIGNS.md §2.6): a row that grants a kit's
|
||||
// permission says how many of the server's linked players have used it
|
||||
// up. A kit's permission is whatever its operator typed into Kits, not
|
||||
// always `kits.*`, so every server is asked — once a minute (cached).
|
||||
const notes = await kitNotes(server)
|
||||
for (const g of data.groups || []) {
|
||||
const group = g && g.name ? String(g.name).toLowerCase() : null
|
||||
if (!group) continue
|
||||
for (const perm of perms) {
|
||||
const value = `${server.id}/${group}/${perm}`
|
||||
if (term && !value.includes(term)) continue
|
||||
rows.push({ value, label: `${group} · ${perm}`, group: server.name || server.id })
|
||||
const note = notes.get(perm)
|
||||
rows.push({ value, label: `${group} · ${perm}`, group: server.name || server.id, ...(note ? { note } : {}) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -437,4 +520,7 @@ module.exports = {
|
||||
pluginError,
|
||||
perServer,
|
||||
bounded,
|
||||
kitNote,
|
||||
kitNotes,
|
||||
kitNoteCache,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user