Merge pull request 'fix(rust): send reward notices, and keep a downed line's credit rule (W21, W4)' (#54) from fix/last-fixes-bugs into edge
Reviewed-on: #54
This commit is contained in:
@@ -72,7 +72,9 @@ const MAX_BATCHES_PER_TICK = 10
|
|||||||
* how to read.
|
* how to read.
|
||||||
*/
|
*/
|
||||||
async function apply(serverId, item, server = null) {
|
async function apply(serverId, item, server = null) {
|
||||||
const frame = (item && item.frame) || {}
|
// A downed helicopter, Chinook or Bradley is stored with the rule that
|
||||||
|
// credits it now, so its line and its counts agree for good (W4).
|
||||||
|
const frame = worldEvents.stampRule((item && item.frame) || {}, server && server.creditRule)
|
||||||
const kind = item.kind || frame.kind
|
const kind = item.kind || frame.kind
|
||||||
const wipeId = frame.wipeId || null
|
const wipeId = frame.wipeId || null
|
||||||
|
|
||||||
@@ -278,8 +280,7 @@ async function apply(serverId, item, server = null) {
|
|||||||
// A protocol-13 plugin still sends `heliKills` / `apcKills` on its tally
|
// A protocol-13 plugin still sends `heliKills` / `apcKills` on its tally
|
||||||
// instead, and the tally path above adds those as it always has.
|
// instead, and the tally path above adds those as it always has.
|
||||||
case 'world.event': {
|
case 'world.event': {
|
||||||
const rule = worldEvents.creditRuleOf(server && server.creditRule)
|
for (const kill of worldEvents.killsOf(frame, worldEvents.ruleOf(frame, server && server.creditRule))) {
|
||||||
for (const kill of worldEvents.killsOf(frame, rule)) {
|
|
||||||
await db.touchPlayer(kill.steamId, kill.name || null)
|
await db.touchPlayer(kill.steamId, kill.name || null)
|
||||||
await db.addTitleStats({ serverId, wipeId, steamId: kill.steamId }, { [kill.field]: 1 })
|
await db.addTitleStats({ serverId, wipeId, steamId: kill.steamId }, { [kill.field]: 1 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -314,13 +314,7 @@ async function syncOne(server, { authored, sync, state, force, policy = 'auto-ad
|
|||||||
held: planned.hold.size,
|
held: planned.hold.size,
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await sidecar.permSync(server, {
|
const result = await sidecar.permSync(server, pushBody(desired.hash, held.payload, pushed, retire))
|
||||||
setId: desired.hash,
|
|
||||||
groups: withExpect(held.payload.groups, pushed),
|
|
||||||
grants: held.payload.grants,
|
|
||||||
credits: held.payload.credits,
|
|
||||||
retire,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
await failed(server, { reason, error: result.status, desiredHash: desired.hash, sync, bootId, wipeId })
|
await failed(server, { reason, error: result.status, desiredHash: desired.hash, sync, bootId, wipeId })
|
||||||
@@ -358,6 +352,23 @@ async function syncOne(server, { authored, sync, state, force, policy = 'auto-ad
|
|||||||
return 'ok'
|
return 'ok'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sync as the wire carries it: every field of the built payload, the
|
||||||
|
* groups with their style `expect`s, and what to retire. `notices` rides here
|
||||||
|
* too (D333) — the push once named its fields one by one and left it out, so
|
||||||
|
* no reward message ever reached the game (W21).
|
||||||
|
*/
|
||||||
|
function pushBody(setId, payload, pushed, retire) {
|
||||||
|
return {
|
||||||
|
setId,
|
||||||
|
groups: withExpect(payload.groups, pushed),
|
||||||
|
grants: payload.grants,
|
||||||
|
credits: payload.credits,
|
||||||
|
notices: payload.notices,
|
||||||
|
retire,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The groups as the wire carries them: each style field with the value this
|
* The groups as the wire carries them: each style field with the value this
|
||||||
* site last pushed to THIS server (`expect`), or null for one it never has.
|
* site last pushed to THIS server (`expect`), or null for one it never has.
|
||||||
@@ -526,6 +537,7 @@ module.exports = {
|
|||||||
syncOne,
|
syncOne,
|
||||||
reasonToSync,
|
reasonToSync,
|
||||||
applyReport,
|
applyReport,
|
||||||
|
pushBody,
|
||||||
withExpect,
|
withExpect,
|
||||||
styleRetirements,
|
styleRetirements,
|
||||||
withLock,
|
withLock,
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ rustRouter.get(
|
|||||||
'/servers/:id/events',
|
'/servers/:id/events',
|
||||||
// #swagger.tags = ['Public · Rust']
|
// #swagger.tags = ['Public · Rust']
|
||||||
// #swagger.summary = 'Recent events on one Rust server'
|
// #swagger.summary = 'Recent events on one Rust server'
|
||||||
// #swagger.description = 'The killfeed and everything else public that happened on a server, newest first. Narrow with `kind` (comma-separated) and `wipe`. Only publicly classified kinds are ever returned — moderation events, login attempts and anything carrying an IP address are stored but never served here. Kinds that name a player who was on the server (connects, respawns, deaths, chat, tallies) are served only to viewers inside the operator’s presence audience, which defaults to staff; `presenceHidden` says when they were withheld. Rust’s own world events (protocol 14, PLAN_REDESIGNS §14.3.1) arrive as `world.event` with `event` naming which of 24: they are public, with a `grid` worked out from the map, but a player’s `name` (and the names in `credited`) only for the presence audience, a Steam id and a position only for staff, and a type this build does not know only for staff. A downed patrol helicopter, Chinook or Bradley carries `credited` and `creditedCount` by the server’s `creditRule`.'
|
// #swagger.description = 'The killfeed and everything else public that happened on a server, newest first. Narrow with `kind` (comma-separated) and `wipe`. Only publicly classified kinds are ever returned — moderation events, login attempts and anything carrying an IP address are stored but never served here. Kinds that name a player who was on the server (connects, respawns, deaths, chat, tallies) are served only to viewers inside the operator’s presence audience, which defaults to staff; `presenceHidden` says when they were withheld. Rust’s own world events (protocol 14, PLAN_REDESIGNS §14.3.1) arrive as `world.event` with `event` naming which of 24: they are public, with a `grid` worked out from the map, but a player’s `name` (and the names in `credited`) only for the presence audience, a Steam id and a position only for staff, and a type this build does not know only for staff. A downed patrol helicopter, Chinook or Bradley carries `credited` and `creditedCount` by the server’s `creditRule` as it was when the event arrived, so a line keeps naming whom the Rotorbane and Tankbreaker counts credited after the rule changes.'
|
||||||
// #swagger.parameters['id'] = { in: 'path', required: true, description: 'The server’s slug', schema: { type: 'string' } }
|
// #swagger.parameters['id'] = { in: 'path', required: true, description: 'The server’s slug', schema: { type: 'string' } }
|
||||||
// #swagger.parameters['kind'] = { in: 'query', required: false, description: 'One kind, or several comma-separated', schema: { type: 'string' } }
|
// #swagger.parameters['kind'] = { in: 'query', required: false, description: 'One kind, or several comma-separated', schema: { type: 'string' } }
|
||||||
// #swagger.parameters['wipe'] = { in: 'query', required: false, description: 'Restrict to one wipe id', schema: { type: 'string' } }
|
// #swagger.parameters['wipe'] = { in: 'query', required: false, description: 'Restrict to one wipe id', schema: { type: 'string' } }
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const rewards = require('../eventRewards')
|
|||||||
const leases = require('../eventLeases')
|
const leases = require('../eventLeases')
|
||||||
const templates = require('../eventTemplates')
|
const templates = require('../eventTemplates')
|
||||||
const model = require('../model/permissions/permissions.model')
|
const model = require('../model/permissions/permissions.model')
|
||||||
|
const reconcile = require('../model/permissions/reconcile')
|
||||||
|
|
||||||
const entitle = rewards.ACTIONS.find((a) => a.id === 'rust.kit.entitle')
|
const entitle = rewards.ACTIONS.find((a) => a.id === 'rust.kit.entitle')
|
||||||
const ok = (data) => ({ ok: true, status: 'ok', data })
|
const ok = (data) => ({ ok: true, status: 'ok', data })
|
||||||
@@ -163,6 +164,19 @@ test('the sync carries each untold message to the account the reward picked', ()
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('the push carries the notices, and every other field the payload built (W21)', () => {
|
||||||
|
const permSync = require('../permSync')
|
||||||
|
const desired = model.buildDesired('main', authored([grantRow({ notice: 'You earned kit vip', noticeDelivery: 'chat' })]))
|
||||||
|
const held = reconcile.withHold(desired, new Set())
|
||||||
|
const body = permSync.pushBody(desired.hash, held.payload, [], [])
|
||||||
|
assert.deepStrictEqual(body.notices, [
|
||||||
|
{ key: '7:3:11', steamId: '76561190000000001', message: 'You earned kit vip', delivery: 'chat' },
|
||||||
|
])
|
||||||
|
// A field the model adds and the push forgets is how W21 happened.
|
||||||
|
for (const field of Object.keys(held.payload)) assert.ok(field in body, `the push drops payload.${field}`)
|
||||||
|
assert.strictEqual(body.setId, desired.hash)
|
||||||
|
})
|
||||||
|
|
||||||
test('a notice moves the digest, so a new one is pushed and a told one is dropped', () => {
|
test('a notice moves the digest, so a new one is pushed and a told one is dropped', () => {
|
||||||
const none = model.buildDesired('main', authored([grantRow({})]))
|
const none = model.buildDesired('main', authored([grantRow({})]))
|
||||||
const one = model.buildDesired('main', authored([grantRow({ notice: 'hi', noticeDelivery: 'chat' })]))
|
const one = model.buildDesired('main', authored([grantRow({ notice: 'hi', noticeDelivery: 'chat' })]))
|
||||||
|
|||||||
@@ -216,6 +216,19 @@ test('a Bradley counts toward Tankbreaker, and an arrival counts nothing', async
|
|||||||
assert.strictEqual(rec2.touching('rust_player_wipe_stats').length, 0)
|
assert.strictEqual(rec2.touching('rust_player_wipe_stats').length, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('a downed one is stored with the rule that credited it; an arrival is stored as sent (W4)', async () => {
|
||||||
|
const { apply } = require('../ingest')
|
||||||
|
const storedRaw = async (frame) => {
|
||||||
|
const rec = withRecorder()
|
||||||
|
await apply('main', { id: 1, kind: 'world.event', frame }, { id: 'main', creditRule: 'all' })
|
||||||
|
const insert = rec.touching('rust_events').find((s) => /INSERT/i.test(s.sql))
|
||||||
|
return JSON.parse(insert.params.find((p) => typeof p === 'string' && p.startsWith('{')))
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.strictEqual((await storedRaw(downed())).creditRule, 'all')
|
||||||
|
assert.strictEqual((await storedRaw(world('heli.arrived', { x: 1, z: 1 }))).creditRule, undefined)
|
||||||
|
})
|
||||||
|
|
||||||
test('the tally’s crates hacked and drops looted are added up (D356)', async () => {
|
test('the tally’s crates hacked and drops looted are added up (D356)', async () => {
|
||||||
const rec = withRecorder()
|
const rec = withRecorder()
|
||||||
const { apply } = require('../ingest')
|
const { apply } = require('../ingest')
|
||||||
@@ -251,6 +264,21 @@ test('the feed reads each world event for its viewer, with the server’s rule a
|
|||||||
assert.deepStrictEqual(member.frame.credited, [{ name: 'Bob' }])
|
assert.deepStrictEqual(member.frame.credited, [{ name: 'Bob' }])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('a line keeps the rule it was stored with after the server’s rule changes (W4)', async () => {
|
||||||
|
const stamped = downed({ creditRule: 'all' })
|
||||||
|
withRecorder((sql) => {
|
||||||
|
if (sql.includes('FROM rust_events')) return [{ id: 9, kind: 'world.event', t: NOW, wipeId: 'w1', steamId: null, raw: JSON.stringify(stamped) }]
|
||||||
|
if (sql.includes('FROM rust_servers')) return [{ id: 'main', name: 'Main', creditRule: 'most', enabled: 1 }]
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
const events = require('../model/events/events.model')
|
||||||
|
|
||||||
|
const [member] = await events.recent({ serverId: 'main', presence: true })
|
||||||
|
assert.strictEqual(member.frame.creditRule, 'all')
|
||||||
|
assert.strictEqual(member.frame.creditedCount, 2)
|
||||||
|
assert.deepStrictEqual(member.frame.credited.map((c) => c.name).sort(), ['Alice', 'Bob'])
|
||||||
|
})
|
||||||
|
|
||||||
// ── Triggers (D355) ────────────────────────────────────────────────────────
|
// ── Triggers (D355) ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
test('one public trigger per type, none of them declaring a Steam id', () => {
|
test('one public trigger per type, none of them declaring a Steam id', () => {
|
||||||
|
|||||||
@@ -178,7 +178,8 @@ function columnName(index) {
|
|||||||
* `staff` the viewer is an admin or a moderator: Steam ids, and a type this
|
* `staff` the viewer is an admin or a moderator: Steam ids, and a type this
|
||||||
* build does not know
|
* build does not know
|
||||||
* `presence` the viewer is in the server's presence audience: names
|
* `presence` the viewer is in the server's presence audience: names
|
||||||
* `rule` the server's credit rule
|
* `rule` the server's credit rule, for a row stored before the frame
|
||||||
|
* carried its own (`stampRule`)
|
||||||
* `geometry` the server's map geometry, for the grid
|
* `geometry` the server's map geometry, for the grid
|
||||||
*
|
*
|
||||||
* What comes back keeps the plugin's own fields for what it describes (`event`,
|
* What comes back keeps the plugin's own fields for what it describes (`event`,
|
||||||
@@ -215,8 +216,8 @@ function shapeFor(frame, { staff = false, presence = false, rule = DEFAULT_CREDI
|
|||||||
|
|
||||||
// Credit, by the server's rule.
|
// Credit, by the server's rule.
|
||||||
if (known && TYPES[event].credit) {
|
if (known && TYPES[event].credit) {
|
||||||
out.creditRule = creditRuleOf(rule)
|
out.creditRule = ruleOf(frame, rule)
|
||||||
const ids = credited(frame, rule)
|
const ids = credited(frame, out.creditRule)
|
||||||
out.creditedCount = ids.length
|
out.creditedCount = ids.length
|
||||||
if (presence || staff) {
|
if (presence || staff) {
|
||||||
out.credited = ids.map((id) => ({
|
out.credited = ids.map((id) => ({
|
||||||
@@ -235,6 +236,25 @@ function shapeFor(frame, { staff = false, presence = false, rule = DEFAULT_CREDI
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The frame as it is stored: a downed helicopter, Chinook or Bradley carries
|
||||||
|
* the rule that credited it when it arrived (`creditRule`), so the feed line
|
||||||
|
* keeps naming who the counts were given to after the server's rule changes
|
||||||
|
* (W4). Every other frame is stored as sent.
|
||||||
|
*/
|
||||||
|
function stampRule(frame, rule) {
|
||||||
|
if (!frame || frame.kind !== 'world.event' || !isKnownType(frame.event) || !TYPES[frame.event].credit) return frame
|
||||||
|
return { ...frame, creditRule: creditRuleOf(rule) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The rule a stored line is read by: its own stamp, or — for a row from before
|
||||||
|
* the stamp — the server's rule now, which is how those rows always read.
|
||||||
|
*/
|
||||||
|
function ruleOf(frame, rule) {
|
||||||
|
return frame && CREDIT_RULE_IDS.includes(frame.creditRule) ? frame.creditRule : creditRuleOf(rule)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What one `world.event` adds to the counts: a credited kill of a helicopter,
|
* What one `world.event` adds to the counts: a credited kill of a helicopter,
|
||||||
* a Chinook or a Bradley, for each player the server's rule names. `[{ steamId,
|
* a Chinook or a Bradley, for each player the server's rule names. `[{ steamId,
|
||||||
@@ -270,6 +290,8 @@ module.exports = {
|
|||||||
creditRuleOf,
|
creditRuleOf,
|
||||||
isKnownType,
|
isKnownType,
|
||||||
credited,
|
credited,
|
||||||
|
stampRule,
|
||||||
|
ruleOf,
|
||||||
nameOf,
|
nameOf,
|
||||||
gridOf,
|
gridOf,
|
||||||
shapeFor,
|
shapeFor,
|
||||||
|
|||||||
@@ -3203,7 +3203,7 @@
|
|||||||
"Public · Rust"
|
"Public · Rust"
|
||||||
],
|
],
|
||||||
"summary": "Recent events on one Rust server",
|
"summary": "Recent events on one Rust server",
|
||||||
"description": "The killfeed and everything else public that happened on a server, newest first. Narrow with `kind` (comma-separated) and `wipe`. Only publicly classified kinds are ever returned — moderation events, login attempts and anything carrying an IP address are stored but never served here. Kinds that name a player who was on the server (connects, respawns, deaths, chat, tallies) are served only to viewers inside the operator’s presence audience, which defaults to staff; `presenceHidden` says when they were withheld. Rust’s own world events (protocol 14, PLAN_REDESIGNS §14.3.1) arrive as `world.event` with `event` naming which of 24: they are public, with a `grid` worked out from the map, but a player’s `name` (and the names in `credited`) only for the presence audience, a Steam id and a position only for staff, and a type this build does not know only for staff. A downed patrol helicopter, Chinook or Bradley carries `credited` and `creditedCount` by the server’s `creditRule`.",
|
"description": "The killfeed and everything else public that happened on a server, newest first. Narrow with `kind` (comma-separated) and `wipe`. Only publicly classified kinds are ever returned — moderation events, login attempts and anything carrying an IP address are stored but never served here. Kinds that name a player who was on the server (connects, respawns, deaths, chat, tallies) are served only to viewers inside the operator’s presence audience, which defaults to staff; `presenceHidden` says when they were withheld. Rust’s own world events (protocol 14, PLAN_REDESIGNS §14.3.1) arrive as `world.event` with `event` naming which of 24: they are public, with a `grid` worked out from the map, but a player’s `name` (and the names in `credited`) only for the presence audience, a Steam id and a position only for staff, and a type this build does not know only for staff. A downed patrol helicopter, Chinook or Bradley carries `credited` and `creditedCount` by the server’s `creditRule` as it was when the event arrived, so a line keeps naming whom the Rotorbane and Tankbreaker counts credited after the rule changes.",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
|
|||||||
Reference in New Issue
Block a user