diff --git a/common/src/cmd.rs b/common/src/cmd.rs index d39b830e31..d9dedc50b3 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -43,6 +43,8 @@ pub enum ChatCommand { Group, Health, Help, + JoinFaction, + JoinGroup, Jump, Kill, KillNpcs, @@ -81,6 +83,8 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[ ChatCommand::Group, ChatCommand::Health, ChatCommand::Help, + ChatCommand::JoinFaction, + ChatCommand::JoinGroup, ChatCommand::Jump, ChatCommand::Kill, ChatCommand::KillNpcs, @@ -218,6 +222,16 @@ impl ChatCommand { "Display information about commands", NoAdmin, ), + ChatCommand::JoinFaction => ChatCommandData::new( + vec![Any("faction", Optional)], + "Join/leave the specified faction", + NoAdmin, + ), + ChatCommand::JoinGroup => ChatCommandData::new( + vec![Any("group", Optional)], + "Join/leave the specified group", + NoAdmin, + ), ChatCommand::Jump => cmd( vec![ Float("x", 0.0, Required), @@ -336,6 +350,8 @@ impl ChatCommand { ChatCommand::Goto => "goto", ChatCommand::Group => "group", ChatCommand::Health => "health", + ChatCommand::JoinFaction => "join_faction", + ChatCommand::JoinGroup => "join_group", ChatCommand::Help => "help", ChatCommand::Jump => "jump", ChatCommand::Kill => "kill", diff --git a/common/src/comp/chat.rs b/common/src/comp/chat.rs index d6b8b7e1fc..33973525ca 100644 --- a/common/src/comp/chat.rs +++ b/common/src/comp/chat.rs @@ -4,7 +4,7 @@ use specs_idvs::IDVStorage; use std::time::{Duration, Instant}; /// A player's current chat mode. -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Debug)] pub enum ChatMode { /// Private message to another player (by uuid) Tell(Uid), @@ -13,9 +13,9 @@ pub enum ChatMode { /// Talk to players in your region of the world Region, /// Talk to your current group of players - Group, + Group(String), /// Talk to your faction - Faction, + Faction(String), /// Talk to every player on the server World, } @@ -31,18 +31,22 @@ impl ChatMode { ChatMode::Tell(to) => ChatType::Tell(from, *to), ChatMode::Say => ChatType::Say(from), ChatMode::Region => ChatType::Region(from), - ChatMode::Group => ChatType::Group(from), - ChatMode::Faction => ChatType::Faction(from), + ChatMode::Group(name) => ChatType::Group(from, name.to_string()), + ChatMode::Faction(name) => ChatType::Faction(from, name.to_string()), ChatMode::World => ChatType::World(from), }; ChatMsg { chat_type, message } } } +impl Default for ChatMode { + fn default() -> Self { Self::World } +} + /// List of chat types. Note that this is a superset of `ChatMode`; this is /// because `ChatType::Kill`, `ChatType::Broadcast`, and `ChatType::Private` /// cannot be sent by players. -#[derive(Copy, Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum ChatType { /// Tell all players something (such as players connecting or alias changes) Broadcast, @@ -55,9 +59,9 @@ pub enum ChatType { /// Chat with nearby players Say(Uid), /// Group chat - Group(Uid), + Group(Uid, String), /// Factional chat - Faction(Uid), + Faction(Uid, String), /// Regional chat Region(Uid), /// World chat @@ -81,23 +85,23 @@ impl ChatMsg { } pub fn to_bubble(&self) -> Option<(SpeechBubble, Uid)> { - let tuple = match self.chat_type { + let tuple = match &self.chat_type { ChatType::Broadcast => None, ChatType::Private => None, ChatType::Kill => None, ChatType::Tell(u, _) => Some((SpeechBubbleIcon::Tell, u, None)), ChatType::Say(u) => Some((SpeechBubbleIcon::Say, u, None)), - ChatType::Group(u) => Some((SpeechBubbleIcon::Group, u, None)), - ChatType::Faction(u) => Some((SpeechBubbleIcon::Faction, u, None)), + ChatType::Group(u, _s) => Some((SpeechBubbleIcon::Group, u, None)), + ChatType::Faction(u, _s) => Some((SpeechBubbleIcon::Faction, u, None)), ChatType::Region(u) => Some((SpeechBubbleIcon::Region, u, None)), ChatType::World(u) => Some((SpeechBubbleIcon::World, u, None)), ChatType::Npc(u, r) => Some((SpeechBubbleIcon::None, u, Some(r))), }; tuple.map(|(icon, from, npc_rand)| { if let Some(r) = npc_rand { - (SpeechBubble::npc_new(self.message.clone(), r, icon), from) + (SpeechBubble::npc_new(self.message.clone(), *r, icon), *from) } else { - (SpeechBubble::player_new(self.message.clone(), icon), from) + (SpeechBubble::player_new(self.message.clone(), icon), *from) } }) } @@ -107,19 +111,27 @@ impl ChatMsg { /// gameplay. /// /// Groups are currently just an associated String (the group's name) -pub struct Group(String); +#[derive(Clone, Debug)] +pub struct Group(pub String); impl Component for Group { type Storage = IDVStorage; } +impl From for Group { + fn from(s: String) -> Self { Group(s) } +} /// Player factions are used to coordinate pvp vs hostile factions or segment /// chat from the world /// /// Factions are currently just an associated String (the faction's name) -pub struct Faction(String); +#[derive(Clone, Debug)] +pub struct Faction(pub String); impl Component for Faction { type Storage = IDVStorage; } +impl From for Faction { + fn from(s: String) -> Self { Faction(s) } +} /// The contents of a speech bubble pub enum SpeechBubbleMessage { diff --git a/common/src/state.rs b/common/src/state.rs index e99b63165a..167e75c0d1 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -155,6 +155,8 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); + ecs.register::(); + ecs.register::(); // Register synced resources used by the ECS. ecs.insert(TimeOfDay(0.0)); diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 399b102cff..89cfa2a8ab 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -8,6 +8,7 @@ use common::{ assets, cmd::{ChatCommand, CHAT_COMMANDS}, comp, + comp::Item, event::{EventBus, ServerEvent}, msg::{Notification, PlayerListUpdate, ServerMsg}, npc::{self, get_npc_name}, @@ -75,6 +76,8 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler { ChatCommand::Group => handle_group, ChatCommand::Health => handle_health, ChatCommand::Help => handle_help, + ChatCommand::JoinFaction => handle_join_faction, + ChatCommand::JoinGroup => handle_join_group, ChatCommand::Jump => handle_jump, ChatCommand::Kill => handle_kill, ChatCommand::KillNpcs => handle_kill_npcs, @@ -1020,13 +1023,20 @@ fn handle_tell( .get(player) .expect("Player must have uid"); let mode = comp::ChatMode::Tell(player_uid); - let _ = server.state.ecs().write_storage().insert(client, mode); + let _ = server + .state + .ecs() + .write_storage() + .insert(client, mode.clone()); let msg = if msg.is_empty() { format!("{} wants to talk to you.", alias) } else { msg.to_string() }; - server.notify_client(player, ServerMsg::chat(mode, client_uid, msg.clone())); + server.notify_client( + player, + ServerMsg::chat(mode.clone(), client_uid, msg.clone()), + ); server.notify_client(client, ServerMsg::chat(mode, client_uid, msg)); } else { server.notify_client( @@ -1057,14 +1067,24 @@ fn handle_faction( ); return; } - let mode = comp::ChatMode::Faction; - let _ = server.state.ecs().write_storage().insert(client, mode); - if !msg.is_empty() { - if let Some(uid) = server.state.ecs().read_storage().get(client) { - server - .state - .notify_registered_clients(ServerMsg::chat(mode, *uid, msg.to_string())); + let ecs = server.state.ecs(); + if let Some(comp::Faction(faction)) = ecs.read_storage().get(client) { + let mode = comp::ChatMode::Faction(faction.to_string()); + let _ = ecs.write_storage().insert(client, mode.clone()); + if !msg.is_empty() { + if let Some(uid) = ecs.read_storage().get(client) { + server.state.notify_registered_clients(ServerMsg::chat( + mode, + *uid, + msg.to_string(), + )); + } } + } else { + server.notify_client( + client, + ServerMsg::private(String::from("Please join a faction with /join_faction")), + ); } } @@ -1083,14 +1103,24 @@ fn handle_group( ); return; } - let mode = comp::ChatMode::Group; - let _ = server.state.ecs().write_storage().insert(client, mode); - if !msg.is_empty() { - if let Some(uid) = server.state.ecs().read_storage().get(client) { - server - .state - .notify_registered_clients(ServerMsg::chat(mode, *uid, msg.to_string())); + let ecs = server.state.ecs(); + if let Some(comp::Group(group)) = ecs.read_storage().get(client) { + let mode = comp::ChatMode::Group(group.to_string()); + let _ = ecs.write_storage().insert(client, mode.clone()); + if !msg.is_empty() { + if let Some(uid) = ecs.read_storage().get(client) { + server.state.notify_registered_clients(ServerMsg::chat( + mode, + *uid, + msg.to_string(), + )); + } } + } else { + server.notify_client( + client, + ServerMsg::private(String::from("Please join a group with /join_group")), + ); } } @@ -1110,7 +1140,11 @@ fn handle_region( return; } let mode = comp::ChatMode::Region; - let _ = server.state.ecs().write_storage().insert(client, mode); + let _ = server + .state + .ecs() + .write_storage() + .insert(client, mode.clone()); if !msg.is_empty() { if let Some(uid) = server.state.ecs().read_storage().get(client) { server @@ -1136,7 +1170,11 @@ fn handle_say( return; } let mode = comp::ChatMode::Say; - let _ = server.state.ecs().write_storage().insert(client, mode); + let _ = server + .state + .ecs() + .write_storage() + .insert(client, mode.clone()); if !msg.is_empty() { if let Some(uid) = server.state.ecs().read_storage().get(client) { server @@ -1162,7 +1200,11 @@ fn handle_world( return; } let mode = comp::ChatMode::World; - let _ = server.state.ecs().write_storage().insert(client, mode); + let _ = server + .state + .ecs() + .write_storage() + .insert(client, mode.clone()); if !msg.is_empty() { if let Some(uid) = server.state.ecs().read_storage().get(client) { server @@ -1172,6 +1214,88 @@ fn handle_world( } } +fn handle_join_faction( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + args: String, + action: &ChatCommand, +) { + if client != target { + // This happens when [ab]using /sudo + server.notify_client( + client, + ServerMsg::private(String::from("It's rude to impersonate people")), + ); + return; + } + if let Ok(faction) = scan_fmt!(&args, &action.arg_fmt(), String) { + let mode = comp::ChatMode::Faction(faction.clone()); + let _ = server.state.ecs().write_storage().insert(client, mode); + let _ = server + .state + .ecs() + .write_storage() + .insert(client, comp::Faction(faction.clone())); + // TODO notify faction + server.notify_client( + client, + ServerMsg::private(format!("Joined faction {{{}}}", faction)), + ); + } else { + let mode = comp::ChatMode::default(); + let _ = server.state.ecs().write_storage().insert(client, mode); + if let Some(comp::Faction(faction)) = server.state.ecs().write_storage().remove(client) { + // TODO notify faction + server.notify_client( + client, + ServerMsg::private(format!("Left faction {{{}}}", faction)), + ); + } + } +} + +fn handle_join_group( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + args: String, + action: &ChatCommand, +) { + if client != target { + // This happens when [ab]using /sudo + server.notify_client( + client, + ServerMsg::private(String::from("It's rude to impersonate people")), + ); + return; + } + if let Ok(group) = scan_fmt!(&args, &action.arg_fmt(), String) { + let mode = comp::ChatMode::Group(group.clone()); + let _ = server.state.ecs().write_storage().insert(client, mode); + let _ = server + .state + .ecs() + .write_storage() + .insert(client, comp::Group(group.clone())); + // TODO notify group + server.notify_client( + client, + ServerMsg::private(format!("Joined group {{{}}}", group)), + ); + } else { + let mode = comp::ChatMode::default(); + let _ = server.state.ecs().write_storage().insert(client, mode); + if let Some(comp::Group(group)) = server.state.ecs().write_storage().remove(client) { + // TODO notify group + server.notify_client( + client, + ServerMsg::private(format!("Left group {{{}}}", group)), + ); + } + } +} + #[cfg(not(feature = "worldgen"))] fn handle_debug_column( server: &mut Server, @@ -1375,7 +1499,6 @@ fn handle_set_level( } } -use common::comp::Item; fn handle_debug( server: &mut Server, client: EcsEntity, diff --git a/server/src/sys/message.rs b/server/src/sys/message.rs index 78a2fcd79a..6922e59b0e 100644 --- a/server/src/sys/message.rs +++ b/server/src/sys/message.rs @@ -328,8 +328,11 @@ impl<'a> System<'a> for Sys { | ClientState::Character => match validate_chat_msg(&message) { Ok(()) => { if let Some(from) = uids.get(entity) { - let mode = chat_modes.get(entity).unwrap_or(&ChatMode::World); - let msg = ServerMsg::chat(*mode, *from, message); + let mode = chat_modes + .get(entity) + .map(Clone::clone) + .unwrap_or(ChatMode::default()); + let msg = ServerMsg::chat(mode, *from, message); new_chat_msgs.push((Some(entity), msg)); } else { tracing::error!("Could not send message. Missing player uid"); diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs index 450cb1ccde..bfa629c4b2 100644 --- a/voxygen/src/hud/chat.rs +++ b/voxygen/src/hud/chat.rs @@ -321,7 +321,13 @@ impl<'a> Widget for Chat<'a> { } }) }; - let message_format = |uid, message| format!("[{}]: {}", alias_of_uid(uid), message); + let message_format = |uid, message, group| { + if let Some(group) = group { + format!("{{{}}} [{}]: {}", group, alias_of_uid(uid), message) + } else { + format!("[{}]: {}", alias_of_uid(uid), message) + } + }; // Message box Rectangle::fill([470.0, 174.0]) .rgba(0.0, 0.0, 0.0, transp) @@ -362,11 +368,13 @@ impl<'a> Widget for Chat<'a> { (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)), + ChatType::Say(uid) => (SAY_COLOR, message_format(uid, message, None)), + ChatType::Group(uid, s) => (GROUP_COLOR, message_format(uid, message, Some(s))), + ChatType::Faction(uid, s) => { + (FACTION_COLOR, message_format(uid, message, Some(s))) + }, + ChatType::Region(uid) => (REGION_COLOR, message_format(uid, message, None)), + ChatType::World(uid) => (WORLD_COLOR, message_format(uid, message, None)), ChatType::Npc(_uid, _r) => continue, // Should be filtered by hud/mod.rs }; let text = Text::new(&msg)