feat(sidecar): protocol 12 — POST /titles (phase 17)
Some checks failed
PR Checks / rust-gates (pull_request) Failing after -35s

One more thin forward: the chat titles each player has earned, as a whole
set the plugin swaps in and BetterChat reads on the chat path. perm.sync's
BetterChat styles and chat.say's delivery and format pass through untouched
like the rest of their bodies. PROTOCOL_VERSION moves to 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-09-25 17:48:05 -05:00
parent 73b2523aa6
commit 46ba35d89a
3 changed files with 31 additions and 2 deletions

View File

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

View File

@@ -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<AppState>, Json(body): Json<Value>) -> 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<AppState>, Json(body): Json<Value>) -> 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]