-
-
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 all 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 |
---|---|---|
@@ -1,4 +1,5 @@ | ||
pub mod boundingbox; | ||
pub mod distance; | ||
pub mod position; | ||
pub mod vector2; | ||
pub mod vector3; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<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); | ||
|
||
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<String> = server.get_online_player_names(); | ||
|
||
if online_player_names.is_empty() { | ||
vec![String::from("nobody")] | ||
} else { | ||
vec![ | ||
online_player_names[rand::random::<usize>() % 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<String> { | ||
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) | ||
} | ||
} | ||
} |
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 |
---|---|---|
|
@@ -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()); | ||
}); | ||
} | ||
Comment on lines
+266
to
+270
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. Nit: Can we make this generic over an arbitrary ChatMessage trait implemented for strings, textcomponents, etc? Or make it generic over something like |
||
|
||
/// Gets all players | ||
pub fn get_player_names(&self) -> Vec<String> { | ||
self.current_players | ||
.lock() | ||
.values() | ||
.map(|p| p.gameprofile.name.clone()) | ||
.collect::<Vec<_>>() | ||
} | ||
|
||
pub fn get_nearest_player_name(&self, target: &Vector3<f64>) -> Option<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. I understand this PR is not intended to give us the functionality for everything, but this should not be let Some(name) = get_nearest_player().map(Player::get_name) else {
return Err(...)
} or something. for a single |
||
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<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: