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
@@ -0,0 +1,483 @@
|
||||
//! Per-server economy system, marketplace, transactions, and baltop.
|
||||
|
||||
use crate::auth::AuthUser;
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::routes::servers::GameServer;
|
||||
use crate::state::AppState;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::Json;
|
||||
use scopenet_shared::{BaltopEntry, EconomyTransaction, MarketListing, ServerEconomyBalance};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Launcher & Public Economy API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Get current user's economy balances across all connected game servers.
|
||||
pub async fn get_my_balances(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Vec<ServerEconomyBalance>>> {
|
||||
let rows: Vec<(i64, String, f64)> = sqlx::query_as(
|
||||
"SELECT se.server_id, gs.name, se.balance
|
||||
FROM server_economy se
|
||||
JOIN game_servers gs ON gs.id = se.server_id
|
||||
WHERE se.uuid = ?
|
||||
ORDER BY se.balance DESC",
|
||||
)
|
||||
.bind(&auth.uuid)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let list = rows
|
||||
.into_iter()
|
||||
.map(|(sid, sname, bal)| ServerEconomyBalance {
|
||||
server_id: sid,
|
||||
server_name: sname,
|
||||
balance: bal,
|
||||
currency_symbol: "$".into(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(list))
|
||||
}
|
||||
|
||||
/// Get current user's recent transactions across all servers.
|
||||
pub async fn get_my_transactions(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Vec<EconomyTransaction>>> {
|
||||
let rows: Vec<(i64, i64, String, String, String, String, String, f64, String, String)> = sqlx::query_as(
|
||||
"SELECT et.id, et.server_id, gs.name, et.from_uuid, et.from_name, et.to_uuid, et.to_name, et.amount, et.description, et.created_at
|
||||
FROM economy_transactions et
|
||||
JOIN game_servers gs ON gs.id = et.server_id
|
||||
WHERE et.from_uuid = ? OR et.to_uuid = ?
|
||||
ORDER BY et.id DESC
|
||||
LIMIT 50",
|
||||
)
|
||||
.bind(&auth.uuid)
|
||||
.bind(&auth.uuid)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let list = rows
|
||||
.into_iter()
|
||||
.map(|(id, sid, sname, fuuid, fname, tuuid, tname, amount, desc, created)| EconomyTransaction {
|
||||
id,
|
||||
server_id: sid,
|
||||
server_name: Some(sname),
|
||||
from_uuid: fuuid,
|
||||
from_name: fname,
|
||||
to_uuid: tuuid,
|
||||
to_name: tname,
|
||||
amount,
|
||||
description: desc,
|
||||
created_at: created,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(list))
|
||||
}
|
||||
|
||||
/// Get top richest players for a specific server.
|
||||
pub async fn get_server_baltop(
|
||||
Path(server_id): Path<i64>,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Vec<BaltopEntry>>> {
|
||||
let rows: Vec<(String, String, f64)> = sqlx::query_as(
|
||||
"SELECT uuid, username, balance
|
||||
FROM server_economy
|
||||
WHERE server_id = ?
|
||||
ORDER BY balance DESC
|
||||
LIMIT 25",
|
||||
)
|
||||
.bind(server_id)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let list = rows
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, (uuid, username, balance))| BaltopEntry {
|
||||
rank: i + 1,
|
||||
uuid,
|
||||
username,
|
||||
balance,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(list))
|
||||
}
|
||||
|
||||
/// Get active marketplace listings for a server.
|
||||
pub async fn get_server_market(
|
||||
Path(server_id): Path<i64>,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Vec<MarketListing>>> {
|
||||
let rows: Vec<(i64, i64, String, String, String, String, i32, f64, String)> = sqlx::query_as(
|
||||
"SELECT id, server_id, seller_uuid, seller_name, item_id, item_name, amount, price, created_at
|
||||
FROM server_market
|
||||
WHERE server_id = ?
|
||||
ORDER BY id DESC
|
||||
LIMIT 100",
|
||||
)
|
||||
.bind(server_id)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let list = rows
|
||||
.into_iter()
|
||||
.map(|(id, sid, suuid, sname, iid, iname, amt, price, created)| MarketListing {
|
||||
id,
|
||||
server_id: sid,
|
||||
seller_uuid: suuid,
|
||||
seller_name: sname,
|
||||
item_id: iid,
|
||||
item_name: iname,
|
||||
amount: amt,
|
||||
price,
|
||||
created_at: created,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(list))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Server Integration Relay Endpoints
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ServerBalanceQuery {
|
||||
pub uuid: String,
|
||||
pub username: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn server_get_balance(
|
||||
GameServer(server): GameServer,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<ServerBalanceQuery>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let username = payload.username.unwrap_or_else(|| "Player".into());
|
||||
|
||||
let bal_opt: Option<f64> = sqlx::query_scalar(
|
||||
"SELECT balance FROM server_economy WHERE server_id = ? AND uuid = ?",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(&payload.uuid)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
|
||||
let balance = match bal_opt {
|
||||
Some(b) => b,
|
||||
None => {
|
||||
let starting_balance = 1000.0;
|
||||
sqlx::query(
|
||||
"INSERT INTO server_economy (server_id, uuid, username, balance, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(server_id, uuid) DO UPDATE SET username = excluded.username",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(&payload.uuid)
|
||||
.bind(&username)
|
||||
.bind(starting_balance)
|
||||
.bind(&now)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
starting_balance
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"server_id": server.id,
|
||||
"uuid": payload.uuid,
|
||||
"balance": balance,
|
||||
"currency_symbol": "$"
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ServerTransferPayload {
|
||||
pub from_uuid: String,
|
||||
pub from_name: String,
|
||||
pub to_uuid: String,
|
||||
pub to_name: String,
|
||||
pub amount: f64,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn server_transfer(
|
||||
GameServer(server): GameServer,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<ServerTransferPayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
if payload.amount <= 0.0 {
|
||||
return Err(AppError::bad_request("Transfer amount must be positive"));
|
||||
}
|
||||
|
||||
let mut tx = state.db.begin().await?;
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
// Check sender balance
|
||||
let from_bal: Option<f64> = sqlx::query_scalar(
|
||||
"SELECT balance FROM server_economy WHERE server_id = ? AND uuid = ?",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(&payload.from_uuid)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let current_from_bal = from_bal.unwrap_or(0.0);
|
||||
if current_from_bal < payload.amount {
|
||||
return Err(AppError::bad_request("Insufficient funds"));
|
||||
}
|
||||
|
||||
let new_from_bal = current_from_bal - payload.amount;
|
||||
sqlx::query("UPDATE server_economy SET balance = ?, updated_at = ? WHERE server_id = ? AND uuid = ?")
|
||||
.bind(new_from_bal)
|
||||
.bind(&now)
|
||||
.bind(server.id)
|
||||
.bind(&payload.from_uuid)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
// Update receiver
|
||||
sqlx::query(
|
||||
"INSERT INTO server_economy (server_id, uuid, username, balance, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT (server_id, uuid) DO UPDATE SET
|
||||
balance = balance + excluded.balance,
|
||||
username = excluded.username,
|
||||
updated_at = excluded.updated_at",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(&payload.to_uuid)
|
||||
.bind(&payload.to_name)
|
||||
.bind(payload.amount)
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let desc = payload.description.unwrap_or_else(|| format!("Payment to {}", payload.to_name));
|
||||
sqlx::query(
|
||||
"INSERT INTO economy_transactions (server_id, from_uuid, from_name, to_uuid, to_name, amount, description, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(&payload.from_uuid)
|
||||
.bind(&payload.from_name)
|
||||
.bind(&payload.to_uuid)
|
||||
.bind(&payload.to_name)
|
||||
.bind(payload.amount)
|
||||
.bind(&desc)
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"ok": true,
|
||||
"from_balance": new_from_bal
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ServerSyncBalancePayload {
|
||||
pub uuid: String,
|
||||
pub username: String,
|
||||
pub new_balance: f64,
|
||||
}
|
||||
|
||||
pub async fn server_sync_balance(
|
||||
GameServer(server): GameServer,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<ServerSyncBalancePayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
sqlx::query(
|
||||
"INSERT INTO server_economy (server_id, uuid, username, balance, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(server_id, uuid) DO UPDATE SET
|
||||
balance = excluded.balance,
|
||||
username = excluded.username,
|
||||
updated_at = excluded.updated_at",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(&payload.uuid)
|
||||
.bind(&payload.username)
|
||||
.bind(payload.new_balance)
|
||||
.bind(&now)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "ok": true })))
|
||||
}
|
||||
|
||||
pub async fn server_baltop(
|
||||
GameServer(server): GameServer,
|
||||
State(state): State<AppState>,
|
||||
) -> AppResult<Json<Vec<BaltopEntry>>> {
|
||||
let rows: Vec<(String, String, f64)> = sqlx::query_as(
|
||||
"SELECT uuid, username, balance
|
||||
FROM server_economy
|
||||
WHERE server_id = ?
|
||||
ORDER BY balance DESC
|
||||
LIMIT 20",
|
||||
)
|
||||
.bind(server.id)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let list = rows
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, (uuid, username, balance))| BaltopEntry {
|
||||
rank: i + 1,
|
||||
uuid,
|
||||
username,
|
||||
balance,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(list))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct MarketListPayload {
|
||||
pub seller_uuid: String,
|
||||
pub seller_name: String,
|
||||
pub item_id: String,
|
||||
pub item_name: String,
|
||||
pub amount: i32,
|
||||
pub price: f64,
|
||||
}
|
||||
|
||||
pub async fn server_market_list(
|
||||
GameServer(server): GameServer,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<MarketListPayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let id: i64 = sqlx::query_scalar(
|
||||
"INSERT INTO server_market (server_id, seller_uuid, seller_name, item_id, item_name, amount, price, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING id",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(payload.seller_uuid)
|
||||
.bind(payload.seller_name)
|
||||
.bind(payload.item_id)
|
||||
.bind(payload.item_name)
|
||||
.bind(payload.amount)
|
||||
.bind(payload.price)
|
||||
.bind(&now)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "id": id, "ok": true })))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct MarketBuyPayload {
|
||||
pub listing_id: i64,
|
||||
pub buyer_uuid: String,
|
||||
pub buyer_name: String,
|
||||
}
|
||||
|
||||
pub async fn server_market_buy(
|
||||
GameServer(server): GameServer,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<MarketBuyPayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let listing: Option<(String, String, String, String, i32, f64)> = sqlx::query_as(
|
||||
"SELECT seller_uuid, seller_name, item_id, item_name, amount, price
|
||||
FROM server_market
|
||||
WHERE id = ? AND server_id = ?",
|
||||
)
|
||||
.bind(payload.listing_id)
|
||||
.bind(server.id)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
|
||||
let Some((seller_uuid, seller_name, item_id, item_name, amount, price)) = listing else {
|
||||
return Err(AppError::not_found("Listing not found"));
|
||||
};
|
||||
|
||||
let mut tx = state.db.begin().await?;
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
// Deduct buyer
|
||||
let buyer_bal: f64 = sqlx::query_scalar(
|
||||
"SELECT balance FROM server_economy WHERE server_id = ? AND uuid = ?",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(&payload.buyer_uuid)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
.unwrap_or(0.0);
|
||||
|
||||
if buyer_bal < price {
|
||||
return Err(AppError::bad_request("Insufficient funds"));
|
||||
}
|
||||
|
||||
sqlx::query("UPDATE server_economy SET balance = balance - ?, updated_at = ? WHERE server_id = ? AND uuid = ?")
|
||||
.bind(price)
|
||||
.bind(&now)
|
||||
.bind(server.id)
|
||||
.bind(&payload.buyer_uuid)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
// Pay seller
|
||||
sqlx::query(
|
||||
"INSERT INTO server_economy (server_id, uuid, username, balance, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT (server_id, uuid) DO UPDATE SET
|
||||
balance = balance + excluded.balance,
|
||||
username = excluded.username,
|
||||
updated_at = excluded.updated_at",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(&seller_uuid)
|
||||
.bind(&seller_name)
|
||||
.bind(price)
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
// Delete listing
|
||||
sqlx::query("DELETE FROM server_market WHERE id = ?")
|
||||
.bind(payload.listing_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
// Log transaction
|
||||
let desc = format!("Market purchase: {}x {} from {}", amount, item_name, seller_name);
|
||||
sqlx::query(
|
||||
"INSERT INTO economy_transactions (server_id, from_uuid, from_name, to_uuid, to_name, amount, description, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(&payload.buyer_uuid)
|
||||
.bind(&payload.buyer_name)
|
||||
.bind(&seller_uuid)
|
||||
.bind(&seller_name)
|
||||
.bind(price)
|
||||
.bind(&desc)
|
||||
.bind(&now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"ok": true,
|
||||
"item_id": item_id,
|
||||
"item_name": item_name,
|
||||
"amount": amount,
|
||||
"price": price
|
||||
})))
|
||||
}
|
||||
Reference in new issue
Block a user