add tree-style command parsing utilities, heavily inspired by Mojang's Brigadier and adjust existing commands to use them
This commit is contained in:
parent
77b205ada0
commit
943f8fcd4c
43
pumpkin/src/commands/arg_player.rs
Normal file
43
pumpkin/src/commands/arg_player.rs
Normal 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())))
|
||||||
|
}
|
||||||
95
pumpkin/src/commands/cmd_gamemode.rs
Normal file
95
pumpkin/src/commands/cmd_gamemode.rs
Normal 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(())
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
21
pumpkin/src/commands/cmd_pumpkin.rs
Normal file
21
pumpkin/src/commands/cmd_pumpkin.rs
Normal 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(())
|
||||||
|
})
|
||||||
|
}
|
||||||
15
pumpkin/src/commands/cmd_stop.rs
Normal file
15
pumpkin/src/commands/cmd_stop.rs
Normal 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)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
83
pumpkin/src/commands/dispatcher.rs
Normal file
83
pumpkin/src/commands/dispatcher.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
// todo: reconsider using constant
|
||||||
// an ugly mess i know
|
const DISPATCHER: OnceLock<CommandDispatcher> = OnceLock::new();
|
||||||
if command.starts_with(PumpkinCommand::NAME) {
|
|
||||||
PumpkinCommand::on_execute(sender, command);
|
fn dispatcher_init<'a>() -> CommandDispatcher<'a> {
|
||||||
return;
|
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),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if command.starts_with(GamemodeCommand::NAME) {
|
|
||||||
GamemodeCommand::on_execute(sender, command);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if command.starts_with(StopCommand::NAME) {
|
|
||||||
StopCommand::on_execute(sender, command);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// TODO: red color
|
|
||||||
sender.send_message("Command not Found".into());
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
87
pumpkin/src/commands/tree.rs
Normal file
87
pumpkin/src/commands/tree.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
134
pumpkin/src/commands/tree_builder.rs
Normal file
134
pumpkin/src/commands/tree_builder.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user