diff --git a/sidecar/src/main.rs b/sidecar/src/main.rs index 5f554e9..9059e01 100644 --- a/sidecar/src/main.rs +++ b/sidecar/src/main.rs @@ -175,11 +175,19 @@ 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. +/// /// `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)) { diff --git a/sidecar/src/web.rs b/sidecar/src/web.rs index bff518a..3338d73 100644 --- a/sidecar/src/web.rs +++ b/sidecar/src/web.rs @@ -130,6 +130,10 @@ 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)) .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 @@ -579,6 +583,13 @@ async fn kits(State(st): State) -> 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, Json(body): Json) -> Response { + forward_object(&st, body, "kits.uses", "a kit-use question").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, Json(body): Json) -> Response { @@ -1158,6 +1169,32 @@ 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 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]