- Shared achievement icon catalogue; launcher resolves icon/background/border ids - Tauri opener capability now allows http(s) URLs (fixes Discord sign-in) - Rewrite live map docs, admin guide progression section Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjMbLQujBHunCCu5GpsHaT
184 lines
8.9 KiB
Rust
184 lines
8.9 KiB
Rust
//! Central live map: ingest from game servers, status for admins, viewer proxy.
|
|
|
|
mod common;
|
|
use common::*;
|
|
|
|
fn record(x: i32, z: i32, ts: u32, data: &[u8]) -> Vec<u8> {
|
|
let mut b = Vec::new();
|
|
b.extend_from_slice(&x.to_be_bytes());
|
|
b.extend_from_slice(&z.to_be_bytes());
|
|
b.extend_from_slice(&ts.to_be_bytes());
|
|
b.push(2);
|
|
b.extend_from_slice(&(data.len() as u32).to_be_bytes());
|
|
b.extend_from_slice(data);
|
|
b
|
|
}
|
|
|
|
async fn post_bytes(t: &TestApp, uri: &str, token: &str, body: Vec<u8>) -> (StatusCode, Value) {
|
|
let req = Request::builder().method("POST").uri(uri).header("authorization", format!("Bearer {token}")).body(Body::from(body)).unwrap();
|
|
t.send(req).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn live_map_ingest_status_and_proxy() {
|
|
let t = setup().await;
|
|
let admin = t.login("admin", "supersecret").await;
|
|
let (_, v) = t.call("POST", "/api/admin/servers", Some(&admin), Some(json!({"name": "Survival"}))).await;
|
|
let id = v["server"]["id"].as_i64().unwrap();
|
|
let token = v["token"].as_str().unwrap().to_string();
|
|
|
|
// Off by default: nothing is accepted.
|
|
let (s, _) = post_bytes(&t, "/api/server/v1/livemap/chunks?dim=overworld", &token, record(0, 0, 5, b"abc")).await;
|
|
assert_eq!(s, StatusCode::FORBIDDEN);
|
|
let (_, hello) = t.call("POST", "/api/server/v1/hello", Some(&token), Some(json!({}))).await;
|
|
assert_eq!(hello["live_map"]["enabled"], false);
|
|
|
|
let (s, v) =
|
|
t.call("PUT", &format!("/api/admin/servers/{id}"), Some(&admin), Some(json!({"name": "Survival", "live_map_enabled": true}))).await;
|
|
assert_eq!(s, StatusCode::OK, "{v}");
|
|
assert_eq!(v["live_map_enabled"], true);
|
|
let (_, hello) = t.call("POST", "/api/server/v1/hello", Some(&token), Some(json!({}))).await;
|
|
assert_eq!(hello["live_map"]["enabled"], true);
|
|
|
|
// Chunks land in the region mirror and show up in the manifest.
|
|
let mut batch = record(1, 2, 100, b"hello-chunk");
|
|
batch.extend(record(-1, 40, 101, b"other-region"));
|
|
let (s, v) = post_bytes(&t, "/api/server/v1/livemap/chunks?dim=overworld", &token, batch).await;
|
|
assert_eq!(s, StatusCode::OK, "{v}");
|
|
assert_eq!(v["stored"], 2);
|
|
let (s, _) = post_bytes(&t, "/api/server/v1/livemap/chunks?dim=../x", &token, record(0, 0, 1, b"x")).await;
|
|
assert_eq!(s, StatusCode::BAD_REQUEST);
|
|
let (s, _) = post_bytes(&t, "/api/server/v1/livemap/chunks", &token, vec![1, 2, 3]).await;
|
|
assert_eq!(s, StatusCode::BAD_REQUEST);
|
|
let (_, m) = t.call("GET", "/api/server/v1/livemap/manifest?dim=overworld", Some(&token), None).await;
|
|
assert_eq!(m["regions"].as_array().unwrap().len(), 2);
|
|
|
|
// level.dat must be a gzip file.
|
|
let (s, _) = post_bytes(&t, "/api/server/v1/livemap/level", &token, b"not gzip at all, sorry!".to_vec()).await;
|
|
assert_eq!(s, StatusCode::BAD_REQUEST);
|
|
let mut gz = vec![0x1f, 0x8b];
|
|
gz.extend([0u8; 30]);
|
|
let (s, _) = post_bytes(&t, "/api/server/v1/livemap/level", &token, gz).await;
|
|
assert_eq!(s, StatusCode::OK);
|
|
|
|
// Players, sanitised.
|
|
let (s, _) = t
|
|
.call(
|
|
"POST",
|
|
"/api/server/v1/livemap/players",
|
|
Some(&token),
|
|
Some(json!({"players": [
|
|
{"uuid": "b50ad385-829d-3141-a216-7e7d7539ba7f", "name": "Notch", "dimension": "minecraft:the_nether", "x": 10.5, "y": 70.0, "z": -4.0, "yaw": 90.0},
|
|
{"uuid": "bad", "name": "Nowhere", "x": 1e99, "y": 0, "z": 0}
|
|
]})),
|
|
)
|
|
.await;
|
|
assert_eq!(s, StatusCode::OK);
|
|
|
|
// Viewers need a session.
|
|
let (s, _) = t.call("GET", &format!("/api/v1/servers/{id}/livemap"), None, None).await;
|
|
assert_eq!(s, StatusCode::UNAUTHORIZED);
|
|
let (s, info) = t.call("GET", &format!("/api/v1/servers/{id}/livemap"), Some(&admin), None).await;
|
|
assert_eq!(s, StatusCode::OK);
|
|
assert_eq!(info["enabled"], true);
|
|
assert_eq!(info["players"], 1);
|
|
assert_eq!(info["base"], format!("/api/livemap/{id}"));
|
|
assert!(info["dimensions"][0]["available"].as_bool().unwrap());
|
|
// No generator is installed in the test environment.
|
|
assert_eq!(info["ready"], false);
|
|
|
|
let (_, pj) = t.call("GET", &format!("/api/livemap/{id}/the_nether/v1/worlds/default/players.json"), Some(&admin), None).await;
|
|
assert_eq!(pj["players"][0]["name"], "Notch");
|
|
assert_eq!(pj["players"][0]["foreign"], false);
|
|
let (_, pj) = t.call("GET", &format!("/api/livemap/{id}/overworld/v1/worlds/default/players.json"), Some(&admin), None).await;
|
|
assert_eq!(pj["players"][0]["foreign"], true);
|
|
|
|
let (s, e) = t.call("GET", &format!("/api/livemap/{id}/overworld/v1/worlds/default/manifest.json"), Some(&admin), None).await;
|
|
assert_eq!(s, StatusCode::SERVICE_UNAVAILABLE);
|
|
assert!(e["error"].as_str().unwrap().contains("generator"));
|
|
let (s, _) = t.call("GET", &format!("/api/livemap/{id}/overworld/v1/openapi.json"), Some(&admin), None).await;
|
|
assert_eq!(s, StatusCode::NOT_FOUND);
|
|
let (s, _) = t.call("GET", &format!("/api/livemap/{id}/overworld/v1/worlds/default/manifest.json"), None, None).await;
|
|
assert_eq!(s, StatusCode::UNAUTHORIZED);
|
|
|
|
let (s, st) = t.call("GET", &format!("/api/admin/servers/{id}/livemap"), Some(&admin), None).await;
|
|
assert_eq!(s, StatusCode::OK);
|
|
assert_eq!(st["stats"]["chunks_received"], 2);
|
|
assert_eq!(st["generator"]["installed"], false);
|
|
|
|
// Public list advertises the map; reset wipes it.
|
|
let (_, list) = t.call("GET", "/api/v1/servers/public", None, None).await;
|
|
assert_eq!(list["servers"][0]["live_map"], true);
|
|
let (s, _) = t.call("DELETE", &format!("/api/admin/servers/{id}/livemap"), Some(&admin), None).await;
|
|
assert_eq!(s, StatusCode::OK);
|
|
let (_, m) = t.call("GET", "/api/server/v1/livemap/manifest?dim=overworld", Some(&token), None).await;
|
|
assert!(m["regions"].as_array().unwrap().is_empty());
|
|
}
|
|
|
|
/// A stand-in for `vantage server` that answers health checks and serves a
|
|
/// manifest, to exercise the supervisor and proxy end to end.
|
|
#[cfg(unix)]
|
|
#[tokio::test]
|
|
async fn proxy_starts_and_reaches_the_generator() {
|
|
if std::process::Command::new("python3").arg("--version").output().is_err() {
|
|
return;
|
|
}
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let t = setup_with(|cfg, dir| {
|
|
let bin = dir.join("fake-vantage");
|
|
std::fs::write(
|
|
&bin,
|
|
r#"#!/usr/bin/env python3
|
|
import sys, os, http.server
|
|
a = sys.argv
|
|
port = int(a[a.index('--port') + 1])
|
|
assert a[1] == 'server' and '--assets' in a and '--players-file' in a
|
|
token = os.environ['VANTAGE_SERVER_TOKEN']
|
|
class H(http.server.BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
if self.path == '/v1/health':
|
|
body = b'ok'
|
|
elif self.headers.get('Authorization') != 'Bearer ' + token:
|
|
self.send_response(401); self.end_headers(); return
|
|
else:
|
|
body = b'{"fake":true,"path":"' + self.path.encode() + b'"}'
|
|
self.send_response(200); self.send_header('ETag', '"v1"'); self.send_header('Content-Type', 'application/json'); self.end_headers(); self.wfile.write(body)
|
|
def log_message(self, *a): pass
|
|
http.server.ThreadingHTTPServer(('127.0.0.1', port), H).serve_forever()
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).unwrap();
|
|
std::fs::create_dir_all(dir.join("assets")).unwrap();
|
|
cfg.vantage_bin = Some(bin);
|
|
cfg.vantage_assets = Some(dir.join("assets"));
|
|
})
|
|
.await;
|
|
let admin = t.login("admin", "supersecret").await;
|
|
let (_, v) = t.call("POST", "/api/admin/servers", Some(&admin), Some(json!({"name": "S", "live_map_enabled": true}))).await;
|
|
let id = v["server"]["id"].as_i64().unwrap();
|
|
let token = v["token"].as_str().unwrap().to_string();
|
|
|
|
let uri = format!("/api/livemap/{id}/overworld/v1/worlds/default/manifest.json");
|
|
let (s, _) = t.call("GET", &uri, Some(&admin), None).await;
|
|
assert_eq!(s, StatusCode::SERVICE_UNAVAILABLE, "no world uploaded yet");
|
|
|
|
post_bytes(&t, &format!("/api/server/v1/livemap/chunks?dim=overworld"), &token, record(0, 0, 5, b"abc")).await;
|
|
let mut gz = vec![0x1f, 0x8b];
|
|
gz.extend([0u8; 30]);
|
|
post_bytes(&t, "/api/server/v1/livemap/level", &token, gz).await;
|
|
|
|
let (_, info) = t.call("GET", &format!("/api/v1/servers/{id}/livemap"), Some(&admin), None).await;
|
|
assert_eq!(info["ready"], true, "{info}");
|
|
let (s, body) = t.call("GET", &uri, Some(&admin), None).await;
|
|
assert_eq!(s, StatusCode::OK, "{body}");
|
|
assert_eq!(body["fake"], true);
|
|
assert_eq!(body["path"], "/v1/worlds/default/manifest.json");
|
|
let (_, st) = t.call("GET", &format!("/api/admin/servers/{id}/livemap"), Some(&admin), None).await;
|
|
assert_eq!(st["running"][0], "overworld");
|
|
// Reset stops the generator and deletes the mirror.
|
|
t.call("DELETE", &format!("/api/admin/servers/{id}/livemap"), Some(&admin), None).await;
|
|
let (_, st) = t.call("GET", &format!("/api/admin/servers/{id}/livemap"), Some(&admin), None).await;
|
|
assert!(st["running"].as_array().unwrap().is_empty());
|
|
}
|