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
This commit is contained in:
2026-10-12 02:13:15 -05:00
parent 703245455b
commit ef4fdb6e25
2 changed files with 48 additions and 3 deletions

View File

@@ -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)) {

View File

@@ -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<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
}
/// 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 +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]