The step-5 walk found demo-player could remove its stand-in Steam link from
its Rust page, and staff could remove it from Admin -> Users. That link is
what the player tour's Rust pages, rewards and grants hang on.
- module.json demoLocks gains DELETE /player/rust/links/:steamId.
- The staff route, DELETE /admin/users/:id/rust/links/:steamId, serves under
core's users mount, which a module's demoLocks may not name, so its handler
refuses with DEMO_LOCKED behind ctx.demoMode before the link is touched.
Off the demo both behave as before. Checked on a real core with the demo
seed: both answer 403 DEMO_LOCKED and the link stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The demo's first restored wipes brought the default set's groups and
permissions back, but not its NPCs or chat titles. The restored rig's
RunicNPC says it is already managed (by the site before this one), so the
fresh site adopts nothing (D244) and its first push empties the profiles;
the placements then wait on missing profiles. The import never adopts a
group's chat style either.
onDemoSeed now also takes:
- npcs.factions: the faction table, through the NPC model's setFactions;
- npcs.profiles: each profile through the model's create/update, so it is
checked exactly as the admin page checks it (an existing one is updated);
- chat: a BetterChat style per server and group, the defaults filled in. A
group the rig's import has not brought in yet is created for that server,
and the import then adopts the rig's title, rank and permissions into it.
The section's new parts are checked before anything is written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
website/DEMO_MODE.md §3.2, §3.4, §3.5, §7.3 and §7.5 (step 2 of §10).
Nothing changes off the demo; everything below sits behind ctx.demoMode.
- demoLocks in module.json: PUT and DELETE /admin/rust/servers/:id.
- The console-command deny list (DM11): quit, restart, oxide.unload,
oxide.reload, rcon.*, fps.limit and server.tickrate, by first word,
with global. stripped and o. read as oxide. Refused at save through
core's demoRefuses (MODULE_API 1.19.0, DM18) and again at run, with
retry: false, before the rig is asked. Teardown commands too.
- The config editor (DM14): the bridge's files are left out of the
list and refused for reading, writing and in the write history, by
name (canonicalised) and by the catalogue's self. Other plugins'
credential fields are redacted in the text and the form and made
read-only; raw-JSON saves are refused; a file that does not parse is
withheld. The admin server list leaves out the sidecar URL.
- onDemoSeed (DM15): upserts the rig's server row with an encrypted
token and demo-player's stand-in Steam link.
- The release's install manifest carries protocol, read from
PROTOCOL_VERSION, for the wipe's bundle match (DM12).
coreApi is ^1.19.0; ci/core-ref.json pins website#224's head.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The bridge names a kill's weapon by the held entity's short prefab name
(ak47u.entity), and the feed printed it with the dots turned to spaces:
"with ak47u entity". Every event read now gives the frame a weaponName
(server/model/titles/weaponNames.js): the game's own English name for the
53 ranged weapons (guns, bows, crossbows, launchers, flamethrowers), read
off the Oxide rig's item list on Rust 26139947, and for anything else the
prefab name as words with its .entity/.weapon ending dropped.
The site prints weaponName and falls back to the same stripping for an
older server. The prefab name stays in the frame: the title lists count by
it. Added at read, so stored rows are renamed too. No protocol change; the
events route's Swagger description names the new field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Twenty-four strings an administrator reads carried an internal decision
number - "(D189)", "(D198)", "(D257, D270)" and the like: the permission
screen's group Servers tab and a chip tooltip, the event editor's step
field and budget descriptions, two option sources, a command reward's
skipped note, and five NPC profile validation errors. A D-number means
something to the plan, never to an operator. Each is removed, and the
sentence around it left as it was. The developer-facing swagger
descriptions and code comments keep theirs.
The servers page's chat-title line read "Last pushed: 1 player hold
one." It now says "holds" for one.
Two tests pinned the old wording and now assert the text instead.
Found while writing the runicgateway.com Rust docs (its PLAN.md §16,
phase 15).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
website#220 cut website edge over to main as 6890a74. The pin pointed at
9a1654f, the sha website#218 merged into edge as, which its own note said
moves to main when the cutover lands. The new tree adds the last fixes'
core half (website#219, no MODULE_API bump), whose route changes are only
core's own event routes, so routes.manifest.json does not change. The
frozen-manifest job, replayed locally at this ref, passes (89 routes, all
documented).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Stage 4 of docs/modules/rust/PLAN_LAST_FIXES.md, the module's half of W1.
A placed zone now reports `until`, reckoned from the answer as now plus
holdMs, so it lands a round trip after the game's own deadline and never
before it. Core's reconcile then files a zone gone past that deadline as
expired, where a reconnect used to log resource.orphaned five seconds
before the game's world.expired arrived. Crates and NPCs send no
holdMs, so they carry no until.
No core pin change: `until` has been in the resource contract all along,
and a core without the reconcile half only shows it on the console.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
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
W21: the permission push named its fields one by one and left out
`notices`, so no kit reward, lend or give ever told its player
anything (D333). The body is now built by `pushBody`, which carries
every field the payload has; a test fails if a payload field is
dropped again.
W4: a downed patrol helicopter, Chinook or Bradley is stored with the
credit rule in force when it arrived, and the feed reads that stamp.
Before, the line re-credited past rows with the server's current rule
while the Rotorbane/Tankbreaker counts kept the old one. Rows stored
before this change keep today's reading.
PLAN_LAST_FIXES.md stage 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
#218 (Steam sign-in, MODULE_API 1.17.0) merged into website edge as
9a1654f. The pin pointed at the PR's branch head, 5e398d8, while it was
open. The two trees differ by one core test file, so routes.manifest.json
does not change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Registers core's MODULE_API 1.17.0 identity hooks (coreApi ^1.17.0):
onIdentityLinking refuses a Steam id that is another website account's Rust
link (D396); onIdentityLinked writes the Rust link with the PRIMARY KEY as the
race guard, reconciles Teams and raises the linked notice, as /link does.
Unlinking Steam leaves the Rust link (D393); the Rust account page says so.
ci/core-ref.json still pins the 1.15.0 core and must move to the website
branch head carrying 1.17.0 before CI can pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
PLAN_REDESIGNS §11.4 (D202-D208, D386-D391), protocol 14. Spec: docs
rust-link/PROTOCOL.md §20.12.
- raid.started, raid.completed, raid.purchased and raid.locked public,
raid.ended staff's; raidableBases.js reads each row per viewer (raiders'
and a buyer's names only for the presence audience, D376/D390; Steam ids,
position and a run's key only for staff). Feed lines in the World filter.
- The step rust.raidbase.spawn, "Spawn a raid base" (ledgered, budget
rust.raids): difficulty, profile and base from option sources listing each
server's own names (D386), a spot as a world call's location with a height
adjustment or an exact height, and PvP/NPCs/lock as on/off/blank. A base
the raids helper made is a world resource, so eventWorld's revert and
reconcile serve it and world.expired marks it expired; one it did not make
records nothing and the run log says it despawns on its own timer. Refused
at verify for a server that said it cannot; a dry run with a spot returns
the spot check as detail.warnings (MODULE_API 1.16.0, D391). rb-not-ready
retries in ten minutes.
- Warlord: raids_completed, title category raidscompleted, the leaderboard's
Raid bases column (sort=raidsCompleted).
- The map's raids layer (D114), public by default with its own audience; the
run's key is dropped before any viewer gets it.
- Server settings' line for RaidableBases; its profiles in Mod configuration
under a virtual path (D205).
- Swagger fragment, bundle and README.
Tests: server 684/684 (raidableBases.test.js new, every name in it invented);
client 99/99, build and externals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Two event steps in server/eventCommands.js, on one new budget,
`rust.commands` (D382-D385):
- rust.command.run: up to 10 typed commands once on one server, with
optional teardown commands. irreversible / ledger: the ledger row keeps
the teardown as it was, and core reverts it when the run ends or is
cancelled. Priced one unit per command, teardown included.
- rust.command.reward: the kit reward's recipient picker (tally modes and
linked), by Steam id, so no website account is needed. The bridge fills
{steamid} and {name}; offline recipients are skipped and named (D383).
irreversible / none. Priced commands x people, the bound for a mode that
is not a count.
Both take core's `multiple` string field as a free text box (no
allowlist, D384) and a required "I am sure" toggle. {steamid}/{name} in a
run step are refused. unknown-command, malformed, events-disabled,
interrupted are permanent; the error says how many ran before it.
D385: the run also hands its teardown to the bridge, which keeps it with
the key. A revert with only the key (the step failed, or its answer was
lost, so core recorded nothing) asks every enabled server to run what it
kept; a server the step never reached runs nothing.
sidecarClient.commandsRun -> POST /commands/run; ci/bundle.json lists the
new file; entry.test.js pins the new ids; 21 tests in
test/commandSteps.test.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Stage 7 of the §14.3 integration plan (D374): Server settings' plugin
line says whether ZLevels is loaded and that an event may lend its XP
boosts as permissions. The integrations schema gains zLevels; the
swagger fragment is regenerated. No leaderboard change, nothing stored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Stage 6 of the §14.3 integration plan, on protocol 14. Spec: docs
rust-link/PROTOCOL.md §20.9.
- gc.started and gc.ended public, gc.failed staff's; guardedCrate.js reads
each row per viewer (the clearer's name for the presence audience, Steam id,
position and key for staff, `won` for everyone).
- Triggers rust.gc.started and rust.gc.ended (a crate a player cleared only).
- The step rust.gc.place: an event from rust.options.gc_events and a world
call's location; refused at verify for Guarded Crate absent, NpcSpawn missing
(D364), console unavailable, or an unknown event. Kept (D373); the run log's
success line says so (D380). Budget rust.gc.crates.
- Warden (D375): guarded_won, title category guardedwon, Crates cleared column.
- Map marker, server settings line, swagger, engagement manifest, bundle, README.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Stage 5 of the §14.3 integration plan, on protocol 14 (docs
rust-link/PROTOCOL.md §20.8).
- dt.started and dt.won are public kinds, dt.ended a staff kind. The new
dangerousTreasures.js reads each row per viewer: grid and invaderCount for
everyone, the winner's name for the presence audience (D376), the position,
the step's key, the winner's Steam id and the invaders for staff.
- Feed lines in the World events filter: "A Dangerous Treasures event has
started · K12", "Alice won the treasure · K12 · 3 players fought for it".
- Triggers rust.dt.started (with byEvent) and rust.dt.won, public, cooldown
subject the server, names only.
- eventDangerousTreasures.js: rust.dt.start, "Start a Dangerous Treasures
event" (risk change, reversible none, budget rust.dt.events), server and
difficulty. Refused at verify where the server said DT is not loaded or
console.plugins is false (Rust-Plugins#34). dt-not-ready answers "Dangerous
Treasures is not ready yet" with retryAfterMs 600000 (D365, D371);
dt-no-spot retries on core's delay; DT's own refusals are permanent.
- Treasure Hunter (D375): treasuresWon -> treasures_won, the title category
treasureswon, and a sortable Treasures column shown where the server
reports DT or a row counts one.
- The map draws a running chest on the world layer; the server settings'
plugin line names DT and whether a step can start one, from the
integrations read's new `console`.
- coreApi ^1.15.0, ci/core-ref.json pinned to website main at #215's merge.
ci/bundle.json ships the two new files; swagger fragment and engagement
manifest regenerated; README lists DT.
Server 627/627 (16 new in test/dangerousTreasures.test.js) and every check;
client 92/92, build and externals. Booted against the pinned core: 45
triggers, 13 actions, 11 budgets, 21 option sources; 89 routes unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Stage 4 of the §14.3 integration plan, on protocol 14 (docs
rust-link/PROTOCOL.md §20.7).
- pvp.zone.created is a public kind, pvp.zone.deleted a staff kind. Each row
is read per viewer by the new dynamicPvp.js: grid for everyone, the event's
name only for Dynamic PVP's own vanilla events, position and zone id for
staff only.
- The feed's world filter carries the opening: "A PvP zone opened around the
downed patrol helicopter · G7 · for 10 min". It names nobody.
- The map gains a fifth layer, `pvp` (PvP zones), public by default with its
own audience in Admin → Rust visibility, drawn as a red ring with its
event's name.
- Server settings' plugin line names Dynamic PVP; the swagger schemas and
route descriptions carry the layer and the integration (fragment
regenerated); ci/bundle.json ships dynamicPvp.js; README lists it.
Tests: server 611/611 (dynamicPvp.test.js new, catalogue and map extended)
and every check; client 89/89 (feed and map tooltip), build and externals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Stage 3 of the §14.3 integration plan (D362, D368), on protocol 14 (docs
rust-link/PROTOCOL.md §20.6).
- rust.zone.open gains noPvpProtection (boolean) and pvpRuleset (string),
sent as world.zone's truePve { exclude: true } or { ruleset }.
- Both set, a name over 64 characters, or a server whose hello says TruePVE
is not loaded is refused for good, at verify too. A server that has not
said is left to the plugin, as a dome is.
- truepve-missing and truepve-ruleset are permanent failures, with the
plugin's words.
- The zone servers list carries each server's truePve from hello; Server
settings' plugin line says whether TruePVE is loaded; the integrations
schema documents it (swagger fragment regenerated).
- README lists TruePVE among the optional plugins.
Tests: five new in zones.test.js; server 603/603 and all checks, client
84/84 and the build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Stage 3 of the §14.3 integration plan (D362, D368): world.zone's optional
truePve ({exclude: true} or {ruleset}), all or nothing, unmapped on every
path that erases one of our zones, with truepve.json for erases while
TruePVE is away. Docs and PRs still to come.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The site half of stage 1, on protocol 14:
- server/worldEvents.js: the 24 types, D372's four credit rules, the grid
from the map's geometry (none off the map), and the per-viewer reading.
- The feed serves `world.event` (a public kind): an unknown type is staff's;
the event and its grid are public; names only for the presence audience
(D376); Steam ids and positions only for staff (D353). A downed one carries
`credited`/`creditedCount` by the server's rule. Client lines for all 24
types and a World events filter.
- rust_servers.credit_rule (blow, last, most, all; default blow), set on the
server's settings. Ingest counts heli_kills (patrol helicopters and
Chinooks, D379) and apc_kills from `world.event` by that rule, so the line
and Rotorbane / Tankbreaker agree. A protocol-13 tally's heliKills/apcKills
still add as before.
- The tally's cratesHacked / dropsLooted → crates_hacked / drops_looted;
title categories Safecracker and Scavenger (D356); leaderboard columns
Crates hacked and Drops looted, sortable (D377).
- 24 engagement triggers rust.world.<event> (D355): ceiling everyone,
cooldown subject the server and event (crates add where), names only.
- server/eventWorldCall.js: rust.world.call, "Call a world event"
(risk change, reversible none, budget rust.world.calls). A position for an
airdrop or a patrol helicopter only; the Chinook takes none (D378).
Server 585/585 (22 new in test/worldEvents.test.js), client 84/84, every
check; booted against the pinned core (43 triggers, 12 actions, 10 budgets,
20 option sources), routes.manifest unchanged at 89. Engagement manifest and
swagger fragment regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
website#213 (MODULE_API 1.14.0, the `multiple` param) merged as a981b2b.
Its tree is identical to the branch head 743f1bc this was pinned to, so
routes.manifest.json is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY