feat(rust): Backpacks — staff and player views, the Bigger backpack step (PLAN_REDESIGNS §11.3)
The site half of §11.3.2, on protocol 14:
- `rust.backpack.size` ("Bigger backpack"): a kit reward with a different
permission (D337) — one rust_perm_run_grants row per website user with
backpacks.size.N, no kit, no credit; the perm mirror pushes it and the run's
revert deletes it. risk change, reversible ledger, a new `rust.backpacks`
budget, rust.kit.entitle's recipient modes including `linked`. Refused with
"Backpacks is not installed on this server" in the dry run too (D199), and a
size the server does not offer is refused by name. Its message is never
silent: empty is the take-back warning (D338).
- `rust.options.backpacksizes`: each server's sizes above its smallest, from
the last hello, grouped by server.
- GET /player/rust/backpacks (own links only, 20 reads a minute) and
GET /admin/users/:id/rust/backpacks (that user's links only). Live, never
stored; a stranger's id is a 404 and never reaches the game.
- Client: a shared Backpack panel (pick server and account, open, read once)
on the account page and the admin user page; the grid draws pages of
pageSize, each slot named by the game (D340, no icons), blueprints by what
they make (D341), overflow marked. Backpacks joins the server's mod line.
Server 548/548 (13 new in test/backpacks.test.js), client 79/79 (5 new),
routes.manifest regenerated against the pinned core (89 routes), 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:
@@ -260,7 +260,21 @@ export function integrationsLine(r) {
|
||||
}
|
||||
const one = (name, x, without) => (x && x.loaded ? `${name} ${x.version || ''} is loaded`.trim() : `${name} is not loaded — ${without}`)
|
||||
const line = `${one('BetterChat', r.integrations.betterChat, 'titles and group styles wait for it')}. ${one('PopupNotifications', r.integrations.popupNotifications, 'a popup is refused, and chat still works')}.`
|
||||
return r.integrations.economics ? `${line} ${economicsLine(r.integrations.economics)}` : line
|
||||
const withEconomics = r.integrations.economics ? `${line} ${economicsLine(r.integrations.economics)}` : line
|
||||
return r.integrations.backpacks ? `${withEconomics} ${backpacksLine(r.integrations.backpacks)}` : withEconomics
|
||||
}
|
||||
|
||||
/**
|
||||
* Backpacks (PLAN_REDESIGNS §11.3.2), from protocol 14. Its sizes are what a
|
||||
* Bigger backpack step may lease (D337), so they are said.
|
||||
*/
|
||||
export function backpacksLine(b) {
|
||||
if (!b.loaded) return 'Backpacks is not loaded — backpack views and bigger-backpack rewards wait for it.'
|
||||
const name = `Backpacks ${b.version || ''}`.trim()
|
||||
const sizes = Array.isArray(b.sizes) ? b.sizes : []
|
||||
return sizes.length > 1
|
||||
? `${name} is loaded — sizes ${sizes.join(', ')}; an event may lend any above ${sizes[0]}.`
|
||||
: `${name} is loaded, but offers no size above its smallest, so an event has nothing bigger to lend.`
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useCallback, useRef, useState } from 'react'
|
||||
import { ago, coins, count, duration } from '../../lib/format.js'
|
||||
import { useAsync } from '../../core.js'
|
||||
import api from '../../api.js'
|
||||
import Backpack from '../../components/Backpack.jsx'
|
||||
|
||||
/** Six lines of furniture the §3.4 kit does not carry, so it is vendored. */
|
||||
function SectionTitle({ children }) {
|
||||
@@ -353,6 +354,27 @@ function EconomicsPanel({ userId }) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Backpacks (PLAN_REDESIGNS §11.3.2): what this person carries, per server and
|
||||
* linked account, read live when staff open it. Read only (R2): staff see the
|
||||
* overflow too, and only the owner's own open moves it (D338).
|
||||
*/
|
||||
function BackpackPanel({ userId, links }) {
|
||||
const { data } = useAsync(() => api.servers.list(), [])
|
||||
const list = (data && data.servers) || []
|
||||
if (!list.length) return null
|
||||
return (
|
||||
<div className="panel" style={{ padding: '14px 16px' }}>
|
||||
<div className="field-label" style={{ marginBottom: 8 }}>Backpack</div>
|
||||
<Backpack
|
||||
servers={list.map((s) => ({ id: s.id, name: s.name }))}
|
||||
accounts={links.map((l) => ({ steamId: l.steamId, name: l.name }))}
|
||||
read={(serverId, steamId) => api.adminUserBackpacks.read(userId, serverId, steamId)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function UserRustSections({ userId }) {
|
||||
// Core's `useAsync` has no refresh, so a counter in the deps is how this
|
||||
// re-reads after its own write (the same shape the player page uses).
|
||||
@@ -405,6 +427,9 @@ export default function UserRustSections({ userId }) {
|
||||
|
||||
{/* Coins belong to a Steam account, so only with one linked. */}
|
||||
{data.links.length > 0 && <EconomicsPanel userId={userId} />}
|
||||
|
||||
{/* A backpack belongs to a Steam account too. */}
|
||||
{data.links.length > 0 && <BackpackPanel userId={userId} links={data.links} />}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useCallback, useState } from 'react'
|
||||
import { ErrorState, Loading, useAsync } from '../../core.js'
|
||||
import { ago, coins, shortId } from '../../lib/format.js'
|
||||
import api from '../../api.js'
|
||||
import Backpack from '../../components/Backpack.jsx'
|
||||
|
||||
/** The code field, and the four answers it can produce. */
|
||||
function LinkForm({ onLinked }) {
|
||||
@@ -327,6 +328,24 @@ function Coins() {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Backpacks (PLAN_REDESIGNS §11.3.2): your own backpack, from one of your
|
||||
* linked accounts, read live when you open it. A server without Backpacks
|
||||
* says so when asked (D199).
|
||||
*/
|
||||
function OwnBackpack({ links }) {
|
||||
const { data } = useAsync(() => api.playerServers.list(), [])
|
||||
const list = (data && data.servers) || []
|
||||
if (!list.length) return null
|
||||
return (
|
||||
<Backpack
|
||||
servers={list.map((s) => ({ id: s.id, name: s.name }))}
|
||||
accounts={links.map((l) => ({ steamId: l.steamId, name: l.name }))}
|
||||
read={(serverId, steamId) => api.playerBackpacks.read(serverId, steamId)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Account() {
|
||||
// `useAsync` rather than this module's `usePolled`: nothing here changes unless
|
||||
// the person looking at it changes it, and a page that re-asked every twenty
|
||||
@@ -398,6 +417,9 @@ export default function Account() {
|
||||
|
||||
<div className="field-label" style={{ margin: '30px 0 12px' }}>Coins</div>
|
||||
{data && links.length > 0 ? <Coins /> : <p className="sans dim" style={{ fontSize: '0.8rem', margin: 0 }}>Link a Steam account to see your balance.</p>}
|
||||
|
||||
<div className="field-label" style={{ margin: '30px 0 12px' }}>Backpack</div>
|
||||
{data && links.length > 0 ? <OwnBackpack links={links} /> : <p className="sans dim" style={{ fontSize: '0.8rem', margin: 0 }}>Link a Steam account to see your backpack.</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user