diff --git a/pumpkin-core/src/math/distance.rs b/pumpkin-core/src/math/distance.rs new file mode 100644 index 000000000..2a38d1c8f --- /dev/null +++ b/pumpkin-core/src/math/distance.rs @@ -0,0 +1,9 @@ +use super::vector3::Vector3; + +pub fn distance(p1: &Vector3, p2: &Vector3) -> f64 { + let dx = p1.x - p2.x; + let dy = p1.y - p2.y; + let dz = p1.z - p2.z; + + dz.mul_add(dz, dx.mul_add(dx, dy * dy)).sqrt() +} diff --git a/pumpkin-core/src/math/mod.rs b/pumpkin-core/src/math/mod.rs index e1f7149f0..d3c37e384 100644 --- a/pumpkin-core/src/math/mod.rs +++ b/pumpkin-core/src/math/mod.rs @@ -1,4 +1,5 @@ pub mod boundingbox; +pub mod distance; pub mod position; pub mod vector2; pub mod vector3; diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index 8e737ab63..89091407b 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -53,7 +53,7 @@ thiserror = "1.0" # icon loading base64 = "0.22.1" -png = "0.17.14" +png = "0.17.14" # logging simple_logger = { version = "5.0.0", features = ["threads"] } diff --git a/pumpkin/src/commands/cmd_say.rs b/pumpkin/src/commands/cmd_say.rs new file mode 100644 index 000000000..4337649db --- /dev/null +++ b/pumpkin/src/commands/cmd_say.rs @@ -0,0 +1,123 @@ +use crate::commands::tree::CommandTree; +use crate::commands::tree::RawArgs; +use crate::commands::tree_builder::argument; +use crate::commands::CommandSender; +use crate::entity::player::Player; +use crate::server::Server; +use pumpkin_core::text::{color::NamedColor, TextComponent}; + +const NAMES: [&str; 1] = ["say"]; +const DESCRIPTION: &str = "Sends a message to all players."; + +const ARG_CONTENT: &str = "content"; + +pub fn consume_arg_content(_src: &CommandSender, args: &mut RawArgs) -> Option { + let mut all_args: Vec = args.drain(..).map(|v| v.to_string()).collect(); + + if all_args.is_empty() { + None + } else { + all_args.reverse(); + Some(all_args.join(" ")) + } +} + +pub fn init_command_tree<'a>() -> CommandTree<'a> { + CommandTree::new(NAMES, DESCRIPTION).with_child( + argument(ARG_CONTENT, consume_arg_content).execute(&|sender, server, args| { + if let Some(content) = args.get("content") { + let content = parse_selectors(content, sender, server); + let message = &format!("[Console]: {content}"); + let message = TextComponent::text(message).color_named(NamedColor::Blue); + + server.broadcast_message(&message); + + if !sender.is_player() { + sender.send_message(message); + } + } else { + sender.send_message( + TextComponent::text("Please provide a message: say [content]") + .color_named(NamedColor::Red), + ); + } + + Ok(()) + }), + ) +} + +fn parse_selectors(content: &str, sender: &CommandSender, server: &Server) -> String { + let mut final_message = String::new(); + + let tokens: Vec<&str> = content.split_whitespace().collect(); + + for token in tokens { + let parsed_token = parse_token(token, sender, server); + final_message.push_str(&parsed_token); + final_message.push(' '); + } + + final_message.trim_end().to_string() +} + +fn parse_token<'a>(token: &'a str, sender: &'a CommandSender, server: &'a Server) -> String { + let result = match token { + "@p" => { + if let CommandSender::Player(player) = sender { + get_nearest_player_name(player) + .map_or_else(Vec::new, |player_name| vec![player_name]) + } else { + return token.to_string(); + } + } + "@r" => { + let online_player_names: Vec = server.get_online_player_names(); + + if online_player_names.is_empty() { + vec![String::from("nobody")] + } else { + vec![ + online_player_names[rand::random::() % online_player_names.len()] + .clone(), + ] + } + } + "@s" => match sender { + CommandSender::Player(p) => vec![p.gameprofile.name.clone()], + _ => vec![String::from("console")], + }, + "@a" => server.get_online_player_names(), + "@here" => server.get_online_player_names(), + _ => { + return token.to_string(); + } + }; + + format_player_names(&result) +} + +// Gets the nearest player name in the same world +fn get_nearest_player_name(player: &Player) -> Option { + let target = player.last_position.load(); + + player + .living_entity + .entity + .world + .get_nearest_player_name(&target) +} + +// Helper function to format player names according to spec +// see https://minecraft.fandom.com/wiki/Commands/say +fn format_player_names(names: &[String]) -> String { + match names.len() { + 0 => String::new(), + 1 => names[0].clone(), + 2 => format!("{} and {}", names[0], names[1]), + _ => { + let (last, rest) = names.split_last().unwrap(); + format!("{}, and {}", rest.join(", "), last) + } + } +} diff --git a/pumpkin/src/commands/mod.rs b/pumpkin/src/commands/mod.rs index ef6e1b230..a2e96d84d 100644 --- a/pumpkin/src/commands/mod.rs +++ b/pumpkin/src/commands/mod.rs @@ -12,6 +12,7 @@ mod cmd_help; mod cmd_kick; mod cmd_kill; mod cmd_pumpkin; +mod cmd_say; mod cmd_stop; pub mod dispatcher; mod tree; @@ -77,6 +78,7 @@ pub fn default_dispatcher<'a>() -> CommandDispatcher<'a> { dispatcher.register(cmd_echest::init_command_tree()); dispatcher.register(cmd_kill::init_command_tree()); dispatcher.register(cmd_kick::init_command_tree()); + dispatcher.register(cmd_say::init_command_tree()); dispatcher } diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index 6deee69e2..5fb8e5737 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -2,6 +2,7 @@ use connection_cache::{CachedBranding, CachedStatus}; use key_store::KeyStore; use parking_lot::{Mutex, RwLock}; use pumpkin_config::BASIC_CONFIG; +use pumpkin_core::text::TextComponent; use pumpkin_core::GameMode; use pumpkin_entity::EntityId; use pumpkin_inventory::drag_handler::DragHandler; @@ -127,6 +128,21 @@ impl Server { } } + /// Sends a message to all players in every world + pub fn broadcast_message(&self, content: &TextComponent) { + self.worlds + .iter() + .for_each(|w| w.broadcast_message(content)); + } + + /// Get all online player names + pub fn get_online_player_names(&self) -> Vec { + self.worlds + .iter() + .flat_map(|world| world.get_player_names()) + .collect::>() + } + /// Searches every world for a player by name pub fn get_player_by_name(&self, name: &str) -> Option> { for world in self.worlds.iter() { diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 78e9f1c31..a9fb9eaf6 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -9,7 +9,10 @@ use crate::{ use num_traits::ToPrimitive; use parking_lot::Mutex; use pumpkin_config::BasicConfiguration; -use pumpkin_core::math::vector2::Vector2; +use pumpkin_core::{ + math::{distance::distance, vector2::Vector2, vector3::Vector3}, + text::TextComponent, +}; use pumpkin_entity::{entity_type::EntityType, EntityId}; use pumpkin_protocol::{ client::play::{ @@ -259,6 +262,34 @@ impl World { dbg!("DONE CHUNKS", inst.elapsed()); } + /// Sends a message to all players + pub fn broadcast_message(&self, content: &TextComponent) { + self.current_players.lock().values().for_each(|player| { + player.send_system_message(content.clone()); + }); + } + + /// Gets all players + pub fn get_player_names(&self) -> Vec { + self.current_players + .lock() + .values() + .map(|p| p.gameprofile.name.clone()) + .collect::>() + } + + pub fn get_nearest_player_name(&self, target: &Vector3) -> Option { + self.current_players + .lock() + .values() + .min_by(|a, b| { + let dist_a = distance(&a.last_position.load(), target); + let dist_b = distance(&b.last_position.load(), target); + dist_a.partial_cmp(&dist_b).unwrap() + }) + .map(|p| p.gameprofile.name.clone()) + } + /// Gets a Player by entity id pub fn get_player_by_entityid(&self, id: EntityId) -> Option> { for player in self.current_players.lock().values() {