From 5041b441eb511fc488c4ea6c96f299adda14c78b Mon Sep 17 00:00:00 2001 From: user622628252416 Date: Wed, 21 Aug 2024 17:59:54 +0200 Subject: [PATCH 1/2] add prettier CommandTree formatting for help command and usage hint --- pumpkin/src/commands/cmd_gamemode.rs | 4 +- pumpkin/src/commands/cmd_help.rs | 24 ++--- pumpkin/src/commands/cmd_pumpkin.rs | 4 +- pumpkin/src/commands/cmd_stop.rs | 4 +- pumpkin/src/commands/dispatcher.rs | 18 +++- pumpkin/src/commands/mod.rs | 16 ++-- pumpkin/src/commands/tree.rs | 56 ++---------- pumpkin/src/commands/tree_builder.rs | 15 +++- pumpkin/src/commands/tree_format.rs | 125 +++++++++++++++++++++++++++ 9 files changed, 185 insertions(+), 81 deletions(-) create mode 100644 pumpkin/src/commands/tree_format.rs diff --git a/pumpkin/src/commands/cmd_gamemode.rs b/pumpkin/src/commands/cmd_gamemode.rs index b054fdc24..88c7ae39f 100644 --- a/pumpkin/src/commands/cmd_gamemode.rs +++ b/pumpkin/src/commands/cmd_gamemode.rs @@ -15,7 +15,7 @@ use crate::commands::CommandSender; use crate::commands::CommandSender::Player; use crate::entity::player::GameMode; -pub(crate) const NAME: &str = "gamemode"; +const NAMES: [&str; 1] = ["gamemode"]; const DESCRIPTION: &str = "Change a player's gamemode."; @@ -57,7 +57,7 @@ pub fn parse_arg_gamemode(consumed_args: &ConsumedArgs) -> Result() -> CommandTree<'a> { - CommandTree::new(DESCRIPTION).with_child( + CommandTree::new(NAMES, DESCRIPTION).with_child( require(&|sender| sender.permission_lvl() >= 2).with_child( argument(ARG_GAMEMODE, consume_arg_gamemode) .with_child( diff --git a/pumpkin/src/commands/cmd_help.rs b/pumpkin/src/commands/cmd_help.rs index 7ffa733c1..1e1b0e5b7 100644 --- a/pumpkin/src/commands/cmd_help.rs +++ b/pumpkin/src/commands/cmd_help.rs @@ -5,8 +5,7 @@ use crate::commands::tree_builder::argument; use crate::commands::{dispatcher_init, CommandSender, DISPATCHER}; use pumpkin_text::TextComponent; -pub(crate) const NAME: &str = "help"; -pub(crate) const ALIAS: &str = "?"; +const NAMES: [&str; 3] = ["help", "h", "?"]; const DESCRIPTION: &str = "Print a help message."; @@ -40,7 +39,7 @@ fn parse_arg_command<'a>( } pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { - CommandTree::new(DESCRIPTION) + CommandTree::new(NAMES, DESCRIPTION) .with_child( argument(ARG_COMMAND, consume_arg_command).execute(&|sender, args| { let dispatcher = DISPATCHER.get_or_init(dispatcher_init); @@ -48,10 +47,8 @@ pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { let (name, tree) = parse_arg_command(args, dispatcher)?; sender.send_message(TextComponent::text(&format!( - "{} - {} Usage:{}", - name, - tree.description, - tree.paths_formatted(name) + "{} - {} Usage: {}", + name, tree.description, tree ))); Ok(()) @@ -60,12 +57,15 @@ pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { .execute(&|sender, _args| { let dispatcher = DISPATCHER.get_or_init(dispatcher_init); - for (name, tree) in &dispatcher.commands { + let mut names: Vec<&str> = dispatcher.commands.keys().copied().collect(); + names.sort(); + + for name in names { + let tree = &dispatcher.commands[name]; + sender.send_message(TextComponent::text(&format!( - "{} - {} Usage:{}", - name, - tree.description, - tree.paths_formatted(name) + "{} - {} Usage: {}", + name, tree.description, tree ))); } diff --git a/pumpkin/src/commands/cmd_pumpkin.rs b/pumpkin/src/commands/cmd_pumpkin.rs index cbbac0680..b4cfb1847 100644 --- a/pumpkin/src/commands/cmd_pumpkin.rs +++ b/pumpkin/src/commands/cmd_pumpkin.rs @@ -4,12 +4,12 @@ use pumpkin_text::{color::NamedColor, TextComponent}; use crate::commands::tree::CommandTree; -pub(crate) const NAME: &str = "pumpkin"; +const NAMES: [&str; 1] = ["pumpkin"]; const DESCRIPTION: &str = "Display information about Pumpkin."; pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { - CommandTree::new(DESCRIPTION).execute(&|sender, _| { + CommandTree::new(NAMES, DESCRIPTION).execute(&|sender, _| { let version = env!("CARGO_PKG_VERSION"); let description = env!("CARGO_PKG_DESCRIPTION"); diff --git a/pumpkin/src/commands/cmd_stop.rs b/pumpkin/src/commands/cmd_stop.rs index b576c299d..60f425b76 100644 --- a/pumpkin/src/commands/cmd_stop.rs +++ b/pumpkin/src/commands/cmd_stop.rs @@ -1,12 +1,12 @@ use crate::commands::tree::CommandTree; use crate::commands::tree_builder::require; -pub(crate) const NAME: &str = "stop"; +const NAMES: [&str; 1] = ["stop"]; const DESCRIPTION: &str = "Stop the server."; pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { - CommandTree::new(DESCRIPTION).with_child( + CommandTree::new(NAMES, DESCRIPTION).with_child( require(&|sender| sender.permission_lvl() >= 4) .execute(&|_sender, _args| std::process::exit(0)), ) diff --git a/pumpkin/src/commands/dispatcher.rs b/pumpkin/src/commands/dispatcher.rs index a01e20b81..d4a829818 100644 --- a/pumpkin/src/commands/dispatcher.rs +++ b/pumpkin/src/commands/dispatcher.rs @@ -49,10 +49,7 @@ impl<'a> CommandDispatcher<'a> { } } - Err(format!( - "Invalid Syntax. Usage:{}", - tree.paths_formatted(key) - )) + Err(format!("Invalid Syntax. Usage: {}", tree)) } fn try_is_fitting_path( @@ -99,4 +96,17 @@ impl<'a> CommandDispatcher<'a> { Ok(false) } + + /// Register a command with the dispatcher. + pub(crate) fn register(&mut self, tree: CommandTree<'a>) { + let mut names = tree.names.iter(); + + let primary_name = names.next().expect("at least one name must be provided"); + + for &name in names { + self.commands.insert(name, tree.clone()); + } + + self.commands.insert(primary_name, tree); + } } diff --git a/pumpkin/src/commands/mod.rs b/pumpkin/src/commands/mod.rs index c622691d6..9f49033ba 100644 --- a/pumpkin/src/commands/mod.rs +++ b/pumpkin/src/commands/mod.rs @@ -12,6 +12,7 @@ mod cmd_stop; mod dispatcher; mod tree; mod tree_builder; +mod tree_format; pub enum CommandSender<'a> { Rcon(&'a mut Vec), @@ -70,15 +71,16 @@ static DISPATCHER: OnceLock = OnceLock::new(); /// create [CommandDispatcher] instance for [DISPATCHER] fn dispatcher_init<'a>() -> CommandDispatcher<'a> { - let mut map = HashMap::new(); + let mut dispatcher = CommandDispatcher { + commands: HashMap::new(), + }; - map.insert(cmd_pumpkin::NAME, cmd_pumpkin::init_command_tree()); - map.insert(cmd_gamemode::NAME, cmd_gamemode::init_command_tree()); - map.insert(cmd_stop::NAME, cmd_stop::init_command_tree()); - map.insert(cmd_help::NAME, cmd_help::init_command_tree()); - map.insert(cmd_help::ALIAS, cmd_help::init_command_tree()); + dispatcher.register(cmd_pumpkin::init_command_tree()); + dispatcher.register(cmd_gamemode::init_command_tree()); + dispatcher.register(cmd_stop::init_command_tree()); + dispatcher.register(cmd_help::init_command_tree()); - CommandDispatcher { commands: map } + dispatcher } pub fn handle_command(sender: &mut CommandSender, cmd: &str) { diff --git a/pumpkin/src/commands/tree.rs b/pumpkin/src/commands/tree.rs index b4132b18e..2e2741f38 100644 --- a/pumpkin/src/commands/tree.rs +++ b/pumpkin/src/commands/tree.rs @@ -2,6 +2,7 @@ use std::collections::{HashMap, VecDeque}; use crate::commands::dispatcher::InvalidTreeError; use crate::commands::CommandSender; + /// see [crate::commands::tree_builder::argument] pub(crate) type RawArgs<'a> = Vec<&'a str>; @@ -11,11 +12,13 @@ pub(crate) type ConsumedArgs<'a> = HashMap<&'a str, String>; /// see [crate::commands::tree_builder::argument] pub(crate) type ArgumentConsumer<'a> = fn(&CommandSender, &mut RawArgs) -> Option; +#[derive(Clone)] pub(crate) struct Node<'a> { pub(crate) children: Vec, pub(crate) node_type: NodeType<'a>, } +#[derive(Clone)] pub(crate) enum NodeType<'a> { ExecuteLeaf { run: &'a (dyn Fn(&mut CommandSender, &ConsumedArgs) -> Result<(), InvalidTreeError> + Sync), @@ -31,9 +34,11 @@ pub(crate) enum NodeType<'a> { }, } +#[derive(Clone)] pub(crate) struct CommandTree<'a> { pub(crate) nodes: Vec>, pub(crate) children: Vec, + pub(crate) names: Vec<&'a str>, pub(crate) description: &'a str, } @@ -51,57 +56,6 @@ impl<'a> CommandTree<'a> { todo, } } - - /// format possible paths as [String], using ```name``` as the command name - /// - /// todo: merge into single line - pub(crate) fn paths_formatted(&'a self, name: &str) -> String { - let paths: Vec> = self - .iter_paths() - .map(|path| path.iter().map(|&i| &self.nodes[i].node_type).collect()) - .collect(); - - let len = paths - .iter() - .map(|path| { - path.iter() - .map(|node| match node { - NodeType::ExecuteLeaf { .. } => 0, - NodeType::Literal { string } => string.len() + 1, - NodeType::Argument { name, .. } => name.len() + 3, - NodeType::Require { .. } => 0, - }) - .sum::() - + name.len() - + 2 - }) - .sum::(); - - let mut s = String::with_capacity(len); - - for path in paths.iter() { - s.push(if paths.len() > 1 { '\n' } else { ' ' }); - s.push('/'); - s.push_str(name); - for node in path { - match node { - NodeType::Literal { string } => { - s.push(' '); - s.push_str(string); - } - NodeType::Argument { name, .. } => { - s.push(' '); - s.push('<'); - s.push_str(name); - s.push('>'); - } - _ => {} - } - } - } - - s - } } struct TraverseAllPathsIter<'a> { diff --git a/pumpkin/src/commands/tree_builder.rs b/pumpkin/src/commands/tree_builder.rs index 63531909b..d34ee3a6a 100644 --- a/pumpkin/src/commands/tree_builder.rs +++ b/pumpkin/src/commands/tree_builder.rs @@ -11,10 +11,23 @@ impl<'a> CommandTree<'a> { self } - pub fn new(description: &'a str) -> Self { + /// provide at least one name + pub fn new( + names: [&'a str; NAME_COUNT], + description: &'a str, + ) -> Self { + assert!(NAME_COUNT > 0); + + let mut names_vec = Vec::with_capacity(NAME_COUNT); + + for name in names { + names_vec.push(name); + } + Self { nodes: Vec::new(), children: Vec::new(), + names: names_vec, description, } } diff --git a/pumpkin/src/commands/tree_format.rs b/pumpkin/src/commands/tree_format.rs new file mode 100644 index 000000000..b13b0e1fe --- /dev/null +++ b/pumpkin/src/commands/tree_format.rs @@ -0,0 +1,125 @@ +use crate::commands::tree::{CommandTree, Node, NodeType}; +use std::collections::VecDeque; +use std::fmt::{Display, Formatter, Write}; + +trait IsVisible { + /// whether node should be printed in help command/usage hint + fn is_visible(&self) -> bool; +} + +impl<'a> IsVisible for Node<'a> { + fn is_visible(&self) -> bool { + match self.node_type { + NodeType::ExecuteLeaf { .. } => false, + NodeType::Literal { .. } => true, + NodeType::Argument { .. } => true, + NodeType::Require { .. } => false, + } + } +} + +impl<'a> Display for Node<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self.node_type { + NodeType::Literal { string } => { + f.write_str(string)?; + } + NodeType::Argument { name, .. } => { + f.write_char('<')?; + f.write_str(name)?; + f.write_char('>')?; + } + _ => {} + }; + + Ok(()) + } +} + +fn flatten_require_nodes(nodes: &[Node], children: &[usize]) -> Vec { + let mut new_children = Vec::with_capacity(children.len()); + + for &i in children { + let node = &nodes[i]; + match &node.node_type { + NodeType::Require { .. } => { + new_children.extend(flatten_require_nodes(nodes, node.children.as_slice())) + } + _ => new_children.push(i), + } + } + + new_children +} + +impl<'a> Display for CommandTree<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_char('/')?; + f.write_str(self.names[0])?; + + let mut todo = VecDeque::<&[usize]>::with_capacity(self.children.len()); + todo.push_back(&self.children); + + loop { + let Some(children) = todo.pop_front() else { + break; + }; + + let flattened_children = flatten_require_nodes(&self.nodes, children); + let visible_children = flattened_children + .iter() + .copied() + .filter(|&i| self.nodes[i].is_visible()) + .collect::>(); + + if visible_children.is_empty() { + break; + }; + + f.write_char(' ')?; + + let is_optional = flattened_children + .iter() + .map(|&i| &self.nodes[i].node_type) + .any(|node| matches!(node, NodeType::ExecuteLeaf { .. })); + + if is_optional { + f.write_char('[')?; + } + + match visible_children.as_slice() { + [] => unreachable!(), + [i] => { + let node = &self.nodes[*i]; + + node.fmt(f)?; + + todo.push_back(&node.children); + } + _ => { + // todo: handle cases where one of these nodes has visible children + f.write_char('(')?; + + let mut iter = visible_children.iter().map(|&i| &self.nodes[i]); + + if let Some(node) = iter.next() { + node.fmt(f)?; + } + + for node in iter { + f.write_char('|')?; + node.fmt(f)?; + } + + f.write_char(')')?; + } + } + + if is_optional { + f.write_char(']')?; + } + } + + Ok(()) + } +} From ab8956a7b5d7b5b4e8fc9f3cc33a2d90cade5451 Mon Sep 17 00:00:00 2001 From: user622628252416 Date: Wed, 21 Aug 2024 19:27:20 +0200 Subject: [PATCH 2/2] remove repeating command aliases in /help --- pumpkin/src/commands/cmd_help.rs | 38 +++++++++++++++--------------- pumpkin/src/commands/dispatcher.rs | 25 ++++++++++++++++---- pumpkin/src/commands/tree.rs | 8 ++++--- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/pumpkin/src/commands/cmd_help.rs b/pumpkin/src/commands/cmd_help.rs index 1e1b0e5b7..71bce754b 100644 --- a/pumpkin/src/commands/cmd_help.rs +++ b/pumpkin/src/commands/cmd_help.rs @@ -1,6 +1,6 @@ use crate::commands::dispatcher::InvalidTreeError::InvalidConsumptionError; use crate::commands::dispatcher::{CommandDispatcher, InvalidTreeError}; -use crate::commands::tree::{CommandTree, ConsumedArgs, RawArgs}; +use crate::commands::tree::{Command, CommandTree, ConsumedArgs, RawArgs}; use crate::commands::tree_builder::argument; use crate::commands::{dispatcher_init, CommandSender, DISPATCHER}; use pumpkin_text::TextComponent; @@ -16,26 +16,20 @@ fn consume_arg_command(_src: &CommandSender, args: &mut RawArgs) -> Option( consumed_args: &'a ConsumedArgs, dispatcher: &'a CommandDispatcher, -) -> Result<(&'a str, &'a CommandTree<'a>), InvalidTreeError> { +) -> Result<&'a CommandTree<'a>, InvalidTreeError> { let command_name = consumed_args .get(ARG_COMMAND) .ok_or(InvalidConsumptionError(None))?; - if let Some(tree) = dispatcher.commands.get::<&str>(&command_name.as_str()) { - Ok((command_name, tree)) - } else { - Err(InvalidConsumptionError(Some(command_name.into()))) - } + dispatcher + .get_tree(command_name) + .map_err(|_| InvalidConsumptionError(Some(command_name.into()))) } pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { @@ -44,11 +38,13 @@ pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { argument(ARG_COMMAND, consume_arg_command).execute(&|sender, args| { let dispatcher = DISPATCHER.get_or_init(dispatcher_init); - let (name, tree) = parse_arg_command(args, dispatcher)?; + let tree = parse_arg_command(args, dispatcher)?; sender.send_message(TextComponent::text(&format!( "{} - {} Usage: {}", - name, tree.description, tree + tree.names.join("/"), + tree.description, + tree ))); Ok(()) @@ -57,15 +53,19 @@ pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { .execute(&|sender, _args| { let dispatcher = DISPATCHER.get_or_init(dispatcher_init); - let mut names: Vec<&str> = dispatcher.commands.keys().copied().collect(); - names.sort(); + let mut keys: Vec<&str> = dispatcher.commands.keys().copied().collect(); + keys.sort(); - for name in names { - let tree = &dispatcher.commands[name]; + for key in keys { + let Command::Tree(tree) = &dispatcher.commands[key] else { + continue; + }; sender.send_message(TextComponent::text(&format!( "{} - {} Usage: {}", - name, tree.description, tree + tree.names.join("/"), + tree.description, + tree ))); } diff --git a/pumpkin/src/commands/dispatcher.rs b/pumpkin/src/commands/dispatcher.rs index d4a829818..dac9c8894 100644 --- a/pumpkin/src/commands/dispatcher.rs +++ b/pumpkin/src/commands/dispatcher.rs @@ -1,7 +1,7 @@ use crate::commands::dispatcher::InvalidTreeError::{ InvalidConsumptionError, InvalidRequirementError, }; -use crate::commands::tree::{CommandTree, ConsumedArgs, NodeType, RawArgs}; +use crate::commands::tree::{Command, CommandTree, ConsumedArgs, NodeType, RawArgs}; use crate::commands::CommandSender; use std::collections::HashMap; @@ -17,7 +17,7 @@ pub(crate) enum InvalidTreeError { } pub(crate) struct CommandDispatcher<'a> { - pub(crate) commands: HashMap<&'a str, CommandTree<'a>>, + pub(crate) commands: HashMap<&'a str, Command<'a>>, } /// Stores registered [CommandTree]s and dispatches commands to them. @@ -28,7 +28,7 @@ impl<'a> CommandDispatcher<'a> { let key = parts.next().ok_or("Empty Command")?; let raw_args: Vec<&str> = parts.rev().collect(); - let tree = self.commands.get(key).ok_or("Command not found")?; + let tree = self.get_tree(key)?; // try paths until fitting path is found for path in tree.iter_paths() { @@ -52,6 +52,21 @@ impl<'a> CommandDispatcher<'a> { Err(format!("Invalid Syntax. Usage: {}", tree)) } + pub(crate) fn get_tree(&'a self, key: &str) -> Result<&'a CommandTree<'a>, String> { + let command = self.commands.get(key).ok_or("Command not found")?; + + match command { + Command::Tree(tree) => Ok(tree), + Command::Alias(target) => { + let Some(Command::Tree(tree)) = &self.commands.get(target) else { + println!("Error while parsing command alias \"{key}\": pointing to \"{target}\" which is not a valid tree"); + return Err("Internal Error (See logs for details)".into()); + }; + Ok(tree) + } + } + } + fn try_is_fitting_path( src: &mut CommandSender, path: Vec, @@ -104,9 +119,9 @@ impl<'a> CommandDispatcher<'a> { let primary_name = names.next().expect("at least one name must be provided"); for &name in names { - self.commands.insert(name, tree.clone()); + self.commands.insert(name, Command::Alias(primary_name)); } - self.commands.insert(primary_name, tree); + self.commands.insert(primary_name, Command::Tree(tree)); } } diff --git a/pumpkin/src/commands/tree.rs b/pumpkin/src/commands/tree.rs index 2e2741f38..59d2473c8 100644 --- a/pumpkin/src/commands/tree.rs +++ b/pumpkin/src/commands/tree.rs @@ -12,13 +12,11 @@ pub(crate) type ConsumedArgs<'a> = HashMap<&'a str, String>; /// see [crate::commands::tree_builder::argument] pub(crate) type ArgumentConsumer<'a> = fn(&CommandSender, &mut RawArgs) -> Option; -#[derive(Clone)] pub(crate) struct Node<'a> { pub(crate) children: Vec, pub(crate) node_type: NodeType<'a>, } -#[derive(Clone)] pub(crate) enum NodeType<'a> { ExecuteLeaf { run: &'a (dyn Fn(&mut CommandSender, &ConsumedArgs) -> Result<(), InvalidTreeError> + Sync), @@ -34,7 +32,11 @@ pub(crate) enum NodeType<'a> { }, } -#[derive(Clone)] +pub(crate) enum Command<'a> { + Tree(CommandTree<'a>), + Alias(&'a str), +} + pub(crate) struct CommandTree<'a> { pub(crate) nodes: Vec>, pub(crate) children: Vec,