feat(sidecar): forward RunicNPC's profiles and placements (runicnpc stage 4)
GET and POST /npc/profiles, GET /npc/placements and POST /npc/placement: four thin forwards of the plugin's npc.* commands, stamped like every other. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -142,6 +142,12 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> {
|
||||
// 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))
|
||||
// Protocol 13, RunicNPC (runicnpc PLAN.md stage 4): the site's NPC profiles and the
|
||||
// server's placements. RunicNPC is called by the plugin, never by this process, and nothing
|
||||
// here knows a profile from a placement. Four more thin forwards.
|
||||
.route("/npc/profiles", get(npc_profiles).post(npc_profiles_set))
|
||||
.route("/npc/placements", get(npc_placements))
|
||||
.route("/npc/placement", post(npc_placement))
|
||||
.route_layer(middleware::from_fn_with_state(state.clone(), gate));
|
||||
|
||||
let app = Router::new()
|
||||
@@ -585,6 +591,32 @@ async fn titles(State(st): State<AppState>, Json(body): Json<Value>) -> Response
|
||||
forward_object(&st, body, "titles.set", "a title set").await
|
||||
}
|
||||
|
||||
/// The server's RunicNPC profiles as RunicNPC holds them: whether a site manages them, each profile,
|
||||
/// and those refused. The website reads this before its first push, to adopt them (D244). Live.
|
||||
async fn npc_profiles(State(st): State<AppState>) -> Response {
|
||||
let req_id = st.rpc.next_req_id();
|
||||
let command = json!({ "cmd": "npc.profiles", "reqId": req_id });
|
||||
respond(st.rpc.call(&st.game, command, &req_id).await)
|
||||
}
|
||||
|
||||
/// Replace the whole profile set (a push). `npc.ok` lists the profiles RunicNPC refused.
|
||||
async fn npc_profiles_set(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
|
||||
forward_object(&st, body, "npc.profiles.set", "a profile push").await
|
||||
}
|
||||
|
||||
/// Every placement, the routes one may walk and the cost warning. Live: an admin's `/rnpc` in game
|
||||
/// changes it at any moment.
|
||||
async fn npc_placements(State(st): State<AppState>) -> Response {
|
||||
let req_id = st.rpc.next_req_id();
|
||||
let command = json!({ "cmd": "npc.placements", "reqId": req_id });
|
||||
respond(st.rpc.call(&st.game, command, &req_id).await)
|
||||
}
|
||||
|
||||
/// One change to one placement, by `op`: add, set, remove, rename or respawn.
|
||||
async fn npc_placement(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
|
||||
forward_object(&st, body, "npc.placement", "a placement change").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.
|
||||
@@ -1092,6 +1124,26 @@ mod tests {
|
||||
assert!(stamp(json!(["9"]), "tally.close", "r-4").is_none());
|
||||
}
|
||||
|
||||
/// RunicNPC's writes are stamped like every other forward: a placement change cannot become a
|
||||
/// profile push, or anything else, by naming one.
|
||||
#[test]
|
||||
fn a_runicnpc_command_cannot_choose_its_own_command() {
|
||||
let body = json!({ "cmd": "npc.profiles.set", "reqId": "theirs", "op": "remove", "id": "bandit-1" });
|
||||
let stamped = stamp(body, "npc.placement", "r-9").expect("an object is stamped");
|
||||
|
||||
assert_eq!(stamped["cmd"], "npc.placement");
|
||||
assert_eq!(stamped["reqId"], "r-9");
|
||||
assert_eq!(stamped["op"], "remove");
|
||||
assert_eq!(stamped["id"], "bandit-1");
|
||||
|
||||
let body = json!({ "cmd": "npc.placement", "profiles": { "warden": { "health": 250 } } });
|
||||
let stamped = stamp(body, "npc.profiles.set", "r-10").expect("an object is stamped");
|
||||
|
||||
assert_eq!(stamped["cmd"], "npc.profiles.set");
|
||||
assert_eq!(stamped["profiles"]["warden"]["health"], 250);
|
||||
assert!(stamp(json!("warden"), "npc.profiles.set", "r-11").is_none());
|
||||
}
|
||||
|
||||
/// Protocol 13: an inventory page request keeps its paging fields and cannot become a sync.
|
||||
#[test]
|
||||
fn an_inventory_request_cannot_choose_its_own_command() {
|
||||
|
||||
Reference in New Issue
Block a user