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
This commit is contained in:
2026-10-16 15:18:51 -05:00
parent d61470503f
commit b8e860d85e
2 changed files with 52 additions and 0 deletions

View File

@@ -213,6 +213,13 @@ use tracing_subscriber::EnvFilter;
/// 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, §20 the step editor;

View File

@@ -135,6 +135,11 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> {
// 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.
@@ -597,6 +602,19 @@ async fn commands_run(State(st): State<AppState>, Json(body): Json<Value>) -> Re
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
@@ -1397,6 +1415,33 @@ mod tests {
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]