diff --git a/client/src/lib.rs b/client/src/lib.rs index 69b400e169..cadd2850eb 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -66,7 +66,7 @@ pub struct Client { thread_pool: ThreadPool, pub server_info: ServerInfo, pub world_map: (Arc, Vec2), - pub player_list: HashMap, + pub player_list: HashMap, pub character_list: CharacterList, pub active_character_id: Option, @@ -759,6 +759,16 @@ impl Client { ); } }, + ServerMsg::PlayerListUpdate(PlayerListUpdate::Admin(uid, admin)) => { + if let Some(player_info) = self.player_list.get_mut(&uid) { + player_info.is_admin = admin; + } else { + warn!( + "Received msg to update admin status of uid {}, but they were not in the list.", + uid + ); + } + }, ServerMsg::PlayerListUpdate(PlayerListUpdate::SelectedCharacter( uid, char_info, @@ -822,7 +832,7 @@ impl Client { }, ServerMsg::ChatMsg(m) => frontend_events.push(Event::Chat(m)), ServerMsg::SetPlayerEntity(uid) => { - if let Some(entity) = self.state.ecs().entity_from_uid(uid) { + if let Some(entity) = self.state.ecs().entity_from_uid(uid.0) { self.entity = entity; } else { return Err(Error::Other("Failed to find entity from uid.".to_owned())); @@ -853,7 +863,7 @@ impl Client { { self.state .ecs_mut() - .delete_entity_and_clear_from_uid_allocator(entity); + .delete_entity_and_clear_from_uid_allocator(entity.0); } }, // Cleanup for when the client goes back to the `Registered` state diff --git a/common/src/msg/server.rs b/common/src/msg/server.rs index 12b6c6f87f..786b9dbc6a 100644 --- a/common/src/msg/server.rs +++ b/common/src/msg/server.rs @@ -2,6 +2,7 @@ use super::{ClientState, EcsCompPacket}; use crate::{ character::CharacterItem, comp, state, sync, + sync::Uid, terrain::{Block, TerrainChunk}, }; use authc::AuthClientError; @@ -17,18 +18,21 @@ pub struct ServerInfo { pub auth_provider: Option, } +/// Inform the client of updates to the player list. #[derive(Debug, Clone, Serialize, Deserialize)] pub enum PlayerListUpdate { - Init(HashMap), - Add(u64, PlayerInfo), - SelectedCharacter(u64, CharacterInfo), - LevelChange(u64, u32), - Remove(u64), - Alias(u64, String), + Init(HashMap), + Add(Uid, PlayerInfo), + SelectedCharacter(Uid, CharacterInfo), + LevelChange(Uid, u32), + Admin(Uid, bool), + Remove(Uid), + Alias(Uid, String), } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PlayerInfo { + pub is_admin: bool, pub player_alias: String, pub character: Option, } @@ -69,12 +73,12 @@ pub enum ServerMsg { /// A message to go into the client chat box. The client is responsible for /// formatting the message. ChatMsg(comp::ChatMsg), - SetPlayerEntity(u64), + SetPlayerEntity(Uid), TimeOfDay(state::TimeOfDay), EntitySync(sync::EntitySyncPackage), CompSync(sync::CompSyncPackage), CreateEntity(sync::EntityPackage), - DeleteEntity(u64), + DeleteEntity(Uid), InventoryUpdate(comp::Inventory, comp::InventoryUpdateEvent), TerrainChunkUpdate { key: Vec2, @@ -112,7 +116,8 @@ impl From for RegisterError { } impl ServerMsg { - /// Sends either say, world, group, etc. based on the player's current chat mode. + /// Sends either say, world, group, etc. based on the player's current chat + /// mode. pub fn chat(mode: comp::ChatMode, uid: sync::Uid, message: String) -> ServerMsg { ServerMsg::ChatMsg(mode.msg_from(uid, message)) } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index ee4e771b39..399b102cff 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -388,7 +388,20 @@ fn handle_alias( args: String, action: &ChatCommand, ) { + if client != target { + // Prevent people abusing /sudo + server.notify_client( + client, + ServerMsg::private(String::from("Don't call people names. It's mean.")), + ); + return; + } if let Ok(alias) = scan_fmt!(&args, &action.arg_fmt(), String) { + if !comp::Player::alias_is_valid(&alias) { + // Prevent silly aliases + server.notify_client(client, ServerMsg::private(String::from("Invalid alias."))); + return; + } let old_alias_optional = server .state .ecs_mut() @@ -932,26 +945,30 @@ fn handle_adminify( let ecs = server.state.ecs(); let opt_player = (&ecs.entities(), &ecs.read_storage::()) .join() - .find(|(_, player)| player.alias == alias) + .find(|(_, player)| alias == player.alias) .map(|(entity, _)| entity); match opt_player { - Some(player) => match server.state.read_component_cloned::(player) { - Some(_admin) => { + Some(player) => { + let is_admin = if server.state.read_component_cloned::(player).is_some() { ecs.write_storage::().remove(player); - }, - None => { - server.state.write_component(player, comp::Admin); - }, + false + } else { + ecs.write_storage().insert(player, comp::Admin).is_ok() + }; + // Update player list so the player shows up as admin in client chat. + let msg = ServerMsg::PlayerListUpdate(PlayerListUpdate::Admin( + *ecs.read_storage::() + .get(player) + .expect("Player should have uid"), + is_admin, + )); + server.state.notify_registered_clients(msg); }, None => { server.notify_client( client, ServerMsg::private(format!("Player '{}' not found!", alias)), ); - server.notify_client( - client, - ServerMsg::private(String::from(action.help_string())), - ); }, } } else { @@ -1319,13 +1336,12 @@ fn handle_set_level( match target { Ok(player) => { - let uid = server + let uid = *server .state .ecs() .read_storage::() .get(player) - .expect("Failed to get uid for player") - .0; + .expect("Failed to get uid for player"); server .state .notify_registered_clients(ServerMsg::PlayerListUpdate( diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 8de46e9df8..e48ec5c29d 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -471,12 +471,11 @@ pub fn handle_level_up(server: &mut Server, entity: EcsEntity, new_level: u32) { let uids = server.state.ecs().read_storage::(); let uid = uids .get(entity) - .expect("Failed to fetch uid component for entity.") - .0; + .expect("Failed to fetch uid component for entity."); server .state .notify_registered_clients(ServerMsg::PlayerListUpdate(PlayerListUpdate::LevelChange( - uid, new_level, + *uid, new_level, ))); } diff --git a/server/src/sys/entity_sync.rs b/server/src/sys/entity_sync.rs index 041c88cfde..d792e4baa8 100644 --- a/server/src/sys/entity_sync.rs +++ b/server/src/sys/entity_sync.rs @@ -301,7 +301,7 @@ impl<'a> System<'a> for Sys { }) { for uid in &deleted { - client.notify(ServerMsg::DeleteEntity(*uid)); + client.notify(ServerMsg::DeleteEntity(Uid(*uid))); } } } diff --git a/server/src/sys/message.rs b/server/src/sys/message.rs index 6f6f3bda2b..e25e159ab7 100644 --- a/server/src/sys/message.rs +++ b/server/src/sys/message.rs @@ -5,7 +5,8 @@ use crate::{ }; use common::{ comp::{ - CanBuild, ChatMode, ControlEvent, Controller, ForceUpdate, Ori, Player, Pos, Stats, Vel, + Admin, CanBuild, ChatMode, ControlEvent, Controller, ForceUpdate, Ori, Player, Pos, Stats, + Vel, }, event::{EventBus, ServerEvent}, msg::{ @@ -33,6 +34,7 @@ impl<'a> System<'a> for Sys { ReadExpect<'a, CharacterLoader>, ReadExpect<'a, TerrainGrid>, Write<'a, SysTimer>, + ReadStorage<'a, Admin>, ReadStorage<'a, Uid>, ReadStorage<'a, CanBuild>, ReadStorage<'a, ForceUpdate>, @@ -62,6 +64,7 @@ impl<'a> System<'a> for Sys { character_loader, terrain, mut timer, + admins, uids, can_build, force_updates, @@ -86,13 +89,13 @@ impl<'a> System<'a> for Sys { let mut new_chat_msgs = Vec::new(); // Player list to send new players. - let player_list = (&uids, &players, &stats) + let player_list = (&uids, &players, stats.maybe(), admins.maybe()) .join() - .map(|(uid, player, stats)| { + .map(|(uid, player, stats, admin)| { ((*uid).into(), PlayerInfo { + is_admin: admin.is_some(), player_alias: player.alias.clone(), - // TODO: player might not have a character selected - character: Some(CharacterInfo { + character: stats.map(|stats| CharacterInfo { name: stats.name.clone(), level: stats.level.level(), }), @@ -441,6 +444,7 @@ impl<'a> System<'a> for Sys { let msg = ServerMsg::PlayerListUpdate(PlayerListUpdate::Add((*uid).into(), PlayerInfo { player_alias: player.alias.clone(), + is_admin: admins.get(entity).is_some(), character: None, // new players will be on character select. })); for client in (&mut clients).join().filter(|c| c.is_registered()) { diff --git a/server/src/sys/subscription.rs b/server/src/sys/subscription.rs index fbff3e431e..4716fc84c9 100644 --- a/server/src/sys/subscription.rs +++ b/server/src/sys/subscription.rs @@ -169,7 +169,7 @@ impl<'a> System<'a> for Sys { .iter() .flat_map(|v| v.iter()) { - client.notify(ServerMsg::DeleteEntity(*uid)); + client.notify(ServerMsg::DeleteEntity(Uid(*uid))); } } diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs index bf94a4c7a4..3e200ad66d 100644 --- a/voxygen/src/hud/chat.rs +++ b/voxygen/src/hud/chat.rs @@ -18,6 +18,7 @@ use conrod_core::{ widget::{self, Button, Id, List, Rectangle, Text, TextEdit}, widget_ids, Colorable, Positionable, Sizeable, Ui, UiCell, Widget, WidgetCommon, }; +use specs::world::WorldExt; use std::collections::VecDeque; widget_ids! { @@ -36,6 +37,7 @@ const MAX_MESSAGES: usize = 100; #[derive(WidgetCommon)] pub struct Chat<'a> { new_messages: &'a mut VecDeque, + client: &'a Client, force_input: Option, force_cursor: Option, force_completions: Option>, @@ -54,12 +56,14 @@ pub struct Chat<'a> { impl<'a> Chat<'a> { pub fn new( new_messages: &'a mut VecDeque, + client: &'a Client, global_state: &'a GlobalState, imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, ) -> Self { Self { new_messages, + client, force_input: None, force_cursor: None, force_completions: None, @@ -71,9 +75,9 @@ impl<'a> Chat<'a> { } } - pub fn prepare_tab_completion(mut self, input: String, client: &Client) -> Self { + pub fn prepare_tab_completion(mut self, input: String) -> Self { if let Some(index) = input.find('\t') { - self.force_completions = Some(cmd::complete(&input[..index], &client)); + self.force_completions = Some(cmd::complete(&input[..index], &self.client)); } else { self.force_completions = None; } @@ -305,6 +309,19 @@ impl<'a> Widget for Chat<'a> { } } + let alias_of_uid = |uid| { + self.client + .player_list + .get(uid) + .map_or("".to_string(), |player_info| { + if player_info.is_admin { + format!("ADMIN - {}", player_info.player_alias) + } else { + player_info.player_alias.to_string() + } + }) + }; + let message_format = |uid, message| format!("[{}]: {}", alias_of_uid(uid), message); // Message box Rectangle::fill([470.0, 174.0]) .rgba(0.0, 0.0, 0.0, transp) @@ -323,20 +340,35 @@ impl<'a> Widget for Chat<'a> { .set(state.ids.message_box, ui); while let Some(item) = items.next(ui) { // This would be easier if conrod used the v-metrics from rusttype. - let widget = if item.i < state.messages.len() { - let msg = &state.messages[item.i]; - let color = match msg.chat_type { - ChatType::Tell(_, _) => TELL_COLOR, - ChatType::Private => PRIVATE_COLOR, - ChatType::Broadcast => BROADCAST_COLOR, - ChatType::Say(_) => SAY_COLOR, - ChatType::Group(_) => GROUP_COLOR, - ChatType::Faction(_) => FACTION_COLOR, - ChatType::Region(_) => REGION_COLOR, - ChatType::Kill => KILL_COLOR, - ChatType::World(_) => WORLD_COLOR, + if item.i < state.messages.len() { + let ChatMsg { chat_type, message } = &state.messages[item.i]; + let (color, msg) = match chat_type { + ChatType::Private => (PRIVATE_COLOR, message.to_string()), + ChatType::Broadcast => (BROADCAST_COLOR, message.to_string()), + ChatType::Kill => (KILL_COLOR, message.to_string()), + ChatType::Tell(from, to) => { + let from_alias = alias_of_uid(&from); + let to_alias = alias_of_uid(&to); + if Some(from) + == self + .client + .state() + .ecs() + .read_storage() + .get(self.client.entity()) + { + (TELL_COLOR, format!("To [{}]: {}", to_alias, message)) + } else { + (TELL_COLOR, format!("From [{}]: {}", from_alias, message)) + } + }, + ChatType::Say(uid) => (SAY_COLOR, message_format(uid, message)), + ChatType::Group(uid) => (GROUP_COLOR, message_format(uid, message)), + ChatType::Faction(uid) => (FACTION_COLOR, message_format(uid, message)), + ChatType::Region(uid) => (REGION_COLOR, message_format(uid, message)), + ChatType::World(uid) => (WORLD_COLOR, message_format(uid, message)), }; - let text = Text::new(&msg.message) + let text = Text::new(&msg) .font_size(self.fonts.opensans.scale(15)) .font_id(self.fonts.opensans.conrod_id) .w(470.0) @@ -347,23 +379,17 @@ impl<'a> Widget for Chat<'a> { Dimension::Absolute(y) => y + 2.0, _ => 0.0, }; - Some(text.h(y)) + let widget = text.h(y); + item.set(widget, ui); } else { // Spacer at bottom of the last message so that it is not cut off. // Needs to be larger than the space above. - Some( - Text::new("") - .font_size(self.fonts.opensans.scale(6)) - .font_id(self.fonts.opensans.conrod_id) - .w(470.0), - ) + let widget = Text::new("") + .font_size(self.fonts.opensans.scale(6)) + .font_id(self.fonts.opensans.conrod_id) + .w(470.0); + item.set(widget, ui); }; - match widget { - Some(widget) => { - item.set(widget, ui); - }, - None => {}, - } } // Chat Arrow diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index c9d1156e93..420183cca3 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -81,7 +81,7 @@ const PRIVATE_COLOR: Color = Color::Rgba(1.0, 1.0, 0.0, 1.0); /// Color for public messages from the server const BROADCAST_COLOR: Color = Color::Rgba(0.28, 0.83, 0.71, 1.0); /// Color for local chat -const SAY_COLOR: Color = Color::Rgba(0.9, 0.2, 0.2, 1.0); +const SAY_COLOR: Color = Color::Rgba(1.0, 0.8, 0.8, 1.0); /// Color for group chat const GROUP_COLOR: Color = Color::Rgba(0.47, 0.84, 1.0, 1.0); /// Color for factional chat @@ -1554,13 +1554,14 @@ impl Hud { // Chat box match Chat::new( &mut self.new_messages, + &client, global_state, &self.imgs, &self.fonts, ) .and_then(self.force_chat_input.take(), |c, input| c.input(input)) .and_then(self.tab_complete.take(), |c, input| { - c.prepare_tab_completion(input, &client) + c.prepare_tab_completion(input) }) .and_then(self.force_chat_cursor.take(), |c, pos| c.cursor_pos(pos)) .set(self.ids.chat, ui_widgets)