17 Commits

Author SHA1 Message Date
92915f7de6 Merge pull request 'feat(bridge): Guarded Crate on protocol 14 (PLAN_REDESIGNS §14.3.5)' (#39) from feat/guarded-crate into edge
Reviewed-on: #39
2026-10-15 23:18:43 +00:00
4e83def0dd feat(bridge): Guarded Crate on protocol 14 (PLAN_REDESIGNS §14.3.5)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 9s
Stage 6 of the §14.3 integration plan. Spec: docs rust-link/PROTOCOL.md §20.9.

- gc.started, gc.ended (the clearer, or nobody) and gc.failed from Guarded
  Crate's three hooks; the start's key joins exactly, inside the call.
- gc.place { key, event, location as a world call } runs `<command> position
  <event> x,y,z`; refuses npcspawn-missing itself, since Guarded Crate's
  `position` does not check and the crate would land unguarded (D364).
- The running set is checked against Guarded Crate's private list (by
  reflection): the hotload seed, `stop all` (no hook), and which event ended.
- Hello's integrations.guardedCrate with npcSpawn and the events from its data
  file; the tally's guardedWon (Warden); map.live's guarded rows; rg.world.
- Guarded Crate's cargo plane no longer says airdrop.called.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-15 12:26:36 -05:00
7b1eabcd8b Merge pull request 'feat(bridge): Dangerous Treasures on protocol 14 (PLAN_REDESIGNS §14.3.4)' (#38) from feat/dangerous-treasures into edge
Reviewed-on: #38
2026-10-15 12:05:02 +00:00
72f51c3d9e feat(bridge): Dangerous Treasures on protocol 14 (PLAN_REDESIGNS §14.3.4)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 14s
Stage 5 of the §14.3 integration plan (docs rust-link/PROTOCOL.md §20.8).

- Three events from DT's own hooks: dt.started { x, z, key? },
  dt.won { steamId, name, invaders (max 32), invaderCount, x, z } and
  dt.ended { x, z }. None while the world is not live; a win still counts.
- The start hook fires inside the call that opens the chest, so the key a
  dt.start is running joins its start exactly, not by a ten-second window.
- dt.start { key, level 0-2 } runs `dtevent <level>` through
  ConsoleSystem.Run and answers dt.opened { key, level, repeat, x, z }. A key
  already answered is answered again and opens nothing (kept seven days in
  dt-starts.json). Refusals: malformed, bad-level, dt-missing,
  console-unavailable (D363), dt-not-ready (nothing opened, DT's own "Manual
  Event Failed" reply, under fifteen minutes since DT loaded, D365),
  dt-no-spot (after), refused (anything else DT said).
- The tally gains treasuresWon (Treasure Hunter, D375); map.live's world gains
  kind "treasure" for each running chest, seeded at load from DT's chests in
  the world (DT has no API that lists them).
- Hello's integrations gain dangerousTreasures, sent again on DT's load and
  unload. rg.world prints dangerousTreasures, treasures, dtStarts and
  dtWarmingUp. README lists Dangerous Treasures.

checkPlugin: 81 hooks, every one void and listed; 18 tests. Compiled and
loaded on both rigs (Rust 25902425, Oxide 2.0.7830, Carbon 2.0.262);
rg.world read dangerousTreasures=False treasures=0, then both went back on
the protocol-13 bridge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-15 06:45:25 -05:00
8e8553c589 Merge pull request 'feat(bridge): Dynamic PVP's zones on protocol 14 (PLAN_REDESIGNS §14.3.3)' (#37) from feat/dynamic-pvp into edge
Reviewed-on: #37
2026-10-15 11:17:45 +00:00
7c9c8bdacb feat(bridge): Dynamic PVP's zones on protocol 14 (PLAN_REDESIGNS §14.3.3)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in -2m8s
Stage 4 of the §14.3 integration plan, specified in docs
rust-link/PROTOCOL.md §20.7 and §17.3.

- pvp.zone.created { zoneId, event, x, z, radius, shape, durationSec } from
  OnCreatedDynamicPVP, and pvp.zone.deleted { zoneId, event } from
  OnDeletedDynamicPVP. Neither is sent while the world is not live.
- Size and place come from ZoneManager: a sphere's Radius, or half a cube's
  longer side (cargo's zone is a cube); the position is the zone's transform,
  so cargo's moves with the ship.
- map.live gains a `pvp` layer of the live zones, kept by the two hooks and
  seeded from Dynamic PVP's own AllDynamicPVPZones / GetEventName at world
  ready and when it loads; a zone ZoneManager no longer holds is dropped.
- hello's integrations.dynamicPvp, sent again when it loads or unloads;
  rg.world prints dynamicPvp= and pvpZones=; both hooks join rg.hooks.
- The bridge never makes, maps or erases one of Dynamic PVP's zones.

Compiled and loaded on both rigs (Rust 25902425, Oxide 2.0.7830, Carbon
2.0.262).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-15 06:12:05 -05:00
49a677e3fb Merge pull request 'feat(bridge): TruePVE in a zone on protocol 14 (PLAN_REDESIGNS §14.3.2)' (#36) from feat/truepve-zone into edge
Reviewed-on: #36
2026-10-15 11:06:25 +00:00
491760d817 feat(bridge): TruePVE in a zone on protocol 14 (PLAN_REDESIGNS §14.3.2)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 10s
Stage 3 of the §14.3 integration plan (D362, D368), specified in docs
rust-link/PROTOCOL.md §20.6.

- world.zone's optional truePve: {"exclude": true} or {"ruleset": "name"},
  exactly one, the name 1-64 characters. Malformed is `malformed`; TruePVE
  not loaded is `truepve-missing`.
- All or nothing: the zone (and its dome) is made, then AddOrUpdateMapping.
  A false erases both and answers `truepve-ruleset` with the name (or
  `refused` for exclude).
- The registry entry remembers its mapping, and every path that erases one
  of our zones calls RemoveMapping first: the deadline, world.revert, a zone
  world.owned finds gone, a new wipe. While TruePVE is away the id waits in
  data/RunicGateway/truepve.json, flushed at OnPluginLoaded(TruePVE) and at
  the bridge's own load.
- hello's integrations.truePve { loaded, version }, sent again when TruePVE
  loads or unloads. rg.world prints truePve= and truePveLeftover=.

Compiled and loaded on both rigs (Rust 25902425, Oxide 2.0.7830, Carbon
2.0.262, TruePVE 2.4.6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-15 05:47:58 -05:00
8fdc374ab3 feat: TruePVE in the zone step (PLAN_REDESIGNS §14.3.2, WIP)
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
2026-10-15 03:03:30 -05:00
51de1b492f Merge pull request 'feat(bridge): Rust's own world events on protocol 14 (PLAN_REDESIGNS §14.3.1)' (#35) from feat/world-events into edge
Reviewed-on: #35
2026-10-15 07:20:06 +00:00
adb9df6a2e 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
2026-10-15 00:39:00 -05:00
6be14c7270 Merge pull request 'feat(bridge): Backpacks on protocol 14 — read a backpack (PLAN_REDESIGNS §11.3)' (#33) from feat/rust-backpacks into edge
Reviewed-on: #33
2026-10-12 19:41:33 +00:00
e3dc918400 feat(bridge): Backpacks on protocol 14 — read a backpack (PLAN_REDESIGNS §11.3)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 23s
WhiteThunder's Backpacks as an optional integration (D199), as §11.3.2 designs it.
Read, never write (R2):

- hello/status `integrations.backpacks` adds `sizes`: the N of every registered
  backpacks.size.N, ascending (dynamic-size profiles left out). Hello is resent
  when Backpacks loads or unloads.
- `backpacks.read {steamId}`: API_ReadBackpackContentsAsJson, and only when that
  answered a backpack, API_GetBackpackCapacityById — the backpack is loaded by
  then, so the capacity follows a later grant (§11.3.1's trap). Each stack is
  reduced to slot, shortname, display (the game's English name, D340), amount,
  skin and name when set; a blueprint carries its target as `blueprint` (D341);
  contents one level deep; `overflow` past the capacity. `pageSize` is
  Backpacks' "Max size per page" from its config, cached by mtime.
- Refusals: backpacks-missing, bad-id, unreadable.

The bigger backpack an event lends is a backpacks.size.N permission pushed by
perm.sync (D337), so it needs no code here. Still protocol 14 (D332).

Compiled on both rigs (Rust changeset 167584; Oxide 2.0.7826, Carbon 2.0.262),
then restored to the bridge they ran before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-12 14:35:54 -05:00
3db35cdd02 Merge pull request 'feat(bridge): Economics on protocol 14 — pay, balances, adjust (PLAN_REDESIGNS §11.2)' (#31) from feat/protocol-14-economics into edge
Reviewed-on: #31
2026-10-12 12:40:24 +00:00
66fbbd6b57 feat(bridge): Economics on protocol 14 — pay, balances, adjust (PLAN_REDESIGNS §11.2)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 10s
Wulf's Economics as an optional integration (D199), as §11.2.2 designs it:

- hello/status `integrations.economics` adds startingBalance and wipesWithMap,
  read from Economics.json (cached by mtime) because its API has neither.
- `economics.pay`: coins from an event step, kept once paid (D335). One main-
  thread call for the list; each deposit measured as after - before, since
  Deposit answers true while rounding and clamping (`at-limit` otherwise).
  The key and its reply are kept in data/RunicGateway/payments.json for seven
  days, so a retry answers `repeat: true` and pays nobody. The optional
  message ({amount} = what landed) is told now if awake, else on wake.
- `economics.balances` (bounded by PermMaxOps) and staff `economics.adjust`
  (deposit / withdraw / set; a withdrawal above the balance is refused
  `insufficient` with the balance).
- `player.tally` gains `balance`, a value: read at connect (D336) and from
  OnEconomicsBalanceUpdated, offline players included.
- `economics.wiped` from OnEconomicsDataWiped(IPlayer), with `newSave`.

Amounts are boxed as double and only Steam ids are sent, per the spike.
Compiled on both rigs (Oxide 2.0.7826, Carbon 2.0.262), then restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-12 06:56:46 -05:00
cc7b2b2a0e Merge pull request 'feat(bridge): protocol 14 — kits.uses and reward notices (PLAN_REDESIGNS §2)' (#30) from feat/protocol-14-step-editor into edge
Reviewed-on: #30
2026-10-12 09:52:10 +00:00
6952fbf123 feat(bridge): protocol 14 — kits.uses and reward notices (PLAN_REDESIGNS §2)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 9s
Opens protocol 14 (D332):
- kits.uses {kit, steamIds} -> {kit, max, cooldown, asked, exhausted,
  onCooldown}: counts only, bounded by the sync's own bound (§2.6).
- perm.sync notices [{key, steamId, message, delivery}] (D333): the site's
  whole untold set. Told at once to a player who is awake, else on
  OnPlayerSleepEnded; popup where PopupNotifications is loaded and chosen,
  chat otherwise. Told keys persist in notices.json until the site stops
  sending them; the report lists noticesTold.

ProtocolVersion and overlay.toml 13 -> 14; OnPlayerSleepEnded joins
ExpectedHooks (checkPlugin: 55 hooks, all void).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-12 02:15:23 -05:00
5 changed files with 4662 additions and 76 deletions

View File

@@ -101,6 +101,44 @@ Clans on the website come from Rust's **own** clan system, not from the uMod `Cl
is optional. The two share no data: a server running the plugin has two unrelated clan systems, and is optional. The two share no data: a server running the plugin has two unrelated clan systems, and
only the game's own clans become Teams on the site. only the game's own clans become Teams on the site.
Wulf's `Economics` is optional too, and not in `overlay.toml`. Where it is loaded the site can pay
coins from an event, show balances and a richest column, and let staff adjust a balance; where it is
not, each of those says "Economics is not installed on this server".
WhiteThunder's `Backpacks` is optional in the same way. Where it is loaded staff can open a player's
backpack from the site, a player can see their own, and an event can lend a bigger one for its run;
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.
nivex's `TruePVE` is optional in the same way. Where it is loaded an event's zone can turn off PvP
protection inside it, or use one of the operator's own TruePVE rulesets by name; where it is not, the
zone step says "TruePVE is not installed on this server". The bridge only ever adds and removes the
mappings of zones it opened, and removes each one with its zone, even when TruePVE was away at the time.
HunterZ's `Dynamic PVP` (which needs ZoneManager and TruePVE) is optional too. Where it is loaded, the
PvP zones it opens around a downed helicopter, a Bradley, cargo and the rest reach the site's feed and
its map. The bridge only reads them: it never makes, maps or erases one of Dynamic PVP's zones.
nivex's `Dangerous Treasures` is optional as well. Where it is loaded, its treasure events reach the
site's feed, its map and the *Treasure Hunter* title, and an event can open one (`dt.start`) through
DT's own `dtevent` console command; DT picks the spot and despawns its own chest. For about ten minutes
after DT loads it is still finding its spots, and a start then is answered "not ready yet". A framework
that cannot run another plugin's console command from code (`rg.world` says `consolePlugins=False`)
cannot open one.
Bazz3l's `Guarded Crate` is optional too, and needs `NpcSpawn` (a free Codefling download, not shipped
here) for its guards. Where both are loaded, a guarded crate landing, being cleared and timing out
reach the site's feed, its map and the *Warden* title, and an event can place one (`gc.place`) at a
monument or a point through Guarded Crate's own `gcrate position` console command. A placed crate is
kept when the event ends: Guarded Crate has no way to end one event alone. Without NpcSpawn a placement
is refused, because the crate would land unguarded; and, as for Dangerous Treasures, a framework with
`consolePlugins=False` cannot place one.
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 ## Diagnosing it
``` ```

View File

@@ -26,15 +26,23 @@
# the same change that alters the emitters, exactly as the sidecar bumps # the same change that alters the emitters, exactly as the sidecar bumps
# PROTOCOL_VERSION and the module bumps its own constant. # PROTOCOL_VERSION and the module bumps its own constant.
# #
# Current: 13 — the transport, the read path, identity, the permission mirror, # Current: 14 — the transport, the read path, identity, the permission mirror,
# configuration from the site, first-party clans, a raid frame that names who # configuration from the site, first-party clans, a raid frame that names who
# lives there, the event leases, the world verbs events place and give back, and # lives there, the event leases, the world verbs events place and give back, and
# the rewards: a run's participation tally, kit credits and chat (PROTOCOL.md §16), # the rewards: a run's participation tally, kit credits and chat (PROTOCOL.md §16),
# the map: its picture and everything that moves on it (PROTOCOL.md §17), and the # the map: its picture and everything that moves on it (PROTOCOL.md §17), and the
# optional mods: BetterChat titles and group styles, and popups (PROTOCOL.md §18), # optional mods: BetterChat titles and group styles, and popups (PROTOCOL.md §18),
# and the first player walk's fixes (PROTOCOL.md §19) — opened by a configuration # and the first player walk's fixes (PROTOCOL.md §19) — opened by a configuration
# save that answers at once and reports its reload as a `config.outcome` frame. # save that answers at once and reports its reload as a `config.outcome` frame,
protocol = 13 # and the step editor (PROTOCOL.md §20): `kits.uses`, and the one-time reward
# 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`; 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 ────────────────────────────────────────────────────── # ── Oxide compatibility ──────────────────────────────────────────────────────
# #

File diff suppressed because it is too large Load Diff

View File

@@ -113,6 +113,16 @@ function readChatCommands(source) {
return found 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) { function readMethods(source) {
const found = [] const found = []
let m let m
@@ -196,7 +206,20 @@ function check(source, toml) {
seen.add(cmd.command) 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) const inCode = /ProtocolVersion\s*=\s*(\d+)\s*;/.exec(source)
// runicnpc stage 4: the RunicNPC API the bridge needs, declared twice like the protocol. // 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) 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() if (require.main === module) main()

View File

@@ -16,7 +16,7 @@ const assert = require('node:assert')
const fs = require('node:fs') const fs = require('node:fs')
const path = require('node:path') 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. */ /** A minimal plugin that passes, as the baseline every case below deviates from. */
function source({ expected = ['OnPlayerDeath'], methods, version = 2 } = {}) { 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('link'), '/link was not found by the chat-command parser')
assert.ok(names.has('unlink'), '/unlink 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')
})