Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25a698e86e | |||
| 3c0fcb0003 | |||
| 38f854d580 | |||
| 48aa62a07a | |||
| e0d6c46850 | |||
| 594acb5351 | |||
| 5104e0dbcd | |||
| b52b8a8f80 | |||
| 9c99aceb65 | |||
| 454ce53937 | |||
| 92915f7de6 | |||
| 4e83def0dd | |||
| 7b1eabcd8b | |||
| 72f51c3d9e | |||
| 8e8553c589 | |||
| 7c9c8bdacb | |||
| 49a677e3fb | |||
| 491760d817 | |||
| 8fdc374ab3 | |||
| 51de1b492f | |||
| adb9df6a2e | |||
| 6be14c7270 | |||
| e3dc918400 | |||
| 3db35cdd02 | |||
| 66fbbd6b57 | |||
| cc7b2b2a0e | |||
| 6952fbf123 |
@@ -30,7 +30,7 @@
|
||||
#
|
||||
# 2. A FRAMEWORK-NEUTRAL LAYOUT. The tarball holds one directory,
|
||||
# `runicgateway-rust-plugin/`, with `RunicGateway.cs`, its helpers
|
||||
# (`RunicGatewayZones.cs`, `RunicGatewayDomes.cs`) and `manifest.json` at its top. Every `.cs` in it is listed in the
|
||||
# (`RunicGatewayZones.cs`, `RunicGatewayDomes.cs`, `RunicGatewayRaids.cs`) and `manifest.json` at its top. Every `.cs` in it is listed in the
|
||||
# manifest's `files`, and the installer and the egg place every one of them. It does NOT mirror `oxide/plugins/`: the same file goes to
|
||||
# `oxide/plugins/` or `carbon/plugins/`, and the installer and the egg each
|
||||
# decide which (§34.2.1). The fixed, unversioned prefix is so a reader never
|
||||
@@ -41,7 +41,8 @@
|
||||
# committed file keeps its placeholder; the manifest records the commit.
|
||||
# Only the bridge is stamped. A helper (docs/modules/rust/PLAN_FIXES.md D168,
|
||||
# D182 — today `RunicGatewayZones.cs` for ZoneManager and `RunicGatewayDomes.cs`
|
||||
# for ZoneDomes, PLAN_REDESIGNS D194) keeps its own
|
||||
# for ZoneDomes, PLAN_REDESIGNS D194, and `RunicGatewayRaids.cs` for RaidableBases,
|
||||
# D204) keeps its own
|
||||
# version: it does one job, changes rarely, and the bridge reads its version at
|
||||
# hello to know which job it does.
|
||||
#
|
||||
@@ -81,7 +82,7 @@ env:
|
||||
PLUGIN: overlay/oxide/plugins/RunicGateway.cs
|
||||
# The helpers shipped beside the bridge (D168, D182). Optional at runtime —
|
||||
# the bridge loads without them — and installed by default.
|
||||
HELPERS: overlay/oxide/plugins/RunicGatewayZones.cs overlay/oxide/plugins/RunicGatewayDomes.cs
|
||||
HELPERS: overlay/oxide/plugins/RunicGatewayZones.cs overlay/oxide/plugins/RunicGatewayDomes.cs overlay/oxide/plugins/RunicGatewayRaids.cs
|
||||
SEED_VERSION: "0.1.0"
|
||||
INSTALLER_REPO: RunicGateway/installer
|
||||
|
||||
|
||||
74
README.md
74
README.md
@@ -37,6 +37,7 @@ unauthenticated command channel on the network.
|
||||
overlay/oxide/plugins/RunicGateway.cs → <server>/oxide/plugins/RunicGateway.cs
|
||||
overlay/oxide/plugins/RunicGatewayZones.cs → <server>/oxide/plugins/RunicGatewayZones.cs
|
||||
overlay/oxide/plugins/RunicGatewayDomes.cs → <server>/oxide/plugins/RunicGatewayDomes.cs
|
||||
overlay/oxide/plugins/RunicGatewayRaids.cs → <server>/oxide/plugins/RunicGatewayRaids.cs
|
||||
```
|
||||
|
||||
`RunicGatewayZones.cs` is the **ZoneManager helper** (docs/modules/rust/PLAN_FIXES.md D181, D182). ZoneManager
|
||||
@@ -52,6 +53,17 @@ helper patches the first with Harmony and tells the bridge when ZoneDomes has st
|
||||
domes back without drawing any twice. It is optional: without it the site offers no dome, and says which piece is
|
||||
missing. `rgd.status` in the server console says whether it patched.
|
||||
|
||||
`RunicGatewayRaids.cs` is the **RaidableBases helper** (docs/modules/rust/PLAN_REDESIGNS.md §11.4, D204–D207,
|
||||
D387). RaidableBases' API spawns nothing and despawns nothing, so an event's base cannot be the event's from the
|
||||
outside. The helper reaches its internals by reflection and patches its spawn queue, `OpenEvent` and each base's
|
||||
`BypassUseOwners()` with Harmony: it tags the base a run asked for at birth, applies that spawn's own PvP, NPCs,
|
||||
despawn time and lock on a copy of its profile, places one at a spot after checking it, despawns that one base at
|
||||
the run's end, and reloads RaidableBases' profiles when the site edits them. It serves both editions, free and
|
||||
paid, and reads every difficulty, profile and base name from the installed plugin. It is optional: without it the
|
||||
bridge still reports raid bases on the feed and the map, and a step can still ask RaidableBases for a base where it
|
||||
picks, but that base despawns on its own timer, and the site says so. `rgr.status` in the server console says
|
||||
whether it patched, which edition's spawn call it found, and whether the plugin's spawn grid is built.
|
||||
|
||||
Oxide compiles and loads it on the write, and writes `oxide/config/RunicGateway.json` on first load:
|
||||
|
||||
```json
|
||||
@@ -101,6 +113,68 @@ 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
|
||||
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.
|
||||
|
||||
nivex's `RaidableBases` is optional, free or paid, and needs `CopyPaste` to paste its bases. Where it is
|
||||
loaded, a base appearing, being raided, bought or locked to a player reach the site's feed, its map's own
|
||||
*Raid bases* layer and the *Warlord* title, and an event can spawn one (`raid.spawn`). With
|
||||
`RunicGatewayRaids.cs` (above) the base is the event's: at a spot or where RaidableBases picks, with its own
|
||||
settings, despawned when the event ends; and the site can edit RaidableBases' profiles from its configuration
|
||||
page, reloading them under watch. For up to ten minutes after RaidableBases loads it builds its spawn grid and
|
||||
refuses every spawn, and a spawn then is answered "not ready". Every difficulty, profile and base name is read
|
||||
from the installed plugin.
|
||||
|
||||
nivex's `ZLevels Remastered` is optional, and the bridge only says whether it is loaded. Its levels are
|
||||
never read or sent. Its XP boosts are its own permissions (`zlevelsremastered.vip1` to `vip3`), so an
|
||||
event lends one as it lends any other permission, and it takes effect at once.
|
||||
|
||||
An event can also run console commands an admin typed (`command.run`): once, as a step, or once for each
|
||||
recipient of a reward who is online, with `{steamid}` and `{name}` filled in (the name as one quoted
|
||||
argument). That is how a plugin with no support of its own here is used from an event, for example
|
||||
RaidableBases' `rbe`. There is no allowlist: the site's switchboard is the switch, and `EventsEnabled`
|
||||
here is the server's. Each request runs at most once: its key is written to
|
||||
`data/RunicGateway/commands-run.json` before anything runs, and is kept for seven days. A step's
|
||||
teardown commands are kept with its key too, so the site can have them run even for a step that failed
|
||||
part-way or whose answer was lost (`teardownOf`). A command the
|
||||
server does not know stops the request there as `unknown-command`. On a framework with
|
||||
`consolePlugins=False`, that is every plugin's command.
|
||||
|
||||
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
|
||||
|
||||
```
|
||||
|
||||
14
overlay.toml
14
overlay.toml
@@ -26,15 +26,23 @@
|
||||
# the same change that alters the emitters, exactly as the sidecar bumps
|
||||
# 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
|
||||
# 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 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),
|
||||
# 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.
|
||||
protocol = 13
|
||||
# save that answers at once and reports its reload as a `config.outcome` frame,
|
||||
# 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 ──────────────────────────────────────────────────────
|
||||
#
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1697
overlay/oxide/plugins/RunicGatewayRaids.cs
Normal file
1697
overlay/oxide/plugins/RunicGatewayRaids.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user