feat(rust): RunicNPC required, protocol 13, edge → main (runicnpc 9e step 3, merge 3 of 3) #37
Reference in New Issue
Block a user
No description provided.
Delete Branch "edge"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What & why
Merge 3 of 3, last. Merge it only after I have reported the protocol 13 bundle carrying RunicNPC 1.0.0: from this release, every Place NPCs step needs RunicNPC on the server (D310), so a site should not get it before a server can.
Everything since v0.3.0: phases 10–17, protocol 13's fixes, the permission manager, zones/dome (dome stack 3, D326), map labels, chat titles, and RunicNPC's stages 4–9 on the site.
coreApi^1.11.0, which websitemaincarries (ci/core-ref.jsonpinsf0e7d2aa, website#209).The order (docs/runicnpc/PLAN.md, 9e step 3)
edge→main— after runicnpc-rust#18 (thefeat!:) has merged intoedge. Releases v1.0.0.edge→main, together — after I have checked v1.0.0 (checksums, manifestapi6). The bridge and the sidecar both move protocol 12 → 13, and the bundle's Gate 1 refuses a pair that disagrees, so one without the other leaves a bundle that cannot compose.v2/rust/current.jsononinstaller'sbundlesbranch moves to protocol 13 with RunicNPC 1.0.0 in it (dispatchingbundle.ymlif a compose run lost the race).edge→main— last, once a server can actually get RunicNPC from a bundle.Please leave
edgestanding (untick "delete branch" on merge): later work lands there.After the merges: the installer's RunicNPC path and the egg's banner from the real bundle (the row the walk left for this step), then the docs PR recording step 3.
How it was tested
Each change on
edgecame in by its own PR with its own checks and rig walk (stages 4–9, the 9d player session, 9e steps 1–2).edgeis 0 commits behindmain, so this merges clean; this PR's checks are the run against the merged tree.Checklist
AI-assisted contributions (required)
Claude Code (Claude Opus 5.5). I have reviewed and understandevery change, and take responsibility for it. AI-authored commits are
marked with a
Co-Authored-By/Assisted-Bytrailer.License
(GNU GPL v3.0 or later), and I have the right to contribute it.
🤖 Generated with Claude Code
https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
rustas the module's identity capabilityrustas the module's identity capability (phase 5, D16)' (#5) from feat/phase-5-capability into edge 28e46771b1Phase 8's website half. Phase 7 made the site the author of in-game privilege and gave an operator every view of it; this is the other side, and it is the first time a player can see what they hold without asking one. `GET /player/rust/permissions` is self-scoped in SQL and read-only by construction — a grant a player could change would not be a grant. Three things make it a different shape from the admin read rather than a filtered one: * the scope arithmetic is answered on the server. A client handed `*` would have to know what the fleet is to say anything, and then `inScope` exists twice. Each entry carries the servers it reaches, already resolved and already marked. * `live` is the pushed ledger, never the authored row. A grant is not a privilege in a game until a sync confirmed it, and phase 7 is careful never to record a push that silently did nothing — so "waiting" is honest, and the alternative is the site claiming to have given something it has not. * nothing says WHY it is waiting. An offline server, a permission no loaded plugin registered and a store that has never seen the account all look the same from here; telling them apart is an operator's diagnosis and an inventory of what is installed. An entitlement that reaches nobody still lists, and the page says so — authored against the website account, it exists before a Steam id does, and hiding it until one turns up is the defect the admin user page shipped in phase 7 (PLAN.md §20.5). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMH6bw1jXMgbyF3ZWGEzSMThe org lead's rule, settled 2026-09-22: who is online is always the narrowest audience - staff - unless an operator deliberately widens it, and a count is fine where a list of names is not. The public site broke that in three places since phase 4. The Online tab named every player, the feed carried joins, respawns, deaths, chat and tallies, and the leaderboard's lastSeen - refreshed every minute by a gather tally - said who was on as plainly as either. All three now sit behind one setting: * PRESENCE_KINDS, a subset of the public allowlist, gated per request. Below the audience the feed keeps the server's own story (wipe, start, shutdown) and says presenceHidden rather than looking quiet. * the Online route answers { players: [], hidden, count, audience } - same shape, so an older client renders empty rather than breaking. * rungs staff / signed_in / public, fleet-wide default in a new rust_settings table with an optional per-server override on rust_servers; an unknown stored word narrows to staff. * the viewer's standing is RE-READ from the users row (ctx.users.getById), not taken from the token, so a demotion or a ban applies on the next request. Walked: a moderator demoted mid-session lost the roll call on the same cookie. * per-viewer answers are Cache-Control: private, no-store. * GET/PUT /admin/rust/visibility (requireRole admin) and an admin page, Rust visibility; every save is one activity-log row. The browser walk also found every empty state in this module rendering as a blank box. Core's EmptyState renders children only; this module passed title/message (the shape the Integration Kit template teaches) and React dropped both without a word. Fixed module-side with a small Empty wrapper - nothing core or module-uo renders changes - and a client test that refuses a titled EmptyState or a PageHeader subtitle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeYThe live walk rendered a generic in-app notice as "A server came online. A server's game started..." Core's structural projection falls back to the trigger's label and description when the payload has no title, and on a multi-server site that never says which server. Core's rule is that the payload wins, so every trigger now declares `title` and `intro`, and the emitter writes the sentence ("Oxide rig is online"). An operator's own template can still ignore it and use the parts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeYThe job cloned a MODULE_API 1.10.0 main and the loader refused the module ("needs core API ^1.11.0, this core is 1.10.0"), so it added no routes and failed by construction. Pinned to #209's head (cc1f49a), where the job's own steps pass: core alone 280 routes up to date, with this module 49 routes all documented. Re-pin to #209's main merge sha once it lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeYRunicNPC stage 5 on the site (docs runicnpc/PLAN.md, D253-D272): - profiles gain the guard role, faction, relations (its own exceptions), alertRadius, turrets, hurtByPlayers, hurtsPlayers and kitUse, checked in RunicNPC's order and words, and defaulting to "players only" (D255); - the faction table: one site-wide table, one row per pair and both ways (D254, D268), stored in rust_npc_factions, edited on the NPC profiles page (PUT /admin/rust/npcs/factions), pushed to every server with its profiles and hashed with them, and a standalone server's own pairs adopted at its first push with the site's winning (D244, D251); - the Place NPCs step takes escort (a Steam id or a {placeholder}), an ally (a clan from the new rust.options.clans source, or the team of a player) and tether (the zone this event made), for a RunicNPC profile only (D269, D270, D272); - a placement may be held inside a zone (tether, D272); - the bridge's RunicNPC API floor is 4. Tests: 488 server, 66 client. routes.manifest.json regenerated against the pinned core (one route added). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY- rust.npc.place's progress() (MODULE_API 1.12.0, D304). It reports the step's NPCs still standing, by its key, of how many it placed, with the profile's label: "Bandit: 2 of 3 left". A boss step is its health, "Walk Juggernaut: 59%", and a boss's adds are not counted. The data comes from the map's live answer, so it costs the game no extra ask. A reader who may not see the map's events layer gets no line. - The map (D302, D303): - an NPC's tooltip is its name and its event's title, read from core's public calendar, which lists only announced events; - a boss is bigger and in its own colour, and an add says so; - a boss outside any event is in the world layer; - a boss's health is removed from what a viewer is sent. - The world layer's visibility hint mentions those bosses. coreApi stays ^1.11.0: an older core ignores the optional member, and the action works without a progress line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeYA Place NPCs step on a server without RunicNPC, or with one older than the bridge needs, is refused before it is sent: "<server> needs RunicNPC to place NPCs, Rust's own scientists included: <why>". Rust's own scientists were the fallback until now (D243); they need RunicNPC too. Crates are unaffected. - npcs.model: API_NEEDED rises from 4 to 6, the bridge's RunicNpcApiNeeded, so a server the site calls ready is one the bridge will not refuse as runicnpc-old. The profile push follows the same floor. - eventWorld: profileMissing becomes npcMissing, asked for every NPC step. - Admin → Rust → Servers: each server carries `runicNpc` ({ready, absence, version, api}) and a server without one that will do reads "Incomplete" with the reason. Swagger documents the field. The event picker is unchanged in this commit (an open question on the PR). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY