feat(rust): the rewards — tally, kit reward, chat and the news leg (phase 13b, protocol 10)
Four event verbs and the announce leg, per PLAN.md §29: - rust.participation.open / .collect: the plugin counts who takes part (seconds, kills or both, in a zone this run opened or the whole server) and collect files them as the run's participants, keyed by Steam id. - rust.kit.entitle: the five recipient modes (D101), rows in the new rust_perm_run_grants (D84) unioned into the permission push, one extra use of the kit per reward as site-held credits on perm.sync (D103), and the rust.kit.entitled notice deferred from phase 10 (D64). - rust.announce: one server or every server (D105). - rust.chat announce leg, speaking only on servers whose new news switch is on (D104) - a card on Admin -> Rust visibility (D106). Budgets rust.grants and rust.announcements; the kit source and four fixed-choice sources (core has no enum param type). rust_perm_run_grants carries core's idempotency key so a revert of a lost answer can find its rows. Protocol 10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -129,6 +129,11 @@ const HEADLINES = Object.freeze({
|
||||
intro: `The Steam account ${d.player || d.steamId} was linked with an in-game code. `
|
||||
+ 'If that was not you, unlink it from your Rust account page.',
|
||||
}),
|
||||
'rust.kit.entitled': (d) => ({
|
||||
title: `You earned ${d.kit} on ${d.server}`,
|
||||
intro: `An event on ${d.server} rewarded you: the ${d.kit} kit is waiting in the Kits menu, `
|
||||
+ 'with one extra use. Redeem it in game.',
|
||||
}),
|
||||
'rust.clan.member.left': (d) => ({
|
||||
title: `${d.member || 'A member'} left ${d.clan}`,
|
||||
intro: `${d.member || 'A member'} left ${d.clan} on ${d.server}.`,
|
||||
@@ -546,12 +551,38 @@ function linked({ userId, steamId, name }) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `rust.kit.entitled` for each user one reward step granted (phase 13b). Never
|
||||
* throws: a notification that could not be raised must not fail the grant it
|
||||
* is about. Returns how many were raised.
|
||||
*/
|
||||
function entitled({ userIds, kit, server, mode, runId, stepId }) {
|
||||
let raised = 0
|
||||
try {
|
||||
const rewardKey = `${runId}:${stepId}`
|
||||
for (const userId of new Set(userIds || [])) {
|
||||
const uid = Number(userId)
|
||||
if (!Number.isInteger(uid) || uid < 1) continue
|
||||
const ok = fire(T['rust.kit.entitled'], {
|
||||
data: { rewardKey, kit: String(kit), ...serverVars(server), ...(mode ? { mode: String(mode) } : {}), accountUrl: PATHS.account },
|
||||
ownerUserId: uid,
|
||||
dedupeKey: dedupeKey('entitled', runId, stepId, uid),
|
||||
})
|
||||
if (ok) raised++
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn('could not raise the reward notification', { error: err.message })
|
||||
}
|
||||
return raised
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
onEvent,
|
||||
serverObserved,
|
||||
checkLeader,
|
||||
sweepLoginDenied,
|
||||
linked,
|
||||
entitled,
|
||||
reset,
|
||||
dedupeKey,
|
||||
headline,
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
// ── One rule group per family ─────────────────────────────────────────────
|
||||
//
|
||||
// A group is seeded ONCE (per deployment, per key), so a rule appended to a
|
||||
// group in a later version reaches fresh installs only. Seven families, seven
|
||||
// group in a later version reaches fresh installs only. Eight families, eight
|
||||
// keys: a future raid rule takes `raid-v2` without disturbing anybody's clan
|
||||
// rules. Every rule is disabled — core ignores `enabled` rather than trusting it
|
||||
// — so installing this module mails nobody until an operator decides it should.
|
||||
@@ -234,6 +234,25 @@ const RULE_GROUPS = Object.freeze([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// Its own group, not a rule appended to `account-v1`: a group is seeded once,
|
||||
// so an appended rule would reach fresh installs only (R7).
|
||||
key: 'rewards-v1',
|
||||
note: 'module-rust: an event rewarded you a kit (disabled)',
|
||||
rules: [
|
||||
{
|
||||
trigger_id: 'rust.kit.entitled',
|
||||
name: 'Kit reward earned',
|
||||
audience: 'owner',
|
||||
// Email as well: a reward granted at 03:00 is news the person reads the
|
||||
// next morning, before they are next in game or on the site.
|
||||
channels: ['email', 'inapp'],
|
||||
template_keys: GENERIC,
|
||||
cooldown_seconds: 0,
|
||||
max_sends_per_hour: 200,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'clans-v1',
|
||||
note: 'module-rust: clan departures and disbands (disabled)',
|
||||
|
||||
@@ -218,6 +218,39 @@ const ACCOUNT = {
|
||||
],
|
||||
}
|
||||
|
||||
// ── A reward (phase 13b) ───────────────────────────────────────────────────
|
||||
//
|
||||
// Deferred from phase 10 (D64) to the phase that grants something. Emitted once
|
||||
// per recipient USER when an event's `rust.kit.entitle` writes their rows, with
|
||||
// that user as `ownerUserId` — so, like the link notice, `owner` is both the
|
||||
// ceiling and the only audience there is: a reward is nobody else's news.
|
||||
//
|
||||
// The subject is the run and the step, so one award is one notification however
|
||||
// often a retried step writes the same rows.
|
||||
|
||||
const REWARD = {
|
||||
id: 'rust.kit.entitled',
|
||||
label: 'An event rewarded you a kit',
|
||||
description: 'An event on a Rust server rewarded you: a kit is waiting in the in-game Kits menu, with one extra use.',
|
||||
kind: 'event',
|
||||
subjectKey: 'rewardKey',
|
||||
audience: 'owner',
|
||||
ceiling: 'owner',
|
||||
version: V1,
|
||||
variables: [
|
||||
...HEADLINE,
|
||||
{ name: 'rewardKey', type: 'string', required: true, example: '41:7',
|
||||
description: 'The run and step that awarded it. The cooldown subject; not meant for display.' },
|
||||
{ name: 'kit', type: 'string', required: true, example: 'vip-starter',
|
||||
description: 'The kit name, as the server Kits plugin has it.' },
|
||||
...SERVER,
|
||||
{ name: 'mode', type: 'string', required: false, example: 'top',
|
||||
description: 'How the recipients were chosen: everyone, top, minScore, random or topPercent.' },
|
||||
{ name: 'accountUrl', type: 'url', required: false, example: '/player/rust',
|
||||
description: 'Site-relative path to your Rust account page.' },
|
||||
],
|
||||
}
|
||||
|
||||
// ── Clans ──────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// `members` ceiling — clan membership is the clan's business (D49). Recipients
|
||||
@@ -374,7 +407,7 @@ const MODERATION = [
|
||||
},
|
||||
]
|
||||
|
||||
const TRIGGERS = Object.freeze([RAID, ...BROADCASTS, ACCOUNT, ...CLANS, ...MODERATION])
|
||||
const TRIGGERS = Object.freeze([RAID, ...BROADCASTS, ACCOUNT, REWARD, ...CLANS, ...MODERATION])
|
||||
|
||||
const TRIGGER_IDS = Object.freeze(Object.fromEntries(TRIGGERS.map((t) => [t.id, t.id])))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user