add tree-style command parsing utilities, heavily inspired by Mojang's Brigadier and adjust existing commands to use them

This commit is contained in:
user622628252416 2024-08-19 00:29:31 +02:00
parent 77b205ada0
commit 943f8fcd4c
11 changed files with 524 additions and 131 deletions

View File

@ -0,0 +1,43 @@
use crate::client::Client;
use crate::commands::CommandSender;
use crate::commands::CommandSender::Player;
use crate::commands::dispatcher::InvalidTreeError;
use crate::commands::dispatcher::InvalidTreeError::InvalidConsumptionError;
use crate::commands::tree::{ConsumedArgs, RawArgs};
/// todo: implement (so far only own name + @s is implemented)
pub fn consume_arg_player(src: &CommandSender, args: &mut RawArgs) -> Option<String> {
let s = args.pop()?;
if let Player(client) = src {
if s == "@s" {
return Some(s.into())
}
if let Some(profile) = &client.gameprofile {
if profile.name == s {
return Some(s.into())
};
};
};
None
}
/// todo: implement (so far only own name + @s 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))?;
if let Player(client) = src {
if s == "@s" {
return Ok(client)
}
if let Some(profile) = &client.gameprofile {
if profile.name == s.as_ref() {
return Ok(client)
};
};
};
Err(InvalidConsumptionError(Some(s.into())))
}

View File

@ -0,0 +1,95 @@
use std::str::FromStr;
use num_traits::FromPrimitive;
use crate::commands::arg_player::{consume_arg_player, parse_arg_player};
use crate::commands::CommandSender;
use crate::commands::CommandSender::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::entity::player::GameMode;
pub(crate) const NAME: &str = "gamemode";
const DESCRIPTION: &str = "Changes the gamemode for a Player";
const ARG_GAMEMODE: &str = "gamemode";
const ARG_TARGET: &str = "target";
pub fn consume_arg_gamemode(_src: &CommandSender, args: &mut RawArgs) -> Option<String> {
let s = args.pop()?;
if let Ok(id) = s.parse::<u8>() {
match GameMode::from_u8(id) {
None | Some(GameMode::Undefined) => {},
Some(_) => return Some(s.into())
};
};
return match GameMode::from_str(s) {
Err(_) | Ok(GameMode::Undefined) => None,
Ok(_) => Some(s.into())
}
}
pub fn parse_arg_gamemode(consumed_args: &ConsumedArgs) -> Result<GameMode, InvalidTreeError> {
let s = consumed_args.get(ARG_GAMEMODE)
.ok_or(InvalidConsumptionError(None))?;
if let Ok(id) = s.parse::<u8>() {
match GameMode::from_u8(id) {
None | Some(GameMode::Undefined) => {},
Some(gamemode) => return Ok(gamemode)
};
};
return 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(format!("Game mode was set to {:?}", gamemode).into());
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(format!("Game mode was set to {:?}", gamemode).into());
Ok(())
})
)
)
)
}

View File

@ -0,0 +1,21 @@
use pumpkin_text::{color::NamedColor, TextComponent};
use pumpkin_protocol::CURRENT_MC_PROTOCOL;
use crate::server::CURRENT_MC_VERSION;
use crate::commands::tree::CommandTree;
pub(crate) const NAME: &str = "pumpkin";
const DESCRIPTION: &str = "Displays 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::from(
format!("Pumpkin {version}, {description} (Minecraft {CURRENT_MC_VERSION}, Protocol {CURRENT_MC_PROTOCOL})")).color_named(NamedColor::Green)
);
Ok(())
})
}

View File

@ -0,0 +1,15 @@
use crate::commands::tree::CommandTree;
use crate::commands::tree_builder::require;
pub(crate) const NAME: &str = "stop";
const DESCRIPTION: &str = "Stops 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)
})
)
}

View File

