diff --git a/CHANGELOG.md b/CHANGELOG.md index 372ff1f3b0..2bb2b31c15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Speech bubbles appear when nearby players talk - NPCs call for help when attacked - Eyebrows and shapes can now be selected +- Character name and level information to chat, social tab and `/players` command. ### Changed diff --git a/client/src/cmd.rs b/client/src/cmd.rs index f24152fd92..2fc1e68db7 100644 --- a/client/src/cmd.rs +++ b/client/src/cmd.rs @@ -40,6 +40,7 @@ fn complete_player(part: &str, client: &Client) -> Vec { client .player_list .values() + .map(|player_info| &player_info.player_alias) .filter(|alias| alias.starts_with(part)) .cloned() .collect() diff --git a/client/src/lib.rs b/client/src/lib.rs index c8d21a84c8..7d1437cf55 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -23,7 +23,7 @@ use common::{ event::{EventBus, SfxEvent, SfxEventItem}, msg::{ validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, Notification, - PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg, + PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg, MAX_BYTES_CHAT_MSG, }, net::PostBox, @@ -68,7 +68,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, postbox: PostBox, @@ -734,16 +734,51 @@ impl Client { ServerMsg::PlayerListUpdate(PlayerListUpdate::Init(list)) => { self.player_list = list }, - ServerMsg::PlayerListUpdate(PlayerListUpdate::Add(uid, name)) => { - if let Some(old_name) = self.player_list.insert(uid, name.clone()) { + ServerMsg::PlayerListUpdate(PlayerListUpdate::Add(uid, player_info)) => { + if let Some(old_player_info) = + self.player_list.insert(uid, player_info.clone()) + { warn!( "Received msg to insert {} with uid {} into the player list but \ there was already an entry for {} with the same uid that was \ overwritten!", - name, uid, old_name + player_info.player_alias, uid, old_player_info.player_alias ); } }, + ServerMsg::PlayerListUpdate(PlayerListUpdate::SelectedCharacter( + uid, + char_info, + )) => { + if let Some(player_info) = self.player_list.get_mut(&uid) { + player_info.character = Some(char_info); + } else { + warn!( + "Received msg to update character info for uid {}, but they were \ + not in the list.", + uid + ); + } + }, + ServerMsg::PlayerListUpdate(PlayerListUpdate::LevelChange(uid, next_level)) => { + if let Some(player_info) = self.player_list.get_mut(&uid) { + player_info.character = match &player_info.character { + Some(character) => Some(common::msg::CharacterInfo { + name: character.name.to_string(), + level: next_level, + }), + None => { + warn!( + "Received msg to update character level info to {} for \ + uid {}, but this player's character is None.", + next_level, uid + ); + + None + }, + }; + } + }, ServerMsg::PlayerListUpdate(PlayerListUpdate::Remove(uid)) => { if self.player_list.remove(&uid).is_none() { warn!( @@ -754,8 +789,8 @@ impl Client { } }, ServerMsg::PlayerListUpdate(PlayerListUpdate::Alias(uid, new_name)) => { - if let Some(name) = self.player_list.get_mut(&uid) { - *name = new_name; + if let Some(player_info) = self.player_list.get_mut(&uid) { + player_info.player_alias = new_name; } else { warn!( "Received msg to alias player with uid {} to {} but this uid is \ diff --git a/common/src/event.rs b/common/src/event.rs index 762703337d..b8f5cc6a80 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -90,6 +90,7 @@ pub enum ServerEvent { Mount(EcsEntity, EcsEntity), Unmount(EcsEntity), Possess(Uid, Uid), + LevelUp(EcsEntity, u32), SelectCharacter { entity: EcsEntity, character_id: i32, diff --git a/common/src/msg/mod.rs b/common/src/msg/mod.rs index c045383dd0..dc3ff97552 100644 --- a/common/src/msg/mod.rs +++ b/common/src/msg/mod.rs @@ -7,7 +7,8 @@ pub use self::{ client::ClientMsg, ecs_packet::EcsCompPacket, server::{ - Notification, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg, + CharacterInfo, Notification, PlayerInfo, PlayerListUpdate, RegisterError, + RequestStateError, ServerInfo, ServerMsg, }, }; diff --git a/common/src/msg/server.rs b/common/src/msg/server.rs index ae0a3a4aa8..42501f5d7f 100644 --- a/common/src/msg/server.rs +++ b/common/src/msg/server.rs @@ -20,12 +20,26 @@ pub struct ServerInfo { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum PlayerListUpdate { - Init(HashMap), - Add(u64, String), + Init(HashMap), + Add(u64, PlayerInfo), + SelectedCharacter(u64, CharacterInfo), + LevelChange(u64, u32), Remove(u64), Alias(u64, String), } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PlayerInfo { + pub player_alias: String, + pub character: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CharacterInfo { + pub name: String, + pub level: u32, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Notification { WaypointSaved, diff --git a/common/src/sys/stats.rs b/common/src/sys/stats.rs index 448b97fec8..5321b54c7a 100644 --- a/common/src/sys/stats.rs +++ b/common/src/sys/stats.rs @@ -66,6 +66,7 @@ impl<'a> System<'a> for Sys { stat.exp.change_by(-(stat.exp.maximum() as i64)); stat.level.change_by(1); stat.exp.update_maximum(stat.level.level()); + server_event_emitter.emit(ServerEvent::LevelUp(entity, stat.level.level())); } stat.update_max_hp(); diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 512769861e..0e380369ab 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -514,26 +514,28 @@ fn handle_players( _action: &ChatCommand, ) { let ecs = server.state.ecs(); - let players = ecs.read_storage::(); - let count = players.join().count(); - let header_message: String = format!("{} online players: \n", count); - if count > 0 { - let mut player_iter = players.join(); - let first = player_iter - .next() - .expect("Player iterator returned none.") - .alias - .to_owned(); - let player_list = player_iter.fold(first, |mut s, p| { - s += ",\n"; - s += &p.alias; - s - }); - server.notify_client(client, ServerMsg::private(header_message + &player_list)); - } else { - server.notify_client(client, ServerMsg::private(header_message)); - } + let entity_tuples = ( + &ecs.entities(), + &ecs.read_storage::(), + &ecs.read_storage::(), + ); + + server.notify_client( + client, + ServerMsg::private(entity_tuples.join().fold( + format!("{} online players:", entity_tuples.join().count()), + |s, (_, player, stat)| { + format!( + "{}\n[{}]{} Lvl {}", + s, + player.alias, + stat.name, + stat.level.level() + ) + }, + )), + ); } fn handle_build( @@ -1125,14 +1127,31 @@ fn handle_set_level( let (a_lvl, a_alias) = scan_fmt_some!(&args, &action.arg_fmt(), u32, String); if let Some(lvl) = a_lvl { - let ecs = server.state.ecs_mut(); - let target = find_target(&ecs, a_alias, target); + let target = find_target(&server.state.ecs(), a_alias, target); let mut error_msg = None; match target { Ok(player) => { - if let Some(stats) = ecs.write_storage::().get_mut(player) { + let uid = server + .state + .ecs() + .read_storage::() + .get(player) + .expect("Failed to get uid for player") + .0; + server + .state + .notify_registered_clients(ServerMsg::PlayerListUpdate( + PlayerListUpdate::LevelChange(uid, lvl), + )); + + if let Some(stats) = server + .state + .ecs_mut() + .write_storage::() + .get_mut(player) + { stats.level.set_level(lvl); stats.update_max_hp(); diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 266145cbe3..45be81d287 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -2,7 +2,7 @@ use crate::{client::Client, Server, SpawnPoint, StateExt}; use common::{ assets, comp::{self, object, Body, HealthChange, HealthSource, Item, Player, Stats}, - msg::ServerMsg, + msg::{PlayerListUpdate, ServerMsg}, state::BlockChange, sync::{Uid, WorldSyncExt}, sys::combat::{BLOCK_ANGLE, BLOCK_EFFICIENCY}, @@ -295,3 +295,17 @@ pub fn handle_explosion(server: &Server, pos: Vec3, power: f32, owner: Opti .cast(); } } + +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; + + server + .state + .notify_registered_clients(ServerMsg::PlayerListUpdate(PlayerListUpdate::LevelChange( + uid, new_level, + ))); +} diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index d36f86e9d4..04d540badc 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -4,7 +4,8 @@ use entity_creation::{ handle_create_character, handle_create_npc, handle_create_waypoint, handle_shoot, }; use entity_manipulation::{ - handle_damage, handle_destroy, handle_explosion, handle_land_on_ground, handle_respawn, + handle_damage, handle_destroy, handle_explosion, handle_land_on_ground, handle_level_up, + handle_respawn, }; use interaction::{handle_lantern, handle_mount, handle_possess, handle_unmount}; use inventory_manip::handle_inventory; @@ -75,6 +76,7 @@ impl Server { body, main, } => handle_create_character(self, entity, character_id, body, main), + ServerEvent::LevelUp(entity, new_level) => handle_level_up(self, entity, new_level), ServerEvent::ExitIngame { entity } => handle_exit_ingame(self, entity), ServerEvent::CreateNpc { pos, diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 5834792165..2783f24ef4 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -6,7 +6,9 @@ use common::{ assets, comp::{self, item}, effect::Effect, - msg::{ClientState, RegisterError, RequestStateError, ServerMsg}, + msg::{ + CharacterInfo, ClientState, PlayerListUpdate, RegisterError, RequestStateError, ServerMsg, + }, state::State, sync::{Uid, WorldSyncExt}, util::Dir, @@ -279,6 +281,24 @@ impl StateExt for State { self.write_component(entity, comp::Admin); } + let uids = &self.ecs().read_storage::(); + let uid = uids + .get(entity) + .expect("Failed to fetch uid component for entity.") + .0; + + let stats = &self.ecs().read_storage::(); + let stat = stats + .get(entity) + .expect("Failed to fetch stats component for entity."); + + self.notify_registered_clients(ServerMsg::PlayerListUpdate( + PlayerListUpdate::SelectedCharacter(uid, CharacterInfo { + name: stat.name.to_string(), + level: stat.level.level(), + }), + )); + // Tell the client its request was successful. if let Some(client) = self.ecs().write_storage::().get_mut(entity) { client.allow_state(ClientState::Character); diff --git a/server/src/sys/message.rs b/server/src/sys/message.rs index 3e18372abd..1c639f61c1 100644 --- a/server/src/sys/message.rs +++ b/server/src/sys/message.rs @@ -10,8 +10,8 @@ use common::{ }, event::{EventBus, ServerEvent}, msg::{ - validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, PlayerListUpdate, - RequestStateError, ServerMsg, MAX_BYTES_CHAT_MSG, + validate_chat_msg, CharacterInfo, ChatMsgValidationError, ClientMsg, ClientState, + PlayerInfo, PlayerListUpdate, RequestStateError, ServerMsg, MAX_BYTES_CHAT_MSG, }, state::{BlockChange, Time}, sync::Uid, @@ -83,9 +83,18 @@ impl<'a> System<'a> for Sys { let mut new_chat_msgs = Vec::new(); // Player list to send new players. - let player_list = (&uids, &players) + let player_list = (&uids, &players, &stats) .join() - .map(|(uid, player)| ((*uid).into(), player.alias.clone())) + .map(|(uid, player, stats)| { + ((*uid).into(), PlayerInfo { + player_alias: player.alias.clone(), + // TODO: player might not have a character selected + character: Some(CharacterInfo { + name: stats.name.clone(), + level: stats.level.level(), + }), + }) + }) .collect::>(); // List of new players to update player lists of all clients. let mut new_players = Vec::new(); @@ -383,10 +392,11 @@ impl<'a> System<'a> for Sys { // Tell all clients to add them to the player list. for entity in new_players { if let (Some(uid), Some(player)) = (uids.get(entity), players.get(entity)) { - let msg = ServerMsg::PlayerListUpdate(PlayerListUpdate::Add( - (*uid).into(), - player.alias.clone(), - )); + let msg = + ServerMsg::PlayerListUpdate(PlayerListUpdate::Add((*uid).into(), PlayerInfo { + player_alias: player.alias.clone(), + character: None, // new players will be on character select. + })); for client in (&mut clients).join().filter(|c| c.is_registered()) { client.notify(msg.clone()) } @@ -406,16 +416,22 @@ impl<'a> System<'a> for Sys { } else { let bubble = SpeechBubble::player_new(message.clone(), *time); let _ = speech_bubbles.insert(entity, bubble); - match players.get(entity) { - Some(player) => { - if admins.get(entity).is_some() { - format!("[ADMIN][{}] {}", &player.alias, message) - } else { - format!("[{}] {}", &player.alias, message) - } + format!( + "{}[{}] {}: {}", + match admins.get(entity) { + Some(_) => "[ADMIN]", + None => "", }, - None => format!("[] {}", message), - } + match players.get(entity) { + Some(player) => &player.alias, + None => "", + }, + match stats.get(entity) { + Some(stat) => &stat.name, + None => "", + }, + message + ) } } else { message diff --git a/voxygen/src/hud/social.rs b/voxygen/src/hud/social.rs index c278ec1963..fd214d098c 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -192,13 +192,20 @@ impl<'a> Widget for Social<'a> { .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) .set(ids.online_title, ui); - for (i, (_, player_alias)) in self.client.player_list.iter().enumerate() { - Text::new(player_alias) - .down(3.0) - .font_size(self.fonts.cyri.scale(15)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(ids.player_names[i], ui); + for (i, (_, player_info)) in self.client.player_list.iter().enumerate() { + Text::new(&format!( + "[{}] {}", + player_info.player_alias, + match &player_info.character { + Some(character) => format!("{} Lvl {}", &character.name, &character.level), + None => "".to_string(), // character select or spectating + } + )) + .down(3.0) + .font_size(self.fonts.cyri.scale(15)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(ids.player_names[i], ui); } }