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:
@@ -138,6 +138,14 @@ async function tick({ force = null } = {}) {
|
||||
*/
|
||||
function reasonToSync({ desiredHash, sync, state, force }) {
|
||||
if (force) return 'requested'
|
||||
// Not while the world is loading (PLAN_FIXES F7). The plugin connects before
|
||||
// the save loads, and the first walk's restart sync went 35 s before "Server
|
||||
// startup complete", timed out behind the busy main thread, and was retried
|
||||
// 2.5 minutes later as "0 applied" — so nothing said what the restart had
|
||||
// restored. The plugin says `worldReady: true` in the hello it sends the moment
|
||||
// the world is up, and the sync goes on the next tick. A human's "sync now" is
|
||||
// not held: they asked, and a failure then is theirs to read.
|
||||
if (state && state.worldReady === false) return null
|
||||
if (!sync) return 'first'
|
||||
if (sync.state !== 'ok' && sync.lastAttemptAt && age(sync.lastAttemptAt) < FAIL_BACKOFF_MS && !sync.dirty) {
|
||||
return null
|
||||
@@ -224,6 +232,10 @@ async function syncOne(server, { authored, sync, state, force }) {
|
||||
})
|
||||
|
||||
if (!result.ok) {
|
||||
// Said in the log as well as on the row (F7): the first walk's restart sync
|
||||
// timed out with nothing in the log at all, while the titles push that failed
|
||||
// beside it did log.
|
||||
log.warn('permission sync failed', { server: server.id, reason, status: result.status })
|
||||
await db.putSyncResult(server.id, {
|
||||
state: 'failed',
|
||||
desiredHash: desired.hash,
|
||||
@@ -244,6 +256,7 @@ async function syncOne(server, { authored, sync, state, force }) {
|
||||
// rather than transport failures, exactly like a refused link code, so they
|
||||
// arrive as a 200 and are told apart by `kind`.
|
||||
if (report.kind === 'perm.error') {
|
||||
log.warn('permission sync refused by the game', { server: server.id, reason, refused: report.reason || 'unknown' })
|
||||
await db.putSyncResult(server.id, {
|
||||
state: 'failed',
|
||||
desiredHash: desired.hash,
|
||||
|
||||
Reference in New Issue
Block a user