Migrate CI to Gitea and improve live map diagnostics
CI / Real Minecraft installs (manual) (push) Skipped
CI / Rust, web and container (push) Canceled after 0s
CI / Windows launcher (push) Canceled after 0s
Server integrations / paper (push) Canceled after 0s
Server integrations / Fabric and Forge / 1.20.1 (push) Canceled after 0s
Server integrations / Fabric and Forge / 1.21.1 (push) Canceled after 0s
Server integrations / Fabric and Forge / 26.1.2 (push) Canceled after 0s
Server integrations / Fabric and Forge / 26.2 (push) Canceled after 0s
Server integrations / Fabric and Forge / 26.3 (push) Canceled after 0s

This commit is contained in:
SCOPEDD committed 2026-10-01 00:07:35 -04:00
1 parent d72d7c2e80
commit 7809d47d39
32 files changed
+518 -325

No files matched your search

+1
View File
@@ -259,6 +259,7 @@ pub fn api(state: &AppState) -> Router<AppState> {
.route("/map/tiles", post(worldmap::game_tiles))
.route("/map/players", post(worldmap::game_players))
.route("/map/overlay", post(worldmap::game_overlay))
.route("/map/diagnostics", post(worldmap::game_diagnostics))
.layer(DefaultBodyLimit::max(10 * 1024 * 1024));
Router::new()
+14
View File
@@ -89,6 +89,19 @@ pub async fn game_overlay(
Ok(Json(json!({ "ok": true })))
}
pub async fn game_diagnostics(
GameServer(server): GameServer,
State(state): State<AppState>,
Json(value): Json<Value>,
) -> AppResult<Json<Value>> {
require_enabled(server.map_enabled)?;
if !value.is_object() || value.to_string().len() > 4096 {
return Err(AppError::bad_request("invalid map diagnostics"));
}
state.worldmap.set_diagnostics(server.id, value);
Ok(Json(json!({ "ok": true })))
}
// ---------------------------------------------------------------------------
// Launcher and admin viewers
// ---------------------------------------------------------------------------
@@ -143,6 +156,7 @@ pub async fn admin_status(_: AdminUser, State(state): State<AppState>, Path(id):
v["stats"] = serde_json::to_value(state.worldmap.stats(id))?;
v["epoch"] = json!(state.worldmap.epoch(id));
v["roster"] = serde_json::to_value(state.worldmap.live_players(id))?;
v["diagnostics"] = state.worldmap.diagnostics(id);
Ok(Json(v))
}
+21
View File
@@ -56,6 +56,7 @@ pub struct WorldMap {
tokens: Mutex<HashMap<String, (i64, Instant)>>,
overlays: Mutex<HashMap<i64, Value>>,
stats: Mutex<HashMap<i64, Stats>>,
diagnostics: Mutex<HashMap<i64, (Instant, Value)>>,
}
/// A dimension id ("minecraft:the_nether") as a safe folder name ("the_nether"). Other namespaces keep theirs ("mod__space").
@@ -133,6 +134,7 @@ impl WorldMap {
tokens: Default::default(),
overlays: Default::default(),
stats: Default::default(),
diagnostics: Default::default(),
}
}
@@ -217,6 +219,7 @@ impl WorldMap {
std::fs::write(dir.join("epoch"), epoch.to_string())?;
self.overlays.lock().unwrap().remove(&id);
*self.stats.lock().unwrap().entry(id).or_default() = Stats::default();
self.diagnostics.lock().unwrap().remove(&id);
Ok(())
}
@@ -231,12 +234,30 @@ impl WorldMap {
self.players.lock().unwrap().remove(&id);
self.overlays.lock().unwrap().remove(&id);
self.stats.lock().unwrap().remove(&id);
self.diagnostics.lock().unwrap().remove(&id);
}
pub fn stats(&self, id: i64) -> Stats {
self.stats.lock().unwrap().get(&id).cloned().unwrap_or_default()
}
pub fn set_diagnostics(&self, id: i64, value: Value) {
self.diagnostics.lock().unwrap().insert(id, (Instant::now(), value));
}
pub fn diagnostics(&self, id: i64) -> Value {
self.diagnostics
.lock()
.unwrap()
.get(&id)
.map(|(at, value)| {
let mut value = value.clone();
value["age_seconds"] = json!(at.elapsed().as_secs());
value
})
.unwrap_or(Value::Null)
}
// ---- players -------------------------------------------------------------------------
pub fn set_players(&self, id: i64, mut players: Vec<LivePlayer>) {
+14
View File
@@ -117,6 +117,20 @@ async fn players_and_overlay_are_served_to_viewers() {
assert_eq!(info["players"], 1);
}
#[tokio::test]
async fn map_diagnostics_are_admin_only_and_reset_with_tiles() {
let w = world().await;
let payload = json!({"stage":"waiting for readable chunks","error":"No readable full chunks","region_files":1});
assert_eq!(w.t.call("POST", "/api/server/v1/map/diagnostics", Some(&w.server), Some(payload)).await.0, StatusCode::OK);
assert_eq!(w.t.call("POST", "/api/server/v1/map/diagnostics", None, Some(json!({}))).await.0, StatusCode::UNAUTHORIZED);
let (_, status) = w.t.call("GET", &format!("/api/admin/servers/{}/map", w.sid), Some(&w.admin), None).await;
assert_eq!(status["diagnostics"]["stage"], "waiting for readable chunks");
assert!(status["diagnostics"]["age_seconds"].is_number());
w.t.call("DELETE", &format!("/api/admin/servers/{}/map", w.sid), Some(&w.admin), None).await;
let (_, status) = w.t.call("GET", &format!("/api/admin/servers/{}/map", w.sid), Some(&w.admin), None).await;
assert!(status["diagnostics"].is_null());
}
#[tokio::test]
async fn the_map_can_be_switched_off_and_reset() {
let w = world().await;