diff --git a/pumpkin-util/src/text/mod.rs b/pumpkin-util/src/text/mod.rs index b32702ac3..b58b9c3e5 100644 --- a/pumpkin-util/src/text/mod.rs +++ b/pumpkin-util/src/text/mod.rs @@ -1,12 +1,13 @@ -use core::str; -use std::borrow::Cow; - use crate::{text::color::ARGBColor, translation::get_translation_en_us}; use click::ClickEvent; use color::Color; use colored::Colorize; +use core::str; use hover::HoverEvent; -use serde::{Deserialize, Serialize}; +use serde::de::{Error, MapAccess, SeqAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serialize}; +use std::borrow::Cow; +use std::fmt::Formatter; use style::Style; pub mod click; @@ -15,9 +16,66 @@ pub mod hover; pub mod style; /// Represents a text component -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, Serialize, PartialEq, Eq, Hash)] pub struct TextComponent(pub TextComponentBase); +impl<'de> Deserialize<'de> for TextComponent { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct TextComponentVisitor; + + impl<'de> Visitor<'de> for TextComponentVisitor { + type Value = TextComponentBase; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("a TextComponentBase or a sequence of TextComponentBase") + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + Ok(TextComponentBase { + content: TextContent::Text { + text: Cow::from(v.to_string()), + }, + style: Default::default(), + extra: vec![], + }) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut bases = Vec::new(); + while let Some(element) = seq.next_element::()? { + bases.push(element.0); + } + + Ok(TextComponentBase { + content: TextContent::Text { text: "".into() }, + style: Default::default(), + extra: bases, + }) + } + + fn visit_map(self, map: A) -> Result + where + A: MapAccess<'de>, + { + TextComponentBase::deserialize(serde::de::value::MapAccessDeserializer::new(map)) + } + } + + deserializer + .deserialize_any(TextComponentVisitor) + .map(TextComponent) + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(rename_all = "camelCase")] pub struct TextComponentBase { diff --git a/pumpkin/src/command/args/textcomponent.rs b/pumpkin/src/command/args/textcomponent.rs index ab53086fb..d74d738f4 100644 --- a/pumpkin/src/command/args/textcomponent.rs +++ b/pumpkin/src/command/args/textcomponent.rs @@ -5,7 +5,7 @@ use crate::command::tree::RawArgs; use crate::server::Server; use async_trait::async_trait; use pumpkin_protocol::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders}; -use pumpkin_util::text::{TextComponent, TextContent}; +use pumpkin_util::text::TextComponent; pub(crate) struct TextComponentArgConsumer; @@ -64,10 +64,11 @@ impl FindArg<'_> for TextComponentArgConsumer { } fn parse_text_component(input: &str) -> Option { - if input.starts_with('{') && input.ends_with('}') { - let text_component: Option = serde_json::from_str(input).unwrap_or(None); - Some(TextComponent::from_content(text_component?)) + let result = serde_json::from_str(input); + if let Err(e) = result { + log::debug!("Failed to parse text component: {e}"); + None } else { - serde_json::from_str(input).unwrap_or(None) + result.unwrap() } } diff --git a/pumpkin/src/command/commands/mod.rs b/pumpkin/src/command/commands/mod.rs index 26ebec08f..476b4a4c1 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -45,6 +45,7 @@ mod worldborder; #[cfg(feature = "dhat-heap")] mod profile; +mod tellraw; #[must_use] pub fn default_dispatcher() -> CommandDispatcher { @@ -69,6 +70,7 @@ pub fn default_dispatcher() -> CommandDispatcher { dispatcher.register(seed::init_command_tree(), PermissionLvl::Two); dispatcher.register(fill::init_command_tree(), PermissionLvl::Two); dispatcher.register(playsound::init_command_tree(), PermissionLvl::Two); + dispatcher.register(tellraw::init_command_tree(), PermissionLvl::Two); dispatcher.register(title::init_command_tree(), PermissionLvl::Two); dispatcher.register(summon::init_command_tree(), PermissionLvl::Two); dispatcher.register(experience::init_command_tree(), PermissionLvl::Two); diff --git a/pumpkin/src/command/commands/tellraw.rs b/pumpkin/src/command/commands/tellraw.rs new file mode 100644 index 000000000..538965c04 --- /dev/null +++ b/pumpkin/src/command/commands/tellraw.rs @@ -0,0 +1,47 @@ +use crate::command::{ + CommandError, CommandExecutor, CommandSender, + args::{ + Arg, ConsumedArgs, FindArg, players::PlayersArgumentConsumer, + textcomponent::TextComponentArgConsumer, + }, + tree::CommandTree, + tree::builder::argument, +}; +use async_trait::async_trait; + +const NAMES: [&str; 1] = ["tellraw"]; + +const DESCRIPTION: &str = "Send raw message to players."; + +const ARG_TARGETS: &str = "targets"; + +const ARG_MESSAGE: &str = "message"; + +struct TellRawExecutor; + +#[async_trait] +impl CommandExecutor for TellRawExecutor { + async fn execute<'a>( + &self, + _sender: &mut CommandSender, + _server: &crate::server::Server, + args: &ConsumedArgs<'a>, + ) -> Result<(), CommandError> { + let Some(Arg::Players(targets)) = args.get(&ARG_TARGETS) else { + return Err(CommandError::InvalidConsumption(Some(ARG_TARGETS.into()))); + }; + + let text = TextComponentArgConsumer::find_arg(args, ARG_MESSAGE)?; + for target in targets { + target.send_system_message(&text).await; + } + Ok(()) + } +} + +pub fn init_command_tree() -> CommandTree { + CommandTree::new(NAMES, DESCRIPTION).then( + argument(ARG_TARGETS, PlayersArgumentConsumer) + .then(argument(ARG_MESSAGE, TextComponentArgConsumer).execute(TellRawExecutor)), + ) +} diff --git a/pumpkin/src/command/dispatcher.rs b/pumpkin/src/command/dispatcher.rs index fb759df76..9fc819648 100644 --- a/pumpkin/src/command/dispatcher.rs +++ b/pumpkin/src/command/dispatcher.rs @@ -153,6 +153,101 @@ impl CommandDispatcher { suggestions } + pub(crate) fn split_parts(cmd: &str) -> Result<(&str, Vec<&str>), CommandError> { + if cmd.is_empty() { + return Err(GeneralCommandIssue("Empty Command".to_string())); + } + let mut args = Vec::new(); + let mut current_arg_start = 0usize; + let mut in_single_quotes = false; + let mut in_double_quotes = false; + let mut in_braces = 0u32; + let mut in_brackets = 0u32; + let mut is_escaping = false; + for (i, c) in cmd.char_indices() { + if c == '\\' { + is_escaping = !is_escaping; + continue; + } + if is_escaping { + is_escaping = false; + continue; + } + match c { + '{' => { + if !in_single_quotes && !in_double_quotes { + in_braces += 1; + } + } + '}' => { + if !in_single_quotes && !in_double_quotes { + if in_braces == 0 { + return Err(GeneralCommandIssue("Unmatched braces".to_string())); + } + in_braces -= 1; + } + } + '[' => { + if !in_single_quotes && !in_double_quotes { + in_brackets += 1; + } + } + ']' => { + if !in_single_quotes && !in_double_quotes { + if in_brackets == 0 { + return Err(GeneralCommandIssue("Unmatched brackets".to_string())); + } + in_brackets -= 1; + } + } + '\'' => { + if !in_double_quotes { + in_single_quotes = !in_single_quotes; + } + } + '"' => { + if !in_single_quotes { + in_double_quotes = !in_double_quotes; + } + } + ' ' if !in_single_quotes + && !in_double_quotes + && in_braces == 0 + && in_brackets == 0 => + { + if current_arg_start != i { + args.push(&cmd[current_arg_start..i]); + } + current_arg_start = i + 1; + } + _ => {} + } + } + if current_arg_start != cmd.len() { + args.push(&cmd[current_arg_start..]); + } + if in_single_quotes || in_double_quotes { + return Err(GeneralCommandIssue( + "Unmatched quotes at the end".to_string(), + )); + } + if in_braces != 0 { + return Err(GeneralCommandIssue( + "Unmatched braces at the end".to_string(), + )); + } + if in_brackets != 0 { + return Err(GeneralCommandIssue( + "Unmatched brackets at the end".to_string(), + )); + } + if args.is_empty() { + return Err(GeneralCommandIssue("Empty Command".to_string())); + } + let key = args.remove(0); + Ok((key, args.into_iter().rev().collect())) + } + /// Execute a command using its corresponding [`CommandTree`]. pub(crate) async fn dispatch<'a>( &'a self, @@ -160,12 +255,7 @@ impl CommandDispatcher { server: &'a Server, cmd: &'a str, ) -> Result<(), CommandError> { - // Other languages dont use the ascii whitespace - let mut parts = cmd.split_whitespace(); - let key = parts - .next() - .ok_or(GeneralCommandIssue("Empty Command".to_string()))?; - let raw_args: Vec<&str> = parts.rev().collect(); + let (key, raw_args) = Self::split_parts(cmd)?; if !self.commands.contains_key(key) { return Err(GeneralCommandIssue(format!("Command {key} does not exist"))); @@ -236,30 +326,42 @@ impl CommandDispatcher { executor.execute(src, server, &parsed_args).await?; Ok(true) } else { + log::debug!( + "Error while parsing command: {raw_args:?} was not consumed, but should have been" + ); Ok(false) }; } NodeType::Literal { string, .. } => { if raw_args.pop() != Some(string) { + log::debug!("Error while parsing command: {raw_args:?}: expected {string}"); return Ok(false); } } NodeType::Argument { consumer, name, .. } => { - match consumer.consume(src, server, raw_args).await { - Some(consumed) => { - parsed_args.insert(name, consumed); - } - None => return Ok(false), + if let Some(consumed) = consumer.consume(src, server, raw_args).await { + parsed_args.insert(name, consumed); + } else { + log::debug!( + "Error while parsing command: {raw_args:?}: cannot parse argument {name}" + ); + return Ok(false); } } NodeType::Require { predicate, .. } => { if !predicate(src) { + log::debug!( + "Error while parsing command: {raw_args:?} does not meet the requirement" + ); return Ok(false); } } } } + log::debug!( + "Error while parsing command: {raw_args:?} was not consumed, but should have been" + ); Ok(false) }