refactor(rust): one placing verb per kind — rust.crate.place and rust.npc.place (D97)

Core learns which caps an action accepts by pricing its declared examples
once, and drops a dimension priced at zero. A single rust.prefab.place whose
cost moved between rust.prefabs and rust.npcs by its prefab param could only
ever show the crates cap, so D89's separate dial for fights was unreachable.

Two verbs, each pricing exactly one dimension, with the prefab source split
to match (rust.options.crates / rust.options.npcs). The switchboard can now
allow crates and leave NPCs off. The plugin's world.place is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-24 02:09:49 -05:00
parent a3bcec9cde
commit fab31f23e8
3 changed files with 144 additions and 110 deletions

View File

@@ -129,7 +129,7 @@ test('a dry run checks everything it can and sends nothing', async (t) => {
const zone = await action('rust.zone.open').perform({
runId: 7, idempotencyKey: 'k1', verify: true, params: { monument: 'main/airfield_1', radius: 40, minutes: 60 },
})
const place = await action('rust.prefab.place').perform({
const place = await action('rust.crate.place').perform({
runId: 7, idempotencyKey: 'k2', verify: true, params: { monument: 'main/airfield_1', prefab: 'crate.elite', count: 3 },
})
assert.deepStrictEqual([zone, place], [{ ok: true }, { ok: true }])
@@ -138,15 +138,18 @@ test('a dry run checks everything it can and sends nothing', async (t) => {
test('every authoring mistake is refused for good, before anything is sent', async (t) => {
const calls = stub(t)
const place = action('rust.prefab.place')
const crates = action('rust.crate.place')
const npcs = action('rust.npc.place')
const zone = action('rust.zone.open')
const run = (a, params) => a.perform({ runId: 7, idempotencyKey: 'k', params })
for (const result of [
await run(place, { monument: 'main/a', prefab: 'minicopter', count: 1 }),
await run(place, { monument: 'main/a', prefab: 'crate.elite', count: 26 }),
await run(place, { monument: 'main/a', prefab: 'npc.scientist', count: 21 }),
await run(place, { monument: 'main/a', prefab: 'crate.elite', count: 1, spread: 51 }),
await run(crates, { monument: 'main/a', prefab: 'minicopter', count: 1 }),
await run(crates, { monument: 'main/a', prefab: 'npc.scientist', count: 1 }), // D97: the other verb's
await run(npcs, { monument: 'main/a', prefab: 'crate.elite', count: 1 }),
await run(crates, { monument: 'main/a', prefab: 'crate.elite', count: 26 }),
await run(npcs, { monument: 'main/a', prefab: 'npc.scientist', count: 21 }),
await run(crates, { monument: 'main/a', prefab: 'crate.elite', count: 1, spread: 51 }),
await run(zone, { monument: 'main/a', radius: 4, minutes: 10 }),
await run(zone, { monument: 'main/a', radius: 40 }), // D96: minutes are required
await run(zone, { monument: 'main/a', radius: 40, minutes: 7 * 24 * 60 + 1 }),
@@ -182,7 +185,7 @@ test('one resource per thing placed, and a repeated key is said to be one', asyn
data: { kind: 'world.ok', repeat: true, placed: [{ id: '101', kind: 'npc', prefab: 'npc.scientist' }] },
}),
})
const result = await action('rust.prefab.place').perform({
const result = await action('rust.npc.place').perform({
runId: 7, idempotencyKey: 'k', params: { x: -604, z: -342, server: 'main', prefab: 'npc.scientist', count: 1 },
})
assert.strictEqual(result.ok, true)
@@ -194,7 +197,7 @@ test('the switch being off is a refusal with the switch named, for good (D94)',
stub(t, {
place: async () => ({ ok: true, data: { kind: 'world.error', reason: 'events-disabled', message: 'set EventsEnabled' } }),
})
const result = await action('rust.prefab.place').perform({
const result = await action('rust.crate.place').perform({
runId: 7, idempotencyKey: 'k', params: { monument: 'main/a', prefab: 'crate.elite', count: 1 },
})
assert.deepStrictEqual(result, { ok: false, retry: false, error: 'set EventsEnabled' })
@@ -202,7 +205,7 @@ test('the switch being off is a refusal with the switch named, for good (D94)',
test('a game that is down or slow is left to core to retry', async (t) => {
stub(t, { place: async () => ({ ok: false, status: 'http-503' }) })
const result = await action('rust.prefab.place').perform({
const result = await action('rust.crate.place').perform({
runId: 7, idempotencyKey: 'k', params: { monument: 'main/a', prefab: 'crate.elite', count: 1 },
})
assert.strictEqual(result.ok, false)
@@ -296,10 +299,13 @@ test('the monument source lists each server\'s map as whole values, numbered whe
assert.deepStrictEqual(narrowed.map((r) => r.value), ['main/powerplant_1'])
})
test('the prefab source answers with every server off', async (t) => {
test('the crate and NPC sources split the allowlist, and answer with every server off (D97)', async (t) => {
const calls = stub(t)
const rows = await source('rust.options.prefabs').resolve()
assert.strictEqual(rows.length, world.PLACEABLE.length)
const crates = await source('rust.options.crates').resolve()
const npcs = await source('rust.options.npcs').resolve()
assert.strictEqual(crates.length + npcs.length, world.PLACEABLE.length)
assert.ok(crates.every((r) => !r.value.startsWith('npc.')))
assert.ok(npcs.every((r) => r.value.startsWith('npc.')))
assert.deepStrictEqual(calls.monuments, [])
})