-
-
Notifications
You must be signed in to change notification settings - Fork 152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add broadcast command #148
Changes from 6 commits
c8df113
118da0d
b3561a5
ca376b7
fa4f1bb
9b624db
36aba13
b2415af
3b03ebe
1b3e040
7ac0d12
07542b1
d80a481
7b1b427
2d249c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
use super::vector3::Vector3; | ||
|
||
pub fn distance(p1: &Vector3<f64>, p2: &Vector3<f64>) -> 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() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
use crate::commands::tree::CommandTree; | ||
use crate::commands::tree::RawArgs; | ||
use crate::commands::tree_builder::argument; | ||
use crate::commands::CommandSender; | ||
use crate::server::Server; | ||
use pumpkin_core::math::vector3::Vector3; | ||
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<String> { | ||
let mut all_args: Vec<String> = 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.clone()); | ||
van-sprundel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 { | ||
// TODO impl @e | ||
let result = match token { | ||
"@p" => { | ||
let position = match sender { | ||
CommandSender::Player(p) => p.last_position.load(), | ||
_ => Vector3::new(0., 0., 0.), | ||
}; | ||
vec![server | ||
.get_nearest_player(&position) | ||
.map_or_else(|| String::from("nobody"), |p| p.gameprofile.name.clone())] | ||
} | ||
"@r" => { | ||
let online_players: Vec<String> = server | ||
.get_online_players() | ||
.map(|p| p.gameprofile.name.clone()) | ||
.collect(); | ||
|
||
if online_players.is_empty() { | ||
vec![String::from("nobody")] | ||
} else { | ||
vec![online_players[rand::random::<usize>() % online_players.len()].clone()] | ||
} | ||
} | ||
"@s" => match sender { | ||
CommandSender::Player(p) => vec![p.gameprofile.name.clone()], | ||
_ => vec![String::from("console")], | ||
}, | ||
"@a" => server | ||
.get_online_players() | ||
.map(|p| p.gameprofile.name.clone()) | ||
.collect::<Vec<_>>(), | ||
"@here" => server | ||
.get_online_players() | ||
.map(|p| p.gameprofile.name.clone()) | ||
.collect::<Vec<_>>(), | ||
_ => vec![token.to_string()], | ||
}; | ||
|
||
// formatted player names | ||
final_message.push_str(&format_player_names(&result)); | ||
final_message.push(' '); | ||
} | ||
|
||
final_message.trim_end().to_string() | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We already have player arguments implemented, See for example gamemode command or kick command... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
||
// 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) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't your problem per-se, but When I see large blocks of repetitive code like this my eyes start to glaze over and I stop being able to read. It sets off alarm bells that the code could be simplified some. It might not be getting to that point right now, but with how large and repetitive this is getting, another abstraction would be nice. List OptionSomething like fn register_commands(&mut self, commands: impl IntoIterator<C>)
where C: CommandTree
{
for command in commands {
dispatcher.register(command::default());
}
}
// in default_dispatcher
dispatcher.register_commands(vec![ cmd_pumpkin, cmd_gamemode, ... ]); Macro Optionmacro_rules! init_commands {
(($command:ident),*) => { $( dispatcher.register($command::init_command_tree()); )*}
}
// In default_dispatcher
init_commands!(cmd_pumpkin, cmd_gamemode, cmd_stop, cmd_help, cmd_echest, cmd_kill); Say what you will about macros, but as long as they're explained, they make the boilerplate much more legible. In the current code, The macro option can be implemented as-is, but the list list one requires you to do some more significant refactors. Neither has to be implemented right now, but it's getting there. |
||
|
||
dispatcher | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,9 @@ use connection_cache::{CachedBranding, CachedStatus}; | |
use key_store::KeyStore; | ||
use parking_lot::{Mutex, RwLock}; | ||
use pumpkin_config::BASIC_CONFIG; | ||
use pumpkin_core::math::distance::distance; | ||
use pumpkin_core::math::vector3::Vector3; | ||
use pumpkin_core::text::TextComponent; | ||
use pumpkin_core::GameMode; | ||
use pumpkin_entity::EntityId; | ||
use pumpkin_inventory::drag_handler::DragHandler; | ||
|
@@ -127,6 +130,35 @@ impl Server { | |
} | ||
} | ||
|
||
/// Sends a message to all players in every world | ||
pub fn broadcast_message(&self, content: TextComponent) { | ||
for world in &self.worlds { | ||
world.broadcast_message(&content); | ||
} | ||
} | ||
|
||
/// Get all online players | ||
pub fn get_online_players(&self) -> impl Iterator<Item = Arc<Player>> + '_ { | ||
self.worlds.iter().flat_map(|world| world.get_players()) | ||
} | ||
|
||
/// Gets the nearest player from the world position | ||
pub fn get_nearest_player(&self, target: &Vector3<f64>) -> Option<Arc<Player>> { | ||
// TODO respect which world the player is in | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can easily get the World from a player by calling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In minecraft, the nearest player has to be in the same world. If that's the plan though, then I can change it |
||
let world = self.worlds.first()?; | ||
|
||
world | ||
.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() | ||
}) | ||
.cloned() | ||
van-sprundel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/// Searches every world for a player by name | ||
pub fn get_player_by_name(&self, name: &str) -> Option<Arc<Player>> { | ||
for world in self.worlds.iter() { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,7 @@ use crate::{ | |
use num_traits::ToPrimitive; | ||
use parking_lot::Mutex; | ||
use pumpkin_config::BasicConfiguration; | ||
use pumpkin_core::math::vector2::Vector2; | ||
use pumpkin_core::{math::vector2::Vector2, text::TextComponent}; | ||
use pumpkin_entity::{entity_type::EntityType, EntityId}; | ||
use pumpkin_protocol::{ | ||
client::play::{ | ||
|
@@ -259,6 +259,22 @@ impl World { | |
dbg!("DONE CHUNKS", inst.elapsed()); | ||
} | ||
|
||
/// Sends a message to all players | ||
pub fn broadcast_message(&self, content: &TextComponent) { | ||
for player in self.current_players.lock().values() { | ||
player.send_system_message(content.clone()); | ||
} | ||
} | ||
|
||
/// Gets all players | ||
pub fn get_players(&self) -> Vec<Arc<Player>> { | ||
self.current_players | ||
.lock() | ||
.values() | ||
.cloned() | ||
.collect::<Vec<_>>() | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like copying the Vec here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it. Changed it to clone just the name string instead of the entire player |
||
|
||
/// Gets a Player by entity id | ||
pub fn get_player_by_entityid(&self, id: EntityId) -> Option<Arc<Player>> { | ||
for player in self.current_players.lock().values() { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I'm a fan of having something as trivial as "distance" be its own file unless we have a larger set of traits. Is there a reason this isn't in
Vector3
?Something like: