// ── Backpacks, as the site sees it (PLAN_REDESIGNS §11.3.2, D199, D337, D338) ─ // // WhiteThunder's Backpacks is optional (D199): a server reports it in hello's // `integrations.backpacks`, with the sizes its `backpacks.size.N` permissions // offer, and every surface that needs it refuses with a reason where it is // absent. Two readers share this file: // // • the step (`rust.backpack.size`, in eventRewards.js), which leases one of // those sizes for a run (D337) and needs the list to check its param; // • the staff view and the player's own page, which read one backpack live. // // **Live, never stored.** Each view is one `backpacks.read`, and nothing about // a backpack is written to the database. The plugin only reads (R2). const client = require('../../sidecarClient') const db = require('./backpacks.db') const servers = require('../servers/servers.model') const { pluginError, transportError } = require('../../eventLeases') /** The sentence every surface says where a server has no Backpacks (D199). */ const MISSING = 'Backpacks is not installed on this server' /** JSON_EXTRACT comes back as text from the driver; an object passes through. */ function parse(value) { if (value === null || value === undefined) return null if (typeof value === 'object') return value try { return JSON.parse(value) } catch { return null } } /** * A server's Backpacks as its last hello said: `{ loaded: false }`, or loaded * with its version and its sizes, ascending and whole. */ function shape(raw) { const b = parse(raw) if (!b || b.loaded !== true) return { loaded: false, sizes: [] } const sizes = [...new Set((Array.isArray(b.sizes) ? b.sizes : []).map(Number).filter((n) => Number.isInteger(n) && n > 0))] sizes.sort((x, y) => x - y) return { loaded: true, version: typeof b.version === 'string' ? b.version : null, sizes } } /** One server's report. `{ loaded: false }` for a server never heard from. */ async function forServer(serverId) { const row = await db.reportFor(serverId) return shape(row && row.backpacks) } /** Every server's report, keyed by id. */ async function byServer() { const rows = await db.reports() return new Map(rows.map((r) => [r.serverId, shape(r.backpacks)])) } /** * The sizes a step may lease on a server: every size but the smallest, which * is the default everybody holds and so gives nobody anything. */ function leasable(report) { return report && report.loaded ? report.sizes.slice(1) : [] } /** One stack as the page draws it; nested `contents` keep the shape one level deep. */ function stack(raw, nest) { const row = { slot: Number(raw.slot) || 0, shortname: typeof raw.shortname === 'string' ? raw.shortname : null, display: typeof raw.display === 'string' ? raw.display : null, amount: Number(raw.amount) || 0, } if (raw.itemId !== undefined) row.itemId = Number(raw.itemId) if (typeof raw.blueprint === 'string') row.blueprint = raw.blueprint // A skin id is past 2^53 more often than not; it is a label here, never a number to do sums with. if (raw.skin !== undefined && raw.skin !== null) row.skin = String(raw.skin) if (typeof raw.name === 'string' && raw.name) row.name = raw.name if (raw.overflow === true) row.overflow = true if (nest && Array.isArray(raw.contents) && raw.contents.length) row.contents = raw.contents.filter(Boolean).map((c) => stack(c, false)) return row } /** * One player's backpack on one server, read live. * * Resolves `{ ok: true, backpack }` — `{ serverId, server, steamId, exists, * capacity, pageSize, items }`, the last three only when it exists — or * `{ ok: false, status, message, reason? }`: 404 for no such server, 409 for a * server without Backpacks, 503 for one that could not be asked. */ async function read({ serverId, steamId }) { const server = await servers.getForCalling(serverId) if (!server) return { ok: false, status: 404, message: 'No such server' } const name = server.name || server.id const result = await client.backpacksRead(server, String(steamId)) if (!result.ok) return { ok: false, status: 503, message: transportError(server, result, 'backpacks') } const data = result.data || {} if (data.kind === 'backpacks.contents') { const base = { serverId: server.id, server: name, steamId: String(steamId) } if (data.exists !== true) return { ok: true, backpack: { ...base, exists: false } } return { ok: true, backpack: { ...base, exists: true, capacity: Number(data.capacity) || 0, pageSize: Number(data.pageSize) || 48, items: (Array.isArray(data.items) ? data.items : []).filter(Boolean).map((s) => stack(s, true)), }, } } if (data.reason === 'backpacks-missing') return { ok: false, status: 409, reason: data.reason, message: `${MISSING} (${name})` } return { ok: false, status: data.reason === 'bad-id' ? 400 : 502, reason: data.reason || null, message: pluginError(data, `${name} did not answer with a backpack`), } } module.exports = { MISSING, shape, forServer, byServer, leasable, stack, read }