11 Commits

Author SHA1 Message Date
24307a95d4 Merge pull request 'release: lock removing a Steam link on the demo (v0.6.2)' (#69) from edge into main
All checks were successful
Release / release (push) Successful in 49s
Reviewed-on: #69
2026-10-28 10:18:35 +00:00
ec90d2d5ae Merge pull request 'fix(demo): lock removing a Steam link on the demo (DM27)' (#67) from fix/demo-lock-steam-unlink into edge
All checks were successful
PR Checks / server-tests (pull_request) Successful in 42s
PR Checks / client-build (pull_request) Successful in 58s
PR Checks / frozen-manifest (pull_request) Successful in 1m7s
Reviewed-on: #67
2026-10-28 09:47:44 +00:00
fb7511d700 fix(demo): lock removing a Steam link on the demo (DM27)
All checks were successful
PR Checks / server-tests (pull_request) Successful in 54s
PR Checks / client-build (pull_request) Successful in 1m5s
PR Checks / frozen-manifest (pull_request) Successful in 1m36s
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
2026-10-28 04:44:28 -05:00
161457dbf3 Merge pull request 'release: seed the demo's NPC profiles, factions and chat styles (v0.6.1)' (#66) from edge into main
All checks were successful
Release / release (push) Successful in 35s
Reviewed-on: #66
2026-10-28 01:32:14 +00:00
5c9cefdbc2 Merge pull request 'fix(demo): seed NPC profiles, factions and chat styles (DM26)' (#65) from feat/demo-seed-npcs-chat into edge
All checks were successful
PR Checks / server-tests (pull_request) Successful in 38s
PR Checks / client-build (pull_request) Successful in 55s
PR Checks / frozen-manifest (pull_request) Successful in 59s
Reviewed-on: #65
2026-10-28 01:22:44 +00:00
1f4ad25edb fix(demo): seed NPC profiles, factions and chat styles (DM26)
All checks were successful
PR Checks / server-tests (pull_request) Successful in 55s
PR Checks / client-build (pull_request) Successful in 1m7s
PR Checks / frozen-manifest (pull_request) Successful in 1m36s
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
2026-10-27 20:15:32 -05:00
228e178475 Merge pull request 'release: the public demo's module-side locks and seed hook (v0.6.0, MODULE_API 1.19.0)' (#64) from edge into main
All checks were successful
Release / release (push) Successful in 45s
Reviewed-on: #64
2026-10-27 17:32:01 +00:00
0cdb046d30 Merge pull request 'chore(ci): pin core-ref to website#224's merge on main' (#63) from chore/core-ref-224-merge into edge
All checks were successful
PR Checks / server-tests (pull_request) Successful in 41s
PR Checks / client-build (pull_request) Successful in 47s
PR Checks / frozen-manifest (pull_request) Successful in 58s
Reviewed-on: #63
2026-10-26 11:03:06 +00:00
791f13235e chore(ci): pin core-ref to website#224's merge on main
All checks were successful
PR Checks / client-build (pull_request) Successful in 1m2s
PR Checks / server-tests (pull_request) Successful in 1m8s
PR Checks / frozen-manifest (pull_request) Successful in 1m35s
website#224 (MODULE_API 1.19.0, demoRefuses) merged as 69998ec on main.
The merge tree equals the branch head the pin held (cee4ab8), 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
2026-10-26 05:53:41 -05:00
bc529962b4 Merge pull request 'release: weapon names and the admin copy fixes (v0.5.1)' (#61) from edge into main
All checks were successful
Release / release (push) Successful in -1m57s
Reviewed-on: #61
2026-10-23 07:07:21 +00:00
3fac860949 Merge pull request 'chore(release): cut edge over to main — protocol 14 and Steam sign-in (MODULE_API 1.17.0)' (#58) from edge into main
All checks were successful
Release / release (push) Successful in 36s
Reviewed-on: #58
2026-10-22 11:16:11 +00:00
6 changed files with 214 additions and 12 deletions

View File

@@ -1,6 +1,6 @@
{ {
"$comment": "The core this module is proved against. MODULE_API.md §5.3: the frozen-manifest job clones RunicGateway/website at this exact ref, drops this module in as modules/rust and runs CORE's own routeManifest.js — nothing else can answer whether the URLs the module claims are the URLs it actually serves, because a mount prefix is a string in server/index.js and a documented path is a string in a JSON file, and whether those name the same URL is a fact about a running core. It also answers the blind spot phase 1 had to check by hand: core mounts several routes at the TIER ROOT (/status, /version), which the loader's collision probe cannot see, so /rust being free is asserted here by a core rather than by a reading. Pinned rather than tracking a branch on purpose: core moves for reasons that have nothing to do with this module, and a bump is then a deliberate commit saying which core the module was last proved against, instead of an unexplained red X on someone else's PR. Bump it, regenerate routes.manifest.json, and commit both together. This module needs MODULE_API 1.10.0 (module.json's coreApi is ^1.10.0), which the Event System cutover put on `main` — so unlike Module-uo, which spent the Event System window pinned to `edge`, this repo starts pinned to `main` and should stay there unless it comes to depend on a contract member that has not shipped yet. **2026-09-27, protocol 13 step 2:** this module calls ctx.events.expired (MODULE_API 1.11.0, PLAN_FIXES D183), so coreApi is ^1.11.0. It was pinned to website#209's branch head while that PR was open — a 1.10.0 core refuses to load it — and is back on `main` at the sha #209 merged as. **2026-10-12, protocol 14 (PLAN_REDESIGNS §2):** this module registers an event template and declares param durations, a lease value example and option notes (MODULE_API 1.13.0), so coreApi is ^1.13.0. It was pinned to website#212's branch head while that PR was open, and is back on `main` at the sha #212 merged as (the same tree). **2026-10-12, PLAN_REDESIGNS §12 (permission rewards):** this module declares a param with multiple: true (MODULE_API 1.14.0), so coreApi is ^1.14.0. It was pinned to website#213's branch head while that PR was open, and is back on `main` at the sha #213 merged as (the same tree). **2026-10-15, PLAN_REDESIGNS §14.3.4 (Dangerous Treasures):** the Start a Dangerous Treasures event step answers DT's warm-up with retryAfterMs (MODULE_API 1.15.0, D371), so coreApi is ^1.15.0, pinned to `main` at the sha website#215 merged as. **2026-10-19, Steam sign-in (PLAN_REDESIGNS §15, D392/D396):** this module registers onIdentityLinking and onIdentityLinked (MODULE_API 1.17.0), so coreApi is ^1.17.0. Pinned to the head of website feat/steam-signin while that PR is open — a 1.16.0 core refuses to load it — and is now on `edge` at the sha website#218 merged as. That is `edge`, not `main`, because #218 went into `edge` for the in-progress cutover. **2026-10-22, the cutover (PLAN_LAST_FIXES stage 6):** website#220 put `edge` on `main`, so this is back on `main` at the sha #220 merged as. That tree adds the last fixes' core half (website#219, no MODULE_API bump), whose only route changes are core's own event routes, so routes.manifest.json does not change. **2026-10-26, the public demo (DEMO_MODE.md §3.4, §3.5, DM18):** this module declares demoLocks, reads ctx.demoMode, registers onDemoSeed (MODULE_API 1.18.0) and gives its two console-command actions demoRefuses (1.19.0), so coreApi is ^1.19.0. Pinned to the head of website feature/demo-step-refusal (website#224) while that PR is open — a 1.18.0 core never asks demoRefuses — and moves to the sha #224 merges as.", "$comment": "The core this module is proved against. MODULE_API.md §5.3: the frozen-manifest job clones RunicGateway/website at this exact ref, drops this module in as modules/rust and runs CORE's own routeManifest.js — nothing else can answer whether the URLs the module claims are the URLs it actually serves, because a mount prefix is a string in server/index.js and a documented path is a string in a JSON file, and whether those name the same URL is a fact about a running core. It also answers the blind spot phase 1 had to check by hand: core mounts several routes at the TIER ROOT (/status, /version), which the loader's collision probe cannot see, so /rust being free is asserted here by a core rather than by a reading. Pinned rather than tracking a branch on purpose: core moves for reasons that have nothing to do with this module, and a bump is then a deliberate commit saying which core the module was last proved against, instead of an unexplained red X on someone else's PR. Bump it, regenerate routes.manifest.json, and commit both together. This module needs MODULE_API 1.10.0 (module.json's coreApi is ^1.10.0), which the Event System cutover put on `main` — so unlike Module-uo, which spent the Event System window pinned to `edge`, this repo starts pinned to `main` and should stay there unless it comes to depend on a contract member that has not shipped yet. **2026-09-27, protocol 13 step 2:** this module calls ctx.events.expired (MODULE_API 1.11.0, PLAN_FIXES D183), so coreApi is ^1.11.0. It was pinned to website#209's branch head while that PR was open — a 1.10.0 core refuses to load it — and is back on `main` at the sha #209 merged as. **2026-10-12, protocol 14 (PLAN_REDESIGNS §2):** this module registers an event template and declares param durations, a lease value example and option notes (MODULE_API 1.13.0), so coreApi is ^1.13.0. It was pinned to website#212's branch head while that PR was open, and is back on `main` at the sha #212 merged as (the same tree). **2026-10-12, PLAN_REDESIGNS §12 (permission rewards):** this module declares a param with multiple: true (MODULE_API 1.14.0), so coreApi is ^1.14.0. It was pinned to website#213's branch head while that PR was open, and is back on `main` at the sha #213 merged as (the same tree). **2026-10-15, PLAN_REDESIGNS §14.3.4 (Dangerous Treasures):** the Start a Dangerous Treasures event step answers DT's warm-up with retryAfterMs (MODULE_API 1.15.0, D371), so coreApi is ^1.15.0, pinned to `main` at the sha website#215 merged as. **2026-10-19, Steam sign-in (PLAN_REDESIGNS §15, D392/D396):** this module registers onIdentityLinking and onIdentityLinked (MODULE_API 1.17.0), so coreApi is ^1.17.0. Pinned to the head of website feat/steam-signin while that PR is open — a 1.16.0 core refuses to load it — and is now on `edge` at the sha website#218 merged as. That is `edge`, not `main`, because #218 went into `edge` for the in-progress cutover. **2026-10-22, the cutover (PLAN_LAST_FIXES stage 6):** website#220 put `edge` on `main`, so this is back on `main` at the sha #220 merged as. That tree adds the last fixes' core half (website#219, no MODULE_API bump), whose only route changes are core's own event routes, so routes.manifest.json does not change. **2026-10-26, the public demo (DEMO_MODE.md §3.4, §3.5, DM18):** this module declares demoLocks, reads ctx.demoMode, registers onDemoSeed (MODULE_API 1.18.0) and gives its two console-command actions demoRefuses (1.19.0), so coreApi is ^1.19.0. It was pinned to the head of website feature/demo-step-refusal (website#224) while that PR was open — a 1.18.0 core never asks demoRefuses — and is back on `main` at the sha #224 merged as (the same tree, so routes.manifest.json does not change).",
"repo": "https://gitea.whitlocktech.com/RunicGateway/website.git", "repo": "https://gitea.whitlocktech.com/RunicGateway/website.git",
"ref": "cee4ab8aeab4dd218724ae9f35b7bc87e97e91aa", "ref": "69998ec54854d46681def92985a2b299ee3582a6",
"refName": "website feature/demo-step-refusal @ website#224 head, MODULE_API 1.19.0" "refName": "website main @ website#224 merge, MODULE_API 1.19.0"
} }

View File

@@ -16,6 +16,7 @@
"capabilities": ["rust", "servers", "killfeed", "leaderboard", "presence", "wipes", "identity", "map"], "capabilities": ["rust", "servers", "killfeed", "leaderboard", "presence", "wipes", "identity", "map"],
"demoLocks": [ "demoLocks": [
{ "method": "PUT", "path": "/admin/rust/servers/:id" }, { "method": "PUT", "path": "/admin/rust/servers/:id" },
{ "method": "DELETE", "path": "/admin/rust/servers/:id" } { "method": "DELETE", "path": "/admin/rust/servers/:id" },
{ "method": "DELETE", "path": "/player/rust/links/:steamId" }
] ]
} }

View File

@@ -1,9 +1,13 @@
// ── The public demo's module-side locks (DEMO_MODE.md §3.4, §3.5) ────────── // ── The public demo's module-side locks (DEMO_MODE.md §3.4, §3.5) ──────────
// //
// Core locks this module's two link routes by `demoLocks` in module.json. What a // Core locks this module's two rig-link routes and the player's Steam unlink by
// route list cannot express lives here, behind `ctx.demoMode` (MODULE_API // `demoLocks` in module.json. What a route list cannot express lives here, behind
// 1.18.0), and every handler that enforces one of these asks this file: // `ctx.demoMode` (MODULE_API 1.18.0), and every handler that enforces one of these
// asks this file:
// //
// the staff Steam unlink on Admin → Users (step-5 walk, W4): it serves under
// core's users mount, which a module's demoLocks may not name, so its handler
// refuses instead. The demo player's seeded link stays put either way;
// the console-command deny list (§3.4, DM11): the commands that would break // the console-command deny list (§3.4, DM11): the commands that would break
// the rig for every visitor until the next wipe. Asked at save, through // the rig for every visitor until the next wipe. Asked at save, through
// core's `demoRefuses` hook (1.19.0, DM18), and again at run; // core's `demoRefuses` hook (1.19.0, DM18), and again at run;

View File

@@ -9,18 +9,44 @@
// .env, so the token is never in the repo // .env, so the token is never in the repo
// links demo-player's stand-in Steam id (DM6), so the player screens have // links demo-player's stand-in Steam id (DM6), so the player screens have
// somebody to show // somebody to show
// npcs the NPC faction table and profiles of the demo default set (DM26)
// chat a BetterChat style per group (DM26)
// //
// { "servers": [{ "id": "main", "name": "Demo", "sidecarBaseUrl": "$env:RUST_SIDECAR_URL", // { "servers": [{ "id": "main", "name": "Demo", "sidecarBaseUrl": "$env:RUST_SIDECAR_URL",
// "sidecarToken": "$env:RUST_SIDECAR_TOKEN" }], // "sidecarToken": "$env:RUST_SIDECAR_TOKEN" }],
// "links": [{ "username": "demo-player", "steamId": "7656119…", "serverId": "main" }] } // "links": [{ "username": "demo-player", "steamId": "7656119…", "serverId": "main" }],
// "npcs": { "factions": [{ "a": "bandits", "b": "guards", "relation": "allied" }],
// "profiles": [{ "name": "bandit", "servers": ["main"], "body": { … } }] },
// "chat": [{ "serverId": "main", "group": "vip", "style": { "Title": "[VIP]", … } }] }
//
// **Why NPCs and chat come from here, not the rig's baseline (DM26).** The rig is
// restored before the site is wiped, and a fresh site imports what it finds
// (§7.1). That holds for permissions, but not for these two. The restored rig's
// RunicNPC says it is already managed, by the site before this one, so the new
// site adopts nothing (D244) and its first push empties the profiles. The import
// never adopts a group's chat style. So the site holds them from the start and
// pushes them, and the placements in the baseline spawn again.
//
// A chat style names a group the rig's import has not brought in yet, because
// the seed runs first. The group is created for that server, and the import
// then adopts the rig's title, rank and permissions into it. A style lists only
// the fields that differ from BetterChat's defaults.
// //
// **It runs on every seed, so it upserts.** Core seeds content on a database's // **It runs on every seed, so it upserts.** Core seeds content on a database's
// first run only, but calls this every time. Everything is checked before // first run only, but calls this every time. The section's shape is checked
// anything is written, and a problem throws, which fails the seed. // before anything is written, and a problem throws, which fails the seed. A
// profile is checked by the NPC model as it is saved, as the admin page saves
// one, so a profile the model refuses fails the seed too.
const linksDb = require('./model/links/links.db') const linksDb = require('./model/links/links.db')
const servers = require('./model/servers/servers.model') const servers = require('./model/servers/servers.model')
const serversDb = require('./model/servers/servers.db') const serversDb = require('./model/servers/servers.db')
const npcs = require('./model/npcs/npcs.model')
const npcsDb = require('./model/npcs/npcs.db')
const npcShape = require('./model/npcs/npcProfile')
const permApply = require('./model/permissions/permissions.apply')
const permDb = require('./model/permissions/permissions.db')
const chatStyle = require('./model/permissions/chatStyle')
const sidecar = require('./sidecarClient') const sidecar = require('./sidecarClient')
const core = require('./core') const core = require('./core')
@@ -28,6 +54,13 @@ const log = core.logger('demo')
const SERVER_ID = /^[a-z0-9][a-z0-9-]{0,63}$/ const SERVER_ID = /^[a-z0-9][a-z0-9-]{0,63}$/
const STEAM_ID = /^7656119\d{10}$/ const STEAM_ID = /^7656119\d{10}$/
// Oxide group names as the permission model keeps them: trimmed and lower-cased.
const GROUP_NAME = /^[a-z0-9_.-]{1,64}$/
/** BetterChat's defaults for the group, with the fixture's fields over them. */
function fullStyle(group, style) {
return { ...chatStyle.defaults(String(group || '')), ...(style && typeof style === 'object' ? style : {}) }
}
/** Every problem with the section, as sentences. Empty when it is good to write. */ /** Every problem with the section, as sentences. Empty when it is good to write. */
function checkSection(data, accounts = {}) { function checkSection(data, accounts = {}) {
@@ -35,11 +68,41 @@ function checkSection(data, accounts = {}) {
if (!data || typeof data !== 'object' || Array.isArray(data)) return ['modules.rust must be an object'] if (!data || typeof data !== 'object' || Array.isArray(data)) return ['modules.rust must be an object']
const list = (key) => (data[key] === undefined ? [] : data[key]) const list = (key) => (data[key] === undefined ? [] : data[key])
for (const key of ['servers', 'links']) { for (const key of ['servers', 'links', 'chat']) {
if (data[key] !== undefined && !Array.isArray(data[key])) problems.push(`modules.rust.${key} must be an array`) if (data[key] !== undefined && !Array.isArray(data[key])) problems.push(`modules.rust.${key} must be an array`)
} }
const n = data.npcs
if (n !== undefined) {
if (!n || typeof n !== 'object' || Array.isArray(n)) problems.push('modules.rust.npcs must be an object')
else {
if (n.factions !== undefined && !Array.isArray(n.factions)) problems.push('modules.rust.npcs.factions must be an array')
if (n.profiles !== undefined && !Array.isArray(n.profiles)) problems.push('modules.rust.npcs.profiles must be an array')
}
}
if (problems.length) return problems if (problems.length) return problems
if (n && n.factions !== undefined) {
const checked = npcShape.checkFactions(n.factions)
if (!checked.ok) problems.push(`modules.rust.npcs.${checked.error}`)
}
;((n && n.profiles) || []).forEach((p, i) => {
const at = `modules.rust.npcs.profiles[${i}]`
if (!p || typeof p !== 'object') return problems.push(`${at} must be an object`)
if (!npcShape.NAME_RULE.test(String(p.name || ''))) problems.push(`${at}.name must be 1 to 40 of a-z, 0-9, _ and -`)
if (!p.body || typeof p.body !== 'object' || Array.isArray(p.body)) problems.push(`${at}.body must be an object`)
return undefined
})
list('chat').forEach((c, i) => {
const at = `modules.rust.chat[${i}]`
if (!c || typeof c !== 'object') return problems.push(`${at} must be an object`)
if (!SERVER_ID.test(String(c.serverId || ''))) problems.push(`${at}.serverId must be a server id`)
if (!GROUP_NAME.test(String(c.group || ''))) problems.push(`${at}.group must be a group name`)
const checked = chatStyle.validateStyle(fullStyle(c.group, c.style))
if (!checked.ok) problems.push(`${at}.style: ${checked.errors.join(' ')}`)
return undefined
})
list('servers').forEach((s, i) => { list('servers').forEach((s, i) => {
const at = `modules.rust.servers[${i}]` const at = `modules.rust.servers[${i}]`
if (!s || typeof s !== 'object') return problems.push(`${at} must be an object`) if (!s || typeof s !== 'object') return problems.push(`${at} must be an object`)
@@ -92,6 +155,36 @@ async function onDemoSeed(data, { accounts = {} } = {}) {
await linksDb.insert({ steamId, userId, name: l.name || null, serverId: l.serverId || null }) await linksDb.insert({ steamId, userId, name: l.name || null, serverId: l.serverId || null })
log.info('demo link ready', { username: l.username }) log.info('demo link ready', { username: l.username })
} }
// After the servers: a profile is refused for a server the site does not have.
const n = data.npcs || {}
if (n.factions !== undefined) {
await npcs.setFactions(n.factions)
log.info('demo NPC factions ready', { pairs: n.factions.length })
}
if (n.profiles && n.profiles.length) {
const existing = await npcsDb.listProfiles()
for (const p of n.profiles) {
const input = { name: p.name, body: p.body, servers: p.servers, allServers: p.allServers, killsScope: p.killsScope }
const same = existing.find((e) => e.name === p.name && !e.replaced)
if (same) await npcs.update(same.id, input)
else await npcs.create(input)
log.info('demo NPC profile ready', { profile: p.name })
}
}
for (const c of data.chat || []) {
const name = String(c.group)
let group = await permApply.groupOn(name, c.serverId)
if (!group) {
const id = await permDb.insertGroup({ name, title: name })
await permDb.setGroupServers(id, { allServers: false, servers: [c.serverId] })
group = { id }
}
await permDb.setGroupChat(group.id, chatStyle.validateStyle(fullStyle(name, c.style)).fields)
await permDb.markDirty(c.serverId)
log.info('demo chat style ready', { server: c.serverId, group: name })
}
} }
module.exports = { checkSection, onDemoSeed } module.exports = { checkSection, onDemoSeed }

View File

@@ -8,6 +8,7 @@
const crypto = require('node:crypto') const crypto = require('node:crypto')
const core = require('../../core') const core = require('../../core')
const demo = require('../../demo')
const backpacks = require('../../model/backpacks/backpacks.model') const backpacks = require('../../model/backpacks/backpacks.model')
const economics = require('../../model/economics/economics.model') const economics = require('../../model/economics/economics.model')
@@ -54,6 +55,10 @@ async function removeLink(req, res) {
const { steamId } = req.params const { steamId } = req.params
const userId = req.params.id const userId = req.params.id
// On the demo the player's own unlink is locked by demoLocks; this route serves
// under core's users mount, which a module may not lock by route, so it asks here.
if (demo.isDemo()) return demo.refuse(res)
try { try {
const removed = await links.unlinkOwned(steamId, userId) const removed = await links.unlinkOwned(steamId, userId)

View File

@@ -375,13 +375,112 @@ test('a re-seed leaves a link the right owner holds, and moves one held by someo
assert.deepEqual(writes, ['DELETE', 'INSERT']) assert.deepEqual(writes, ['DELETE', 'INSERT'])
}) })
// ── NPCs and chat styles from the seed (DM26) ──────────────────────────────
const NPC_SECTION = {
npcs: {
factions: [{ a: 'bandits', b: 'guards', relation: 'allied' }],
profiles: [
{ name: 'bandit', servers: ['main'], body: { kits: ['rnpc_raider'], role: 'roamer' } },
{ name: 'guard', servers: ['main'], body: { kits: ['rnpc_campguard'], role: 'sentry' } },
],
},
chat: [
{ serverId: 'main', group: 'vip', style: { Title: '[VIP]', TitleColor: '#ffaa00' } },
{ serverId: 'main', group: 'moderator', style: { Title: '[MOD]' } },
],
}
/** Stubs the models the seed writes NPCs and chat styles through; returns the calls. */
function stubSeedModels({ profiles = [], groups = {} } = {}) {
const calls = []
const npcs = require('../model/npcs/npcs.model')
const npcsDb = require('../model/npcs/npcs.db')
const apply = require('../model/permissions/permissions.apply')
const permDb = require('../model/permissions/permissions.db')
npcs.setFactions = async (f) => calls.push(['setFactions', f])
npcs.create = async (input) => calls.push(['create', input.name, input.servers])
npcs.update = async (id, input) => calls.push(['update', id, input.name])
npcsDb.listProfiles = async () => profiles
apply.groupOn = async (name) => groups[name] || null
permDb.insertGroup = async ({ name }) => {
calls.push(['insertGroup', name])
return 50
}
permDb.setGroupServers = async (id, s) => calls.push(['setGroupServers', id, s.servers])
permDb.setGroupChat = async (id, style) => calls.push(['setGroupChat', id, style.Title, style.TitleColor, Object.keys(style).length])
permDb.markDirty = async (scope) => calls.push(['markDirty', scope])
return calls
}
test('the NPC and chat parts of the seed section are checked before anything is written', async () => {
withCore()
const seed = require('../demoSeed')
assert.deepEqual(seed.checkSection(NPC_SECTION, {}), [])
const problems = seed.checkSection({
npcs: { factions: [{ a: 'scientists', b: 'animals', relation: 'hostile' }], profiles: [{ name: 'Bad Name', body: [] }] },
chat: [{ serverId: 'Main', group: '', style: { Title: '[VIP]', Nope: 1 } }],
}, {})
assert.equal(problems.length, 6, problems.join(' | '))
assert.deepEqual(seed.checkSection({ npcs: [] }, {}), ['modules.rust.npcs must be an object'])
const calls = stubSeedModels()
await assert.rejects(seed.onDemoSeed({ chat: [{ serverId: 'main', group: 'vip', style: { Priority: 'x' } }] }, { accounts: {} }), /style/)
assert.deepEqual(calls, [])
})
test('the seed sets the faction table, saves each profile, and styles each group', async () => {
withCore()
const calls = stubSeedModels({ profiles: [{ id: 7, name: 'bandit', replaced: false }], groups: { vip: { id: 13 } } })
await require('../demoSeed').onDemoSeed(NPC_SECTION, { accounts: {} })
assert.deepEqual(calls, [
['setFactions', NPC_SECTION.npcs.factions],
['update', 7, 'bandit'], // already there: updated, not created twice
['create', 'guard', ['main']],
['setGroupChat', 13, '[VIP]', '#ffaa00', 12], // all twelve fields, the defaults filled in
['markDirty', 'main'],
['insertGroup', 'moderator'], // the rig's import has not brought it in yet
['setGroupServers', 50, ['main']],
['setGroupChat', 50, '[MOD]', '#55aaff', 12],
['markDirty', 'main'],
])
})
// ── The manifest (§3.2) ──────────────────────────────────────────────────── // ── The manifest (§3.2) ────────────────────────────────────────────────────
test('module.json locks the two link routes and needs a core that asks demoRefuses', () => { test('module.json locks the two rig-link routes and the player Steam unlink, and needs a core that asks demoRefuses', () => {
const manifest = require('../../module.json') const manifest = require('../../module.json')
assert.deepEqual(manifest.demoLocks, [ assert.deepEqual(manifest.demoLocks, [
{ method: 'PUT', path: '/admin/rust/servers/:id' }, { method: 'PUT', path: '/admin/rust/servers/:id' },
{ method: 'DELETE', path: '/admin/rust/servers/:id' }, { method: 'DELETE', path: '/admin/rust/servers/:id' },
{ method: 'DELETE', path: '/player/rust/links/:steamId' },
]) ])
assert.equal(manifest.coreApi, '^1.19.0') assert.equal(manifest.coreApi, '^1.19.0')
}) })
// ── The staff Steam unlink (W4) ────────────────────────────────────────────
test('staff cannot sever a Steam link on the demo, and the link is never touched', async () => {
for (const demoMode of [true, false]) {
withCore({ demo: demoMode })
const links = require('../model/links/links.model')
const asked = []
links.unlinkOwned = async (steamId, userId) => {
asked.push([steamId, userId])
return true
}
const res = fakeRes()
await require('../router/admin/usersRust.controller').removeLink(
{ params: { id: '3', steamId: '76561190000000001' }, user: { id: 1 } },
res,
)
if (demoMode) {
assert.equal(res.statusCode, 403)
assert.equal(res.body.code, 'DEMO_LOCKED')
assert.deepEqual(asked, [])
} else {
assert.equal(res.statusCode, 200)
assert.deepEqual(res.body, { unlinked: true })
assert.deepEqual(asked, [['76561190000000001', '3']])
}
}
})