feat(rust): Rust's own world events — feed, credit rule, triggers, Call a world event (PLAN_REDESIGNS §14.3.1)
The site half of stage 1, on protocol 14: - server/worldEvents.js: the 24 types, D372's four credit rules, the grid from the map's geometry (none off the map), and the per-viewer reading. - The feed serves `world.event` (a public kind): an unknown type is staff's; the event and its grid are public; names only for the presence audience (D376); Steam ids and positions only for staff (D353). A downed one carries `credited`/`creditedCount` by the server's rule. Client lines for all 24 types and a World events filter. - rust_servers.credit_rule (blow, last, most, all; default blow), set on the server's settings. Ingest counts heli_kills (patrol helicopters and Chinooks, D379) and apc_kills from `world.event` by that rule, so the line and Rotorbane / Tankbreaker agree. A protocol-13 tally's heliKills/apcKills still add as before. - The tally's cratesHacked / dropsLooted → crates_hacked / drops_looted; title categories Safecracker and Scavenger (D356); leaderboard columns Crates hacked and Drops looted, sortable (D377). - 24 engagement triggers rust.world.<event> (D355): ceiling everyone, cooldown subject the server and event (crates add where), names only. - server/eventWorldCall.js: rust.world.call, "Call a world event" (risk change, reversible none, budget rust.world.calls). A position for an airdrop or a patrol helicopter only; the Chinook takes none (D378). Server 585/585 (22 new in test/worldEvents.test.js), client 84/84, every check; booted against the pinned core (43 triggers, 12 actions, 10 budgets, 20 option sources), routes.manifest unchanged at 89. Engagement manifest and swagger fragment regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -42,6 +42,8 @@ const clansDb = require('../model/clans/clans.db')
|
||||
const eventsDb = require('../model/events/events.db')
|
||||
const linksDb = require('../model/links/links.db')
|
||||
const serversDb = require('../model/servers/servers.db')
|
||||
const mapDb = require('../model/map/map.db')
|
||||
const worldEvents = require('../worldEvents')
|
||||
const { TRIGGER_IDS: T, PATHS, serverPath, leaderboardPath, clanPath } = require('./triggers')
|
||||
|
||||
const log = core.logger('engagement')
|
||||
@@ -186,9 +188,65 @@ const HEADLINES = Object.freeze({
|
||||
}),
|
||||
})
|
||||
|
||||
/** Where a crate is, as a sentence ends: "on Large Oil Rig", "dropped by a Chinook". */
|
||||
const CRATE_PLACES = Object.freeze({
|
||||
oilrig_large: 'on Large Oil Rig',
|
||||
oilrig_small: 'on Small Oil Rig',
|
||||
cargo: 'on the cargo ship',
|
||||
ghostship: 'on a ghost ship',
|
||||
chinook: 'dropped by a Chinook',
|
||||
})
|
||||
|
||||
/**
|
||||
* The headline of a world event (protocol 14), one sentence per type. Plain
|
||||
* register like the rest; a part that is missing drops out rather than leaving
|
||||
* a hole, so "at G7" disappears when the server has no map geometry yet.
|
||||
*/
|
||||
function worldHeadline(event, d) {
|
||||
const at = d.grid ? ` at ${d.grid}` : ''
|
||||
const on = ` on ${d.server}`
|
||||
const by = d.player ? ` by ${d.player}` : ''
|
||||
const place = CRATE_PLACES[d.where] || 'somewhere on the map'
|
||||
const bradley = d.where === 'launch_site' ? "Launch Site's Bradley" : d.where === 'road' ? 'A road Bradley' : 'A Bradley'
|
||||
const lines = {
|
||||
'heli.arrived': ['A patrol helicopter is overhead', `A patrol helicopter flew onto the map${at}${on}.`],
|
||||
'heli.left': ['The patrol helicopter has left', `The patrol helicopter left the map${on}.`],
|
||||
'heli.downed': ['Patrol helicopter downed', `The patrol helicopter was downed${by}${at}${on}.`],
|
||||
'bradley.spawned': [
|
||||
d.where === 'road' ? (d.count > 1 ? `${d.count} road Bradleys are out` : 'A road Bradley is out') : 'Bradley is patrolling Launch Site',
|
||||
d.where === 'road' ? `Road Bradleys took to the roads${on}.` : `Launch Site's Bradley is back${on}.`,
|
||||
],
|
||||
'bradley.destroyed': [`${bradley} was destroyed`, `${bradley} was destroyed${by}${at}${on}.`],
|
||||
'cargo.arrived': ['The cargo ship has arrived', `The cargo ship sailed onto the map${at}${on}.`],
|
||||
'cargo.docking': ['The cargo ship is docking', `The cargo ship is coming in to dock${d.harbour ? ` at ${d.harbour}` : ''}${on}.`],
|
||||
'cargo.docked': ['The cargo ship has docked', `The cargo ship docked${d.harbour ? ` at ${d.harbour}` : ''}${on}, with fresh crates.`],
|
||||
'cargo.undocked': ['The cargo ship cast off', `The cargo ship left${d.harbour ? ` ${d.harbour}` : ' the harbour'}${on}.`],
|
||||
'cargo.left': ['The cargo ship is leaving', `The cargo ship turned for the edge of the map${on}.`],
|
||||
'chinook.arrived': ['A Chinook is inbound', `A Chinook flew onto the map${at}${on} with a locked crate.`],
|
||||
'chinook.dropped': ['A Chinook dropped a locked crate', `A Chinook dropped a locked crate${d.monument ? ` at ${d.monument}` : at}${on}.`],
|
||||
'chinook.downed': ['A Chinook was shot down', `A Chinook was shot down${by}${at}${on}.`],
|
||||
'airdrop.called': ['An airdrop is on its way', `An airdrop is on its way${at}${on}${d.player ? `, called by ${d.player}` : ''}.`],
|
||||
'airdrop.landed': ['An airdrop has landed', `An airdrop landed${at}${on}.`],
|
||||
'airdrop.looted': ['An airdrop was looted', `${d.player || 'Somebody'} looted an airdrop${at}${on}.`],
|
||||
'crate.hack.started': ['A locked crate is being hacked', `${d.player || 'Somebody'} started hacking the locked crate ${place}${on}.`],
|
||||
'crate.unlocked': ['A locked crate is open', `The locked crate ${place} finished unlocking${on}.`],
|
||||
'crate.looted': ['A locked crate was looted', `${d.player || 'Somebody'} looted the locked crate ${place}${on}.`],
|
||||
'vendor.arrived': ['The Travelling Vendor is here', `The Travelling Vendor drove onto the map${at}${on}.`],
|
||||
'vendor.left': ['The Travelling Vendor has left', `The Travelling Vendor left the map${on}.`],
|
||||
'satellite.crashed': ['A satellite has crashed', `A satellite came down${at}${on}. Its crates are burning.`],
|
||||
'deepsea.opened': ['The Deep Sea is open', `The Deep Sea opened${on}.`],
|
||||
'deepsea.closed': ['The Deep Sea has closed', `The Deep Sea closed${on}.`],
|
||||
}
|
||||
const line = lines[event]
|
||||
return line ? { title: `${line[0]}${on}`, intro: line[1] } : {}
|
||||
}
|
||||
|
||||
const WORLD_PREFIX = 'rust.world.'
|
||||
|
||||
function headline(triggerId, data) {
|
||||
const make = HEADLINES[triggerId]
|
||||
return make ? make(data || {}) : {}
|
||||
if (make) return make(data || {})
|
||||
return String(triggerId).startsWith(WORLD_PREFIX) ? worldHeadline(triggerId.slice(WORLD_PREFIX.length), data || {}) : {}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -460,7 +518,51 @@ async function onBoss(server, item, frame) {
|
||||
}) ? 1 : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Rust's own world events (protocol 14, D355): one trigger per type. Public
|
||||
* news, so a replay older than a broadcast's window tells nobody (D63). The
|
||||
* grid comes from the map's geometry, as the feed's does; a downed helicopter,
|
||||
* Chinook or Bradley names whom the server's credit rule names, as its feed
|
||||
* line and its count do (D372). Names only, never a Steam id.
|
||||
*/
|
||||
async function onWorld(server, item, frame) {
|
||||
const t = frameTime(item, frame)
|
||||
const event = str(frame.event)
|
||||
if (!event || !worldEvents.isKnownType(event) || !stillNews(t, BROADCAST_MAX_AGE_MS)) return 0
|
||||
|
||||
const meta = frame.x !== undefined ? await mapDb.getMeta(server.id).catch(() => null) : null
|
||||
const grid = meta ? worldEvents.gridOf({ worldSize: Number(meta.worldSize), gridCells: Number(meta.gridCells), gridCellSize: Number(meta.gridCellSize) }, frame.x, frame.z) : null
|
||||
const type = worldEvents.TYPES[event]
|
||||
const crate = event.startsWith('crate.')
|
||||
|
||||
const data = {
|
||||
...serverVars(server),
|
||||
subject: crate ? `${server.id}:${event}:${frame.where || ''}` : `${server.id}:${event}`,
|
||||
...(grid ? { grid } : {}),
|
||||
...(frame.where !== undefined ? { where: str(frame.where) } : {}),
|
||||
...(event === 'bradley.spawned' ? { count: Math.max(1, Math.floor(Number(frame.count) || 1)) } : {}),
|
||||
...(frame.harbourName || frame.harbour ? { harbour: str(frame.harbourName) || str(frame.harbour) } : {}),
|
||||
...(frame.monumentName || frame.monument ? { monument: str(frame.monumentName) || str(frame.monument) } : {}),
|
||||
}
|
||||
|
||||
if (type.player && str(frame.name)) data.player = str(frame.name)
|
||||
|
||||
if (type.credit) {
|
||||
const ids = worldEvents.credited(frame, worldEvents.creditRuleOf(server.creditRule))
|
||||
const phrase = worldEvents.namesPhrase(ids.map((id) => worldEvents.nameOf(frame, id)), ids.length)
|
||||
data.credited = ids.length
|
||||
if (phrase) data.player = phrase
|
||||
}
|
||||
|
||||
return fire(`${WORLD_PREFIX}${event}`, {
|
||||
data,
|
||||
dedupeKey: dedupeKey('world', server.id, event, t, frame.x, frame.z),
|
||||
occurredAt: t,
|
||||
}) ? 1 : 0
|
||||
}
|
||||
|
||||
const HANDLERS = Object.freeze({
|
||||
'world.event': onWorld,
|
||||
'npc.boss.spawned': onBoss,
|
||||
'npc.boss.phase': onBoss,
|
||||
'npc.boss.died': onBoss,
|
||||
|
||||
Reference in New Issue
Block a user