fix(rust): the last fixes' small ones (W3 W6 W7/W11 W8 W10 W13-W18)
All checks were successful
PR Checks / client-build (pull_request) Successful in 24s
PR Checks / server-tests (pull_request) Successful in 32s
PR Checks / frozen-manifest (pull_request) Successful in 44s

Stage 2 of PLAN_LAST_FIXES (docs#366).

- W7/W11: an "is it loaded" refusal is confirmed live before it is given.
  New liveStatus.js asks the sidecar once (boot.refreshOne, as a poll tick
  would), at most once per server per 5 s and waiting at most 3 s, then
  reads the stored status again. Used by Economics (step and staff page),
  Backpacks, Dangerous Treasures, Guarded Crate, RaidableBases, ZoneDomes,
  TruePVE and RunicNPC.
- W3: a world call answered `already-out` (the bridge half comes in stage
  3) succeeds as called 0, saying a Travelling Vendor is already out.
- W6: "A locked crate dropped by a Chinook is being hacked."
- W8: a RaidableBases profile's audit row names RaidableBases, not "@data".
- W10: the backpack sizes picker lists each size once, naming its servers.
- W13: a RaidableBases name both servers describe differently shows each
  server's own description.
- W14: each RaidableBases note after the first starts its own sentence.
- W15: a staff deposit refused at Economics' limit answers 409.
- W16: a payment's failedNote explains only the reasons that happened.
- W17: the sync state counts player grants that did not land
  (waitingToLand), and the Permissions page says "in sync · 1 waits for
  their player to join" instead of plain "in sync".
- W18: Guarded Crate's kept note drops the "30 minutes" it could not know.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-21 07:42:24 -05:00
parent d69c2108c6
commit 3aa142bf06
29 changed files with 479 additions and 41 deletions

View File

@@ -47,6 +47,7 @@ const linksDb = require('./model/links/links.db')
const emit = require('./engagement/emit')
const economics = require('./model/economics/economics.model')
const backpacks = require('./model/backpacks/backpacks.model')
const live = require('./liveStatus')
const voice = require('./model/permissions/voice')
const { serverFor, transportError, pluginError, perServer, bounded } = require('./eventLeases')
const { BUDGET_MS } = require('./eventWorld')
@@ -741,6 +742,22 @@ function coinsNotice(params, amount) {
return { ok: true, text, delivery }
}
/** What each reason in a payment's `failed` list means, in the plugin's order. */
const FAILED_REASONS = Object.freeze({
'at-limit': "the balance is at Economics' limit",
refused: 'Economics did not take the deposit',
unmeasured: 'Economics did not report the deposit',
})
/** A payment's `failedNote`: only the reasons that happened, each explained (W16). */
function failedNote(failed) {
const seen = new Set(failed.map((row) => row.reason))
return [...Object.keys(FAILED_REASONS), ...[...seen].filter((r) => !FAILED_REASONS[r])]
.filter((reason) => seen.has(reason))
.map((reason) => (FAILED_REASONS[reason] ? `${reason}: ${FAILED_REASONS[reason]}` : `${reason}: Economics' own reason`))
.join('; ')
}
/** The key the plugin remembers a payment by: core's, or a digest of it past the plugin's bound. */
function paymentKey(idempotencyKey, runId, stepId) {
const key = String(idempotencyKey || `coins:${runId}:${stepId}`)
@@ -806,7 +823,8 @@ const coinsPay = {
// D199: refused with the reason where Economics is absent, in the dry run
// too — the author learns it on the form, not at the run's reward.
const econ = await economics.forServer(server.id)
// Confirmed live before it refuses: the stored status lags a restart (W7).
const econ = await live.confirmed(server, () => economics.forServer(server.id), (e) => !e.loaded)
if (!econ.loaded) return { ok: false, retry: false, error: `${economics.MISSING} (${name})` }
if ((mode === 'top' || mode === 'random') && count.value > MAX_RECIPIENTS) {
@@ -906,7 +924,7 @@ const coinsPay = {
paid: paid.length,
total,
received: paid.slice(0, 50),
...(failed.length ? { failed: failed.slice(0, 50), failedNote: 'at-limit: the balance is at Economics\' limit; refused or unmeasured: Economics did not take or report the deposit' } : {}),
...(failed.length ? { failed: failed.slice(0, 50), failedNote: failedNote(failed) } : {}),
...(notice.text && paid.length ? { told: `${paid.length} will be told in game, by ${notice.delivery}` } : {}),
...(data.repeat ? { repeat: true, repeatNote: 'answered from the first attempt; nobody was paid twice' } : {}),
...missedDetail,
@@ -995,7 +1013,8 @@ const backpackSize = {
// D199: refused with the reason where Backpacks is absent, in the dry run
// too, and a size the server does not offer is refused by name.
const report = await backpacks.forServer(server.id)
// Confirmed live before it refuses: the stored status lags a restart (W7).
const report = await live.confirmed(server, () => backpacks.forServer(server.id), (r) => !r.loaded)
if (!report.loaded) return { ok: false, retry: false, error: `${backpacks.MISSING} (${name})` }
const offered = backpacks.leasable(report)
if (!offered.includes(size)) {
@@ -1356,17 +1375,23 @@ const OPTION_SOURCES = [
]),
{
// From each server's last hello: the sizes its Backpacks offers above the
// smallest, grouped by server (D337). The step checks the pair it is given.
// smallest (D337). Each size once, naming every server that offers it, as
// the other pickers merge a value (W10). The step checks the pair it is given.
id: 'rust.options.backpacksizes',
label: 'Backpack sizes',
description: "Each server's Backpacks sizes above its smallest — its backpacks.size.N permissions.",
async resolve() {
const [list, reports] = await Promise.all([servers.listForPolling(), backpacks.byServer()])
const rows = []
const bySize = new Map()
for (const s of list) {
for (const n of backpacks.leasable(reports.get(s.id))) rows.push({ value: n, label: `${n} slots`, group: s.name || s.id })
for (const n of backpacks.leasable(reports.get(s.id))) {
if (!bySize.has(n)) bySize.set(n, [])
bySize.get(n).push(s.name || s.id)
}
}
return rows
return [...bySize.keys()]
.sort((x, y) => x - y)
.map((n) => ({ value: n, label: `${n} slots — ${bySize.get(n).join(', ')}` }))
},
},
{
@@ -1398,6 +1423,7 @@ module.exports = {
usersFor,
readTally,
entitlementRef,
failedNote,
pickRecipients,
checkCount,
splitKit,