17 Commits

Author SHA1 Message Date
5c4eaf5a31 Merge pull request 'chore(release): cut edge over to main — protocol 14' (#32) from edge into main
All checks were successful
Release sidecar / release (push) Successful in -2m18s
Reviewed-on: #32
2026-10-22 10:23:04 +00:00
a61d7794d9 Merge pull request 'feat(sidecar): protocol 14 — the raid base spawn and spot check forwards (PLAN_REDESIGNS §11.4)' (#31) from feat/raidable-bases into edge
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 10m45s
Reviewed-on: #31
2026-10-17 06:32:54 +00:00
b8e860d85e feat(sidecar): protocol 14 — the raid base spawn and spot check forwards (PLAN_REDESIGNS §11.4)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 7m7s
POST /raid/spawn stamps the body raid.spawn and POST /raid/check stamps it
raid.check, and each forwards it. The plugin keeps the key and the run's
base in its world registry, so world.revert takes it back like a zone;
nothing is kept here. The five raid.* events ride the feed like any other.
Spec: docs rust-link/PROTOCOL.md §20.12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-16 15:18:51 -05:00
d61470503f Merge pull request 'feat(sidecar): protocol 14 — the console command run forward (PLAN_REDESIGNS §16)' (#30) from feat/command-steps into edge
Reviewed-on: #30
2026-10-16 07:47:50 +00:00
6bf8a3624e feat(sidecar): protocol 14 — the console command run forward (PLAN_REDESIGNS §16)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 3m21s
`POST /commands/run`, stamped `command.run` the way `world.call`,
`dt.start` and `gc.place` are (PROTOCOL.md §20.11). The plugin keeps the
key, fills {steamid} and {name} and decides who is online; the sidecar
moves the line and knows nothing of what a command does (D384, no
allowlist). Joins protocol 14, still unreleased on edge.

Stamp test: the body's key, commands and recipients pass through byte for
byte, placeholders and quotes included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-16 02:37:12 -05:00
532abc5761 Merge pull request 'feat(sidecar): protocol 14 — the guarded crate placement forward (PLAN_REDESIGNS §14.3.5)' (#29) from feat/guarded-crate into edge
Reviewed-on: #29
2026-10-15 23:18:55 +00:00
f96878441d feat(sidecar): protocol 14 — the guarded crate placement forward (PLAN_REDESIGNS §14.3.5)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m44s
POST /gc/place stamps the body gc.place and forwards it; the plugin keeps the
key and the crate is kept when the run ends, so nothing is kept here. Spec:
docs rust-link/PROTOCOL.md §20.9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-15 12:26:47 -05:00
42d63121e0 Merge pull request 'feat(sidecar): protocol 14 — the Dangerous Treasures start forward (PLAN_REDESIGNS §14.3.4)' (#28) from feat/dangerous-treasures into edge
Reviewed-on: #28
2026-10-15 12:04:43 +00:00
1ab0cab80a feat(sidecar): protocol 14 — the Dangerous Treasures start forward (PLAN_REDESIGNS §14.3.4)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m46s
POST /dt/start, stamped dt.start whatever its body names, forwarded to the
plugin and answered with its dt.opened or dt.error. The plugin remembers the
key; DT despawns its own chest; nothing is kept here. dt.started, dt.won and
dt.ended ride the feed like any other event (docs rust-link/PROTOCOL.md §20.8).

72 tests (one new), fmt and clippy clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-15 06:45:27 -05:00
847c3baa07 Merge pull request 'feat(sidecar): protocol 14 — the world event call forward (PLAN_REDESIGNS §14.3.1)' (#27) from feat/world-events into edge
Reviewed-on: #27
2026-10-15 07:19:33 +00:00
4ad5cb0545 feat(sidecar): protocol 14 — the world event call forward (PLAN_REDESIGNS §14.3.1)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 5m49s
POST /world/call, a thin forward like /world/place: the body is stamped
`world.call` whatever it names, so the route cannot place, revert or zone
anything. What a call starts is kept, so nothing is remembered here; the
plugin keeps the call's key. The `world.event` frames (24 types, told apart by
`event`) ride the feed like any other event, and hello's `console` like any
other field. Still protocol 14 (D332); one new test, 71 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
5b2471b110 Merge pull request 'feat(sidecar): protocol 14 — the Backpacks forward (PLAN_REDESIGNS §11.3)' (#26) from feat/rust-backpacks into edge
Reviewed-on: #26
2026-10-12 19:41:45 +00:00
a95ecb8e25 feat(sidecar): protocol 14 — the Backpacks forward (PLAN_REDESIGNS §11.3)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m29s
POST /backpacks/read, a thin forward like /kits/uses: the body is stamped
`backpacks.read` whatever it names, so the route can only ever read. Nothing is
kept here. The bigger backpack an event lends is a permission in an ordinary
perm.sync (D337) and needs nothing new. Still protocol 14 (D332); one test.

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
4eac5297e8 Merge pull request 'feat(sidecar): protocol 14 — the Economics forwards (PLAN_REDESIGNS §11.2)' (#25) from feat/protocol-14-economics into edge
Reviewed-on: #25
2026-10-12 12:40:38 +00:00
e720c21c09 feat(sidecar): protocol 14 — the Economics forwards (PLAN_REDESIGNS §11.2)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 4m7s
POST /economics/pay, /economics/balances and /economics/adjust, thin
forwards like /kits/uses: each body is stamped with its own command and
passed to the plugin, which remembers a payment's key. Nothing is kept here.
The tally's `balance` and the `economics.wiped` event ride the feed like any
frame. Still protocol 14 (D332); one forward test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-12 06:53:51 -05:00
21c9af645d Merge pull request 'feat(sidecar): protocol 14 — POST /kits/uses (PLAN_REDESIGNS §2.6)' (#24) from feat/protocol-14-step-editor into edge
Reviewed-on: #24
2026-10-12 09:52:22 +00:00
ef4fdb6e25 feat(sidecar): protocol 14 — POST /kits/uses (PLAN_REDESIGNS §2.6)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 16m11s
Opens protocol 14 (D332) for the step editor: one thin forward, POST /kits/uses
-> kits.uses ({kit, steamIds} in, counts out), for the kit weekend's warning.
perm.sync's new notices (D333) pass through untouched like the rest of the
body. PROTOCOL_VERSION 13 -> 14; one forward test.

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

View File

@@ -175,11 +175,56 @@ use tracing_subscriber::EnvFilter;
/// `POST /permissions/inventory`: the plugin's whole store, in pages it sizes to the game link's
/// line cap. Forwarded like every other object; the pages are never kept here.
///
/// # Protocol 14 — the step editor (opened 2026-10-12)
///
/// Opened by the module's PLAN_REDESIGNS.md §2 (D332), after protocol 13 shipped with RunicNPC.
/// One route, `POST /kits/uses`: how many of a list of players have used a kit up, or are waiting
/// out its cooldown — counts, never a list. And `perm.sync` gains `notices`, the one-time "you
/// earned a kit" messages (D333), with `noticesTold` in its report; both pass through untouched
/// like the rest of that body. The first-class optional plugins (§11) join this protocol too.
///
/// Economics (§20.3, the module's PLAN_REDESIGNS.md §11.2) adds three forwards:
/// `POST /economics/pay` (coins from an event step, kept once paid), `POST /economics/balances`
/// and `POST /economics/adjust` (staff). The plugin remembers a payment's key; this process keeps
/// nothing. The tally's `balance` and the `economics.wiped` event ride the feed like any frame.
///
/// Backpacks (§20.4, the module's PLAN_REDESIGNS.md §11.3) adds one: `POST /backpacks/read`,
/// one player's backpack, read live. The plugin never writes a backpack, and a bigger one an event
/// lends is a `backpacks.size.N` permission in an ordinary `perm.sync`, so it needs nothing here.
///
/// Rust's own world events (§20.5, the module's PLAN_REDESIGNS.md §14.3.1) add one forward,
/// `POST /world/call`: run one of the game's own world events now, for a run. It is kept, so the
/// plugin remembers only the call's key and its run, and this process nothing. The `world.event`
/// frames (24 of them, told apart by `event`) ride the feed like any other event, and hello's
/// `console` like any other field.
///
/// Dangerous Treasures (§20.8, the module's PLAN_REDESIGNS.md §14.3.4) adds one forward,
/// `POST /dt/start`: open one treasure event for a run's step. The plugin remembers the key so a
/// retry never opens a second; DT despawns its own chest. `dt.started`, `dt.won` and `dt.ended`
/// ride the feed like any other event.
///
/// Guarded Crate (§20.9, the module's PLAN_REDESIGNS.md §14.3.5) adds one forward,
/// `POST /gc/place`: place one guarded crate for a run's step. The plugin remembers the key so a
/// retry never places a second; the crate is kept when the run ends (D373). `gc.started`,
/// `gc.ended` and `gc.failed` ride the feed like any other event.
///
/// Console commands as event steps (§20.11, the module's PLAN_REDESIGNS.md §16) add one forward,
/// `POST /commands/run`: the commands a step's author typed, run once or once per online
/// recipient. The plugin remembers the key so nothing runs twice; this process keeps nothing and
/// knows nothing of what a command does.
///
/// RaidableBases (§20.12, the module's PLAN_REDESIGNS.md §11.4) adds two forwards: `POST
/// /raid/spawn`, one base for a run's step, and `POST /raid/check`, a spot's checks with nothing
/// spawned. The plugin keeps the key and the run's base in its world registry, so `world.revert`
/// takes it back like a zone and `world.expired` says when the plugin ended it first. `raid.started`,
/// `raid.completed`, `raid.ended`, `raid.purchased` and `raid.locked` ride the feed like any other
/// event.
///
/// `docs/rust-link/PROTOCOL.md` is the specification — §8 the read path, §9 identity, §10 the
/// mirror, §11 configuration, §12 clans, §13 the raid frame, §14 the leases, §15 the world verbs,
/// §16 the rewards, §17 the map, §18 the optional mods, §19 the walk's fixes; this constant is one
/// of its four declaration sites.
pub const PROTOCOL_VERSION: u32 = 13;
/// §16 the rewards, §17 the map, §18 the optional mods, §19 the walk's fixes, §20 the step editor;
/// this constant is one of its four declaration sites.
pub const PROTOCOL_VERSION: u32 = 14;
fn main() -> anyhow::Result<()> {
let args = match cli::parse(std::env::args().skip(1)) {

View File

@@ -123,6 +123,23 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> {
.route("/world/zone", post(world_zone))
.route("/world/place", post(world_place))
.route("/world/revert", post(world_revert))
// Protocol 14 (§20.5): one of the game's own world events, now. Kept, never reverted.
.route("/world/call", post(world_call))
// Protocol 14 (§20.8): open one Dangerous Treasures event. The plugin remembers the key;
// DT picks the spot and despawns its own chest, so nothing here is kept either.
.route("/dt/start", post(dt_start))
// Protocol 14 (§20.9): place one guarded crate. The plugin remembers the key; the crate is
// kept when the run ends (Guarded Crate cannot end one alone), so nothing is kept here.
.route("/gc/place", post(gc_place))
// Protocol 14 (§20.11): console commands a step's author typed. The plugin keeps the key
// so nothing runs twice, fills the placeholders and decides who is online; this process
// moves the line and knows nothing of what any command does (no allowlist, D384).
.route("/commands/run", post(commands_run))
// Protocol 14 (§20.12): one RaidableBases base for a run, and a spot's checks. The plugin
// keeps the key and the run's base in its world registry, so `world.revert` takes it back
// like a zone; nothing is kept here.
.route("/raid/spawn", post(raid_spawn))
.route("/raid/check", post(raid_check))
// Protocol 10 (§29): who took part, what they may redeem, and a line in chat. The tally
// is counted and held by the plugin, the kit catalogue is Kits' own, and whether a chat
// line was already said is the plugin's memory. These move lines, like everything above.
@@ -130,6 +147,17 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> {
.route("/tally/snapshot", get(tally_snapshot))
.route("/tally/close", post(tally_close))
.route("/kits", get(kits))
// Protocol 14 (the module's PLAN_REDESIGNS.md §2.6): how many of the given players have
// used a kit up or are on its cooldown, for the warning beside a kit weekend. The plugin
// reads Kits and answers counts; the list of players is the website's and is not kept.
.route("/kits/uses", post(kits_uses))
// Protocol 14, Economics (§20.3): thin forwards. The plugin remembers a payment's key so a
// retry never pays twice; nothing about a payment or a balance is kept here.
.route("/economics/pay", post(economics_pay))
.route("/economics/balances", post(economics_balances))
.route("/economics/adjust", post(economics_adjust))
// Protocol 14, Backpacks (§20.4): one player's backpack, read live and never kept here.
.route("/backpacks/read", post(backpacks_read))
.route("/chat", post(chat))
// Protocol 11 (§30): the map. The picture passes through in slices and is never kept
// here, and positions are asked for while somebody is looking and never filed (D111). The
@@ -543,6 +571,50 @@ async fn world_revert(State(st): State<AppState>, Json(body): Json<Value>) -> Re
forward_object(&st, body, "world.revert", "a revert").await
}
/// Run one of the game's own world events for a run (protocol 14): a patrol helicopter, a cargo
/// ship, an airdrop and the rest. `world.called` (`called`, and `repeat` for a key already seen)
/// or `world.error` with a reason. What it starts is kept, so nothing is remembered here.
async fn world_call(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
forward_object(&st, body, "world.call", "a world event call").await
}
/// Open one Dangerous Treasures event for a run's step (protocol 14). `dt.opened` (where it
/// opened, and `repeat` for a key already seen) or `dt.error` with a reason, `dt-not-ready` among
/// them. DT despawns its own chest, so nothing is remembered here.
async fn dt_start(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
forward_object(&st, body, "dt.start", "a Dangerous Treasures start").await
}
/// Place one guarded crate for a run's step (protocol 14). `gc.placed` (where it landed, and
/// `repeat` for a key already seen) or `gc.error` with a reason, `npcspawn-missing` among them.
/// Guarded Crate ends its own event, so nothing is remembered here.
async fn gc_place(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
forward_object(&st, body, "gc.place", "a guarded crate placement").await
}
/// Run the console commands a step's author typed (protocol 14): once, or once per online
/// recipient. `command.ran` (how many ran, the console's replies, who was skipped as offline, and
/// `repeat` for a key already seen) or `command.error` with a reason, `unknown-command` among
/// them. A step's `teardown` rides its run and the plugin keeps it with the key; `teardownOf`
/// asks for the teardown kept under another key (D385). The plugin remembers the key, so nothing
/// is remembered here.
async fn commands_run(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
forward_object(&st, body, "command.run", "a console command run").await
}
/// Spawn one RaidableBases base for a run's step (protocol 14). `raid.queued` (its id, when the
/// raids helper made it the run's, and `repeat` for a key already seen) or `raid.error` with a
/// reason. The plugin's world registry holds the run's base; nothing is remembered here.
async fn raid_spawn(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
forward_object(&st, body, "raid.spawn", "a raid base spawn").await
}
/// Check a spot for a raid base without spawning one (protocol 14): `raid.checked`, with the
/// refusal's reason and the warnings, or `raid.error` when the spot cannot be checked at all.
async fn raid_check(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
forward_object(&st, body, "raid.check", "a raid base spot check").await
}
/// Start counting who takes part in a run. `tally.ok` or `tally.error` with a reason.
async fn tally_open(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
forward_object(&st, body, "tally.open", "a tally").await
@@ -579,6 +651,38 @@ async fn kits(State(st): State<AppState>) -> Response {
respond(st.rpc.call(&st.game, command, &req_id).await)
}
/// How many of a list of players have used one kit up, and how many are on its cooldown (protocol
/// 14). The body is `{kit, steamIds}` and is forwarded as it is; the answer is counts. A POST
/// because the list rides in the body — it can be as long as a server's linked players.
async fn kits_uses(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
forward_object(&st, body, "kits.uses", "a kit-use question").await
}
/// Coins to a list of players from an event step (protocol 14). The body is `{key, amount,
/// steamIds, message?, delivery?}`; the answer is `economics.paid` with each player's balance
/// before and after, or `economics.error` with a reason.
async fn economics_pay(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
forward_object(&st, body, "economics.pay", "a payment").await
}
/// The balances of a list of players, with the server's starting balance (protocol 14).
async fn economics_balances(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
forward_object(&st, body, "economics.balances", "a balance question").await
}
/// Staff deposit, withdraw or set one player's balance (protocol 14). The body is `{key, steamId,
/// op, amount}`; the answer is `economics.adjusted` with the balance before and after.
async fn economics_adjust(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
forward_object(&st, body, "economics.adjust", "a balance adjustment").await
}
/// One player's backpack (protocol 14). The body is `{steamId}`; the answer is
/// `backpacks.contents` (`exists`, and with a backpack its `capacity`, `pageSize` and `items`), or
/// `backpacks.error` with a reason. The plugin only reads, and nothing is kept here.
async fn backpacks_read(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
forward_object(&st, body, "backpacks.read", "a backpack read").await
}
/// Say one line in the server's chat. A repeated key is answered `ok` by the plugin and not said
/// again, so a retried step never repeats itself.
async fn chat(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
@@ -1158,6 +1262,186 @@ mod tests {
assert!(stamp(json!([0]), "perm.inventory", "r-8").is_none());
}
/// Protocol 14: a kit-use question keeps its list and cannot become another command, and a
/// sync's `notices` pass through it untouched like every other part of the body.
#[test]
fn a_protocol_14_kit_use_question_and_notices_pass_through() {
let body = json!({ "cmd": "perm.sync", "kit": "vip", "steamIds": ["76561190000000001", "76561190000000002"] });
let stamped = stamp(body, "kits.uses", "r-12").expect("an object is stamped");
assert_eq!(stamped["cmd"], "kits.uses");
assert_eq!(stamped["reqId"], "r-12");
assert_eq!(stamped["kit"], "vip");
assert_eq!(stamped["steamIds"][1], "76561190000000002");
assert!(stamp(json!(["vip"]), "kits.uses", "r-13").is_none());
let body = json!({
"setId": "s-2",
"notices": [{ "key": "run:4:step:9", "steamId": "76561190000000001", "message": "You earned kit vip — type /kit vip", "delivery": "popup" }],
});
let stamped = stamp(body, "perm.sync", "r-14").expect("an object is stamped");
assert_eq!(
stamped["notices"][0]["message"],
"You earned kit vip — type /kit vip"
);
assert_eq!(stamped["notices"][0]["delivery"], "popup");
}
/// Protocol 14, Economics: each body keeps its fields and is stamped with its own command, so
/// a payment cannot be sent as an adjustment by naming one in its body.
#[test]
fn protocol_14_economics_bodies_pass_through() {
let body = json!({ "cmd": "economics.adjust", "key": "7:4", "amount": 500.0, "steamIds": ["76561190000000001"], "message": "You earned {amount} coins", "delivery": "popup" });
let stamped = stamp(body, "economics.pay", "r-31").expect("an object is stamped");
assert_eq!(stamped["cmd"], "economics.pay");
assert_eq!(stamped["reqId"], "r-31");
assert_eq!(stamped["key"], "7:4");
assert_eq!(stamped["amount"], 500.0);
assert_eq!(stamped["message"], "You earned {amount} coins");
let stamped = stamp(
json!({ "steamIds": ["76561190000000001"] }),
"economics.balances",
"r-32",
)
.expect("an object is stamped");
assert_eq!(stamped["cmd"], "economics.balances");
assert_eq!(stamped["steamIds"][0], "76561190000000001");
let body = json!({ "key": "adj-1", "steamId": "76561190000000001", "op": "withdraw", "amount": 340.5 });
let stamped = stamp(body, "economics.adjust", "r-33").expect("an object is stamped");
assert_eq!(stamped["cmd"], "economics.adjust");
assert_eq!(stamped["op"], "withdraw");
assert_eq!(stamped["amount"], 340.5);
assert!(stamp(json!([1]), "economics.pay", "r-34").is_none());
}
/// Protocol 14, Backpacks: a read is stamped `backpacks.read` whatever its body names, so the
/// route can only ever read.
#[test]
fn protocol_14_backpacks_read_is_stamped() {
let body = json!({ "cmd": "backpacks.write", "steamId": "76561198000000001" });
let stamped = stamp(body, "backpacks.read", "r-40").expect("an object is stamped");
assert_eq!(stamped["cmd"], "backpacks.read");
assert_eq!(stamped["reqId"], "r-40");
assert_eq!(stamped["steamId"], "76561198000000001");
assert!(stamp(json!("76561198000000001"), "backpacks.read", "r-41").is_none());
}
/// Protocol 14, world events: a call is stamped `world.call` whatever its body names, so the
/// route cannot place, revert or zone anything, and its params pass through untouched.
#[test]
fn protocol_14_world_call_is_stamped() {
let body = json!({ "cmd": "world.revert", "reqId": "theirs", "runId": "9", "key": "9:2",
"event": "airdrop", "count": 3, "x": -412.0, "z": 1180.5 });
let stamped = stamp(body, "world.call", "r-50").expect("an object is stamped");
assert_eq!(stamped["cmd"], "world.call");
assert_eq!(stamped["reqId"], "r-50");
assert_eq!(stamped["event"], "airdrop");
assert_eq!(stamped["count"], 3);
assert_eq!(stamped["x"], -412.0);
assert!(stamp(json!("airdrop"), "world.call", "r-51").is_none());
}
/// Protocol 14, Dangerous Treasures: a start is stamped `dt.start` whatever its body names, so
/// the route can open a treasure event and nothing else, and its key and level pass through.
#[test]
fn protocol_14_dt_start_is_stamped() {
let body = json!({ "cmd": "world.call", "reqId": "theirs", "key": "12:3", "level": 2 });
let stamped = stamp(body, "dt.start", "r-60").expect("an object is stamped");
assert_eq!(stamped["cmd"], "dt.start");
assert_eq!(stamped["reqId"], "r-60");
assert_eq!(stamped["key"], "12:3");
assert_eq!(stamped["level"], 2);
assert!(stamp(json!(2), "dt.start", "r-61").is_none());
}
/// Protocol 14, Guarded Crate: a placement is stamped `gc.place` whatever its body names, so
/// the route can place a guarded crate and nothing else, and its key, event and location pass
/// through.
#[test]
fn protocol_14_gc_place_is_stamped() {
let body = json!({ "cmd": "dt.start", "reqId": "theirs", "key": "12:4", "event": "Elite", "monument": "powerplant_1", "offsetX": 10.0 });
let stamped = stamp(body, "gc.place", "r-62").expect("an object is stamped");
assert_eq!(stamped["cmd"], "gc.place");
assert_eq!(stamped["reqId"], "r-62");
assert_eq!(stamped["key"], "12:4");
assert_eq!(stamped["event"], "Elite");
assert_eq!(stamped["monument"], "powerplant_1");
assert_eq!(stamped["offsetX"], 10.0);
assert!(stamp(json!("Elite"), "gc.place", "r-63").is_none());
}
/// Protocol 14, console commands: a run is stamped `command.run` whatever its body names, so
/// the route runs commands and nothing else, and its key, commands and recipients pass through
/// byte for byte; the placeholders are the plugin's to fill.
#[test]
fn protocol_14_command_run_is_stamped() {
let body = json!({ "cmd": "gc.place", "reqId": "theirs", "key": "12:5",
"commands": ["sr add {steamid} 100", "say \"{name} won\""],
"recipients": [{ "steamId": "76561198000000001", "name": "Some \"One\"" }] });
let stamped = stamp(body, "command.run", "r-64").expect("an object is stamped");
assert_eq!(stamped["cmd"], "command.run");
assert_eq!(stamped["reqId"], "r-64");
assert_eq!(stamped["key"], "12:5");
assert_eq!(stamped["commands"][0], "sr add {steamid} 100");
assert_eq!(stamped["commands"][1], "say \"{name} won\"");
assert_eq!(stamped["recipients"][0]["name"], "Some \"One\"");
assert!(stamp(json!(["rbe"]), "command.run", "r-65").is_none());
// D385: a step's teardown rides its run, and a teardown asked for by key alone names the
// run it is for; both are the plugin's to read.
let run = stamp(
json!({ "key": "12:6", "commands": ["rbe 1"], "teardown": ["rbe stop"] }),
"command.run",
"r-66",
)
.expect("an object is stamped");
assert_eq!(run["teardown"][0], "rbe stop");
let teardown = stamp(
json!({ "key": "12:6:teardown", "teardownOf": "12:6" }),
"command.run",
"r-67",
)
.expect("an object is stamped");
assert_eq!(teardown["cmd"], "command.run");
assert_eq!(teardown["teardownOf"], "12:6");
}
/// Protocol 14, RaidableBases: a spawn is stamped `raid.spawn` and a check `raid.check`
/// whatever their bodies name, so each route does its one thing; the plugin's own names for
/// difficulties, profiles and bases (D386), the spot and the settings pass through untouched.
#[test]
fn protocol_14_raid_spawn_and_check_are_stamped() {
let body = json!({ "cmd": "world.revert", "reqId": "theirs", "key": "12:7", "runId": "12",
"difficulty": "Wobbly", "profile": "Wobbly Bases", "base": "wobbly1",
"x": 650.0, "z": 2037.0, "heightAdjust": 5.0,
"pvp": false, "npcs": true, "npcCount": 0, "despawnMinutes": 90, "lock": true });
let spawn = stamp(body.clone(), "raid.spawn", "r-68").expect("an object is stamped");
assert_eq!(spawn["cmd"], "raid.spawn");
assert_eq!(spawn["reqId"], "r-68");
assert_eq!(spawn["key"], "12:7");
assert_eq!(spawn["difficulty"], "Wobbly");
assert_eq!(spawn["base"], "wobbly1");
assert_eq!(spawn["heightAdjust"], 5.0);
assert_eq!(spawn["npcCount"], 0);
assert_eq!(spawn["pvp"], false);
assert!(stamp(json!("Wobbly"), "raid.spawn", "r-69").is_none());
let check = stamp(body, "raid.check", "r-70").expect("an object is stamped");
assert_eq!(check["cmd"], "raid.check");
assert_eq!(check["x"], 650.0);
assert_eq!(check["profile"], "Wobbly Bases");
}
/// Protocol 12: a title set is forwarded whole. The markup inside each `text` is the module's
/// and the plugin's business, so it must arrive byte for byte as the website composed it.
#[test]