feat: complete in-game commands, GUIs, admin panel fixes, and launcher enhancements
This commit is contained in:
1 parent
a9ff92e1d1
commit
752f0cc43d
34 files changed
+4170
-181
No files matched your search
@@ -1,6 +1,6 @@
|
||||
//! Guilds system, in-game relay, roles, posts, and land-claiming.
|
||||
|
||||
use crate::auth::AuthUser;
|
||||
use crate::auth::{AdminUser, AuthUser};
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::routes::servers::GameServer;
|
||||
use crate::state::AppState;
|
||||
@@ -710,10 +710,11 @@ pub async fn unclaim_by_id(
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChunkGridQuery {
|
||||
pub server_id: i64,
|
||||
pub server_id: Option<i64>,
|
||||
pub instance_id: Option<String>,
|
||||
pub dimension: Option<String>,
|
||||
pub center_x: i32,
|
||||
pub center_z: i32,
|
||||
pub center_x: Option<i32>,
|
||||
pub center_z: Option<i32>,
|
||||
pub radius: Option<i32>,
|
||||
}
|
||||
|
||||
@@ -723,23 +724,31 @@ pub async fn get_chunk_grid(
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Vec<GuildClaim>>> {
|
||||
let dim = query.dimension.unwrap_or_else(|| "minecraft:overworld".into());
|
||||
let r = query.radius.unwrap_or(8).clamp(2, 24);
|
||||
let min_x = query.center_x - r;
|
||||
let max_x = query.center_x + r;
|
||||
let min_z = query.center_z - r;
|
||||
let max_z = query.center_z + r;
|
||||
let inst_id = query.instance_id.unwrap_or_default();
|
||||
let cx = query.center_x.unwrap_or(0);
|
||||
let cz = query.center_z.unwrap_or(0);
|
||||
let r = query.radius.unwrap_or(32).clamp(2, 1024);
|
||||
let min_x = cx - r;
|
||||
let max_x = cx + r;
|
||||
let min_z = cz - r;
|
||||
let max_z = cz + r;
|
||||
|
||||
let rows: Vec<(i64, String, String, String, i64, String, i32, i32, String, String)> = sqlx::query_as(
|
||||
let rows: Vec<(i64, String, String, String, Option<i64>, String, i32, i32, String, String)> = sqlx::query_as(
|
||||
"SELECT gc.id, gc.guild_id, g.name, g.tag, gc.server_id, gc.dimension,
|
||||
gc.chunk_x, gc.chunk_z, gc.claimed_by_uuid, gc.claimed_at
|
||||
FROM guild_claims gc
|
||||
JOIN guilds g ON g.id = gc.guild_id
|
||||
WHERE gc.server_id = ? AND gc.dimension = ?
|
||||
WHERE (? IS NULL OR gc.server_id = ?)
|
||||
AND (? = '' OR g.instance_id = ?)
|
||||
AND gc.dimension = ?
|
||||
AND gc.chunk_x BETWEEN ? AND ?
|
||||
AND gc.chunk_z BETWEEN ? AND ?
|
||||
LIMIT 500",
|
||||
LIMIT 1000",
|
||||
)
|
||||
.bind(query.server_id)
|
||||
.bind(query.server_id)
|
||||
.bind(&inst_id)
|
||||
.bind(&inst_id)
|
||||
.bind(&dim)
|
||||
.bind(min_x)
|
||||
.bind(max_x)
|
||||
@@ -756,7 +765,7 @@ pub async fn get_chunk_grid(
|
||||
guild_id: gid,
|
||||
guild_name: gname,
|
||||
guild_tag: gtag,
|
||||
server_id: sid,
|
||||
server_id: sid.unwrap_or(0),
|
||||
dimension: gdim,
|
||||
chunk_x: cx,
|
||||
chunk_z: cz,
|
||||
@@ -769,6 +778,106 @@ pub async fn get_chunk_grid(
|
||||
Ok(Json(list))
|
||||
}
|
||||
|
||||
/// Admin list all guilds.
|
||||
pub async fn admin_list_guilds(
|
||||
_admin: AdminUser,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Vec<Guild>>> {
|
||||
let rows: Vec<(
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
i64,
|
||||
i64,
|
||||
i64,
|
||||
String,
|
||||
i64,
|
||||
i64,
|
||||
)> = sqlx::query_as(
|
||||
"SELECT g.id, g.instance_id, g.name, g.tag, g.description, g.motd, g.leader_uuid,
|
||||
g.icon_url, g.banner_url, g.level, g.xp, g.max_claims, g.created_at,
|
||||
(SELECT COUNT(*) FROM guild_members gm WHERE gm.guild_id = g.id) as member_count,
|
||||
(SELECT COUNT(*) FROM guild_claims gc WHERE gc.guild_id = g.id) as claims_count
|
||||
FROM guilds g
|
||||
ORDER BY g.level DESC, member_count DESC",
|
||||
)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let list = rows
|
||||
.into_iter()
|
||||
.map(
|
||||
|(
|
||||
id,
|
||||
inst_id,
|
||||
name,
|
||||
tag,
|
||||
desc,
|
||||
motd,
|
||||
leader,
|
||||
icon,
|
||||
banner,
|
||||
lvl,
|
||||
xp,
|
||||
max_c,
|
||||
created,
|
||||
members,
|
||||
claims,
|
||||
)| Guild {
|
||||
id,
|
||||
instance_id: inst_id,
|
||||
name,
|
||||
tag,
|
||||
description: desc,
|
||||
motd,
|
||||
leader_uuid: leader,
|
||||
icon_url: icon,
|
||||
banner_url: banner,
|
||||
level: lvl,
|
||||
xp,
|
||||
max_claims: max_c,
|
||||
member_count: members,
|
||||
claims_count: claims,
|
||||
created_at: created,
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
|
||||
Ok(Json(list))
|
||||
}
|
||||
|
||||
/// Admin delete a guild.
|
||||
pub async fn admin_delete_guild(
|
||||
_admin: AdminUser,
|
||||
Path(id): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
sqlx::query("DELETE FROM guild_claims WHERE guild_id = ?")
|
||||
.bind(&id)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
sqlx::query("DELETE FROM guild_members WHERE guild_id = ?")
|
||||
.bind(&id)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
sqlx::query("DELETE FROM guild_posts WHERE guild_id = ?")
|
||||
.bind(&id)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
sqlx::query("DELETE FROM guilds WHERE id = ?")
|
||||
.bind(&id)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "ok": true })))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Server Integration / Plugin In-Game Land Claim Relay
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -827,3 +936,307 @@ pub async fn server_check_chunk(
|
||||
"message": if is_member { None } else { Some(format!("This land is claimed by [{guild_tag}] {guild_name}")) }
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ServerClaimChunkPayload {
|
||||
pub uuid: String,
|
||||
pub dimension: String,
|
||||
pub chunk_x: i32,
|
||||
pub chunk_z: i32,
|
||||
}
|
||||
|
||||
pub async fn server_claim_chunk(
|
||||
GameServer(server): GameServer,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<ServerClaimChunkPayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let member: Option<(String, String)> = sqlx::query_as(
|
||||
"SELECT guild_id, role FROM guild_members WHERE uuid = ?",
|
||||
)
|
||||
.bind(&payload.uuid)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
|
||||
let Some((guild_id, _role)) = member else {
|
||||
return Err(AppError::bad_request("You must be in a guild to claim land. Create one with /guild create <name> <tag>"));
|
||||
};
|
||||
|
||||
let max_claims: i64 = sqlx::query_scalar("SELECT max_claims FROM guilds WHERE id = ?")
|
||||
.bind(&guild_id)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.unwrap_or(16);
|
||||
|
||||
let current_claims: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM guild_claims WHERE guild_id = ?")
|
||||
.bind(&guild_id)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
if current_claims >= max_claims {
|
||||
return Err(AppError::bad_request(format!("Guild reached its max claim limit of {max_claims} chunks")));
|
||||
}
|
||||
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let res = sqlx::query(
|
||||
"INSERT INTO guild_claims (guild_id, server_id, dimension, chunk_x, chunk_z, claimed_by_uuid, claimed_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(&guild_id)
|
||||
.bind(server.id)
|
||||
.bind(&payload.dimension)
|
||||
.bind(payload.chunk_x)
|
||||
.bind(payload.chunk_z)
|
||||
.bind(&payload.uuid)
|
||||
.bind(&now)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = res {
|
||||
tracing::warn!("server claim chunk failed: {e}");
|
||||
return Err(AppError::bad_request("Chunk is already claimed by another guild"));
|
||||
}
|
||||
|
||||
let guild_name: String = sqlx::query_scalar("SELECT name FROM guilds WHERE id = ?")
|
||||
.bind(&guild_id)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.unwrap_or_else(|_| "Guild".into());
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"ok": true,
|
||||
"guild_id": guild_id,
|
||||
"guild_name": guild_name,
|
||||
"chunk_x": payload.chunk_x,
|
||||
"chunk_z": payload.chunk_z
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn server_unclaim_chunk(
|
||||
GameServer(server): GameServer,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<ServerClaimChunkPayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let claim: Option<(i64, String)> = sqlx::query_as(
|
||||
"SELECT id, guild_id FROM guild_claims WHERE server_id = ? AND dimension = ? AND chunk_x = ? AND chunk_z = ?",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(&payload.dimension)
|
||||
.bind(payload.chunk_x)
|
||||
.bind(payload.chunk_z)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
|
||||
let Some((claim_id, guild_id)) = claim else {
|
||||
return Err(AppError::not_found("This chunk is not claimed"));
|
||||
};
|
||||
|
||||
let member: Option<(String,)> = sqlx::query_as(
|
||||
"SELECT role FROM guild_members WHERE guild_id = ? AND uuid = ?",
|
||||
)
|
||||
.bind(&guild_id)
|
||||
.bind(&payload.uuid)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
|
||||
if member.is_none() {
|
||||
return Err(AppError::forbidden("You cannot unclaim land belonging to another guild"));
|
||||
}
|
||||
|
||||
sqlx::query("DELETE FROM guild_claims WHERE id = ?")
|
||||
.bind(claim_id)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "ok": true })))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ServerGuildPlayerQuery {
|
||||
pub uuid: String,
|
||||
}
|
||||
|
||||
pub async fn server_get_player_guild(
|
||||
GameServer(_server): GameServer,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<ServerGuildPlayerQuery>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let member_opt: Option<(String, String)> = sqlx::query_as(
|
||||
"SELECT guild_id, role FROM guild_members WHERE uuid = ?",
|
||||
)
|
||||
.bind(&payload.uuid)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
|
||||
let Some((guild_id, player_role)) = member_opt else {
|
||||
return Ok(Json(serde_json::json!({ "in_guild": false })));
|
||||
};
|
||||
|
||||
let guild_opt: Option<(String, String, String, String, String, i64, i64, i64)> = sqlx::query_as(
|
||||
"SELECT name, tag, description, motd, leader_uuid, level, xp, max_claims FROM guilds WHERE id = ?",
|
||||
)
|
||||
.bind(&guild_id)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
|
||||
let Some((name, tag, desc, motd, leader_uuid, level, xp, max_claims)) = guild_opt else {
|
||||
return Ok(Json(serde_json::json!({ "in_guild": false })));
|
||||
};
|
||||
|
||||
let member_rows: Vec<(String, String, String)> = sqlx::query_as(
|
||||
"SELECT uuid, name, role FROM guild_members WHERE guild_id = ?",
|
||||
)
|
||||
.bind(&guild_id)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let claims_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM guild_claims WHERE guild_id = ?")
|
||||
.bind(&guild_id)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let members_val: Vec<Value> = member_rows
|
||||
.into_iter()
|
||||
.map(|(u, n, r)| serde_json::json!({ "uuid": u, "name": n, "role": r }))
|
||||
.collect();
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"in_guild": true,
|
||||
"guild": {
|
||||
"id": guild_id,
|
||||
"name": name,
|
||||
"tag": tag,
|
||||
"description": desc,
|
||||
"motd": motd,
|
||||
"leader_uuid": leader_uuid,
|
||||
"role": player_role,
|
||||
"level": level,
|
||||
"xp": xp,
|
||||
"claims_count": claims_count,
|
||||
"max_claims": max_claims,
|
||||
"members": members_val
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ServerCreateGuildPayload {
|
||||
pub uuid: String,
|
||||
pub username: String,
|
||||
pub name: String,
|
||||
pub tag: String,
|
||||
}
|
||||
|
||||
pub async fn server_create_guild(
|
||||
GameServer(_server): GameServer,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<ServerCreateGuildPayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let name = payload.name.trim();
|
||||
let tag = payload.tag.trim();
|
||||
if name.len() < 3 || name.len() > 32 {
|
||||
return Err(AppError::bad_request("Guild name must be between 3 and 32 characters"));
|
||||
}
|
||||
if tag.len() < 2 || tag.len() > 6 {
|
||||
return Err(AppError::bad_request("Guild tag must be between 2 and 6 characters"));
|
||||
}
|
||||
|
||||
let in_guild: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM guild_members WHERE uuid = ?)")
|
||||
.bind(&payload.uuid)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
|
||||
if in_guild {
|
||||
return Err(AppError::bad_request("You are already in a guild. Leave your current guild first"));
|
||||
}
|
||||
|
||||
let guild_id = format!("guild_{}", uuid::Uuid::new_v4().simple());
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
let mut tx = state.db.begin().await?;
|
||||
|
||||
let res = sqlx::query(
|
||||
"INSERT INTO guilds (id, instance_id, name, tag, description, motd, leader_uuid, created_at)
|
||||
VALUES (?, '', ?, ?, '', 'Welcome to the guild!', ?, ?)",
|
||||
)
|
||||
.bind(&guild_id)
|
||||
.bind(name)
|
||||
.bind(tag)
|
||||
.bind(&payload.uuid)
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
|
||||
if let Err(e) = res {
|
||||
tracing::warn!("server_create_guild failed: {e}");
|
||||
return Err(AppError::bad_request("A guild with that name or tag already exists"));
|
||||
}
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO guild_members (guild_id, uuid, name, role, joined_at)
|
||||
VALUES (?, ?, ?, 'leader', ?)",
|
||||
)
|
||||
.bind(&guild_id)
|
||||
.bind(&payload.uuid)
|
||||
.bind(&payload.username)
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"ok": true,
|
||||
"guild_id": guild_id,
|
||||
"name": name,
|
||||
"tag": tag
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ServerGuildLeavePayload {
|
||||
pub uuid: String,
|
||||
}
|
||||
|
||||
pub async fn server_guild_leave(
|
||||
GameServer(_server): GameServer,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<ServerGuildLeavePayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let member_opt: Option<(String, String)> = sqlx::query_as(
|
||||
"SELECT guild_id, role FROM guild_members WHERE uuid = ?",
|
||||
)
|
||||
.bind(&payload.uuid)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
|
||||
let Some((guild_id, role)) = member_opt else {
|
||||
return Err(AppError::bad_request("You are not in a guild"));
|
||||
};
|
||||
|
||||
if role == "leader" {
|
||||
let member_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM guild_members WHERE guild_id = ?")
|
||||
.bind(&guild_id)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.unwrap_or(1);
|
||||
|
||||
if member_count > 1 {
|
||||
return Err(AppError::bad_request("Guild leader cannot leave without transferring leadership"));
|
||||
} else {
|
||||
sqlx::query("DELETE FROM guild_claims WHERE guild_id = ?").bind(&guild_id).execute(&state.db).await?;
|
||||
sqlx::query("DELETE FROM guild_members WHERE guild_id = ?").bind(&guild_id).execute(&state.db).await?;
|
||||
sqlx::query("DELETE FROM guilds WHERE id = ?").bind(&guild_id).execute(&state.db).await?;
|
||||
return Ok(Json(serde_json::json!({ "ok": true, "disbanded": true })));
|
||||
}
|
||||
}
|
||||
|
||||
sqlx::query("DELETE FROM guild_members WHERE guild_id = ? AND uuid = ?")
|
||||
.bind(&guild_id)
|
||||
.bind(&payload.uuid)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "ok": true, "disbanded": false })))
|
||||
}
|
||||
Reference in new issue
Block a user