diff --git a/pumpkin/src/commands/arg_player.rs b/pumpkin/src/commands/arg_player.rs new file mode 100644 index 000000000..a8ee8f460 --- /dev/null +++ b/pumpkin/src/commands/arg_player.rs @@ -0,0 +1,59 @@ +use crate::client::Client; +use crate::commands::dispatcher::InvalidTreeError; +use crate::commands::dispatcher::InvalidTreeError::InvalidConsumptionError; +use crate::commands::tree::{ConsumedArgs, RawArgs}; +use crate::commands::CommandSender; +use crate::commands::CommandSender::Player; + +/// todo: implement (so far only own name + @s/@p is implemented) +pub fn consume_arg_player(src: &CommandSender, args: &mut RawArgs) -> Option { + let s = args.pop()?; + + match s { + "@s" if src.is_player() => Some(s.into()), + "@p" if src.is_player() => Some(s.into()), + "@r" => None, // todo: implement random player target selector + "@a" | "@e" => None, // todo: implement all players target selector + _ => { + // todo: implement any other player than sender + if let Player(client) = src { + if let Some(profile) = &client.gameprofile { + if profile.name == s { + return Some(s.into()); + }; + }; + }; + None + } + } +} + +/// todo: implement (so far only own name + @s/@p is implemented) +pub fn parse_arg_player<'a>( + src: &'a mut CommandSender, + arg_name: &str, + consumed_args: &ConsumedArgs, +) -> Result<&'a mut Client, InvalidTreeError> { + let s = consumed_args + .get(arg_name) + .ok_or(InvalidConsumptionError(None))? + .as_str(); + + match s { + "@s" if src.is_player() => Ok(src.as_mut_player().unwrap()), + "@p" if src.is_player() => Ok(src.as_mut_player().unwrap()), + "@r" => Err(InvalidConsumptionError(Some(s.into()))), // todo: implement random player target selector + "@a" | "@e" => Err(InvalidConsumptionError(Some(s.into()))), // todo: implement all players target selector + _ => { + // todo: implement any other player than sender + if let Player(client) = src { + if let Some(profile) = &client.gameprofile { + if profile.name == s { + return Ok(client); + }; + }; + }; + Err(InvalidConsumptionError(Some(s.into()))) + } + } +} diff --git a/pumpkin/src/commands/cmd_gamemode.rs b/pumpkin/src/commands/cmd_gamemode.rs new file mode 100644 index 000000000..b054fdc24 --- /dev/null +++ b/pumpkin/src/commands/cmd_gamemode.rs @@ -0,0 +1,96 @@ +use std::str::FromStr; + +use num_traits::FromPrimitive; +use pumpkin_text::TextComponent; + +use crate::commands::arg_player::{consume_arg_player, parse_arg_player}; + +use crate::commands::dispatcher::InvalidTreeError; +use crate::commands::dispatcher::InvalidTreeError::{ + InvalidConsumptionError, InvalidRequirementError, +}; +use crate::commands::tree::{CommandTree, ConsumedArgs, RawArgs}; +use crate::commands::tree_builder::{argument, require}; +use crate::commands::CommandSender; +use crate::commands::CommandSender::Player; +use crate::entity::player::GameMode; + +pub(crate) const NAME: &str = "gamemode"; + +const DESCRIPTION: &str = "Change a player's gamemode."; + +const ARG_GAMEMODE: &str = "gamemode"; +const ARG_TARGET: &str = "target"; + +pub fn consume_arg_gamemode(_src: &CommandSender, args: &mut RawArgs) -> Option { + let s = args.pop()?; + + if let Ok(id) = s.parse::() { + match GameMode::from_u8(id) { + None | Some(GameMode::Undefined) => {} + Some(_) => return Some(s.into()), + }; + }; + + match GameMode::from_str(s) { + Err(_) | Ok(GameMode::Undefined) => None, + Ok(_) => Some(s.into()), + } +} + +pub fn parse_arg_gamemode(consumed_args: &ConsumedArgs) -> Result { + let s = consumed_args + .get(ARG_GAMEMODE) + .ok_or(InvalidConsumptionError(None))?; + + if let Ok(id) = s.parse::() { + match GameMode::from_u8(id) { + None | Some(GameMode::Undefined) => {} + Some(gamemode) => return Ok(gamemode), + }; + }; + + match GameMode::from_str(s) { + Err(_) | Ok(GameMode::Undefined) => Err(InvalidConsumptionError(Some(s.into()))), + Ok(gamemode) => Ok(gamemode), + } +} + +pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { + CommandTree::new(DESCRIPTION).with_child( + require(&|sender| sender.permission_lvl() >= 2).with_child( + argument(ARG_GAMEMODE, consume_arg_gamemode) + .with_child( + require(&|sender| sender.is_player()).execute(&|sender, args| { + let gamemode = parse_arg_gamemode(args)?; + + return if let Player(target) = sender { + target.set_gamemode(gamemode); + target.send_system_message(TextComponent::text(&format!( + "Game mode was set to {:?}", + gamemode + ))); + + Ok(()) + } else { + Err(InvalidRequirementError) + }; + }), + ) + .with_child( + argument(ARG_TARGET, consume_arg_player).execute(&|sender, args| { + let gamemode = parse_arg_gamemode(args)?; + let target = parse_arg_player(sender, ARG_TARGET, args)?; + + target.set_gamemode(gamemode); + target.send_system_message(TextComponent::text(&format!( + "Set own game mode to {:?}", + gamemode + ))); + + Ok(()) + }), + ), + ), + ) +} diff --git a/pumpkin/src/commands/cmd_help.rs b/pumpkin/src/commands/cmd_help.rs new file mode 100644 index 000000000..7ffa733c1 --- /dev/null +++ b/pumpkin/src/commands/cmd_help.rs @@ -0,0 +1,74 @@ +use crate::commands::dispatcher::InvalidTreeError::InvalidConsumptionError; +use crate::commands::dispatcher::{CommandDispatcher, InvalidTreeError}; +use crate::commands::tree::{CommandTree, ConsumedArgs, RawArgs}; +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 DESCRIPTION: &str = "Print a help message."; + +const ARG_COMMAND: &str = "command"; + +fn consume_arg_command(_src: &CommandSender, args: &mut RawArgs) -> Option { + let s = args.pop()?; + + let dispatcher = DISPATCHER.get_or_init(dispatcher_init); + + if dispatcher.commands.contains_key(s) { + Some(s.into()) + } else { + None + } +} + +fn parse_arg_command<'a>( + consumed_args: &'a ConsumedArgs, + dispatcher: &'a CommandDispatcher, +) -> Result<(&'a str, &'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()))) + } +} + +pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { + CommandTree::new(DESCRIPTION) + .with_child( + 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)?; + + sender.send_message(TextComponent::text(&format!( + "{} - {} Usage:{}", + name, + tree.description, + tree.paths_formatted(name) + ))); + + Ok(()) + }), + ) + .execute(&|sender, _args| { + let dispatcher = DISPATCHER.get_or_init(dispatcher_init); + + for (name, tree) in &dispatcher.commands { + sender.send_message(TextComponent::text(&format!( + "{} - {} Usage:{}", + name, + tree.description, + tree.paths_formatted(name) + ))); + } + + Ok(()) + }) +} diff --git a/pumpkin/src/commands/cmd_pumpkin.rs b/pumpkin/src/commands/cmd_pumpkin.rs new file mode 100644 index 000000000..cbbac0680 --- /dev/null +++ b/pumpkin/src/commands/cmd_pumpkin.rs @@ -0,0 +1,22 @@ +use crate::server::CURRENT_MC_VERSION; +use pumpkin_protocol::CURRENT_MC_PROTOCOL; +use pumpkin_text::{color::NamedColor, TextComponent}; + +use crate::commands::tree::CommandTree; + +pub(crate) const NAME: &str = "pumpkin"; + +const DESCRIPTION: &str = "Display information about Pumpkin."; + +pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { + CommandTree::new(DESCRIPTION).execute(&|sender, _| { + let version = env!("CARGO_PKG_VERSION"); + let description = env!("CARGO_PKG_DESCRIPTION"); + + sender.send_message(TextComponent::text( + &format!("Pumpkin {version}, {description} (Minecraft {CURRENT_MC_VERSION}, Protocol {CURRENT_MC_PROTOCOL})") + ).color_named(NamedColor::Green)); + + Ok(()) + }) +} diff --git a/pumpkin/src/commands/cmd_stop.rs b/pumpkin/src/commands/cmd_stop.rs new file mode 100644 index 000000000..b576c299d --- /dev/null +++ b/pumpkin/src/commands/cmd_stop.rs @@ -0,0 +1,13 @@ +use crate::commands::tree::CommandTree; +use crate::commands::tree_builder::require; + +pub(crate) const NAME: &str = "stop"; + +const DESCRIPTION: &str = "Stop the server."; + +pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { + CommandTree::new(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 new file mode 100644 index 000000000..a01e20b81 --- /dev/null +++ b/pumpkin/src/commands/dispatcher.rs @@ -0,0 +1,102 @@ +use crate::commands::dispatcher::InvalidTreeError::{ + InvalidConsumptionError, InvalidRequirementError, +}; +use crate::commands::tree::{CommandTree, ConsumedArgs, NodeType, RawArgs}; +use crate::commands::CommandSender; +use std::collections::HashMap; + +#[derive(Debug)] +pub(crate) enum InvalidTreeError { + /// 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. + InvalidConsumptionError(Option), + + /// Return this if a condition that a [Node::Require] should ensure is met is not met. + InvalidRequirementError, +} + +pub(crate) struct CommandDispatcher<'a> { + pub(crate) commands: HashMap<&'a str, CommandTree<'a>>, +} + +/// Stores registered [CommandTree]s and dispatches commands to them. +impl<'a> CommandDispatcher<'a> { + /// Execute a command using its corresponding [CommandTree]. + pub(crate) fn dispatch(&'a self, src: &mut CommandSender, cmd: &str) -> Result<(), String> { + let mut parts = cmd.split_ascii_whitespace(); + 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")?; + + // try paths until fitting path is found + for path in tree.iter_paths() { + match Self::try_is_fitting_path(src, path, tree, raw_args.clone()) { + Err(InvalidConsumptionError(s)) => { + println!("Error while parsing command \"{cmd}\": {s:?} was consumed, but couldn't be parsed"); + return Err("Internal Error (See logs for details)".into()); + } + Err(InvalidRequirementError) => { + println!("Error while parsing command \"{cmd}\": a requirement that was expected was not met."); + return Err("Internal Error (See logs for details)".into()); + } + Ok(is_fitting_path) => { + if is_fitting_path { + return Ok(()); + } + } + } + } + + Err(format!( + "Invalid Syntax. Usage:{}", + tree.paths_formatted(key) + )) + } + + fn try_is_fitting_path( + src: &mut CommandSender, + path: Vec, + tree: &CommandTree, + mut raw_args: RawArgs, + ) -> Result { + let mut parsed_args: ConsumedArgs = HashMap::new(); + + for node in path.iter().map(|&i| &tree.nodes[i]) { + match node.node_type { + NodeType::ExecuteLeaf { run } => { + return if raw_args.is_empty() { + run(src, &parsed_args)?; + Ok(true) + } else { + Ok(false) + }; + } + NodeType::Literal { string, .. } => { + if raw_args.pop() != Some(string) { + return Ok(false); + } + } + NodeType::Argument { + consumer: consume, + name, + .. + } => { + if let Some(consumed) = consume(src, &mut raw_args) { + parsed_args.insert(name, consumed); + } else { + return Ok(false); + } + } + NodeType::Require { predicate, .. } => { + if !predicate(src) { + return Ok(false); + } + } + } + } + + Ok(false) + } +} diff --git a/pumpkin/src/commands/gamemode.rs b/pumpkin/src/commands/gamemode.rs deleted file mode 100644 index 3c0003697..000000000 --- a/pumpkin/src/commands/gamemode.rs +++ /dev/null @@ -1,61 +0,0 @@ -use num_traits::FromPrimitive; -use pumpkin_text::TextComponent; - -use crate::entity::player::GameMode; - -use super::Command; - -pub struct GamemodeCommand {} - -impl<'a> Command<'a> for GamemodeCommand { - const NAME: &'a str = "gamemode"; - - const DESCRIPTION: &'a str = "Changes the gamemode for a Player"; - - fn on_execute(sender: &mut super::CommandSender<'a>, command: String) { - let player = sender.as_mut_player().unwrap(); - let args: Vec<&str> = command.split_whitespace().collect(); - - if args.len() != 2 { - player.send_system_message( - TextComponent::text("Usage: /gamemode ") - .color_named(pumpkin_text::color::NamedColor::Red), - ); - return; - } - - let mode_str = args[1].to_lowercase(); - match mode_str.parse() { - Ok(mode) => { - player.set_gamemode(mode); - player.send_system_message(TextComponent::text(&format!( - "Set own game mode to {:?}", - mode - ))); - } - Err(_) => { - // try to parse from number - if let Ok(i) = mode_str.parse::() { - if let Some(mode) = GameMode::from_u8(i) { - player.set_gamemode(mode); - player.send_system_message(TextComponent::text(&format!( - "Set own game mode to {:?}", - mode - ))); - return; - } - } - - player.send_system_message( - TextComponent::text("Invalid gamemode") - .color_named(pumpkin_text::color::NamedColor::Red), - ); - } - } - } - - // TODO: support console, (name required) - fn player_required() -> bool { - true - } -} diff --git a/pumpkin/src/commands/mod.rs b/pumpkin/src/commands/mod.rs index 8f294c7bd..c622691d6 100644 --- a/pumpkin/src/commands/mod.rs +++ b/pumpkin/src/commands/mod.rs @@ -1,28 +1,17 @@ -use gamemode::GamemodeCommand; -use pumpkin::PumpkinCommand; use pumpkin_text::TextComponent; -use stop::StopCommand; +use std::collections::HashMap; +use std::sync::OnceLock; use crate::client::Client; - -mod gamemode; -mod pumpkin; -mod stop; - -/// I think it would be great to split this up into a seperate crate, But idk how i should do that, Because we have to rely on Client and Server -pub trait Command<'a> { - // Name of the Plugin, Use lower case - const NAME: &'a str; - const DESCRIPTION: &'a str; - - fn on_execute(sender: &mut CommandSender<'a>, command: String); - - /// Specifies wether the Command Sender has to be a Player - /// TODO: implement - fn player_required() -> bool { - false - } -} +use crate::commands::dispatcher::CommandDispatcher; +mod arg_player; +mod cmd_gamemode; +mod cmd_help; +mod cmd_pumpkin; +mod cmd_stop; +mod dispatcher; +mod tree; +mod tree_builder; pub enum CommandSender<'a> { Rcon(&'a mut Vec), @@ -40,7 +29,7 @@ impl<'a> CommandSender<'a> { } } - pub fn is_player(&mut self) -> bool { + pub fn is_player(&self) -> bool { match self { CommandSender::Console => false, CommandSender::Player(_) => true, @@ -48,7 +37,7 @@ impl<'a> CommandSender<'a> { } } - pub fn is_console(&mut self) -> bool { + pub fn is_console(&self) -> bool { match self { CommandSender::Console => true, CommandSender::Player(_) => false, @@ -62,22 +51,42 @@ impl<'a> CommandSender<'a> { CommandSender::Rcon(_) => None, } } -} -pub fn handle_command(sender: &mut CommandSender, command: &str) { - let command = command.to_lowercase(); - // an ugly mess i know - if command.starts_with(PumpkinCommand::NAME) { - PumpkinCommand::on_execute(sender, command); - return; - } - if command.starts_with(GamemodeCommand::NAME) { - GamemodeCommand::on_execute(sender, command); - return; + + /// todo: implement + pub fn permission_lvl(&self) -> i32 { + match self { + CommandSender::Rcon(_) => 4, + CommandSender::Console => 4, + CommandSender::Player(_) => 4, + } } - if command.starts_with(StopCommand::NAME) { - StopCommand::on_execute(sender, command); - return; +} + +/// todo: reconsider using constant +/// +/// Central point from which commands are dispatched. Should always be initialized using +/// [dispatcher_init]. +static DISPATCHER: OnceLock = OnceLock::new(); + +/// create [CommandDispatcher] instance for [DISPATCHER] +fn dispatcher_init<'a>() -> CommandDispatcher<'a> { + let mut map = 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()); + + CommandDispatcher { commands: map } +} + +pub fn handle_command(sender: &mut CommandSender, cmd: &str) { + let dispatcher = DISPATCHER.get_or_init(dispatcher_init); + + if let Err(err) = dispatcher.dispatch(sender, cmd) { + sender.send_message( + TextComponent::text(&err).color_named(pumpkin_text::color::NamedColor::Red), + ) } - // TODO: red color - sender.send_message(TextComponent::text("Command not Found")); } diff --git a/pumpkin/src/commands/pumpkin.rs b/pumpkin/src/commands/pumpkin.rs deleted file mode 100644 index 27e960d90..000000000 --- a/pumpkin/src/commands/pumpkin.rs +++ /dev/null @@ -1,20 +0,0 @@ -use pumpkin_protocol::CURRENT_MC_PROTOCOL; -use pumpkin_text::{color::NamedColor, TextComponent}; - -use crate::server::CURRENT_MC_VERSION; - -use super::Command; - -pub struct PumpkinCommand {} - -impl<'a> Command<'a> for PumpkinCommand { - const NAME: &'a str = "pumpkin"; - - const DESCRIPTION: &'a str = "Displays information about Pumpkin"; - - fn on_execute(sender: &mut super::CommandSender<'a>, _command: String) { - let version = env!("CARGO_PKG_VERSION"); - let description = env!("CARGO_PKG_DESCRIPTION"); - sender.send_message(TextComponent::text(&format!("Pumpkin {version}, {description} (Minecraft {CURRENT_MC_VERSION}, Protocol {CURRENT_MC_PROTOCOL})")).color_named(NamedColor::Green)) - } -} diff --git a/pumpkin/src/commands/stop.rs b/pumpkin/src/commands/stop.rs deleted file mode 100644 index 852a066b7..000000000 --- a/pumpkin/src/commands/stop.rs +++ /dev/null @@ -1,15 +0,0 @@ -use super::Command; - -pub struct StopCommand {} - -impl<'a> Command<'a> for StopCommand { - const NAME: &'static str = "stop"; - const DESCRIPTION: &'static str = "Stops the server"; - - fn on_execute(_sender: &mut super::CommandSender<'a>, _command: String) { - std::process::exit(0); - } - fn player_required() -> bool { - true - } -} diff --git a/pumpkin/src/commands/tree.rs b/pumpkin/src/commands/tree.rs new file mode 100644 index 000000000..b4132b18e --- /dev/null +++ b/pumpkin/src/commands/tree.rs @@ -0,0 +1,140 @@ +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>; + +/// see [crate::commands::tree_builder::argument] and [CommandTree::execute]/[crate::commands::tree_builder::NonLeafNodeBuilder::execute] +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; + +pub(crate) struct Node<'a> { + pub(crate) children: Vec, + pub(crate) node_type: NodeType<'a>, +} + +pub(crate) enum NodeType<'a> { + ExecuteLeaf { + run: &'a (dyn Fn(&mut CommandSender, &ConsumedArgs) -> Result<(), InvalidTreeError> + Sync), + }, + #[allow(dead_code)] // todo: remove (so far no commands requiring this are implemented) + Literal { string: &'a str }, + Argument { + name: &'a str, + consumer: ArgumentConsumer<'a>, + }, + Require { + predicate: &'a (dyn Fn(&CommandSender) -> bool + Sync), + }, +} + +pub(crate) struct CommandTree<'a> { + pub(crate) nodes: Vec>, + pub(crate) children: Vec, + pub(crate) description: &'a str, +} + +impl<'a> CommandTree<'a> { + /// iterate over all possible paths that end in a [NodeType::ExecuteLeaf] + pub(crate) fn iter_paths(&'a self) -> impl Iterator> + 'a { + let mut todo = VecDeque::<(usize, usize)>::new(); + + // add root's children + todo.extend(self.children.iter().map(|&i| (0, i))); + + TraverseAllPathsIter::<'a> { + tree: self, + path: Vec::::new(), + 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> { + tree: &'a CommandTree<'a>, + path: Vec, + /// (depth, i) + todo: VecDeque<(usize, usize)>, +} + +impl<'a> Iterator for TraverseAllPathsIter<'a> { + type Item = Vec; + + fn next(&mut self) -> Option { + loop { + let (depth, i) = self.todo.pop_front()?; + let node = &self.tree.nodes[i]; + + // add new children to front + self.todo.reserve(node.children.len()); + node.children + .iter() + .rev() + .for_each(|&c| self.todo.push_front((depth + 1, c))); + + // update path + while self.path.len() > depth { + self.path.pop(); + } + self.path.push(i); + + if let NodeType::ExecuteLeaf { .. } = node.node_type { + return Some(self.path.clone()); + } + } + } +} diff --git a/pumpkin/src/commands/tree_builder.rs b/pumpkin/src/commands/tree_builder.rs new file mode 100644 index 000000000..63531909b --- /dev/null +++ b/pumpkin/src/commands/tree_builder.rs @@ -0,0 +1,151 @@ +use crate::commands::dispatcher::InvalidTreeError; +use crate::commands::tree::{ArgumentConsumer, CommandTree, ConsumedArgs, Node, NodeType}; +use crate::commands::CommandSender; + +impl<'a> CommandTree<'a> { + /// Add a child [Node] to the root of this [CommandTree]. + pub fn with_child(mut self, child: impl NodeBuilder<'a>) -> Self { + let node = child.build(&mut self); + self.children.push(self.nodes.len()); + self.nodes.push(node); + self + } + + pub fn new(description: &'a str) -> Self { + Self { + nodes: Vec::new(), + children: Vec::new(), + description, + } + } + + /// Executes if a command terminates at this [Node], i.e. without any arguments. + /// + /// [ConsumedArgs] maps the names of all + /// arguments to the result of their consumption, i.e. a string that can be parsed to the + /// desired type. + /// + /// Also see [NonLeafNodeBuilder::execute]. + pub fn execute( + mut self, + run: &'a (dyn Fn(&mut CommandSender, &ConsumedArgs) -> Result<(), InvalidTreeError> + Sync), + ) -> Self { + let node = Node { + node_type: NodeType::ExecuteLeaf { run }, + children: Vec::new(), + }; + + self.children.push(self.nodes.len()); + self.nodes.push(node); + + self + } +} + +pub trait NodeBuilder<'a> { + fn build(self, tree: &mut CommandTree<'a>) -> Node<'a>; +} + +struct LeafNodeBuilder<'a> { + node_type: NodeType<'a>, +} + +impl<'a> NodeBuilder<'a> for LeafNodeBuilder<'a> { + fn build(self, _tree: &mut CommandTree<'a>) -> Node<'a> { + Node { + children: Vec::new(), + node_type: self.node_type, + } + } +} + +pub struct NonLeafNodeBuilder<'a> { + node_type: NodeType<'a>, + child_nodes: Vec>, + leaf_nodes: Vec>, +} + +impl<'a> NodeBuilder<'a> for NonLeafNodeBuilder<'a> { + fn build(self, tree: &mut CommandTree<'a>) -> Node<'a> { + let mut child_indices = Vec::new(); + + for node_builder in self.child_nodes { + let node = node_builder.build(tree); + child_indices.push(tree.nodes.len()); + tree.nodes.push(node); + } + + for node_builder in self.leaf_nodes { + let node = node_builder.build(tree); + child_indices.push(tree.nodes.len()); + tree.nodes.push(node); + } + + Node { + children: child_indices, + node_type: self.node_type, + } + } +} + +impl<'a> NonLeafNodeBuilder<'a> { + /// Add a child [Node] to this one. + pub fn with_child(mut self, child: NonLeafNodeBuilder<'a>) -> Self { + self.child_nodes.push(child); + self + } + + /// Executes if a command terminates at this [Node]. + /// + /// [ConsumedArgs] maps the names of all + /// arguments to the result of their consumption, i.e. a string that can be parsed to the + /// desired type. + /// + /// Also see [CommandTree::execute]. + pub fn execute( + mut self, + run: &'a (dyn Fn(&mut CommandSender, &ConsumedArgs) -> Result<(), InvalidTreeError> + Sync), + ) -> Self { + self.leaf_nodes.push(LeafNodeBuilder { + node_type: NodeType::ExecuteLeaf { run }, + }); + + self + } +} + +/// Matches a sting literal. +#[allow(dead_code)] // todo: remove (so far no commands requiring this are implemented) +pub fn literal(string: &str) -> NonLeafNodeBuilder { + NonLeafNodeBuilder { + node_type: NodeType::Literal { string }, + child_nodes: Vec::new(), + leaf_nodes: Vec::new(), + } +} + +/// ```name``` identifies this argument in [ConsumedArgs]. +/// +/// ```consumer: ArgumentConsumer``` has the purpose of validating arguments. Conversion may start +/// here, as long as the result remains a [String] (e.g. convert offset to absolute position actual +/// coordinates), because the result of this function will be passed to following +/// [NonLeafNodeBuilder::execute] nodes in a [ConsumedArgs] instance. It must remove consumed arg(s) +/// from [RawArgs] and return them. It must return None if [RawArgs] are invalid. [RawArgs] is +/// reversed, so [Vec::pop] can be used to obtain args in ltr order. +pub fn argument<'a>(name: &'a str, consumer: ArgumentConsumer) -> NonLeafNodeBuilder<'a> { + NonLeafNodeBuilder { + node_type: NodeType::Argument { name, consumer }, + child_nodes: Vec::new(), + leaf_nodes: Vec::new(), + } +} + +/// ```predicate``` should return ```false``` if requirement for reaching following [Node]s is not +/// met. +pub fn require(predicate: &(dyn Fn(&CommandSender) -> bool + Sync)) -> NonLeafNodeBuilder { + NonLeafNodeBuilder { + node_type: NodeType::Require { predicate }, + child_nodes: Vec::new(), + leaf_nodes: Vec::new(), + } +}