Pumpkin/pumpkin/src/command/dispatcher.rs
2026-01-17 21:59:51 +01:00

452 lines
16 KiB
Rust

use pumpkin_protocol::java::client::play::CommandSuggestion;
use pumpkin_util::text::TextComponent;
use super::args::ConsumedArgs;
use crate::command::CommandSender;
use crate::command::dispatcher::CommandError::{
CommandFailed, InvalidConsumption, InvalidRequirement, PermissionDenied,
};
use crate::command::tree::{Command, CommandTree, NodeType, RawArgs};
use crate::server::Server;
use std::collections::{HashMap, HashSet};
#[derive(Debug)]
pub enum CommandError {
/// This error means that there was an error while parsing a previously consumed argument.
/// That only happens when consumption is wrongly implemented, as it should ensure parsing may
/// never fail.
InvalidConsumption(Option<String>),
/// Return this if a condition that a [`Node::Require`] should ensure is met is not met.
InvalidRequirement,
/// The command could not be executed due to insufficient permissions.
/// The user attempting to run the command lacks the necessary authorization.
PermissionDenied,
/// A general error occurred during command execution that doesn't fit into
/// more specific `CommandError` variants.
CommandFailed(TextComponent),
}
impl CommandError {
#[must_use]
pub fn into_component(self, cmd: &str) -> TextComponent {
match self {
InvalidConsumption(s) => {
log::error!(
"Error while parsing command \"{cmd}\": {s:?} was consumed, but couldn't be parsed"
);
TextComponent::text("Internal error (See logs for details)")
}
InvalidRequirement => {
log::error!(
"Error while parsing command \"{cmd}\": a requirement that was expected was not met."
);
TextComponent::text("Internal error (See logs for details)")
}
PermissionDenied => {
log::warn!("Permission denied for command \"{cmd}\"");
TextComponent::text(
"I'm sorry, but you do not have permission to perform this command. Please contact the server administrator if you believe this is an error.",
)
}
CommandFailed(s) => s,
}
}
}
#[derive(Default)]
pub struct CommandDispatcher {
pub commands: HashMap<String, Command>,
pub permissions: HashMap<String, String>,
}
/// Stores registered [`CommandTree`]s and dispatches commands to them.
impl CommandDispatcher {
pub async fn handle_command<'a>(
&'a self,
sender: &CommandSender,
server: &'a Server,
cmd: &'a str,
) {
let result = self.dispatch(sender, server, cmd).await;
sender.set_success_count(u32::from(result.is_ok()));
if let Err(e) = result {
let text = e.into_component(cmd);
sender
.send_message(text.color_named(pumpkin_util::text::color::NamedColor::Red))
.await;
}
}
/// server side suggestions (client side suggestions work independently)
///
/// # todo
/// - make this less ugly
/// - do not query suggestions for the same consumer multiple times just because they are on different paths through the tree
pub(crate) async fn find_suggestions<'a>(
&'a self,
src: &CommandSender,
server: &'a Server,
cmd: &'a str,
) -> Vec<CommandSuggestion> {
let mut parts = cmd.split_whitespace();
let Some(key) = parts.next() else {
return Vec::new();
};
let mut raw_args: Vec<&str> = parts.rev().collect();
let Ok(tree) = self.get_tree(key) else {
return Vec::new();
};
let mut suggestions = HashSet::new();
// try paths and collect the nodes that fail
// todo: make this more fine-grained
for path in tree.iter_paths() {
match Self::try_find_suggestions_on_path(src, server, &path, tree, &mut raw_args, cmd)
.await
{
Err(InvalidConsumption(s)) => {
log::debug!(
"Error while parsing command \"{cmd}\": {s:?} was consumed, but couldn't be parsed"
);
return Vec::new();
}
Err(InvalidRequirement) => {
log::debug!(
"Error while parsing command \"{cmd}\": a requirement that was expected was not met."
);
return Vec::new();
}
Err(PermissionDenied) => {
log::debug!("Permission denied for command \"{cmd}\"");
return Vec::new();
}
Err(CommandFailed(_)) => {
log::debug!("Command failed");
return Vec::new();
}
Ok(Some(new_suggestions)) => {
suggestions.extend(new_suggestions);
}
Ok(None) => {
log::debug!("Command none");
}
}
}
let mut suggestions = Vec::from_iter(suggestions);
suggestions.sort_by(|a, b| a.suggestion.cmp(&b.suggestion));
suggestions
}
pub(crate) fn split_parts(cmd: &str) -> Result<(&str, Vec<&str>), CommandError> {
if cmd.is_empty() {
return Err(CommandFailed(TextComponent::text("Empty Command")));
}
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(CommandFailed(TextComponent::text("Unmatched braces")));
}
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(CommandFailed(TextComponent::text("Unmatched brackets")));
}
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(CommandFailed(TextComponent::text(
"Unmatched quotes at the end",
)));
}
if in_braces != 0 {
return Err(CommandFailed(TextComponent::text(
"Unmatched braces at the end",
)));
}
if in_brackets != 0 {
return Err(CommandFailed(TextComponent::text(
"Unmatched brackets at the end",
)));
}
if args.is_empty() {
return Err(CommandFailed(TextComponent::text("Empty Command")));
}
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,
src: &CommandSender,
server: &'a Server,
cmd: &'a str,
) -> Result<(), CommandError> {
let (key, raw_args) = Self::split_parts(cmd)?;
if !self.commands.contains_key(key) {
return Err(CommandFailed(TextComponent::text(format!(
"Command {key} does not exist"
))));
}
let Some(permission) = self.permissions.get(key) else {
return Err(CommandFailed(TextComponent::text(
"Permission for Command not found".to_string(),
)));
};
if !src.has_permission(permission.as_str()).await {
return Err(PermissionDenied);
}
let tree = self.get_tree(key)?;
// try paths until fitting path is found
for path in tree.iter_paths() {
if Self::try_is_fitting_path(src, server, &path, tree, &mut raw_args.clone()).await? {
return Ok(());
}
}
Err(CommandFailed(TextComponent::text(format!(
"Invalid Syntax. Usage: {tree}"
))))
}
pub fn get_tree<'a>(&'a self, key: &str) -> Result<&'a CommandTree, CommandError> {
let command = self
.commands
.get(key)
.ok_or(CommandFailed(TextComponent::text("Command not found")))?;
match command {
Command::Tree(tree) => Ok(tree),
Command::Alias(target) => {
let Some(Command::Tree(tree)) = self.commands.get(target) else {
log::error!(
"Error while parsing command alias \"{key}\": pointing to \"{target}\" which is not a valid tree"
);
return Err(CommandFailed(TextComponent::text(
"Internal Error (See logs for details)",
)));
};
Ok(tree)
}
}
}
async fn try_is_fitting_path<'a>(
src: &'a CommandSender,
server: &'a Server,
path: &[usize],
tree: &'a CommandTree,
raw_args: &mut RawArgs<'a>,
) -> Result<bool, CommandError> {
let mut parsed_args: ConsumedArgs = HashMap::new();
for node in path.iter().map(|&i| &tree.nodes[i]) {
match &node.node_type {
NodeType::ExecuteLeaf { executor } => {
return if raw_args.is_empty() {
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, .. } => {
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)
}
async fn try_find_suggestions_on_path<'a>(
src: &'a CommandSender,
server: &'a Server,
path: &[usize],
tree: &'a CommandTree,
raw_args: &mut RawArgs<'a>,
input: &'a str,
) -> Result<Option<Vec<CommandSuggestion>>, CommandError> {
let mut parsed_args: ConsumedArgs = HashMap::new();
for node in path.iter().map(|&i| &tree.nodes[i]) {
match &node.node_type {
NodeType::ExecuteLeaf { .. } => {
return Ok(None);
}
NodeType::Literal { string, .. } => {
if raw_args.pop() != Some(string) {
return Ok(None);
}
}
NodeType::Argument { consumer, name } => {
match consumer.consume(src, server, raw_args).await {
Some(consumed) => {
parsed_args.insert(name, consumed);
}
None => {
return if raw_args.is_empty() {
let suggestions = consumer.suggest(src, server, input).await?;
Ok(suggestions)
} else {
Ok(None)
};
}
}
}
NodeType::Require { predicate, .. } => {
if !predicate(src) {
return Ok(None);
}
}
}
}
Ok(None)
}
/// Register a command with the dispatcher.
pub fn register<P: Into<String>>(&mut self, tree: CommandTree, permission: P) {
let mut names = tree.names.iter();
let permission = permission.into();
let primary_name = names.next().expect("at least one name must be provided");
for name in names {
self.commands
.insert(name.clone(), Command::Alias(primary_name.clone()));
self.permissions.insert(name.clone(), permission.clone());
}
self.permissions.insert(primary_name.clone(), permission);
self.commands
.insert(primary_name.clone(), Command::Tree(tree));
}
/// Remove a command from the dispatcher by its primary name.
pub fn unregister(&mut self, name: &str) {
let mut to_remove = Vec::new();
for (key, value) in &self.commands {
if key == name {
to_remove.push(key.clone());
} else if let Command::Alias(target) = value
&& target == name
{
to_remove.push(key.clone());
}
}
for key in to_remove {
self.commands.remove(&key);
self.permissions.remove(&key);
}
}
}
#[cfg(test)]
mod test {
use pumpkin_config::BasicConfiguration;
use crate::command::{commands::default_dispatcher, tree::CommandTree};
#[tokio::test]
async fn test_dynamic_command() {
let config = BasicConfiguration::default();
let mut dispatcher = default_dispatcher(&config).await;
let tree = CommandTree::new(["test"], "test_desc");
dispatcher.register(tree, "minecraft:test");
}
}