feat(bridge): Rust's own world events on protocol 14 (PLAN_REDESIGNS §14.3.1)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in -1m55s

Stage 1 of the §14.3 integration plan: the patrol helicopter, Bradley, the
cargo ship, the Chinook, airdrops, locked crates, the Travelling Vendor, the
satellite and the Deep Sea, with no plugin (D349).

- `world.event`, one frame kind whose `event` names which of 24 types
  (PROTOCOL.md §20.5). The type is in `event`, never `type`: `type` is the
  envelope's, and the rig's sidecar dropped every world event as untyped when
  the plan's example put it there. Nothing is said before the world loads or
  while the server quits. Positions are x/z; the site works out the grid.
- Credit (D372, D379): a damage table for the patrol helicopter, the Chinook
  and Bradley; `heli.downed`, `chinook.downed` and `bradley.destroyed` carry
  every candidate (blow, last, most, damagers) and the plugin credits nobody.
  D174's `CreditVehicle` and the tally's apcKills/heliKills are gone.
- Locked crates told positively (oil rigs, cargo, ghost ship, a Chinook drop
  joined at the crate's landing); anything else says nothing. A Bradley's
  `where` comes from the game's spawners, not `RoadSpawned` (which defaults to
  true). `airdrop.called` comes from the cargo plane's spawn: `supply.call`
  raised neither OnAirdrop nor OnCargoPlaneSignaled.
- The tally gains cratesHacked and dropsLooted (Safecracker, Scavenger, D356).
- `world.call` (D354, D367, D378): one of a schedule's TriggeredEvents at
  random, without Trigger (which would reset its clock); satellite.random for
  the satellite. A position for the airdrop (InitDropPosition) and the patrol
  helicopter (SetInitialDestination) only, both set before the spawn, as the
  stage-1 spike measured (0-1 m and 10-54 m). Kept; the key is remembered seven
  days in world-calls.json; EventsMaxWorldCalls (default and ceiling 10) per run.
- Hello/status `console.plugins` (D363): the bridge runs its own rg.probe
  through ConsoleSystem.Run from a timer. Carbon 2.0.262 refuses plugin
  commands only inside a console command (or a NextTick one queued), which is
  how §14.2's spike tested; from a timer, and from the link reader's NextTick,
  it finds them. Reads true on both rigs.
- checkPlugin.js: the probe must name a [ConsoleCommand] the plugin declares.

Compiled and loaded on both rigs (Rust 25902425, Oxide 2.0.7830, Carbon
2.0.262); heli.arrived, cargo.arrived and airdrop.called read back from each
rig's sidecar store. Rigs restored. 18 check tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-15 00:39:00 -05:00
parent 6be14c7270
commit adb9df6a2e
5 changed files with 1578 additions and 68 deletions

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')
})