// ── What can happen, as core's engagement engine is told it ─────────────── // // The payload contracts behind every notification this module can cause // (MODULE_API.md §2.4, `registerEventTriggers`; PLAN.md §25). A trigger says // what an event IS, what a template may interpolate, and — the part that is a // security boundary — the widest audience a rule on it may EVER be given. // // ── The ceiling is containment, not size ────────────────────────────────── // // `owner` is not a small `staff`, and `staff` does not permit `owner`. For the // raid alert "one person" is the person whose base it was; for a ban it is // nobody outside the staff room. Each ceiling below is chosen against that // lattice and not against a ladder, and core refuses a rule that widens one. // // ── What no variable here carries, on purpose ───────────────────────────── // // • An IP address. The login and ban frames carry one; the triggers do not, // so no template an operator writes can put an address in a mail. The // admin feed still shows it, to staff, where it is useful. // • The raider (D66). The raid alert says what was destroyed, where and // when. Who did it is gameplay intelligence the game does not hand the // victim, and a variable that is not declared cannot be interpolated. // • A Steam id other than the subject's own. // // ── Why `subjectKey` is what it is ──────────────────────────────────────── // // Core's cooldown is per (rule, user, subject, channel). So the subject is the // thing a recipient should hear about once per cooldown: a BUILDING for a raid // (however many walls fall), a SERVER for a broadcast (however often it // bounces), a CLAN for a membership change. A subject that changed every firing // — a boot id, a timestamp — would make every cooldown a no-op. // // ── `version` ────────────────────────────────────────────────────────────── // // The prop-schema version a template records it was authored against. Bump one // on a rename or a type change, never for a label. const ID = 'rust' /** Site-relative paths, built the way the client registers them. */ const PATHS = { servers: `/${ID}`, account: `/player/${ID}`, } // A server id is VARCHAR(64) of the operator's choosing, and a clan's // `externalId` is `::`. Core validates a `url` // variable against a character class with no `:` in it, so every id that goes // into a path is percent-encoded — without it the clan link would be dropped at // emit in production, silently, for every clan there is. const serverPath = (serverId) => `/${ID}/servers/${encodeURIComponent(serverId)}` const leaderboardPath = (serverId) => `${serverPath(serverId)}?tab=leaderboard` const clanPath = (externalId) => `/${ID}/clans/${encodeURIComponent(externalId)}` const V1 = 1 // ── Shared variables ─────────────────────────────────────────────────────── // **Every trigger carries its own headline.** Most rules here point at core's // generic `notify.event` / `inapp.event`, and core's structural projection fills // `title` and `intro` from the trigger's LABEL and DESCRIPTION only when the // payload does not define them — "the payload wins, the projection fills gaps" // (ENGAGEMENT.md §4.6.1). Without these two, the phase-10 walk rendered a // multi-server site's notice as "A server came online. A server's game // started…" — true, and useless, because it never said which. The emitter // writes the sentence (`emit.js` `headline`); an operator's own template can // still ignore it and interpolate the parts. const HEADLINE = [ { name: 'title', type: 'string', required: false, example: 'Main is back online', description: 'A one-line headline naming what happened and where. Core generic bodies use it as the title.' }, { name: 'intro', type: 'string', required: false, example: 'Main is back up and taking players.', description: 'One sentence of detail. Core generic bodies use it as the body.' }, ] const SERVER = [ { name: 'serverId', type: 'string', required: true, example: 'main', description: 'The server the event happened on, as configured in Admin -> Rust. Also the cooldown subject for broadcasts.' }, { name: 'server', type: 'string', required: true, example: 'Runic Gateway | Main', description: 'The server\'s display name.' }, { name: 'serverUrl', type: 'url', required: false, example: '/rust/servers/main', description: 'Site-relative path to the server\'s page.' }, ] const CLAN = [ { name: 'clanKey', type: 'string', required: true, example: 'main:12:1790142840000', description: 'The clan\'s stable identity. The cooldown subject; not meant for display.' }, { name: 'clan', type: 'string', required: true, example: 'The Rust Belt', description: 'The clan\'s name.' }, { name: 'clanUrl', type: 'url', required: false, example: '/rust/clans/main%3A12%3A1790142840000', description: 'Site-relative path to the clan\'s page.' }, ] // ── The raid alert ───────────────────────────────────────────────────────── const RAID = { id: 'rust.base.destroyed', label: 'Your base was raided', description: 'Part of a base you are authorised on was destroyed by another player: a wall, a door, ' + 'an external wall or gate, or the tool cupboard.', kind: 'event', // One per base per cooldown, however many walls fall. The building is the // tool cupboard's id — the game's own answer to "which base is this". subjectKey: 'building', // One emit per authorised, linked person, each with `ownerUserId` set (D59). // `owner` is the ceiling AND the default: there is nobody else this may reach. audience: 'owner', ceiling: 'owner', version: V1, variables: [ ...SERVER, ...HEADLINE, { name: 'building', type: 'string', required: true, example: '8113', description: 'The base, as the id of its tool cupboard. The cooldown subject.' }, { name: 'structure', type: 'string', required: true, example: 'door', description: 'What was destroyed: "building block", "door", "external wall" or "tool cupboard".' }, { name: 'grid', type: 'string', required: false, example: 'H7', description: 'The map grid square. Absent when the server could not work one out.' }, // A FRAGMENT, for use inside a sentence. An unset optional interpolates to // the empty string, so "your door in {{grid}} was destroyed" reads "your // door in was destroyed" when the grid is unknown; this carries its own // leading space and vanishes cleanly instead. { name: 'atGrid', type: 'string', required: false, example: ' in H7', description: 'Sentence fragment: " in H7" with its own leading space, or nothing when the grid is unknown.' }, { name: 'ownerOnline', type: 'boolean', required: true, example: false, description: 'Whether YOU were online when it happened. The seeded rule alerts only when this is false.' }, ], } // ── Server lifecycle ─────────────────────────────────────────────────────── // // `everyone` because a server being up is what a server page already says to // anyone. The DEFAULT is `subscribers` — the people who asked — and an operator // widens deliberately. const BROADCASTS = [ { id: 'rust.wipe.started', label: 'A server wiped', description: 'A server started a new wipe: a fresh map, and everything built on the old one gone.', kind: 'event', subjectKey: 'serverId', audience: 'subscribers', ceiling: 'everyone', version: V1, variables: [ ...SERVER, ...HEADLINE, { name: 'wipeId', type: 'string', required: true, example: '1790142840-3000-1234', description: 'The new wipe\'s identity.' }, ], }, { id: 'rust.server.online', label: 'A server came online', description: 'A server\'s game started, or came back after being unreachable.', kind: 'event', subjectKey: 'serverId', audience: 'subscribers', ceiling: 'everyone', version: V1, variables: [...SERVER, ...HEADLINE], }, { id: 'rust.server.offline', label: 'A server went offline', description: 'A server\'s game stopped, crashed, or stopped talking to the website.', kind: 'event', subjectKey: 'serverId', audience: 'subscribers', ceiling: 'everyone', version: V1, variables: [...SERVER, ...HEADLINE], }, { id: 'rust.leaderboard.topped', label: 'A new kills leader', description: 'Somebody new leads the current wipe\'s kills on a server.', kind: 'event', subjectKey: 'serverId', audience: 'subscribers', ceiling: 'everyone', version: V1, variables: [ ...SERVER, ...HEADLINE, { name: 'leader', type: 'string', required: true, example: 'Marisol', description: 'The new leader\'s in-game name.' }, { name: 'kills', type: 'int', required: true, example: 42, description: 'Their kills this wipe.' }, { name: 'leaderboardUrl', type: 'url', required: false, example: '/rust/servers/main?tab=leaderboard', description: 'Site-relative path to the server\'s leaderboard.' }, ], }, ] // ── The player's own account ─────────────────────────────────────────────── const ACCOUNT = { id: 'rust.player.linked', label: 'A Steam account was linked', description: 'A Steam account was linked to your website account with an in-game code.', kind: 'event', subjectKey: 'steamId', // PLAN.md §10 said `self`; core has no such ceiling (§25.1). `owner` with the // linking user as `ownerUserId` is the value that exists and means the same. audience: 'owner', ceiling: 'owner', version: V1, variables: [ ...HEADLINE, { name: 'steamId', type: 'string', required: true, example: '76561198000000001', description: 'The Steam account that was linked. Also the cooldown subject.' }, { name: 'player', type: 'string', required: false, example: 'Marisol', description: 'The in-game name the game reported when it was linked.' }, { name: 'accountUrl', type: 'url', required: false, example: '/player/rust', description: 'Site-relative path to your Rust account page.' }, ], } // ── 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 // travel on the envelope as `recipientUserIds`, because "the clan this was // about" is a different answer every firing and cannot be a saved audience. // // No `rust.clan.member.added`: core already fires `team.member.joined` for our // clans through the Team sync, and a second trigger would notify twice (D64). const CLANS = [ { id: 'rust.clan.member.left', label: 'Someone left your clan', description: 'A member left a clan you are in.', kind: 'event', subjectKey: 'clanKey', audience: 'members', ceiling: 'members', version: V1, variables: [ ...CLAN, ...SERVER, ...HEADLINE, { name: 'member', type: 'string', required: false, example: 'Darrow', description: 'Who left.' }, ], }, { id: 'rust.clan.member.kicked', label: 'Someone was removed from your clan', description: 'A member was removed from a clan you are in — or you were.', kind: 'event', subjectKey: 'clanKey', audience: 'members', ceiling: 'members', version: V1, variables: [ ...CLAN, ...SERVER, ...HEADLINE, { name: 'member', type: 'string', required: false, example: 'Darrow', description: 'Who was removed.' }, { name: 'by', type: 'string', required: false, example: 'Marisol', description: 'Who removed them.' }, ], }, { id: 'rust.clan.disbanded', label: 'Your clan was disbanded', description: 'A clan you were in was disbanded.', kind: 'event', subjectKey: 'clanKey', audience: 'members', ceiling: 'members', version: V1, variables: [ ...CLAN, ...SERVER, ...HEADLINE, { name: 'by', type: 'string', required: false, example: 'Marisol', description: 'Who disbanded it.' }, ], }, ] // ── Moderation — staff, and never wider ──────────────────────────────────── const MODERATION = [ { id: 'rust.player.reported', label: 'A player was reported', description: 'A player filed an in-game report against another.', kind: 'event', subjectKey: 'steamId', audience: 'staff', ceiling: 'staff', version: V1, variables: [ ...SERVER, ...HEADLINE, { name: 'steamId', type: 'string', required: true, example: '76561198000000002', description: 'The reported player\'s Steam id. The cooldown subject.' }, { name: 'player', type: 'string', required: false, example: 'Darrow', description: 'The reported player\'s name.' }, { name: 'reporter', type: 'string', required: false, example: 'Marisol', description: 'Who filed the report.' }, { name: 'reportType', type: 'string', required: false, example: 'cheat', description: 'The category the reporter chose.' }, { name: 'topic', type: 'string', required: false, example: 'Aimbot at the dome', description: 'The report\'s subject line.' }, { name: 'message', type: 'string', required: false, example: 'Headshots through two walls.', description: 'The report\'s text.' }, ], }, { id: 'rust.player.banned', label: 'A player was banned', description: 'A player was banned on a server.', kind: 'event', subjectKey: 'steamId', audience: 'staff', ceiling: 'staff', version: V1, variables: [ ...SERVER, ...HEADLINE, { name: 'steamId', type: 'string', required: true, example: '76561198000000002', description: 'The banned player\'s Steam id. The cooldown subject.' }, { name: 'player', type: 'string', required: false, example: 'Darrow', description: 'The banned player\'s name.' }, { name: 'reason', type: 'string', required: false, example: 'Cheating', description: 'The reason given.' }, ], }, { id: 'rust.player.unbanned', label: 'A player was unbanned', description: 'A ban on a server was lifted.', kind: 'event', subjectKey: 'steamId', audience: 'staff', ceiling: 'staff', version: V1, variables: [ ...SERVER, ...HEADLINE, { name: 'steamId', type: 'string', required: true, example: '76561198000000002', description: 'The player\'s Steam id. The cooldown subject.' }, { name: 'player', type: 'string', required: false, example: 'Darrow', description: 'The player\'s name.' }, ], }, { id: 'rust.login.denied', label: 'A login was not approved', description: 'Somebody tried to join a server and was not let in within a minute: a ban, a failed ' + 'authentication, or a player who gave up while connecting.', kind: 'event', subjectKey: 'steamId', audience: 'staff', ceiling: 'staff', version: V1, variables: [ ...SERVER, ...HEADLINE, { name: 'steamId', type: 'string', required: true, example: '76561198000000002', description: 'The Steam id that tried to connect. The cooldown subject.' }, { name: 'player', type: 'string', required: false, example: 'Darrow', description: 'The name it connected with.' }, { name: 'attemptedAt', type: 'datetime', required: true, example: '2026-09-23T03:10:00Z', description: 'When the attempt was made.' }, ], }, ] // ── RunicNPC (docs/runicnpc/PLAN.md stage 4) ─────────────────────────────── // // What an event's phase waits on: "8 guards died" (waves) and "the boss fell // below 50%". Core counts a gate's firings from phase entry and does not know // which run an NPC belongs to, so each firing says its profile, whether an event // placed it and which run did; a gate's `where` names what it waits for. A // profile only events place is the plain way to count only an event's NPCs. // // `staff`, both halves: a death names who killed it. A boss announced to // players is stage 6's, with its own trigger. const NPC_VARS = [ { name: 'profile', type: 'string', required: true, example: 'guard', description: 'The RunicNPC profile the NPC was made from. The cooldown subject, and what a phase gate usually names.' }, { name: 'npc', type: 'string', required: false, example: 'Gate Guard', description: 'The NPC\'s own name, as the victim\'s death screen shows it.' }, { name: 'byEvent', type: 'boolean', required: true, example: true, description: 'Whether an event placed it (rather than an admin\'s placement or another plugin).' }, { name: 'runId', type: 'string', required: false, example: '42', description: 'The event run that placed it, when one did.' }, { name: 'placement', type: 'string', required: false, example: 'guard-3', description: 'The placement it came from, when it came from one.' }, ] const NPCS = [ { id: 'rust.npc.died', label: 'A RunicNPC NPC died', description: 'One of RunicNPC\'s NPCs was killed. A phase can wait for a number of them: "8 guards died".', kind: 'event', subjectKey: 'profile', audience: 'staff', ceiling: 'staff', version: V1, variables: [ ...SERVER, ...HEADLINE, ...NPC_VARS, { name: 'killer', type: 'string', required: false, example: 'Marisol', description: 'The player who landed the killing blow. Absent when no player did.' }, { name: 'killerSteamId', type: 'string', required: false, example: '76561198000000002', description: 'Their Steam id.' }, { name: 'contributors', type: 'int', required: true, example: 3, description: 'How many players took health from it, the killer included.' }, ], }, { id: 'rust.npc.health', label: 'A RunicNPC NPC fell to a health threshold', description: 'One of RunicNPC\'s NPCs fell to a fraction of its health its profile names. A phase can wait for "the boss below 50%".', kind: 'event', subjectKey: 'profile', audience: 'staff', ceiling: 'staff', version: V1, variables: [ ...SERVER, ...HEADLINE, ...NPC_VARS, { name: 'percent', type: 'int', required: true, example: 50, description: 'The threshold it fell to, as a percentage of its health: 50 for half.' }, ], }, ] // ── A RunicNPC boss (docs/runicnpc/PLAN.md stage 6, D277) ─────────────────── // // Public, unlike the two above: a boss rising, changing phase and falling is // server news, as a wipe is. `everyone` is the ceiling and `subscribers` the // default, as for the broadcasts. The game also says these lines in chat; these // are for the app, the in-app inbox and email. Discord hears a boss only through // an event's own announce step (D277), so core does not change. // // No Steam id is declared, the killer's included: a public notice names the // killer by their in-game name only. const BOSS_VARS = [ { name: 'profile', type: 'string', required: true, example: 'juggernaut', description: 'The RunicNPC profile the boss was made from.' }, { name: 'boss', type: 'string', required: true, example: 'Juggernaut', description: 'The boss\'s own name, as players see it.' }, { name: 'byEvent', type: 'boolean', required: true, example: true, description: 'Whether an event placed it.' }, ] const BOSSES = [ { id: 'rust.boss.spawned', label: 'A boss appeared', description: 'A RunicNPC boss spawned on a server: an admin\'s placement, an event, or another plugin.', kind: 'event', // One per kind of boss per cooldown, however often it respawns. subjectKey: 'profile', audience: 'subscribers', ceiling: 'everyone', version: V1, variables: [...SERVER, ...HEADLINE, ...BOSS_VARS], }, { id: 'rust.boss.phase', label: 'A boss changed phase', description: 'A RunicNPC boss fell to one of its phases\' health thresholds and changed: stronger, faster, or calling for help.', kind: 'event', // A profile's phase: phase 2 is not swallowed by phase 1's cooldown. subjectKey: 'phaseKey', audience: 'subscribers', ceiling: 'everyone', version: V1, variables: [ ...SERVER, ...HEADLINE, ...BOSS_VARS, { name: 'phaseKey', type: 'string', required: true, example: 'juggernaut:2', description: 'The profile and the phase. The cooldown subject; not meant for display.' }, { name: 'phase', type: 'int', required: true, example: 2, description: 'Which phase it entered, counting from 1.' }, { name: 'percent', type: 'int', required: true, example: 25, description: 'The health it fell to, as a percentage.' }, ], }, { id: 'rust.boss.killed', label: 'A boss was killed', description: 'Players killed a RunicNPC boss on a server.', kind: 'event', subjectKey: 'profile', audience: 'subscribers', ceiling: 'everyone', version: V1, variables: [ ...SERVER, ...HEADLINE, ...BOSS_VARS, { name: 'killer', type: 'string', required: false, example: 'Marisol', description: 'The player who landed the killing blow, by in-game name. Absent when no player did.' }, { name: 'damagers', type: 'int', required: true, example: 5, description: 'How many players hurt it, the killer included.' }, ], }, ] // ── Rust's own world events (protocol 14, PLAN_REDESIGNS §14.3.1, D355) ───── // // One trigger per `world.event` type, `rust.world.`. Public news, as a // boss is: `everyone` is the ceiling and `subscribers` the default. A phase // gate reads the same catalogue, so "advance when Bradley is destroyed" is // `{ on: 'rust.world.bradley.destroyed' }` with no further code. // // The cooldown subject is the server and the type, and the crate types add // where the crate is — one line per oil rig per cooldown, not one per hack on // the map. `player` is the in-game name only, as a boss's killer is; no Steam // id is declared. const WORLD_SUBJECT = { name: 'subject', type: 'string', required: true, example: 'main:heli.downed', description: 'The server and the event (and, for a crate, where it is). The cooldown subject; not meant for display.' } const WORLD_GRID = { name: 'grid', type: 'string', required: false, example: 'G7', description: 'The map grid square. Absent when the server has no map geometry yet, or the event is off the map.' } const WORLD_PLAYER = { name: 'player', type: 'string', required: false, example: 'Marisol', description: 'The player it names, by in-game name. Absent when nobody is named.' } const WORLD_CREDITED = [ { name: 'player', type: 'string', required: false, example: 'Marisol and Darrow', description: 'Who is credited, by in-game name and by the server\'s credit rule: "Alice", "Alice and Bob", "Alice, Bob and 2 others". Absent when nobody is.' }, { name: 'credited', type: 'int', required: true, example: 2, description: 'How many players the credit rule names.' }, ] const WORLD_WHERE = (example, list) => ({ name: 'where', type: 'string', required: true, example, description: `Where it is: ${list}.` }) const CRATE_WHERE = WORLD_WHERE('oilrig_large', 'oilrig_large, oilrig_small, cargo, ghostship or chinook (a crate a Chinook dropped)') const WORLD_HARBOUR = { name: 'harbour', type: 'string', required: false, example: 'Harbor', description: 'The harbour, by the game\'s own name for it.' } const WORLD_MONUMENT = { name: 'monument', type: 'string', required: false, example: 'Water Treatment Plant', description: 'The monument it dropped at, by the game\'s own name for it.' } const WORLD_COUNT = { name: 'count', type: 'int', required: true, example: 12, description: 'How many: road Bradleys come several at once.' } /** Each world event's label, description and the variables it carries beyond the server's. */ const WORLD_EVENTS = [ ['heli.arrived', 'A patrol helicopter arrived', 'A patrol helicopter flew onto the map.', [WORLD_GRID]], ['heli.left', 'The patrol helicopter left', 'A patrol helicopter left the map without being downed.', [WORLD_GRID]], ['heli.downed', 'A patrol helicopter was downed', 'Players brought down a patrol helicopter.', [WORLD_GRID, ...WORLD_CREDITED]], ['bradley.spawned', 'Bradley is out', 'Launch Site\'s Bradley spawned, or road Bradleys took to the roads.', [WORLD_WHERE('road', 'launch_site or road'), WORLD_COUNT]], ['bradley.destroyed', 'A Bradley was destroyed', 'Players destroyed a Bradley APC: Launch Site\'s, a road one, or another.', [WORLD_WHERE('launch_site', 'launch_site, road or other'), WORLD_GRID, ...WORLD_CREDITED]], ['cargo.arrived', 'The cargo ship arrived', 'The cargo ship sailed onto the map.', [WORLD_GRID]], ['cargo.docking', 'The cargo ship is docking', 'The cargo ship turned in to dock at a harbour.', [WORLD_GRID, WORLD_HARBOUR]], ['cargo.docked', 'The cargo ship docked', 'The cargo ship docked at a harbour, with fresh crates.', [WORLD_GRID, WORLD_HARBOUR]], ['cargo.undocked', 'The cargo ship cast off', 'The cargo ship left the harbour it was docked at.', [WORLD_GRID, WORLD_HARBOUR]], ['cargo.left', 'The cargo ship is leaving', 'The cargo ship turned for the edge of the map.', [WORLD_GRID]], ['chinook.arrived', 'A Chinook arrived', 'A Chinook flew onto the map with a locked crate.', [WORLD_GRID]], ['chinook.dropped', 'A Chinook dropped its crate', 'A Chinook dropped a locked crate at a monument.', [WORLD_GRID, WORLD_MONUMENT]], ['chinook.downed', 'A Chinook was shot down', 'Players shot down a Chinook.', [WORLD_GRID, ...WORLD_CREDITED]], ['airdrop.called', 'An airdrop is on its way', 'A cargo plane is bringing an airdrop: called by a supply signal, or by the schedule.', [WORLD_GRID, WORLD_PLAYER]], ['airdrop.landed', 'An airdrop landed', 'An airdrop reached the ground.', [WORLD_GRID, WORLD_PLAYER]], ['airdrop.looted', 'An airdrop was looted', 'Somebody opened an airdrop first.', [WORLD_GRID, WORLD_PLAYER]], ['crate.hack.started', 'A locked crate is being hacked', 'Somebody started hacking a locked crate. On an oil rig this also calls its scientists.', [CRATE_WHERE, WORLD_GRID, WORLD_PLAYER]], ['crate.unlocked', 'A locked crate unlocked', 'A hacked locked crate finished unlocking.', [CRATE_WHERE, WORLD_GRID, WORLD_PLAYER]], ['crate.looted', 'A locked crate was looted', 'Somebody opened an unlocked locked crate first.', [CRATE_WHERE, WORLD_GRID, WORLD_PLAYER]], ['vendor.arrived', 'The Travelling Vendor arrived', 'The Travelling Vendor drove onto the map.', [WORLD_GRID]], ['vendor.left', 'The Travelling Vendor left', 'The Travelling Vendor left the map.', [WORLD_GRID]], ['satellite.crashed', 'A satellite crashed', 'A satellite came down, and its crates are burning.', [WORLD_GRID]], ['deepsea.opened', 'The Deep Sea opened', 'The Deep Sea is open: its ghost ships and islands can be reached.', []], ['deepsea.closed', 'The Deep Sea closed', 'The Deep Sea closed.', []], ] const WORLD = WORLD_EVENTS.map(([event, label, description, vars]) => ({ id: `rust.world.${event}`, label, description, kind: 'event', subjectKey: 'subject', audience: 'subscribers', ceiling: 'everyone', version: V1, variables: [...SERVER, ...HEADLINE, WORLD_SUBJECT, ...vars], })) // ── Dangerous Treasures (protocol 14, PLAN_REDESIGNS §14.3.4, §14.3.9) ────── // // A treasure event opening and being won are triggers, as a world event is // (D355): public news, `everyone` the ceiling and `subscribers` the default. // The cooldown subject is the server, so a busy hour of treasures is one line // per cooldown. `player` is the winner's in-game name only; no Steam id is // declared. Its ending is staff's on the feed and raises nothing. const DT_SUBJECT = { name: 'subject', type: 'string', required: true, example: 'main:dt.won', description: 'The server and the event. The cooldown subject; not meant for display.' } const DT_BY_EVENT = { name: 'byEvent', type: 'boolean', required: true, example: false, description: 'Whether one of this site\'s events started it (a Start a Dangerous Treasures event step).' } const DANGEROUS_TREASURES = [ { id: 'rust.dt.started', label: 'A treasure event started', description: 'Dangerous Treasures opened a guarded, burning treasure chest on a server: on its own schedule, by an admin, or by an event.', kind: 'event', subjectKey: 'subject', audience: 'subscribers', ceiling: 'everyone', version: V1, variables: [...SERVER, ...HEADLINE, DT_SUBJECT, WORLD_GRID, DT_BY_EVENT], }, { id: 'rust.dt.won', label: 'A treasure was won', description: 'A player emptied a Dangerous Treasures chest on a server.', kind: 'event', subjectKey: 'subject', audience: 'subscribers', ceiling: 'everyone', version: V1, variables: [ ...SERVER, ...HEADLINE, DT_SUBJECT, WORLD_GRID, { name: 'player', type: 'string', required: false, example: 'Marisol', description: 'Who emptied the chest, by in-game name.' }, { name: 'invaders', type: 'int', required: true, example: 3, description: 'How many players fought for it, the winner included.' }, ], }, ] // ── Guarded Crate (protocol 14, PLAN_REDESIGNS §14.3.5, §14.3.9) ──────────── // // A guarded crate landing and being cleared are triggers, as Dangerous // Treasures' are: public news, the server the cooldown subject. `player` is the // clearer's in-game name only. A crate whose guards went with nobody killing // the last (NpcSpawn reloaded under it) and one that timed out raise nothing: // neither is news. const GC_BY_EVENT = { name: 'byEvent', type: 'boolean', required: true, example: false, description: 'Whether one of this site\'s events placed it (a Place a guarded crate step).' } const GUARDED_CRATE = [ { id: 'rust.gc.started', label: 'A guarded crate landed', description: 'Guarded Crate dropped a locked crate ringed by guards on a server: on its own schedule, by an admin, or by an event.', kind: 'event', subjectKey: 'subject', audience: 'subscribers', ceiling: 'everyone', version: V1, variables: [...SERVER, ...HEADLINE, { ...DT_SUBJECT, example: 'main:gc.started' }, WORLD_GRID, GC_BY_EVENT], }, { id: 'rust.gc.ended', label: 'A guarded crate was cleared', description: 'A player killed the last guard of a guarded crate on a server.', kind: 'event', subjectKey: 'subject', audience: 'subscribers', ceiling: 'everyone', version: V1, variables: [ ...SERVER, ...HEADLINE, { ...DT_SUBJECT, example: 'main:gc.ended' }, WORLD_GRID, { name: 'player', type: 'string', required: true, example: 'Marisol', description: 'Who killed the last guard, by in-game name.' }, ], }, ] const TRIGGERS = Object.freeze([RAID, ...BROADCASTS, ACCOUNT, REWARD, ...CLANS, ...MODERATION, ...NPCS, ...BOSSES, ...WORLD, ...DANGEROUS_TREASURES, ...GUARDED_CRATE]) const TRIGGER_IDS = Object.freeze(Object.fromEntries(TRIGGERS.map((t) => [t.id, t.id]))) module.exports = { TRIGGERS, TRIGGER_IDS, PATHS, serverPath, leaderboardPath, clanPath }