@ -0,0 +1,83 @@
use std::collections::HashMap;
use crate::commands::CommandSender;
use crate::commands::dispatcher::InvalidTreeError::{InvalidConsumptionError, InvalidRequirementError};
use crate::commands::tree::{ConsumedArgs, NodeType, RawArgs, CommandTree};
#[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<String>),
/// 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>>
}
impl <'a> CommandDispatcher<'a> {
pub(crate) fn dispatch(&'a self, src: &mut CommandSender, cmd: &str) -> Result<(), &str> {
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")?;
for path in tree.iter_paths() {
match Self::try_path(src, path, tree, raw_args.clone()) {
Err(InvalidConsumptionError(s)) => {
println!("Error while parsing command \"{cmd}\": {s:?} was consumed, but couldn't be parsed");
},
Err(InvalidRequirementError) => {
println!("Error while parsing command \"{cmd}\": a requirement that was expected was not met.");
},
Ok(fitting_path) => {
if fitting_path { return Ok(()) }
}
}
}
Err("Invalid Syntax: ")
}
fn try_path(src: &mut CommandSender, path: Vec<usize>, tree: &CommandTree, mut raw_args: RawArgs) -> Result<bool, InvalidTreeError> {
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)
}
}

View File

@ -1,56 +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::from("Usage: /gamemode <mode>")
.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(format!("Set own game mode to {:?}", mode).into());
}
Err(_) => {
// try to parse from number
if let Ok(i) = mode_str.parse::<u8>() {
if let Some(mode) = GameMode::from_u8(i) {
player.set_gamemode(mode);
player
.send_system_message(format!("Set own game mode to {:?}", mode).into());
return;
}
}
player.send_system_message(
TextComponent::from("Invalid gamemode")
.color_named(pumpkin_text::color::NamedColor::Red),
);
}
}
}
// TODO: support console, (name required)
fn player_required() -> bool {
true
}
}

View File

@ -1,28 +1,16 @@
use gamemode::GamemodeCommand; use std::collections::HashMap;
use pumpkin::PumpkinCommand; use std::sync::OnceLock;
use pumpkin_text::TextComponent; use pumpkin_text::TextComponent;
use stop::StopCommand;
use crate::client::Client; use crate::client::Client;
use crate::commands::dispatcher::CommandDispatcher;
mod gamemode; mod cmd_gamemode;
mod pumpkin; mod cmd_pumpkin;
mod stop; mod cmd_stop;
mod tree;
/// 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 mod tree_builder;
pub trait Command<'a> { mod dispatcher;
// Name of the Plugin, Use lower case mod arg_player;
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
}
}
pub enum CommandSender<'a> { pub enum CommandSender<'a> {
Rcon(&'a mut Vec<String>), Rcon(&'a mut Vec<String>),
@ -40,7 +28,7 @@ impl<'a> CommandSender<'a> {
} }
} }
pub fn is_player(&mut self) -> bool { pub fn is_player(&self) -> bool {
match self { match self {
CommandSender::Console => false, CommandSender::Console => false,
CommandSender::Player(_) => true, CommandSender::Player(_) => true,
@ -48,7 +36,7 @@ impl<'a> CommandSender<'a> {
} }
} }
pub fn is_console(&mut self) -> bool { pub fn is_console(&self) -> bool {
match self { match self {
CommandSender::Console => true, CommandSender::Console => true,
CommandSender::Player(_) => false, CommandSender::Player(_) => false,
@ -62,22 +50,40 @@ impl<'a> CommandSender<'a> {
CommandSender::Rcon(_) => None, CommandSender::Rcon(_) => None,
} }
} }
/// todo: implement
pub fn permission_lvl(&self) -> i32 {
match self {
CommandSender::Rcon(_) => 4,
CommandSender::Console => 4,
CommandSender::Player(_) => 4,
} }
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;
} }
if command.starts_with(StopCommand::NAME) {
StopCommand::on_execute(sender, command); // todo: reconsider using constant
return; const DISPATCHER: OnceLock<CommandDispatcher> = OnceLock::new();
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());
CommandDispatcher {
commands: map,
}
}
pub fn handle_command(sender: &mut CommandSender, cmd: &str) {
let dispatcher = DISPATCHER;
let dispatcher = dispatcher.get_or_init(dispatcher_init);
if let Err(err) = dispatcher.dispatch(sender, cmd) {
sender.send_message(
TextComponent::from(err)
.color_named(pumpkin_text::color::NamedColor::Red),
)
} }
// TODO: red color
sender.send_message("Command not Found".into());
} }

