Add admin system, registration gate, mudtool database editor, and test checklist
- Add is_admin flag to player DB schema with migration for existing databases - Add server_settings table for key-value config (registration_open, etc.) - Add 10 in-game admin commands: promote, demote, kick, teleport, registration, announce, heal, info, setattitude, list — all gated behind admin flag - Registration gate: new players rejected when registration_open=false, existing players can still reconnect - Add mudtool binary with CLI mode (players/settings/attitudes CRUD) and interactive ratatui TUI with tabbed interface for database management - Restructure to lib.rs + main.rs so mudtool shares DB code with server - Add TESTING.md with comprehensive pre-commit checklist and smoke test script - Stats and who commands show [ADMIN] badge; help shows admin section for admins Made-with: Cursor
This commit is contained in:
633
src/admin.rs
Normal file
633
src/admin.rs
Normal file
@@ -0,0 +1,633 @@
|
||||
use russh::CryptoVec;
|
||||
|
||||
use crate::ansi;
|
||||
use crate::commands::{BroadcastMsg, CommandResult, KickTarget};
|
||||
use crate::game::SharedState;
|
||||
|
||||
fn simple(msg: &str) -> CommandResult {
|
||||
CommandResult {
|
||||
output: msg.to_string(),
|
||||
broadcasts: Vec::new(),
|
||||
kick_targets: Vec::new(),
|
||||
quit: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute_admin(args: &str, player_id: usize, state: &SharedState) -> CommandResult {
|
||||
let (subcmd, subargs) = match args.split_once(' ') {
|
||||
Some((c, a)) => (c.to_lowercase(), a.trim().to_string()),
|
||||
None => (args.to_lowercase(), String::new()),
|
||||
};
|
||||
|
||||
match subcmd.as_str() {
|
||||
"help" | "h" | "" => admin_help(),
|
||||
"promote" => admin_promote(&subargs, state).await,
|
||||
"demote" => admin_demote(&subargs, state).await,
|
||||
"kick" => admin_kick(&subargs, player_id, state).await,
|
||||
"teleport" | "tp" => admin_teleport(&subargs, player_id, state).await,
|
||||
"registration" | "reg" => admin_registration(&subargs, state).await,
|
||||
"announce" => admin_announce(&subargs, player_id, state).await,
|
||||
"heal" => admin_heal(&subargs, player_id, state).await,
|
||||
"info" => admin_info(&subargs, state).await,
|
||||
"setattitude" | "setatt" => admin_setattitude(&subargs, state).await,
|
||||
"list" | "ls" => admin_list(player_id, state).await,
|
||||
_ => simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::error_msg(&format!("Unknown admin command: '{subcmd}'. Use 'admin help'."))
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn admin_help() -> CommandResult {
|
||||
let mut out = format!("\r\n{}\r\n", ansi::bold("=== Admin Commands ==="));
|
||||
let cmds = [
|
||||
("admin promote <player>", "Grant admin privileges"),
|
||||
("admin demote <player>", "Revoke admin privileges"),
|
||||
("admin kick <player>", "Disconnect a player"),
|
||||
("admin teleport <room_id>", "Teleport to a room"),
|
||||
("admin registration on|off", "Toggle new player registration"),
|
||||
("admin announce <msg>", "Broadcast to all players"),
|
||||
("admin heal [player]", "Fully heal self or another player"),
|
||||
("admin info <player>", "View detailed player info"),
|
||||
(
|
||||
"admin setattitude <player> <npc> <val>",
|
||||
"Set NPC attitude",
|
||||
),
|
||||
("admin list", "List all players (online + saved)"),
|
||||
];
|
||||
for (c, d) in cmds {
|
||||
out.push_str(&format!(
|
||||
" {:<42} {}\r\n",
|
||||
ansi::color(ansi::YELLOW, c),
|
||||
ansi::color(ansi::DIM, d)
|
||||
));
|
||||
}
|
||||
CommandResult {
|
||||
output: out,
|
||||
broadcasts: Vec::new(),
|
||||
kick_targets: Vec::new(),
|
||||
quit: false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_promote(target: &str, state: &SharedState) -> CommandResult {
|
||||
if target.is_empty() {
|
||||
return simple(&format!("{}\r\n", ansi::error_msg("Usage: admin promote <player>")));
|
||||
}
|
||||
let st = state.lock().await;
|
||||
if st.db.set_admin(target, true) {
|
||||
// Also update in-memory if online
|
||||
for conn in st.players.values() {
|
||||
if conn.player.name == target {
|
||||
// Can't mutate through shared ref, but DB is updated.
|
||||
// They'll get the flag on next login. Notify them.
|
||||
let msg = CryptoVec::from(
|
||||
format!(
|
||||
"\r\n{}\r\n{}",
|
||||
ansi::system_msg("*** You have been granted admin privileges. ***"),
|
||||
ansi::prompt()
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
return CommandResult {
|
||||
output: format!(
|
||||
"{}\r\n",
|
||||
ansi::system_msg(&format!("{target} has been promoted to admin."))
|
||||
),
|
||||
broadcasts: vec![BroadcastMsg {
|
||||
channel: conn.channel,
|
||||
handle: conn.handle.clone(),
|
||||
data: msg,
|
||||
}],
|
||||
kick_targets: Vec::new(),
|
||||
quit: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::system_msg(&format!(
|
||||
"{target} promoted to admin (offline, effective next login)."
|
||||
))
|
||||
))
|
||||
} else {
|
||||
simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::error_msg(&format!("Player '{target}' not found in database."))
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_demote(target: &str, state: &SharedState) -> CommandResult {
|
||||
if target.is_empty() {
|
||||
return simple(&format!("{}\r\n", ansi::error_msg("Usage: admin demote <player>")));
|
||||
}
|
||||
let st = state.lock().await;
|
||||
if st.db.set_admin(target, false) {
|
||||
simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::system_msg(&format!("{target} has been demoted from admin."))
|
||||
))
|
||||
} else {
|
||||
simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::error_msg(&format!("Player '{target}' not found in database."))
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_kick(target: &str, player_id: usize, state: &SharedState) -> CommandResult {
|
||||
if target.is_empty() {
|
||||
return simple(&format!("{}\r\n", ansi::error_msg("Usage: admin kick <player>")));
|
||||
}
|
||||
let mut st = state.lock().await;
|
||||
let low = target.to_lowercase();
|
||||
|
||||
let target_id = st
|
||||
.players
|
||||
.iter()
|
||||
.find(|(_, c)| c.player.name.to_lowercase() == low)
|
||||
.map(|(&id, _)| id);
|
||||
|
||||
let tid = match target_id {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
return simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::error_msg(&format!("Player '{target}' is not online."))
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if tid == player_id {
|
||||
return simple(&format!("{}\r\n", ansi::error_msg("You can't kick yourself.")));
|
||||
}
|
||||
|
||||
let kick_msg = CryptoVec::from(
|
||||
format!(
|
||||
"\r\n{}\r\n",
|
||||
ansi::error_msg("*** You have been kicked by an admin. ***")
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
|
||||
let conn = st.remove_player(tid);
|
||||
match conn {
|
||||
Some(c) => {
|
||||
let name = c.player.name.clone();
|
||||
let room_id = c.player.room_id.clone();
|
||||
|
||||
let departure = CryptoVec::from(
|
||||
format!(
|
||||
"\r\n{}\r\n{}",
|
||||
ansi::system_msg(&format!("{name} has been kicked.")),
|
||||
ansi::prompt()
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
let mut bcast: Vec<BroadcastMsg> = st
|
||||
.players_in_room(&room_id, player_id)
|
||||
.iter()
|
||||
.map(|p| BroadcastMsg {
|
||||
channel: p.channel,
|
||||
handle: p.handle.clone(),
|
||||
data: departure.clone(),
|
||||
})
|
||||
.collect();
|
||||
// Send kick message to the target before closing
|
||||
bcast.push(BroadcastMsg {
|
||||
channel: c.channel,
|
||||
handle: c.handle.clone(),
|
||||
data: kick_msg,
|
||||
});
|
||||
|
||||
CommandResult {
|
||||
output: format!(
|
||||
"{}\r\n",
|
||||
ansi::system_msg(&format!("Kicked {name} from the server."))
|
||||
),
|
||||
broadcasts: bcast,
|
||||
kick_targets: vec![KickTarget {
|
||||
channel: c.channel,
|
||||
handle: c.handle.clone(),
|
||||
}],
|
||||
quit: false,
|
||||
}
|
||||
}
|
||||
None => simple(&format!("{}\r\n", ansi::error_msg("Failed to kick player."))),
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_teleport(room_id: &str, player_id: usize, state: &SharedState) -> CommandResult {
|
||||
if room_id.is_empty() {
|
||||
return simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::error_msg("Usage: admin teleport <room_id>")
|
||||
));
|
||||
}
|
||||
let mut st = state.lock().await;
|
||||
if st.world.get_room(room_id).is_none() {
|
||||
let rooms: Vec<&String> = st.world.rooms.keys().collect();
|
||||
let mut sorted = rooms;
|
||||
sorted.sort();
|
||||
let list = sorted
|
||||
.iter()
|
||||
.map(|r| ansi::color(ansi::CYAN, r))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
return simple(&format!(
|
||||
"{}\r\nAvailable rooms: {}\r\n",
|
||||
ansi::error_msg(&format!("Room '{room_id}' not found.")),
|
||||
list
|
||||
));
|
||||
}
|
||||
|
||||
let old_rid = st
|
||||
.players
|
||||
.get(&player_id)
|
||||
.map(|c| c.player.room_id.clone())
|
||||
.unwrap_or_default();
|
||||
let pname = st
|
||||
.players
|
||||
.get(&player_id)
|
||||
.map(|c| c.player.name.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Departure broadcast
|
||||
let leave = CryptoVec::from(
|
||||
format!(
|
||||
"\r\n{}\r\n{}",
|
||||
ansi::system_msg(&format!("{pname} vanishes in a flash of light.")),
|
||||
ansi::prompt()
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
let mut bcast: Vec<BroadcastMsg> = st
|
||||
.players_in_room(&old_rid, player_id)
|
||||
.iter()
|
||||
.map(|c| BroadcastMsg {
|
||||
channel: c.channel,
|
||||
handle: c.handle.clone(),
|
||||
data: leave.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let Some(c) = st.players.get_mut(&player_id) {
|
||||
c.player.room_id = room_id.to_string();
|
||||
}
|
||||
|
||||
// Arrival broadcast
|
||||
let arrive = CryptoVec::from(
|
||||
format!(
|
||||
"\r\n{}\r\n{}",
|
||||
ansi::system_msg(&format!("{pname} appears in a flash of light.")),
|
||||
ansi::prompt()
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
for c in st.players_in_room(room_id, player_id) {
|
||||
bcast.push(BroadcastMsg {
|
||||
channel: c.channel,
|
||||
handle: c.handle.clone(),
|
||||
data: arrive.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
st.save_player_to_db(player_id);
|
||||
let view = crate::commands::render_room_view(room_id, player_id, &st);
|
||||
|
||||
CommandResult {
|
||||
output: format!(
|
||||
"{}\r\n{}",
|
||||
ansi::system_msg(&format!("Teleported to {room_id}.")),
|
||||
view
|
||||
),
|
||||
broadcasts: bcast,
|
||||
kick_targets: Vec::new(),
|
||||
quit: false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_registration(args: &str, state: &SharedState) -> CommandResult {
|
||||
let st = state.lock().await;
|
||||
match args.to_lowercase().as_str() {
|
||||
"on" | "true" | "open" => {
|
||||
st.db.set_setting("registration_open", "true");
|
||||
simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::system_msg("Registration is now OPEN.")
|
||||
))
|
||||
}
|
||||
"off" | "false" | "closed" => {
|
||||
st.db.set_setting("registration_open", "false");
|
||||
simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::system_msg("Registration is now CLOSED.")
|
||||
))
|
||||
}
|
||||
_ => {
|
||||
let current = st
|
||||
.db
|
||||
.get_setting("registration_open")
|
||||
.unwrap_or_else(|| "true".into());
|
||||
simple(&format!(
|
||||
"Registration is currently: {}\r\nUsage: admin registration on|off\r\n",
|
||||
ansi::bold(¤t)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_announce(msg: &str, player_id: usize, state: &SharedState) -> CommandResult {
|
||||
if msg.is_empty() {
|
||||
return simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::error_msg("Usage: admin announce <message>")
|
||||
));
|
||||
}
|
||||
let st = state.lock().await;
|
||||
let announcement = CryptoVec::from(
|
||||
format!(
|
||||
"\r\n{}\r\n {}\r\n{}",
|
||||
ansi::color(ansi::YELLOW, "*** ANNOUNCEMENT ***"),
|
||||
ansi::bold(msg),
|
||||
ansi::prompt()
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
|
||||
let bcast: Vec<BroadcastMsg> = st
|
||||
.players
|
||||
.iter()
|
||||
.filter(|(&id, _)| id != player_id)
|
||||
.map(|(_, c)| BroadcastMsg {
|
||||
channel: c.channel,
|
||||
handle: c.handle.clone(),
|
||||
data: announcement.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
CommandResult {
|
||||
output: format!(
|
||||
"{}\r\n",
|
||||
ansi::system_msg(&format!("Announced to {} player(s).", bcast.len()))
|
||||
),
|
||||
broadcasts: bcast,
|
||||
kick_targets: Vec::new(),
|
||||
quit: false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_heal(args: &str, player_id: usize, state: &SharedState) -> CommandResult {
|
||||
let mut st = state.lock().await;
|
||||
|
||||
if args.is_empty() {
|
||||
if let Some(c) = st.players.get_mut(&player_id) {
|
||||
c.player.stats.hp = c.player.stats.max_hp;
|
||||
let hp = c.player.stats.max_hp;
|
||||
let _ = c;
|
||||
st.save_player_to_db(player_id);
|
||||
return simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::system_msg(&format!("You have been fully healed. HP: {hp}/{hp}"))
|
||||
));
|
||||
}
|
||||
return simple(&format!("{}\r\n", ansi::error_msg("Error.")));
|
||||
}
|
||||
|
||||
let low = args.to_lowercase();
|
||||
let target_id = st
|
||||
.players
|
||||
.iter()
|
||||
.find(|(_, c)| c.player.name.to_lowercase() == low)
|
||||
.map(|(&id, _)| id);
|
||||
|
||||
match target_id {
|
||||
Some(tid) => {
|
||||
if let Some(c) = st.players.get_mut(&tid) {
|
||||
c.player.stats.hp = c.player.stats.max_hp;
|
||||
let name = c.player.name.clone();
|
||||
let hp = c.player.stats.max_hp;
|
||||
let notify = CryptoVec::from(
|
||||
format!(
|
||||
"\r\n{}\r\n{}",
|
||||
ansi::system_msg(&format!("An admin has fully healed you. HP: {hp}/{hp}")),
|
||||
ansi::prompt()
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
let bcast = vec![BroadcastMsg {
|
||||
channel: c.channel,
|
||||
handle: c.handle.clone(),
|
||||
data: notify,
|
||||
}];
|
||||
let _ = c;
|
||||
st.save_player_to_db(tid);
|
||||
return CommandResult {
|
||||
output: format!(
|
||||
"{}\r\n",
|
||||
ansi::system_msg(&format!("Healed {name}. HP: {hp}/{hp}"))
|
||||
),
|
||||
broadcasts: bcast,
|
||||
kick_targets: Vec::new(),
|
||||
quit: false,
|
||||
};
|
||||
}
|
||||
simple(&format!("{}\r\n", ansi::error_msg("Error.")))
|
||||
}
|
||||
None => simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::error_msg(&format!("Player '{args}' is not online."))
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_info(target: &str, state: &SharedState) -> CommandResult {
|
||||
if target.is_empty() {
|
||||
return simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::error_msg("Usage: admin info <player>")
|
||||
));
|
||||
}
|
||||
let st = state.lock().await;
|
||||
|
||||
// Check online first
|
||||
let online = st
|
||||
.players
|
||||
.values()
|
||||
.find(|c| c.player.name.to_lowercase() == target.to_lowercase());
|
||||
|
||||
if let Some(conn) = online {
|
||||
let p = &conn.player;
|
||||
let s = &p.stats;
|
||||
let mut out = format!("\r\n{} {}\r\n", ansi::bold(&p.name), ansi::color(ansi::GREEN, "(online)"));
|
||||
out.push_str(&format!(
|
||||
" Race: {} | Class: {} | Admin: {}\r\n",
|
||||
p.race_id, p.class_id, p.is_admin
|
||||
));
|
||||
out.push_str(&format!(
|
||||
" HP: {}/{} | ATK: {} | DEF: {} | Level: {} | XP: {}/{}\r\n",
|
||||
s.hp, s.max_hp, s.attack, s.defense, s.level, s.xp, s.xp_to_next
|
||||
));
|
||||
out.push_str(&format!(" Room: {}\r\n", p.room_id));
|
||||
out.push_str(&format!(
|
||||
" Inventory: {} item(s) | Weapon: {} | Armor: {}\r\n",
|
||||
p.inventory.len(),
|
||||
p.equipped_weapon
|
||||
.as_ref()
|
||||
.map(|w| w.name.as_str())
|
||||
.unwrap_or("none"),
|
||||
p.equipped_armor
|
||||
.as_ref()
|
||||
.map(|a| a.name.as_str())
|
||||
.unwrap_or("none"),
|
||||
));
|
||||
let attitudes = st.db.load_attitudes(&p.name);
|
||||
if !attitudes.is_empty() {
|
||||
out.push_str(&format!(" Attitudes ({}):\r\n", attitudes.len()));
|
||||
for att in &attitudes {
|
||||
let label = crate::world::Attitude::from_value(att.value).label();
|
||||
out.push_str(&format!(" {}: {} ({})\r\n", att.npc_id, att.value, label));
|
||||
}
|
||||
}
|
||||
return CommandResult {
|
||||
output: out,
|
||||
broadcasts: Vec::new(),
|
||||
kick_targets: Vec::new(),
|
||||
quit: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Check DB
|
||||
if let Some(saved) = st.db.load_player(target) {
|
||||
let mut out = format!(
|
||||
"\r\n{} {}\r\n",
|
||||
ansi::bold(&saved.name),
|
||||
ansi::color(ansi::DIM, "(offline)")
|
||||
);
|
||||
out.push_str(&format!(
|
||||
" Race: {} | Class: {} | Admin: {}\r\n",
|
||||
saved.race_id, saved.class_id, saved.is_admin
|
||||
));
|
||||
out.push_str(&format!(
|
||||
" HP: {}/{} | ATK: {} | DEF: {} | Level: {} | XP: {}\r\n",
|
||||
saved.hp, saved.max_hp, saved.attack, saved.defense, saved.level, saved.xp
|
||||
));
|
||||
out.push_str(&format!(" Room: {}\r\n", saved.room_id));
|
||||
let attitudes = st.db.load_attitudes(&saved.name);
|
||||
if !attitudes.is_empty() {
|
||||
out.push_str(&format!(" Attitudes ({}):\r\n", attitudes.len()));
|
||||
for att in &attitudes {
|
||||
let label = crate::world::Attitude::from_value(att.value).label();
|
||||
out.push_str(&format!(" {}: {} ({})\r\n", att.npc_id, att.value, label));
|
||||
}
|
||||
}
|
||||
return CommandResult {
|
||||
output: out,
|
||||
broadcasts: Vec::new(),
|
||||
kick_targets: Vec::new(),
|
||||
quit: false,
|
||||
};
|
||||
}
|
||||
|
||||
simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::error_msg(&format!("Player '{target}' not found."))
|
||||
))
|
||||
}
|
||||
|
||||
async fn admin_setattitude(args: &str, state: &SharedState) -> CommandResult {
|
||||
let parts: Vec<&str> = args.splitn(3, ' ').collect();
|
||||
if parts.len() < 3 {
|
||||
return simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::error_msg("Usage: admin setattitude <player> <npc_id> <value>")
|
||||
));
|
||||
}
|
||||
let player_name = parts[0];
|
||||
let npc_id = parts[1];
|
||||
let value: i32 = match parts[2].parse() {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
return simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::error_msg("Value must be a number (-100 to 100).")
|
||||
))
|
||||
}
|
||||
};
|
||||
let value = value.clamp(-100, 100);
|
||||
|
||||
let st = state.lock().await;
|
||||
if st.db.load_player(player_name).is_none() {
|
||||
return simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::error_msg(&format!("Player '{player_name}' not found."))
|
||||
));
|
||||
}
|
||||
st.db.save_attitude(player_name, npc_id, value);
|
||||
let label = crate::world::Attitude::from_value(value).label();
|
||||
|
||||
simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::system_msg(&format!(
|
||||
"Set {npc_id} attitude toward {player_name} to {value} ({label})."
|
||||
))
|
||||
))
|
||||
}
|
||||
|
||||
async fn admin_list(_player_id: usize, state: &SharedState) -> CommandResult {
|
||||
let st = state.lock().await;
|
||||
let all_saved = st.db.list_all_players();
|
||||
let online_names: Vec<String> = st.players.values().map(|c| c.player.name.clone()).collect();
|
||||
|
||||
let mut out = format!("\r\n{}\r\n", ansi::bold("=== All Players ==="));
|
||||
out.push_str(&format!(
|
||||
" {:<20} {:<10} {:<10} {:<5} {:<12} {:<6} {}\r\n",
|
||||
ansi::color(ansi::DIM, "Name"),
|
||||
ansi::color(ansi::DIM, "Race"),
|
||||
ansi::color(ansi::DIM, "Class"),
|
||||
ansi::color(ansi::DIM, "Lvl"),
|
||||
ansi::color(ansi::DIM, "HP"),
|
||||
ansi::color(ansi::DIM, "Admin"),
|
||||
ansi::color(ansi::DIM, "Status"),
|
||||
));
|
||||
|
||||
for p in &all_saved {
|
||||
let status = if online_names.contains(&p.name) {
|
||||
ansi::color(ansi::GREEN, "online")
|
||||
} else {
|
||||
ansi::color(ansi::DIM, "offline")
|
||||
};
|
||||
let admin_str = if p.is_admin { "YES" } else { "no" };
|
||||
let name_str = if online_names.contains(&p.name) {
|
||||
ansi::player_name(&p.name)
|
||||
} else {
|
||||
p.name.clone()
|
||||
};
|
||||
out.push_str(&format!(
|
||||
" {:<20} {:<10} {:<10} {:<5} {:<12} {:<6} {}\r\n",
|
||||
name_str,
|
||||
p.race_id.split(':').last().unwrap_or(&p.race_id),
|
||||
p.class_id.split(':').last().unwrap_or(&p.class_id),
|
||||
p.level,
|
||||
format!("{}/{}", p.hp, p.max_hp),
|
||||
admin_str,
|
||||
status,
|
||||
));
|
||||
}
|
||||
out.push_str(&format!(
|
||||
"{}\r\n",
|
||||
ansi::system_msg(&format!(
|
||||
"{} total, {} online",
|
||||
all_saved.len(),
|
||||
online_names.len()
|
||||
))
|
||||
));
|
||||
|
||||
CommandResult {
|
||||
output: out,
|
||||
broadcasts: Vec::new(),
|
||||
kick_targets: Vec::new(),
|
||||
quit: false,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user