mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-31 08:22:33 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
47
pumpkin/src/command/commands/tellraw.rs
Normal file
47
pumpkin/src/command/commands/tellraw.rs
Normal 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)),
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user