Implement /tellraw and fix command parser (#778)

* fix command parser and implement tellraw

* fix

* fix

* fix

* fix

* rename

* fix

* fix

* fix

* fix

* fix

* fix

* fix: support escaping

* fix

* fix
This commit is contained in:
Liyan Zhao
2025-05-03 01:51:57 +08:00
committed by GitHub
parent a516a68a24
commit f18084e9a2
5 changed files with 231 additions and 21 deletions

View File

@@ -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<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
Ok(TextComponentBase {
content: TextContent::Text {
text: Cow::from(v.to_string()),
},
style: Default::default(),
extra: vec![],
})
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut bases = Vec::new();
while let Some(element) = seq.next_element::<TextComponent>()? {
bases.push(element.0);
}
Ok(TextComponentBase {
content: TextContent::Text { text: "".into() },
style: Default::default(),
extra: bases,
})
}
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
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 {

View File

@@ -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<TextComponent> {
if input.starts_with('{') && input.ends_with('}') {
let text_component: Option<TextContent> = 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()
}
}

View File

@@ -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);

View File

@@ -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)),
)
}

View File

@@ -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)
}