From f428bd210f15dcd18fdd2eb0020f199353ca4cd7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 00:47:10 +0000 Subject: [PATCH] Rank title renames now reach every player (server ranks recomputed too) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01DjMbLQujBHunCCu5GpsHaT --- panel/server/src/routes/leveling.rs | 22 ++++++++++++++++--- panel/server/tests/rank_titles.rs | 33 +++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 panel/server/tests/rank_titles.rs diff --git a/panel/server/src/routes/leveling.rs b/panel/server/src/routes/leveling.rs index 1df29fb..3c46009 100644 --- a/panel/server/src/routes/leveling.rs +++ b/panel/server/src/routes/leveling.rs @@ -297,14 +297,25 @@ fn validate_rank_mapping(data: &serde_json::Map) -> AppResult<()> Ok(()) } +/// Titles and rank names are shown from a copy kept on each player, so they must be recomputed whenever a reward is +/// created, renamed, moved or deleted. Otherwise a rename on the admin page never reaches the launcher. async fn refresh_global_titles(state: &AppState) -> AppResult<()> { - let players: Vec<(String, i64)> = sqlx::query_as("SELECT uuid, global_xp FROM user_levels").fetch_all(&state.db).await?; + let mut tx = state.db.begin().await?; + let players: Vec<(String, i64)> = sqlx::query_as("SELECT uuid, global_xp FROM user_levels").fetch_all(&mut *tx).await?; for (uuid, xp) in players { let level = level_from_xp(xp).0; let title: Option = sqlx::query_scalar("SELECT reward_name FROM level_rewards WHERE level_type='global' AND reward_type='title' AND level_req<=? ORDER BY level_req DESC,id DESC LIMIT 1") - .bind(level).fetch_optional(&state.db).await?; - sqlx::query("UPDATE user_levels SET title=? WHERE uuid=?").bind(title).bind(uuid).execute(&state.db).await?; + .bind(level).fetch_optional(&mut *tx).await?; + sqlx::query("UPDATE user_levels SET title=? WHERE uuid=?").bind(title).bind(uuid).execute(&mut *tx).await?; } + let members: Vec<(i64, String, i64)> = sqlx::query_as("SELECT server_id, uuid, server_xp FROM server_levels").fetch_all(&mut *tx).await?; + for (server_id, uuid, xp) in members { + let level = level_from_xp(xp).0; + let rank: Option = sqlx::query_scalar("SELECT reward_name FROM level_rewards WHERE level_type='server' AND server_id=? AND reward_type='title' AND level_req<=? ORDER BY level_req DESC,id DESC LIMIT 1") + .bind(server_id).bind(level).fetch_optional(&mut *tx).await?; + sqlx::query("UPDATE server_levels SET rank_name=? WHERE server_id=? AND uuid=?").bind(rank).bind(server_id).bind(uuid).execute(&mut *tx).await?; + } + tx.commit().await?; Ok(()) } @@ -379,12 +390,17 @@ pub async fn admin_update_reward( sqlx::query( "UPDATE level_rewards SET + level_type = COALESCE(?, level_type), + server_id = CASE WHEN ? = 1 THEN ? ELSE server_id END, level_req = ?, reward_type = ?, reward_name = ?, reward_data = ? WHERE id = ?", ) + .bind(payload.level_type.clone()) + .bind(payload.level_type.is_some() as i64) + .bind(payload.server_id) .bind(level_req) .bind(payload.reward_type) .bind(reward_name) diff --git a/panel/server/tests/rank_titles.rs b/panel/server/tests/rank_titles.rs new file mode 100644 index 0000000..82857c2 --- /dev/null +++ b/panel/server/tests/rank_titles.rs @@ -0,0 +1,33 @@ +mod common; +use common::*; + +#[tokio::test] +async fn renaming_a_rank_title_reaches_players() { + let t = setup().await; + let admin = t.login("admin", "supersecret").await; + let (_, user) = t.call("POST", "/api/admin/users", Some(&admin), Some(json!({"username":"Steve","password":"password123"}))).await; + assert!(user.get("id").is_some() || user.get("user").is_some(), "{user}"); + let steve = t.login("Steve", "password123").await; + let uuid = t.uuid("Steve").await; + let (_, server) = t.call("POST", "/api/admin/servers", Some(&admin), Some(json!({"name":"SMP","instance_id":"smp"}))).await; + let sid = server["server"]["id"].as_i64().unwrap(); + sqlx::query("INSERT INTO server_levels (server_id, uuid, server_xp, server_level, updated_at) VALUES (?, ?, 100000, 10, 'now') ON CONFLICT(server_id, uuid) DO UPDATE SET server_xp = 100000") + .bind(sid).bind(&uuid).execute(&t.db).await.unwrap(); + sqlx::query("INSERT INTO user_levels (uuid, global_xp, global_level, updated_at) VALUES (?, 100000, 10, 'now') ON CONFLICT(uuid) DO UPDATE SET global_xp = 100000").bind(&uuid).execute(&t.db).await.unwrap(); + + let (s, made) = t.call("POST", "/api/admin/rewards", Some(&admin), Some(json!({"level_type":"server","server_id":sid,"level_req":2,"reward_type":"title","reward_name":"Squire"}))).await; + assert_eq!(s, StatusCode::OK, "{made}"); + let id = made["id"].as_i64().unwrap(); + let rank = |v: &Value| v["server_levels"][0]["rank_name"].clone(); + let (_, levels) = t.call("GET", &format!("/api/v1/levels/player/{uuid}"), Some(&steve), None).await; + assert_eq!(rank(&levels), "Squire", "{levels}"); + + let (s, r) = t.call("PUT", &format!("/api/admin/rewards/{id}"), Some(&admin), Some(json!({"level_type":"server","server_id":sid,"level_req":2,"reward_type":"title","reward_name":"Knight"}))).await; + assert_eq!(s, StatusCode::OK, "{r}"); + let (_, levels) = t.call("GET", &format!("/api/v1/levels/player/{uuid}"), Some(&steve), None).await; + assert_eq!(rank(&levels), "Knight", "a renamed rank shows up straight away"); + + assert_eq!(t.call("DELETE", &format!("/api/admin/rewards/{id}"), Some(&admin), None).await.0, StatusCode::OK); + let (_, levels) = t.call("GET", &format!("/api/v1/levels/player/{uuid}"), Some(&steve), None).await; + assert!(rank(&levels).is_null(), "{levels}"); +}