View File

@ -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::from(format!("Pumpkin {version}, {description} (Minecraft {CURRENT_MC_VERSION}, Protocol {CURRENT_MC_PROTOCOL})")).color_named(NamedColor::Green))
}
}

View File

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

View File

@ -0,0 +1,87 @@
use std::collections::{HashMap, VecDeque};
use crate::commands::CommandSender;
use crate::commands::dispatcher::InvalidTreeError;
pub(crate) type RawArgs<'a> = Vec<&'a str>;
pub(crate) type ConsumedArgs<'a> = HashMap<&'a str, String>;
pub(crate) type ArgumentConsumer<'a> = fn(&CommandSender, &mut RawArgs) -> Option<String>;
pub(crate) struct Node<'a> {
pub(crate) children: Vec<usize>,
pub(crate) node_type: NodeType<'a>,
}
pub(crate) enum NodeType<'a> {
ExecuteLeaf {
run: &'a dyn Fn(&mut CommandSender, &ConsumedArgs) -> Result<(), InvalidTreeError>,
},
Literal {
string: &'a str,
},
Argument {
name: &'a str,
consumer: ArgumentConsumer<'a>,
},
Require {
predicate: &'a dyn Fn(&CommandSender) -> bool,
}
}
pub(crate) struct CommandTree<'a> {
pub(crate) nodes: Vec<Node<'a>>,
pub(crate) children: Vec<usize>,
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<Item = Vec<usize>> + '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::<usize>::new(),
todo,
}
}
}
struct TraverseAllPathsIter<'a> {
tree: &'a CommandTree<'a>,
path: Vec<usize>,
/// (depth, i)
todo: VecDeque<(usize, usize)>,
}
impl <'a>Iterator for TraverseAllPathsIter<'a> {
type Item = Vec<usize>;
fn next(&mut self) -> Option<Self::Item> {
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());
}
}
}
}

View File

@ -0,0 +1,134 @@
use crate::commands::CommandSender;
use crate::commands::dispatcher::InvalidTreeError;
use crate::commands::tree::{ArgumentConsumer, ConsumedArgs, Node, NodeType, CommandTree};
impl <'a> CommandTree<'a> {
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.
pub fn execute(mut self, run: &'a dyn Fn(&mut CommandSender, &ConsumedArgs) -> Result<(), InvalidTreeError>) -> 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<NonLeafNodeBuilder<'a>>,
leaf_nodes: Vec<LeafNodeBuilder<'a>>
}
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> {
pub fn with_child(mut self, child: NonLeafNodeBuilder<'a>) -> Self {
self.child_nodes.push(child);
self
}
/// Executes if a command terminates at this [Node].
pub fn execute(mut self, run: &'a dyn Fn(&mut CommandSender, &ConsumedArgs) -> Result<(), InvalidTreeError>) -> Self {
self.leaf_nodes.push(LeafNodeBuilder {
node_type: NodeType::ExecuteLeaf {
run
},
});
self
}
}
/// Matches a sting literal.
pub fn literal(string: &str) -> NonLeafNodeBuilder {
NonLeafNodeBuilder {
node_type: NodeType::Literal {
string
},
child_nodes: Vec::new(),
leaf_nodes: Vec::new(),
}
}
/// ```name``` identifies this argument.
/// ```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). 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) -> NonLeafNodeBuilder {
NonLeafNodeBuilder {
node_type: NodeType::Require {
predicate
},
child_nodes: Vec::new(),
leaf_nodes: Vec::new(),
}
}