diff --git a/sidecar/README.md b/sidecar/README.md index dac655e..7fcc47a 100644 --- a/sidecar/README.md +++ b/sidecar/README.md @@ -57,6 +57,7 @@ still authenticate). Every response carries `X-RustLink-Version`. | `GET /feed?since=&limit=` | store | **Oldest first, from a cursor.** For a consumer that must not miss a row. Omitting `since` asks where the end is | | `GET /status` | plugin (RPC) | A live round trip. `503` with no plugin, `504` on no reply | | `GET /ws` | broadcast | The live feed. Sends `ws.hello` on connect | +| every later route | plugin (RPC) | One thin forward per command, from `POST /link/confirm` (protocol 3) to `POST /titles` (protocol 12). Each is listed beside its protocol in `src/web.rs`, and its body in `docs/rust-link/PROTOCOL.md` | The split is the point: the store-backed reads answer while the game is off, which is what lets the website render a server list during a wipe or a restart. `/status` is the one route that fails when diff --git a/sidecar/src/main.rs b/sidecar/src/main.rs index 6572502..b61a2e6 100644 --- a/sidecar/src/main.rs +++ b/sidecar/src/main.rs @@ -163,8 +163,8 @@ use tracing_subscriber::EnvFilter; /// /// `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; this constant is one of its four declaration sites. -pub const PROTOCOL_VERSION: u32 = 11; +/// §16 the rewards, §17 the map, §18 the optional mods; this constant is one of its four declaration sites. +pub const PROTOCOL_VERSION: u32 = 12; 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 12b027c..6a38d1f 100644 --- a/sidecar/src/web.rs +++ b/sidecar/src/web.rs @@ -135,6 +135,10 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> { .route("/map/chunk", get(map_chunk)) .route("/map/render", post(map_render)) .route("/map/live", get(map_live)) + // Protocol 12 (§33): the chat titles a player has earned. A whole set, replaced each time, + // held by the plugin in memory and read by BetterChat on the chat path. Nothing here knows + // what a title is. + .route("/titles", post(titles)) .route_layer(middleware::from_fn_with_state(state.clone(), gate)); let app = Router::new() @@ -565,6 +569,12 @@ async fn chat(State(st): State, Json(body): Json) -> Response { forward_object(&st, body, "chat.say", "a chat line").await } +/// Replace the chat titles this server's plugin holds (protocol 12). The set is whole, so a retry +/// is harmless, and the plugin answers whether BetterChat is there to show them. +async fn titles(State(st): State, Json(body): Json) -> Response { + forward_object(&st, body, "titles.set", "a title set").await +} + /// What this map is, where its picture comes from, and its monuments (protocol 11, stage one). /// Live, and never cached here: a wipe changes the answer, and the module compares its key and /// hash against what it holds to decide whether to fetch at all. @@ -1097,6 +1107,24 @@ mod tests { assert!(stamp(json!(["9"]), "tally.close", "r-4").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] + fn a_protocol_12_title_set_passes_through_untouched() { + let body = json!({ + "cmd": "chat.say", + "setId": "s-1", + "titles": [{ "steamId": "76561198000000001", "text": "[#ff8800]Top Killer[/#]" }], + }); + let stamped = stamp(body, "titles.set", "r-5").expect("an object is stamped"); + + assert_eq!(stamped["cmd"], "titles.set"); + assert_eq!(stamped["reqId"], "r-5"); + assert_eq!(stamped["setId"], "s-1"); + assert_eq!(stamped["titles"][0]["text"], "[#ff8800]Top Killer[/#]"); + assert!(stamp(json!("titles"), "titles.set", "r-6").is_none()); + } + /// Protocol 11's one opaque forward is stamped like the rest: a render cannot become a world /// placement by naming one. #[test]