feat(bridge): Rust's own world events on protocol 14 (PLAN_REDESIGNS §14.3.1) #35

Merged
whitlocktech merged 1 commits from feat/world-events into edge 2026-10-15 02:20:08 -05:00
5 changed files with 1578 additions and 68 deletions
Showing only changes of commit adb9df6a2e - Show all commits

View File

@@ -110,6 +110,11 @@ backpack from the site, a player can see their own, and an event can lend a bigg
the bridge only ever reads a backpack, and the bigger size is one of Backpacks' own `backpacks.size.N`
permissions, pushed like any other grant.
Rust's own world events need no plugin at all. The patrol helicopter, Bradley, the cargo ship, the
Chinook, airdrops, locked crates, the Travelling Vendor, the satellite and the Deep Sea reach the
site's feed and its event triggers on every server, and an event can call one of them (`world.call`).
What a call starts is kept when the event ends; `EventsMaxWorldCalls` caps how many one run may call.
## Diagnosing it
```

View File

@@ -38,7 +38,10 @@
# messages a `perm.sync` carries as `notices`; and Economics (PROTOCOL.md §20.3):
# `economics.pay`, `economics.balances`, `economics.adjust`, the tally's
# `balance` and the `economics.wiped` event; and Backpacks (PROTOCOL.md §20.4):
# `backpacks.read` and hello's `integrations.backpacks.sizes`.
# `backpacks.read` and hello's `integrations.backpacks.sizes`; and Rust's own world
# events (PROTOCOL.md §20.5): `world.event` with its `event`, `world.call`, hello's
# `console`, and the tally's `cratesHacked` and `dropsLooted` in place of `apcKills`
# and `heliKills`.
protocol = 14
# ── Oxide compatibility ──────────────────────────────────────────────────────

File diff suppressed because it is too large Load Diff

View File

@@ -113,6 +113,16 @@ function readChatCommands(source) {
return found
}
/** Every `[ConsoleCommand("x")]` the plugin declares. */
function readConsoleCommands(source) {
return [...source.matchAll(/\[ConsoleCommand\("([^"]+)"\)\]/g)].map((m) => m[1])
}
/** The `rg.*` commands the plugin runs on itself through `ConsoleSystem.Run`. */
function readOwnConsoleRuns(source) {
return [...source.matchAll(/ConsoleSystem\.Run\([^,]+,\s*"(rg\.[\w.]+)"/g)].map((m) => m[1])
}
function readMethods(source) {
const found = []
let m
@@ -196,7 +206,20 @@ function check(source, toml) {
seen.add(cmd.command)
}
// 5. The two declaration sites this repository owns must agree.
// 5. Protocol 14 (D363): the console probe runs one of this plugin's own commands
// through `ConsoleSystem.Run` to learn whether the framework runs a plugin's
// command from code. A probe naming a command the plugin does not declare
// answers "no" on every framework, which reads exactly like Carbon's bug.
for (const name of readOwnConsoleRuns(source)) {
if (!readConsoleCommands(source).includes(name)) {
problems.push(
`ConsoleSystem.Run runs "${name}", but the plugin declares no [ConsoleCommand("${name}")]: ` +
'the console probe would report false on every framework (D363).'
)
}
}
// 6. The two declaration sites this repository owns must agree.
const inCode = /ProtocolVersion\s*=\s*(\d+)\s*;/.exec(source)
// runicnpc stage 4: the RunicNPC API the bridge needs, declared twice like the protocol.
const npcToml = /^\s*runicnpc_api\s*=\s*(\d+)\s*$/m.exec(toml)
@@ -242,6 +265,6 @@ function main() {
)
}
module.exports = { check, readExpectedHooks, readMethods, readChatCommands, HOOK_NAME }
module.exports = { check, readExpectedHooks, readMethods, readChatCommands, readConsoleCommands, readOwnConsoleRuns, HOOK_NAME }
if (require.main === module) main()

View File

@@ -16,7 +16,7 @@ const assert = require('node:assert')
const fs = require('node:fs')
const path = require('node:path')
const { check, readExpectedHooks, readMethods, readChatCommands, HOOK_NAME } = require('./checkPlugin')
const { check, readExpectedHooks, readMethods, readChatCommands, readConsoleCommands, readOwnConsoleRuns, HOOK_NAME } = require('./checkPlugin')
/** A minimal plugin that passes, as the baseline every case below deviates from. */
function source({ expected = ['OnPlayerDeath'], methods, version = 2 } = {}) {
@@ -210,3 +210,55 @@ test('the chat-command parser reads the real plugin, rather than quietly matchin
assert.ok(names.has('link'), '/link was not found by the chat-command parser')
assert.ok(names.has('unlink'), '/unlink was not found by the chat-command parser')
})
// ── The console probe (protocol 14, D363) ──────────────────────────────────
//
// The plugin runs its own `rg.probe` through `ConsoleSystem.Run` to learn whether
// the framework runs a plugin's command from code. A probe that names a command
// the plugin does not declare answers false everywhere, and false is exactly what
// Carbon's bug looks like, so nobody would ever notice.
/** A plugin that runs `ran` on itself and declares `declared`. */
function withProbe(ran, declared) {
return source({
expected: [],
methods: ` private void StartWorldEvents()
{
ConsoleSystem.Run(ConsoleSystem.Option.Server.Quiet(), "${ran}");
}
[ConsoleCommand("${declared}")]
private void CmdProbe(ConsoleSystem.Arg arg)
{
}`,
})
}
test('a probe that runs a command the plugin declares passes', () => {
assert.deepEqual(check(withProbe('rg.probe', 'rg.probe'), toml()), [])
})
test('a probe that runs a command the plugin does not declare is caught', () => {
const problems = check(withProbe('rg.probe', 'rg.prob'), toml())
assert.equal(problems.length, 1)
assert.match(problems[0], /ConsoleSystem\.Run runs "rg\.probe", but the plugin declares no \[ConsoleCommand\("rg\.probe"\)\]/)
})
test('a native command run through the console is not the probe, and is left alone', () => {
const methods = ` private void Call()
{
ConsoleSystem.Run(ConsoleSystem.Option.Server.Quiet(), "satellite.random");
}`
assert.deepEqual(check(source({ expected: [], methods }), toml()), [])
})
test('the probe parsers read the real plugin, rather than quietly matching nothing', () => {
const real = fs.readFileSync(
path.resolve(__dirname, '..', 'overlay', 'oxide', 'plugins', 'RunicGateway.cs'),
'utf8'
)
assert.ok(readOwnConsoleRuns(real).includes('rg.probe'), 'the console probe was not found by the parser')
assert.ok(readConsoleCommands(real).includes('rg.probe'), 'rg.probe was not found among the console commands')
assert.ok(readConsoleCommands(real).includes('rg.hooks'), 'rg.hooks was not found among the console commands')
})