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
This commit is contained in:
2026-10-15 12:26:47 -05:00
parent 42d63121e0
commit f96878441d
2 changed files with 32 additions and 0 deletions

View File

@@ -128,6 +128,9 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> {
// 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 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.
@@ -573,6 +576,13 @@ async fn dt_start(State(st): State<AppState>, Json(body): Json<Value>) -> Respon
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
}
/// 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
@@ -1319,6 +1329,23 @@ mod tests {
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 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]