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 ", "Grant admin privileges"), ("admin demote ", "Revoke admin privileges"), ("admin kick ", "Disconnect a player"), ("admin teleport ", "Teleport to a room"), ("admin registration on|off", "Toggle new player registration"), ("admin announce ", "Broadcast to all players"), ("admin heal [player]", "Fully heal self or another player"), ("admin info ", "View detailed player info"), ( "admin setattitude ", "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 "))); } 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 "))); } 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 "))); } 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 = 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 ") )); } 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::>() .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 = 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 ") )); } 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 = 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 ") )); } 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)); let equipped_str = if p.equipped.is_empty() { "none".to_string() } else { p.equipped.iter().map(|(s, o)| format!("{}={}", s, o.name)).collect::>().join(", ") }; out.push_str(&format!( " Inventory: {} item(s) | Equipped: {}\r\n", p.inventory.len(), equipped_str, )); 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 ") )); } 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 = 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, } }