fix(rust): protocol 13 step 2 — expiry, plugin loads, the loading hold, NPC names, the link fleet (F2 F5 F6 F7 F8 F13 F14)
The module's half of PLAN_FIXES §6 step 2 (decisions D181-D185, docs#288). - F13/F14 (D170, D183): `world.expired`, recognisable from protocol 13 by its `what`, is handed to core as the resource the zone step ledgered (`world`, `<serverId>:<id>`) through ctx.events.expired, which records it `expired`. coreApi moves to ^1.11.0 (website#209). - F8 (D184): `plugin.loaded` / `plugin.unloaded` mark the permission sync dirty when the plugin added or removed permissions, so an unresolved grant lands on the next tick instead of the fifteen-minute audit. - Catalogue: plugin.loaded/unloaded, world.expired and lease.expired are staff kinds. The last two were never classified (default deny kept them off public pages); the test now covers every event kind through protocol 13. - F7: permission and title pushes hold while the stored hello says `worldReady: false` (a human's "sync now" does not); a failed or refused permission sync now logs at warn. - F2 (D185): the killfeed names an NPC attacker — a family (Scientist, Bandit guard, Bradley APC…) or the prefab without its variant digits (wolf2 → Wolf). - F5/F6: a link code is asked of the servers that minted one in the last six minutes first, then of the rest, each group in parallel; "unsure" only when one of the minting servers is unreachable. - D182: the admin server list carries the ZoneManager helper's state from the hello, and the servers page says what a missing or failed helper costs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -101,15 +101,54 @@ async function deleteServer(id) {
|
||||
await core.query(`DELETE FROM ${SERVERS} WHERE id = ?`, [id])
|
||||
}
|
||||
|
||||
/**
|
||||
* `worldReady` from the hello the row keeps whole (`raw`): true, false, or null for
|
||||
* a plugin that never says — which PLAN.md §28.6 reads as ready. The permission and
|
||||
* title pushes hold while it is false (PLAN_FIXES F7): the plugin connects before the
|
||||
* save loads, and a sync sent then waits on a main thread that is busy loading, times
|
||||
* out, and the restart it was for is never shown as restored.
|
||||
*/
|
||||
function withWorldReady(row) {
|
||||
if (!row) return row
|
||||
const value = row.worldReady
|
||||
const ready = value === null || value === undefined ? null : value === true || value === 1 || String(value) === 'true'
|
||||
return { ...row, worldReady: ready, zoneHelper: helperOf(row.zoneHelper) }
|
||||
}
|
||||
|
||||
/**
|
||||
* The ZoneManager helper's state from the same hello (PLAN_FIXES D182): `{ state,
|
||||
* version?, reason? }`, or null when the plugin reported none — no ZoneManager, or
|
||||
* a plugin older than protocol 13. The driver hands JSON_EXTRACT back as text.
|
||||
*/
|
||||
function helperOf(value) {
|
||||
if (value === null || value === undefined) return null
|
||||
let parsed = value
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
parsed = JSON.parse(value)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (!parsed || typeof parsed !== 'object' || typeof parsed.state !== 'string') return null
|
||||
return {
|
||||
state: parsed.state,
|
||||
...(typeof parsed.version === 'string' ? { version: parsed.version } : {}),
|
||||
...(typeof parsed.reason === 'string' ? { reason: parsed.reason } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
/** The last thing each server said about itself, keyed by server id. */
|
||||
async function listState() {
|
||||
return core.query(
|
||||
return (await core.query(
|
||||
`SELECT server_id AS serverId, reachable, online, players, max_players AS maxPlayers,
|
||||
hostname, level, seed, world_size AS worldSize, boot_id AS bootId,
|
||||
save_created_at AS saveCreatedAt, wipe_id AS wipeId, protocol,
|
||||
last_seen_at AS lastSeenAt, updated_at AS updatedAt
|
||||
last_seen_at AS lastSeenAt, updated_at AS updatedAt,
|
||||
JSON_EXTRACT(raw, '$.worldReady') AS worldReady,
|
||||
JSON_EXTRACT(raw, '$.zoneHelper') AS zoneHelper
|
||||
FROM ${STATE}`,
|
||||
)
|
||||
)).map(withWorldReady)
|
||||
}
|
||||
|
||||
/** One server's observed state, or `null`. The single-row twin of `listState`. */
|
||||
@@ -118,12 +157,14 @@ async function getState(serverId) {
|
||||
`SELECT server_id AS serverId, reachable, online, players, max_players AS maxPlayers,
|
||||
hostname, level, seed, world_size AS worldSize, boot_id AS bootId,
|
||||
save_created_at AS saveCreatedAt, wipe_id AS wipeId, protocol,
|
||||
last_seen_at AS lastSeenAt, updated_at AS updatedAt
|
||||
last_seen_at AS lastSeenAt, updated_at AS updatedAt,
|
||||
JSON_EXTRACT(raw, '$.worldReady') AS worldReady,
|
||||
JSON_EXTRACT(raw, '$.zoneHelper') AS zoneHelper
|
||||
FROM ${STATE}
|
||||
WHERE server_id = ?`,
|
||||
[serverId],
|
||||
)
|
||||
return rows[0] || null
|
||||
return withWorldReady(rows[0] || null)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -208,6 +208,9 @@ async function listForAdmin(now = Date.now()) {
|
||||
reachable: Boolean(state && state.reachable),
|
||||
bootId: (state && state.bootId) || null,
|
||||
sidecarProtocol: state && state.protocol != null ? Number(state.protocol) : null,
|
||||
// D182: whether ZoneManager counts somebody already standing in a zone the
|
||||
// bridge makes. Anything but `patched` is said on the servers page.
|
||||
zoneHelper: (state && state.zoneHelper) || null,
|
||||
schedule: scheduleOf(row),
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user