feat: implement new command system to achieve complex vanilla commands (#1681)

* added StringReader and its errors

* fixed clippy errors in tests

* removed unnecessary '_ from &

* removed unnecessary `Box`

* added argument types, string range and suggestions

* removed Simple and Dynamic error types in favor of one struct and changed size of `TextComponent`

* formatted with `cargo fmt`

* fixed spelling error

* removed unnecessary (default) leftover from experimentation

* added tests and adjusted for arg type changes

* fixed some spellings in doc comments

* added some node-related structs

* initial commit

* no compile-time errors now

* removed code with compile-time testing

* formatted code

* fixed some clippy warnings

* fixed the remaining clippy warnings

* added some tests, refactored due to test results

* forget to remove fork, using redirect instead, formatted code

* fixed clippy warnings

* added some tests

* added some more tests & sorting `Suggestion`s

* using new pumpkin_data::translation for translation keys

* formatted

* removing misleading panic message

* added check for adding command node type

* added more documentation

* added permission requirement support for any node

* removed remnants of freeing nodes in tree for now, as it has risks with redirection

* removing doc comment that was true earlier while experimentation

* fixed doctest error

* relaxed Clone restriction on `Item` in `ArgumentType` and made `get_argument` return a reference instead of cloning

* fix doctest
This commit is contained in:
Laptop59
2026-02-22 01:27:54 +05:30
committed by GitHub
parent 4afe60aca9
commit 8557cd93c7
30 changed files with 5638 additions and 61 deletions

View File

@@ -51,7 +51,7 @@ pub mod smoker;
pub mod trapped_chest;
//TODO: We need a mark_dirty for chests
pub trait BlockEntity: Send + Sync {
pub trait BlockEntity: Any + Send + Sync {
fn write_nbt<'a>(
&'a self,
nbt: &'a mut NbtCompound,

View File

@@ -131,7 +131,9 @@ impl CommandBlock {
if !command_blocks_work {
return;
}
let command_entity: &CommandBlockEntity = block_entity.as_any().downcast_ref().unwrap();
let command_entity: Arc<CommandBlockEntity> = Arc::downcast(block_entity).unwrap();
if command.is_empty() {
command_entity.success_count.store(0, Ordering::Release);
} else {
@@ -140,7 +142,7 @@ impl CommandBlock {
.read()
.await
.handle_command(
&crate::command::CommandSender::CommandBlock(block_entity, world),
&crate::command::CommandSender::CommandBlock(command_entity, world),
server,
command,
)

View File

@@ -0,0 +1,513 @@
use crate::command::argument_builder::private::Sealed;
use crate::command::argument_types::argument_type::AnyArgumentType;
use crate::command::node::detached::{
ArgumentDetachedNode, CommandDetachedNode, DetachedNode, GlobalNodeId, LiteralDetachedNode,
};
use crate::command::node::{Command, CommandExecutor, RedirectModifier, Redirection, Requirement};
use rustc_hash::FxHashMap;
use std::borrow::Cow;
use std::sync::Arc;
/// Represents an intermediate struct for
/// building arguments for commands.
///
/// # Note
///
/// This is an implementation detail.
struct CommonArgumentBuilder {
pub global_id: GlobalNodeId,
pub arguments: FxHashMap<String, DetachedNode>,
pub command: Option<Command>,
pub requirement: Requirement,
pub target: Option<Redirection>,
pub modifier: RedirectModifier,
pub permission: Option<Cow<'static, str>>,
pub forks: bool,
}
impl CommonArgumentBuilder {
fn new() -> Self {
Self {
global_id: GlobalNodeId::new(),
arguments: FxHashMap::default(),
command: None,
requirement: Requirement::AlwaysQualified,
target: None,
modifier: RedirectModifier::OneSource,
permission: None,
forks: false,
}
}
}
impl Default for CommonArgumentBuilder {
fn default() -> Self {
Self::new()
}
}
/// A short-form way to create a new [`CommandArgumentBuilder`]
/// from a literal and a command description.
///
/// This can be imported directly without a prefix, or imported with the `argument_builder::` prefix. Here's an example of usage:
/// ```
/// use pumpkin::command::argument_builder::command;
///
/// let builder = command("foo", "A test command");
/// ```
///
/// The builder returned will eventually construct a [`CommandDetachedNode`].
/// This node can then be registered into a dispatcher.
pub fn command(
literal: impl Into<Cow<'static, str>>,
description: impl Into<Cow<'static, str>>,
) -> CommandArgumentBuilder {
CommandArgumentBuilder::new(literal, description)
}
/// A short-form way to create a new [`LiteralArgumentBuilder`]
/// from a literal.
///
/// This can be imported directly without a prefix, or imported with the `argument_builder::` prefix. Here's an example of usage:
/// ```
/// use pumpkin::command::argument_builder::literal;
///
/// let builder = literal("bar");
/// ```
///
/// The builder returned will eventually construct a [`LiteralDetachedNode`].
pub fn literal(literal: impl Into<Cow<'static, str>>) -> LiteralArgumentBuilder {
LiteralArgumentBuilder::new(literal)
}
/// A short-form way to create a new [`RequiredArgumentBuilder`]
/// from an argument type and name.
///
/// This can be imported directly without a prefix, or imported with the `argument_builder::` prefix. Here's an example of usage:
/// ```
/// use pumpkin::command::{
/// argument_builder::argument,
/// argument_types::core::integer::IntegerArgumentType
/// };
///
/// let argument_builder = argument("bar", IntegerArgumentType::new(1, 10));
/// ```
///
/// The builder returned will eventually construct a [`ArgumentDetachedNode`].
pub fn argument(
name: impl Into<Cow<'static, str>>,
arg_type: impl AnyArgumentType + 'static,
) -> RequiredArgumentBuilder {
RequiredArgumentBuilder::new(name, arg_type)
}
/// A builder that builds a literal, non-command [`DetachedNode`].
pub struct LiteralArgumentBuilder {
common: CommonArgumentBuilder,
literal: Cow<'static, str>,
}
/// A builder that builds a command [`DetachedNode`].
pub struct CommandArgumentBuilder {
common: CommonArgumentBuilder,
literal: Cow<'static, str>,
description: Cow<'static, str>,
}
/// A builder that builds an argument [`DetachedNode`].
pub struct RequiredArgumentBuilder {
common: CommonArgumentBuilder,
name: Cow<'static, str>,
argument_type: Arc<dyn AnyArgumentType>,
}
mod private {
// We want to make this trait private so that
// we can implement it for only our
// argument builders defined here.
pub trait Sealed {}
}
pub trait ArgumentBuilder<N: Into<DetachedNode>>: Sized + Sealed {
/// Puts an argument to be specified, right after this one is specified.
///
/// # Panics
///
/// Panics if this node is redirected to another node, or the child
/// provided is of the type [`CommandDetachedNode`].
#[must_use]
fn then(self, child: impl Into<DetachedNode>) -> Self;
/// Gets the command to execute for the node being built.
#[must_use]
fn command(&self) -> Option<Command>;
/// Sets the command to execute for the node being built.
#[must_use]
fn executes(self, command: impl CommandExecutor + 'static) -> Self;
/// Sets the redirect target of the node being built to another, without a modifier.
#[must_use]
fn redirect(self, redirection: impl Into<Redirection>) -> Self;
/// Sets the redirect target of the node being built to another, with a given modifier.
#[must_use]
fn redirect_with_modifier(
self,
redirection: impl Into<Redirection>,
redirect_modifier: RedirectModifier,
) -> Self;
/// Forks the given context, using multiple for later.
#[must_use]
fn fork(self, redirection: impl Into<Redirection>, redirect_modifier: RedirectModifier)
-> Self;
/// Forwards the given context, with the given `fork` flag.
#[must_use]
fn forward(
self,
redirection: impl Into<Redirection>,
redirect_modifier: RedirectModifier,
fork: bool,
) -> Self;
/// Gets a reference to the arguments of the node to be built.
#[must_use]
fn arguments(&self) -> &FxHashMap<String, DetachedNode>;
/// Gets the node to which the node being built by this [`ArgumentBuilder`] redirects.
#[must_use]
fn target(&self) -> Option<Redirection>;
/// Gets the permission required by this node to run, in addition to the requirement in the node.
#[must_use]
fn permission(&self) -> Option<&str>;
/// Gets the redirect modifier of the node this [`ArgumentBuilder`] is building.
#[must_use]
fn redirect_modifier(&self) -> RedirectModifier;
/// Whether this builder forks.
#[must_use]
fn forks(&self) -> bool;
/// Returns the 'future [`GlobalId`]' of the node that will be produced by this Builder.
/// Very useful for redirects.
#[must_use]
fn id(&self) -> GlobalNodeId;
/// Builds the node represented by this builder, consuming itself in the process.
#[must_use]
fn build(self) -> N;
}
// Implement the private trait for our builders!
impl Sealed for LiteralArgumentBuilder {}
impl Sealed for CommandArgumentBuilder {}
impl Sealed for RequiredArgumentBuilder {}
/// Helper macro to implement repeated code of `ArgumentBuilder` for our types.
macro_rules! impl_boilerplate_argument_builder {
() => {
fn then(mut self, argument: impl Into<DetachedNode>) -> Self {
assert!(
self.target().is_none(),
"Cannot add children to a redirected node"
);
let node = argument.into();
assert!(
!matches!(node, DetachedNode::Command(_)),
"Cannot add a CommandDetachedNode as a child of a builder"
);
self.common.arguments.insert(node.name(), node);
self
}
fn command(&self) -> Option<Command> {
self.common.command.clone()
}
fn executes(mut self, command: impl CommandExecutor + 'static) -> Self {
self.common.command = Some(Arc::new(command));
self
}
fn redirect(self, redirection: impl Into<Redirection>) -> Self {
self.forward(redirection.into(), RedirectModifier::OneSource, false)
}
fn redirect_with_modifier(self, redirection: impl Into<Redirection>, redirect_modifier: RedirectModifier) -> Self {
self.forward(redirection.into(), redirect_modifier, false)
}
fn fork(self, redirection: impl Into<Redirection>, redirect_modifier: RedirectModifier) -> Self {
self.forward(redirection.into(), redirect_modifier, true)
}
fn forward(mut self, redirection: impl Into<Redirection>, redirect_modifier: RedirectModifier, fork: bool) -> Self {
assert!(self.common.arguments.is_empty(), "Cannot forward a node with children. The node must have no children to redirect somewhere else");
self.common.target = Some(redirection.into());
self.common.modifier = redirect_modifier;
self.common.forks = fork;
self
}
fn arguments(&self) -> &FxHashMap<String, DetachedNode> {
&self.common.arguments
}
fn target(&self) -> Option<Redirection> {
self.common.target.clone()
}
fn permission(&self) -> Option<&str> {
self.common.permission.as_deref()
}
fn redirect_modifier(&self) -> RedirectModifier {
self.common.modifier.clone()
}
fn forks(&self) -> bool {
self.common.forks
}
fn id(&self) -> GlobalNodeId {
self.common.global_id
}
};
}
/// Helper macro to generate `From` impl blocks for each builder.
macro_rules! impl_builder_from_impls {
($builder: ty => $detached_node: ty) => {
impl From<$builder> for $detached_node {
fn from(value: $builder) -> Self {
value.build()
}
}
impl From<$builder> for DetachedNode {
fn from(value: $builder) -> Self {
value.build().into()
}
}
};
}
impl_builder_from_impls!(LiteralArgumentBuilder => LiteralDetachedNode);
impl_builder_from_impls!(CommandArgumentBuilder => CommandDetachedNode);
impl_builder_from_impls!(RequiredArgumentBuilder => ArgumentDetachedNode);
impl LiteralArgumentBuilder {
/// Creates a new [`LiteralArgumentBuilder`] from a literal.
pub fn new(literal: impl Into<Cow<'static, str>>) -> Self {
Self {
common: CommonArgumentBuilder::new(),
literal: literal.into(),
}
}
}
impl CommandArgumentBuilder {
/// Creates a new [`CommandArgumentBuilder`] from a literal and a command description.
pub fn new(
literal: impl Into<Cow<'static, str>>,
description: impl Into<Cow<'static, str>>,
) -> Self {
Self {
common: CommonArgumentBuilder::new(),
literal: literal.into(),
description: description.into(),
}
}
}
impl RequiredArgumentBuilder {
/// Creates a new [`RequiredArgumentBuilder`] from a name and an argument type.
pub fn new(
name: impl Into<Cow<'static, str>>,
arg_type: impl AnyArgumentType + 'static,
) -> Self {
Self {
common: CommonArgumentBuilder::new(),
name: name.into(),
argument_type: Arc::new(arg_type),
}
}
}
impl ArgumentBuilder<LiteralDetachedNode> for LiteralArgumentBuilder {
impl_boilerplate_argument_builder!();
fn build(self) -> LiteralDetachedNode {
let mut node = LiteralDetachedNode::new(
self.common.global_id,
self.literal,
self.common.command,
self.common.requirement,
self.common.target,
self.common.modifier,
self.common.permission,
self.common.forks,
);
node.children = self.common.arguments;
node
}
}
impl ArgumentBuilder<CommandDetachedNode> for CommandArgumentBuilder {
impl_boilerplate_argument_builder!();
fn build(self) -> CommandDetachedNode {
let mut node = CommandDetachedNode::new(
self.common.global_id,
self.literal,
self.description,
self.common.command,
self.common.requirement,
self.common.target,
self.common.modifier,
self.common.permission,
self.common.forks,
);
node.children = self.common.arguments;
node
}
}
impl ArgumentBuilder<ArgumentDetachedNode> for RequiredArgumentBuilder {
impl_boilerplate_argument_builder!();
fn build(self) -> ArgumentDetachedNode {
let mut node = ArgumentDetachedNode::new(
self.common.global_id,
self.name,
self.argument_type,
self.common.command,
self.common.requirement,
self.common.target,
self.common.modifier,
self.common.permission,
self.common.forks,
);
node.children = self.common.arguments;
node
}
}
#[cfg(test)]
mod test {
use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal};
use crate::command::argument_types::core::double::DoubleArgumentType;
use crate::command::argument_types::core::integer::IntegerArgumentType;
use crate::command::argument_types::core::string::StringArgumentType;
use crate::command::errors::error_types;
use crate::command::node::Redirection;
use crate::command::node::attached::AttachedNode;
use crate::command::node::tree::Tree;
use crate::command::string_reader::StringReader;
#[test]
fn literal_one() {
let builder = literal("test");
let node = builder.build();
assert_eq!(node.meta.literal, "test");
}
#[test]
fn required_one() {
let builder = argument("test", IntegerArgumentType::new(1, 10));
let node = builder.build();
assert_eq!(node.meta.name, "test");
let mut reader1 = StringReader::new("5");
let mut reader2 = StringReader::new("11");
let boxed_result = node
.meta
.argument_type
.parse(&mut reader1)
.expect("The parsing should not have errored");
let result = boxed_result
.downcast::<i32>()
.expect("Downcasting shouldn't have failed");
assert_eq!(result, Box::new(5));
let error = node
.meta
.argument_type
.parse(&mut reader2)
.expect_err("The parsing should have errored as 11 is outside the range");
assert!(error.is(&error_types::INTEGER_TOO_HIGH));
}
#[test]
fn literal_multiple() {
let mut builder = command("letter", "A test command");
for letter in 'a'..='z' {
// Add a node per letter for the argument.
let letter_string = letter.to_string();
builder = builder.then(literal(letter_string));
}
let node = builder.build();
assert_eq!(node.children.len(), 26);
}
#[test]
fn required_multiple() {
let builder = command("test", "A test command")
.then(argument("number", DoubleArgumentType::any()))
.then(argument("word", StringArgumentType::SingleWord));
let node = builder.build();
assert_eq!(node.children.len(), 2);
}
#[test]
fn redirect() {
let builder = command("test", "A test command").redirect(Redirection::Root);
let mut tree = Tree::new();
let node_id = tree.add_child_to_root(builder);
let node = &tree[node_id];
let redirect = node
.redirect
.expect("Redirection should exist as it was added before");
let target_id = tree
.resolve(redirect)
.expect("Target should have been resolved properly");
let target = &tree[target_id];
assert!(matches!(target, AttachedNode::Root(_)));
}
#[test]
#[should_panic = "Cannot forward a node with children. The node must have no children to redirect somewhere else"]
fn redirect_after_child() {
let _ = command("test", "A test command")
.then(literal("child"))
.redirect(Redirection::Root);
}
#[test]
#[should_panic = "Cannot add children to a redirected node"]
fn redirect_before_child() {
let _ = command("test", "A test command")
.redirect(Redirection::Root)
.then(literal("child"));
}
#[test]
#[should_panic = "Cannot add a CommandDetachedNode as a child of a builder"]
fn add_command_as_child() {
let _ = command("foo", "A test command").then(command("bar", "Another test command"));
}
}

View File

@@ -0,0 +1,139 @@
use crate::command::argument_types::argument_type::sealed::Sealed;
use crate::command::context::command_context::CommandContext;
use crate::command::context::command_source::CommandSource;
use crate::command::suggestion::suggestions::Suggestions;
use crate::command::suggestion::suggestions::SuggestionsBuilder;
use crate::command::{
errors::command_syntax_error::CommandSyntaxError, string_reader::StringReader,
};
use std::any::Any;
use std::pin::Pin;
/// Represents an argument type that parses a particular type `Item`.
pub trait ArgumentType: Send + Sync {
/// The data type that this argument type parses.
type Item: Send + Sync;
/// Parses a `T` by using a [`StringReader`]. Call this only if you have no source.
///
/// Errors should be propagated using the `?` operator, which will
/// replicate Brigadier's behavior of exceptions.
fn parse(&self, reader: &mut StringReader) -> Result<Self::Item, CommandSyntaxError>;
/// Parses a `T` by using a [`StringReader`],
/// along with a particular source of type `S`.
///
/// Errors should be propagated using the `?` operator, which will
/// replicate Brigadier's behavior of exceptions.
fn parse_with_source(
&self,
reader: &mut StringReader,
_source: &CommandSource,
) -> Result<Self::Item, CommandSyntaxError> {
self.parse(reader)
}
/// Provides a list of suggestions from this argument type.
#[must_use]
fn list_suggestions(
&self,
_context: &CommandContext,
_suggestions_builder: &mut SuggestionsBuilder,
) -> Pin<Box<dyn Future<Output = Suggestions> + Send>> {
Box::pin(async move { Suggestions::empty() })
}
/// Gets a selected list of examples which are considered
/// valid when parsed into type `T`.
///
/// Used for conflicts.
#[must_use]
fn examples(&self) -> Vec<String> {
Vec::new()
}
}
// Prevent other crates from using this trait
// Thus, we can effectively 'seal' our trait meant
// only for `AnyArgumentType`.
mod sealed {
/// Private trait to ensure only types implementing `ArgumentType` can implement `AnyArgumentType`.
pub trait Sealed {}
}
/// Represents an argument type with any parsable type.
pub trait AnyArgumentType: Sealed + Send + Sync {
/// Parses a value by using a [`StringReader`]. Call this only if you have no source.
///
/// Errors should be propagated using the `?` operator, which will
/// replicate Brigadier's behavior of exceptions.
fn parse(
&self,
reader: &mut StringReader,
) -> Result<Box<dyn Any + Send + Sync>, CommandSyntaxError>;
/// Parses a value by using a [`StringReader`]. Call this only if you have no source.
///
/// Errors should be propagated using the `?` operator, which will
/// replicate Brigadier's behavior of exceptions.
fn parse_with_source(
&self,
reader: &mut StringReader,
source: &CommandSource,
) -> Result<Box<dyn Any + Send + Sync>, CommandSyntaxError>;
/// Provides a list of suggestions from this argument type.
#[must_use]
fn list_suggestions(
&self,
context: &CommandContext,
suggestions_builder: &mut SuggestionsBuilder,
) -> Pin<Box<dyn Future<Output = Suggestions> + Send>>;
/// Gets a selected list of examples which are considered
/// valid when parsed into type `T`.
///
/// Used for conflicts.
#[must_use]
fn examples(&self) -> Vec<String> {
Vec::new()
}
}
// Implement our private trait for all argument types.
impl<U: ArgumentType<Item = T>, T: Send + Sync + 'static> Sealed for U {}
impl<U: ArgumentType<Item = T>, T: Send + Sync + 'static> AnyArgumentType for U {
fn parse(
&self,
reader: &mut StringReader,
) -> Result<Box<dyn Any + Send + Sync>, CommandSyntaxError> {
match self.parse(reader) {
Ok(value) => Ok(Box::new(value)),
Err(error) => Err(error),
}
}
fn parse_with_source(
&self,
reader: &mut StringReader,
source: &CommandSource,
) -> Result<Box<dyn Any + Send + Sync>, CommandSyntaxError> {
match self.parse_with_source(reader, source) {
Ok(value) => Ok(Box::new(value)),
Err(error) => Err(error),
}
}
fn list_suggestions(
&self,
context: &CommandContext,
suggestions_builder: &mut SuggestionsBuilder,
) -> Pin<Box<dyn Future<Output = Suggestions> + Send>> {
self.list_suggestions(context, suggestions_builder)
}
fn examples(&self) -> Vec<String> {
self.examples()
}
}

View File

@@ -0,0 +1,45 @@
use crate::command::{
argument_types::argument_type::ArgumentType, errors::command_syntax_error::CommandSyntaxError,
string_reader::StringReader,
};
/// Represents an argument type parsing a [`bool`].
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct BoolArgumentType;
impl ArgumentType for BoolArgumentType {
type Item = bool;
fn parse(&self, reader: &mut StringReader) -> Result<bool, CommandSyntaxError> {
reader.read_bool()
}
fn examples(&self) -> Vec<String> {
examples!("true", "false")
}
}
#[cfg(test)]
mod test {
use crate::command::{
argument_types::{argument_type::ArgumentType, core::bool::BoolArgumentType},
errors::error_types,
string_reader::StringReader,
};
#[test]
fn parse_test() {
let mut reader = StringReader::new("true");
assert_parse_ok_reset!(&mut reader, BoolArgumentType, true);
reader = StringReader::new("false");
assert_parse_ok_reset!(&mut reader, BoolArgumentType, false);
reader = StringReader::new("1");
assert_parse_err_reset!(
&mut reader,
BoolArgumentType,
&error_types::READER_INVALID_BOOL
);
}
}

View File

@@ -0,0 +1,106 @@
use crate::command::{
argument_types::{argument_type::ArgumentType, core::within_or_err},
errors::{command_syntax_error::CommandSyntaxError, error_types},
string_reader::StringReader,
};
/// Represents an argument type parsing an [`f64`].
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct DoubleArgumentType {
pub min: f64,
pub max: f64,
}
impl ArgumentType for DoubleArgumentType {
type Item = f64;
fn parse(&self, reader: &mut StringReader) -> Result<f64, CommandSyntaxError> {
let reader_start = reader.cursor();
let result = reader.read_double()?;
within_or_err(
reader,
reader_start,
result,
self.min,
self.max,
&error_types::DOUBLE_TOO_LOW,
&error_types::DOUBLE_TOO_HIGH,
)
}
fn examples(&self) -> Vec<String> {
examples!("0", "1.2", ".5", "-1", "-.5", "-1234.56")
}
}
impl DoubleArgumentType {
/// Constructs a new [`DoubleArgumentType`] with no minimum or maximum bounds.
#[must_use]
pub const fn any() -> Self {
Self {
min: f64::MIN,
max: f64::MAX,
}
}
/// Constructs a new [`DoubleArgumentType`] with *only* the specified minimum bound.
#[must_use]
pub const fn with_min(min: f64) -> Self {
Self { min, max: f64::MAX }
}
/// Constructs a new [`DoubleArgumentType`] with *only* the specified maximum bound.
#[must_use]
pub const fn with_max(max: f64) -> Self {
Self { min: f64::MIN, max }
}
/// Constructs a new [`DoubleArgumentType`] with the given bounds.
#[must_use]
pub const fn new(min: f64, max: f64) -> Self {
Self { min, max }
}
}
#[cfg(test)]
mod test {
use crate::command::{
argument_types::{argument_type::ArgumentType, core::double::DoubleArgumentType},
errors::error_types,
string_reader::StringReader,
};
#[test]
fn parse_test() {
let mut reader = StringReader::new("-1234.56");
assert_parse_ok_reset!(&mut reader, DoubleArgumentType::any());
assert_parse_ok_reset!(&mut reader, DoubleArgumentType::with_min(-1240.0));
assert_parse_err_reset!(
&mut reader,
DoubleArgumentType::with_min(-1230.0),
&error_types::DOUBLE_TOO_LOW
);
assert_parse_ok_reset!(&mut reader, DoubleArgumentType::with_max(-1230.0));
assert_parse_err_reset!(
&mut reader,
DoubleArgumentType::with_max(-1240.0),
&error_types::DOUBLE_TOO_HIGH
);
assert_parse_ok_reset!(&mut reader, DoubleArgumentType::new(-1235.0, -1230.0));
assert_parse_err_reset!(
&mut reader,
DoubleArgumentType::new(-1240.0, -1235.0),
&error_types::DOUBLE_TOO_HIGH
);
assert_parse_err_reset!(
&mut reader,
DoubleArgumentType::new(-1230.0, -1225.0),
&error_types::DOUBLE_TOO_LOW
);
}
}

View File

@@ -0,0 +1,106 @@
use crate::command::{
argument_types::{argument_type::ArgumentType, core::within_or_err},
errors::{command_syntax_error::CommandSyntaxError, error_types},
string_reader::StringReader,
};
/// Represents an argument type parsing an [`f32`].
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct FloatArgumentType {
pub min: f32,
pub max: f32,
}
impl ArgumentType for FloatArgumentType {
type Item = f32;
fn parse(&self, reader: &mut StringReader) -> Result<f32, CommandSyntaxError> {
let reader_start = reader.cursor();
let result = reader.read_float()?;
within_or_err(
reader,
reader_start,
result,
self.min,
self.max,
&error_types::FLOAT_TOO_LOW,
&error_types::FLOAT_TOO_HIGH,
)
}
fn examples(&self) -> Vec<String> {
examples!("0", "1.2", ".5", "-1", "-.5", "-1234.56")
}
}
impl FloatArgumentType {
/// Constructs a new [`FloatArgumentType`] with no minimum or maximum bounds.
#[must_use]
pub const fn any() -> Self {
Self {
min: f32::MIN,
max: f32::MAX,
}
}
/// Constructs a new [`FloatArgumentType`] with *only* the specified minimum bound.
#[must_use]
pub const fn with_min(min: f32) -> Self {
Self { min, max: f32::MAX }
}
/// Constructs a new [`FloatArgumentType`] with *only* the specified maximum bound.
#[must_use]
pub const fn with_max(max: f32) -> Self {
Self { min: f32::MIN, max }
}
/// Constructs a new [`FloatArgumentType`] with the given bounds.
#[must_use]
pub const fn new(min: f32, max: f32) -> Self {
Self { min, max }
}
}
#[cfg(test)]
mod test {
use crate::command::{
argument_types::{argument_type::ArgumentType, core::float::FloatArgumentType},
errors::error_types,
string_reader::StringReader,
};
#[test]
fn parse_test() {
let mut reader = StringReader::new("-1234.56");
assert_parse_ok_reset!(&mut reader, FloatArgumentType::any());
assert_parse_ok_reset!(&mut reader, FloatArgumentType::with_min(-1240.0));
assert_parse_err_reset!(
&mut reader,
FloatArgumentType::with_min(-1230.0),
&error_types::FLOAT_TOO_LOW
);
assert_parse_ok_reset!(&mut reader, FloatArgumentType::with_max(-1230.0));
assert_parse_err_reset!(
&mut reader,
FloatArgumentType::with_max(-1240.0),
&error_types::FLOAT_TOO_HIGH
);
assert_parse_ok_reset!(&mut reader, FloatArgumentType::new(-1235.0, -1230.0));
assert_parse_err_reset!(
&mut reader,
FloatArgumentType::new(-1240.0, -1235.0),
&error_types::FLOAT_TOO_HIGH
);
assert_parse_err_reset!(
&mut reader,
FloatArgumentType::new(-1230.0, -1225.0),
&error_types::FLOAT_TOO_LOW
);
}
}

View File

@@ -0,0 +1,117 @@
use crate::command::{
argument_types::{argument_type::ArgumentType, core::within_or_err},
errors::{command_syntax_error::CommandSyntaxError, error_types},
string_reader::StringReader,
};
/// Represents an argument type parsing an [`i32`].
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct IntegerArgumentType {
pub min: i32,
pub max: i32,
}
impl ArgumentType for IntegerArgumentType {
type Item = i32;
fn parse(&self, reader: &mut StringReader) -> Result<i32, CommandSyntaxError> {
let reader_start = reader.cursor();
let result = reader.read_int()?;
within_or_err(
reader,
reader_start,
result,
self.min,
self.max,
&error_types::INTEGER_TOO_LOW,
&error_types::INTEGER_TOO_HIGH,
)
}
fn examples(&self) -> Vec<String> {
examples!("0", "123", "-123")
}
}
impl IntegerArgumentType {
/// Constructs a new [`IntegerArgumentType`] with no minimum or maximum bounds.
#[must_use]
pub const fn any() -> Self {
Self {
min: i32::MIN,
max: i32::MAX,
}
}
/// Constructs a new [`IntegerArgumentType`] with *only* the specified minimum bound.
#[must_use]
pub const fn with_min(min: i32) -> Self {
Self { min, max: i32::MAX }
}
/// Constructs a new [`IntegerArgumentType`] with *only* the specified maximum bound.
#[must_use]
pub const fn with_max(max: i32) -> Self {
Self { min: i32::MIN, max }
}
/// Constructs a new [`IntegerArgumentType`] with the given bounds.
#[must_use]
pub const fn new(min: i32, max: i32) -> Self {
Self { min, max }
}
}
#[cfg(test)]
mod test {
use crate::command::{
argument_types::{argument_type::ArgumentType, core::integer::IntegerArgumentType},
errors::error_types,
string_reader::StringReader,
};
#[test]
fn parse_test() {
let mut reader = StringReader::new("123");
assert_parse_ok_reset!(&mut reader, IntegerArgumentType::any(), 123);
assert_parse_ok_reset!(&mut reader, IntegerArgumentType::with_min(120), 123);
assert_parse_err_reset!(
&mut reader,
IntegerArgumentType::with_min(130),
&error_types::INTEGER_TOO_LOW
);
assert_parse_ok_reset!(&mut reader, IntegerArgumentType::with_max(200), 123);
assert_parse_err_reset!(
&mut reader,
IntegerArgumentType::with_max(100),
&error_types::INTEGER_TOO_HIGH
);
assert_parse_ok_reset!(&mut reader, IntegerArgumentType::new(100, 125), 123);
assert_parse_err_reset!(
&mut reader,
IntegerArgumentType::new(100, 120),
&error_types::INTEGER_TOO_HIGH
);
assert_parse_err_reset!(
&mut reader,
IntegerArgumentType::new(125, 150),
&error_types::INTEGER_TOO_LOW
);
// 500_000_000 fits into an i32.
reader = StringReader::new("500000000");
assert_parse_ok_reset!(&mut reader, IntegerArgumentType::any(), 500_000_000);
// 5_000_000_000 does not fit into an i32.
reader = StringReader::new("5000000000");
assert_parse_err_reset!(
&mut reader,
IntegerArgumentType::any(),
&error_types::READER_INVALID_INT
);
}
}

View File

@@ -0,0 +1,121 @@
use crate::command::{
argument_types::{argument_type::ArgumentType, core::within_or_err},
errors::{command_syntax_error::CommandSyntaxError, error_types},
string_reader::StringReader,
};
/// Represents an argument type parsing an [`i64`].
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct LongArgumentType {
pub min: i64,
pub max: i64,
}
impl ArgumentType for LongArgumentType {
type Item = i64;
fn parse(&self, reader: &mut StringReader) -> Result<i64, CommandSyntaxError> {
let reader_start = reader.cursor();
let result = reader.read_long()?;
within_or_err(
reader,
reader_start,
result,
self.min,
self.max,
&error_types::LONG_TOO_LOW,
&error_types::LONG_TOO_HIGH,
)
}
fn examples(&self) -> Vec<String> {
examples!("0", "123", "-123")
}
}
impl LongArgumentType {
/// Constructs a new [`LongArgumentType`] with no minimum or maximum bounds.
#[must_use]
pub const fn any() -> Self {
Self {
min: i64::MIN,
max: i64::MAX,
}
}
/// Constructs a new [`LongArgumentType`] with *only* the specified minimum bound.
#[must_use]
pub const fn with_min(min: i64) -> Self {
Self { min, max: i64::MAX }
}
/// Constructs a new [`LongArgumentType`] with *only* the specified maximum bound.
#[must_use]
pub const fn with_max(max: i64) -> Self {
Self { min: i64::MIN, max }
}
/// Constructs a new [`LongArgumentType`] with the given bounds.
#[must_use]
pub const fn new(min: i64, max: i64) -> Self {
Self { min, max }
}
}
#[cfg(test)]
mod test {
use crate::command::{
argument_types::{argument_type::ArgumentType, core::long::LongArgumentType},
errors::error_types,
string_reader::StringReader,
};
#[test]
fn parse_test() {
let mut reader = StringReader::new("123");
assert_parse_ok_reset!(&mut reader, LongArgumentType::any(), 123);
assert_parse_ok_reset!(&mut reader, LongArgumentType::with_min(120), 123);
assert_parse_err_reset!(
&mut reader,
LongArgumentType::with_min(130),
&error_types::LONG_TOO_LOW
);
assert_parse_ok_reset!(&mut reader, LongArgumentType::with_max(200), 123);
assert_parse_err_reset!(
&mut reader,
LongArgumentType::with_max(100),
&error_types::LONG_TOO_HIGH
);
assert_parse_ok_reset!(&mut reader, LongArgumentType::new(100, 125), 123);
assert_parse_err_reset!(
&mut reader,
LongArgumentType::new(100, 120),
&error_types::LONG_TOO_HIGH
);
assert_parse_err_reset!(
&mut reader,
LongArgumentType::new(125, 150),
&error_types::LONG_TOO_LOW
);
// 5_000_000_000_000_000_000 fits into an i64.
reader = StringReader::new("5000000000000000000");
assert_parse_ok_reset!(
&mut reader,
LongArgumentType::any(),
5_000_000_000_000_000_000
);
// 10_000_000_000_000_000_000 does not fit into an i64.
reader = StringReader::new("10000000000000000000");
assert_parse_err_reset!(
&mut reader,
LongArgumentType::any(),
&error_types::READER_INVALID_LONG
);
}
}

View File

@@ -0,0 +1,46 @@
use pumpkin_util::text::TextComponent;
use crate::command::{
errors::{command_syntax_error::CommandSyntaxError, error_types::CommandErrorType},
string_reader::StringReader,
};
pub mod bool;
pub mod double;
pub mod float;
pub mod integer;
pub mod long;
pub mod string;
/// Helper method for parsing with a reader and returning an [`Err`] if outside the range.
#[inline]
pub fn within_or_err<T>(
reader: &mut StringReader,
reader_start: usize,
value: T,
min: T,
max: T,
too_low_error_type: &'static CommandErrorType<2>,
too_high_error_type: &'static CommandErrorType<2>,
) -> Result<T, CommandSyntaxError>
where
T: PartialOrd + ToString + Copy,
{
if value < min {
reader.set_cursor(reader_start);
Err(too_low_error_type.create(
reader,
TextComponent::text(value.to_string()),
TextComponent::text(min.to_string()),
))
} else if value > max {
reader.set_cursor(reader_start);
Err(too_high_error_type.create(
reader,
TextComponent::text(value.to_string()),
TextComponent::text(max.to_string()),
))
} else {
Ok(value)
}
}

View File

@@ -0,0 +1,128 @@
use crate::command::{
argument_types::argument_type::ArgumentType, errors::command_syntax_error::CommandSyntaxError,
string_reader::StringReader,
};
pub enum StringArgumentType {
/// Accepts a single unquoted word.
SingleWord,
/// Accepts a quoted or unquoted string.
QuotablePhrase,
/// Takes the remaining text from the [`StringReader`] and returns that.
GreedyPhrase,
}
impl ArgumentType for StringArgumentType {
type Item = String;
fn parse(&self, reader: &mut StringReader) -> Result<String, CommandSyntaxError> {
match self {
Self::SingleWord => reader.read_unquoted_string(),
Self::QuotablePhrase => reader.read_string(),
Self::GreedyPhrase => {
let text = reader.remaining_part().to_owned();
reader.set_cursor(reader.total_length());
Ok(text)
}
}
}
fn examples(&self) -> Vec<String> {
match self {
Self::SingleWord => examples!("word", "words_with_underscores"),
Self::QuotablePhrase => examples!("\"quoted phrase\"", "word", "\"\""),
Self::GreedyPhrase => examples!("word", "words with spaces", "\"and symbols\""),
}
}
}
#[cfg(test)]
mod test {
use crate::command::{
argument_types::{argument_type::ArgumentType, core::string::StringArgumentType},
errors::error_types,
string_reader::StringReader,
};
#[test]
fn parse_single_quoted() {
let mut reader = StringReader::new("'single-quoted string!'");
assert_parse_ok_reset!(
&mut reader,
StringArgumentType::QuotablePhrase,
"single-quoted string!".to_owned()
);
assert_parse_ok_reset!(&mut reader, StringArgumentType::SingleWord, String::new());
assert_parse_ok_reset!(
&mut reader,
StringArgumentType::GreedyPhrase,
"'single-quoted string!'".to_owned()
);
}
#[test]
fn parse_double_quoted() {
let mut reader = StringReader::new("\"double-quoted string!\"");
assert_parse_ok_reset!(
&mut reader,
StringArgumentType::QuotablePhrase,
"double-quoted string!".to_owned()
);
assert_parse_ok_reset!(&mut reader, StringArgumentType::SingleWord, String::new());
assert_parse_ok_reset!(
&mut reader,
StringArgumentType::GreedyPhrase,
"\"double-quoted string!\"".to_owned()
);
}
#[test]
fn parse_identifier() {
let mut reader = StringReader::new(".i_AM_an-1den+ifier.");
assert_parse_ok_reset!(
&mut reader,
StringArgumentType::QuotablePhrase,
".i_AM_an-1den+ifier.".to_owned()
);
assert_parse_ok_reset!(
&mut reader,
StringArgumentType::SingleWord,
".i_AM_an-1den+ifier.".to_owned()
);
assert_parse_ok_reset!(
&mut reader,
StringArgumentType::GreedyPhrase,
".i_AM_an-1den+ifier.".to_owned()
);
}
#[test]
fn quoted_incorrectly() {
let mut reader = StringReader::new("'incorrect\"");
assert_parse_err_reset!(
&mut reader,
StringArgumentType::QuotablePhrase,
&error_types::READER_EXPECTED_END_QUOTE
);
assert_parse_ok_reset!(&mut reader, StringArgumentType::SingleWord, String::new());
assert_parse_ok_reset!(
&mut reader,
StringArgumentType::GreedyPhrase,
"'incorrect\"".to_owned()
);
}
}

View File

@@ -0,0 +1,45 @@
/// Creates a [`Vec<String>`] of examples from
/// the given string literals.
macro_rules! examples {
( $( $example:literal ),* ) => {
vec! [
$( $example.to_string(), )*
]
};
}
// Helper methods for assertion with a `StringReader`:
/// Asserts that the result read by `reader` with the argument
/// type `$argument_type` used to parse is equal to `Ok($value)`.
/// Also resets the reader's cursor back to the start.
#[cfg(test)]
macro_rules! assert_parse_ok_reset {
($reader: expr, $argument_type: expr, $value: expr) => {{
assert_eq!($argument_type.parse(&mut $reader), Ok($value));
$reader.set_cursor(0)
}};
($reader: expr, $argument_type: expr) => {{
assert!($argument_type.parse(&mut $reader).is_ok());
$reader.set_cursor(0)
}};
}
/// Asserts that the result read by `reader` with the argument
/// type `$argument_type` used to parse is an `Err` containing the type of error as `$error_type`.
/// Also resets the reader's cursor back to the start.
#[cfg(test)]
macro_rules! assert_parse_err_reset {
($reader: expr, $argument_type: expr, $error_type: expr) => {
let error_type_dyn: &'static dyn crate::command::errors::error_types::AnyCommandErrorType =
$error_type;
assert_eq!(
$argument_type.parse(&mut $reader).map_err(|e| e.error_type),
Err(error_type_dyn)
);
$reader.set_cursor(0)
};
}
pub mod argument_type;
pub mod core;

View File

@@ -1,5 +1,3 @@
use pumpkin_util::{math::vector3::Vector3, text::TextComponent};
use crate::command::{
CommandError, CommandExecutor, CommandResult, CommandSender,
args::{
@@ -8,6 +6,8 @@ use crate::command::{
},
tree::{CommandTree, builder::argument},
};
use pumpkin_util::{math::vector3::Vector3, text::TextComponent};
use pumpkin_world::block::entities::BlockEntity;
const NAMES: [&str; 1] = ["particle"];
const DESCRIPTION: &str = "Spawns a Particle at position.";
@@ -40,7 +40,7 @@ impl CommandExecutor for Executor {
let speed = speed.unwrap_or(Ok(0.0))?;
let count = count.unwrap_or(Ok(0))?;
let (world, pos) = match sender {
CommandSender::Console | CommandSender::Rcon(_) => {
CommandSender::Console | CommandSender::Rcon(_) | CommandSender::Dummy => {
let guard = server.worlds.load();
let world = guard
.first()

View File

@@ -45,7 +45,7 @@ impl CommandExecutor for Executor {
let pos = BlockPosArgumentConsumer::find_arg(args, ARG_BLOCK_POS)?;
let mode = self.0;
let world = match sender {
CommandSender::Console | CommandSender::Rcon(_) => {
CommandSender::Console | CommandSender::Rcon(_) | CommandSender::Dummy => {
let guard = server.worlds.load();
guard

View File

@@ -1,7 +1,3 @@
use pumpkin_data::translation;
use pumpkin_util::{math::vector3::Vector3, text::TextComponent};
use uuid::Uuid;
use crate::{
command::{
CommandError, CommandExecutor, CommandResult, CommandSender,
@@ -13,6 +9,12 @@ use crate::{
},
entity::r#type::from_type,
};
use pumpkin_data::translation;
use pumpkin_util::{math::vector3::Vector3, text::TextComponent};
use uuid::Uuid;
use pumpkin_world::block::entities::BlockEntity;
const NAMES: [&str; 1] = ["summon"];
const DESCRIPTION: &str = "Spawns a Entity at position.";
@@ -34,7 +36,7 @@ impl CommandExecutor for Executor {
let entity_type = SummonableEntitiesArgumentConsumer::find_arg(args, ARG_ENTITY)?;
let pos = Position3DArgumentConsumer::find_arg(args, ARG_POS);
let (world, pos) = match sender {
CommandSender::Console | CommandSender::Rcon(_) => {
CommandSender::Console | CommandSender::Rcon(_) | CommandSender::Dummy => {
let guard = server.worlds.load();
let world = guard
.first()

View File

@@ -111,7 +111,7 @@ impl CommandExecutor for EntitiesToPosFacingPosExecutor {
let (yaw, pitch) = yaw_pitch_facing_position(&pos, &facing_pos);
//todo
let world = match sender {
CommandSender::Rcon(_) | CommandSender::Console => {
CommandSender::Rcon(_) | CommandSender::Console | CommandSender::Dummy => {
server.worlds.load().first().unwrap().clone()
}
CommandSender::Player(player) => player.world().clone(),
@@ -228,7 +228,7 @@ impl CommandExecutor for EntitiesToPosExecutor {
}
// todo command context
let world = match sender {
CommandSender::Rcon(_) | CommandSender::Console => {
CommandSender::Rcon(_) | CommandSender::Console | CommandSender::Dummy => {
server.worlds.load().first().unwrap().clone()
}
CommandSender::Player(player) => player.world().clone(),

View File

@@ -0,0 +1,698 @@
use crate::command::context::command_source::{CommandSource, ReturnValue};
use crate::command::context::string_range::StringRange;
use crate::command::errors::command_syntax_error::CommandSyntaxError;
use crate::command::errors::error_types::DISPATCHER_PARSE_EXCEPTION;
use crate::command::node::attached::NodeId;
use crate::command::node::dispatcher::{CommandDispatcher, ResultConsumer};
use crate::command::node::tree::Tree;
use crate::command::node::{Command, RedirectModifier};
use pumpkin_util::text::TextComponent;
use rustc_hash::FxHashMap;
use std::any::Any;
use std::sync::Arc;
/// Represents the current stage of the chain.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Stage {
MODIFY,
EXECUTE,
}
/// Represents a parsed node.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct ParsedNode {
pub node: NodeId,
pub range: StringRange,
}
/// Represents a suggestional context involving a node.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct SuggestionContext {
pub parent: NodeId,
pub starting_position: usize,
}
/// Represents a parsed argument of any type.
pub struct ParsedArgument {
/// The range of this parsed argument.
pub range: StringRange,
/// The result of this parsed argument.
pub result: Box<dyn Any + Send + Sync>,
}
impl ParsedArgument {
/// Creates a new [`ParsedArgument`] from its range and resultant value.
#[must_use]
pub fn new(range: StringRange, result: Box<dyn Any + Send + Sync>) -> Self {
Self { range, result }
}
}
/// Represents the context used when commands are run.
#[derive(Clone)]
pub struct CommandContext<'a> {
/// The source running the commands.
pub source: Arc<CommandSource>,
/// The input string ran as the command.
pub input: String,
/// Arguments which have been parsed and
/// can be fetched for command execution.
pub arguments: FxHashMap<String, Arc<ParsedArgument>>,
/// The tree this context is related to.
pub tree: &'a Tree,
/// The root that this context will use, bound to the tree
/// Not necessarily the root node of the tree, however.
pub root: NodeId,
/// All the parsed nodes of the command.
pub nodes: Vec<ParsedNode>,
/// The string range of input.
pub range: StringRange,
/// The child context of this context.
pub child: Option<Arc<Self>>,
/// The redirect modifier of this context.
pub modifier: RedirectModifier,
/// Whether this context forks or not.
pub forks: bool,
/// The command stored in this context which
/// is run to get a command result.
pub command: Option<Command>,
}
impl CommandContext<'_> {
/// Copies this context with the source provided.
#[must_use]
pub fn with_source(&self, source: Arc<CommandSource>) -> Self {
Self {
source,
input: self.input.clone(),
arguments: self.arguments.clone(),
nodes: self.nodes.clone(),
range: self.range,
child: self.child.clone(),
modifier: self.modifier.clone(),
forks: self.forks,
command: self.command.clone(),
tree: self.tree,
root: self.root,
}
}
/// Returns the child immediately below this node.
#[must_use]
pub const fn get_child(&self) -> Option<&Arc<Self>> {
self.child.as_ref()
}
/// Returns the child which does not have a child which originated from this node.
/// This may return itself.
#[must_use]
pub fn get_last_child(&self) -> &Self {
let mut current_child = self;
while let Some(child) = &current_child.child {
current_child = child;
}
current_child
}
/// Returns a reference to a particular argument with type `T`.
/// If it fails, an error with the appropriate message is returned.
///
/// Ideally should be used with the `?` operator.
///
/// # Example
/// A simple example that takes two arguments specified from the node
/// and returns their sum as the status output of the `Executor`:
/// ```
/// use pumpkin::command::context::command_context::CommandContext;
/// use pumpkin::command::node::{CommandExecutor, CommandExecutorResult};
///
/// struct Executor;
/// impl CommandExecutor for Executor {
/// fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
/// Box::pin(async move {
/// // The `get_argument` method returns a `Result<&i32, CommandSyntaxError>`.
/// // We apply the `?` operator first, propagating the `CommandSyntaxError` if contained.
/// // Finally, we dereference the `&i32`, as `i32` implements Copy.
/// let operand1: i32 = *context.get_argument("operand1")?;
/// let operand2: i32 = *context.get_argument("operand2")?;
/// Ok(operand1 + operand2)
/// })
/// }
/// }
/// ```
pub fn get_argument<T: 'static>(&self, name: &str) -> Result<&T, CommandSyntaxError> {
// The errors below should never happen due to user input.
// If an error below this comment is returned, that means the command was
// improperly defined.
//
// Still, we provide helpful errors instead of panicking.
let arg = self.arguments.get(name).ok_or_else(|| {
DISPATCHER_PARSE_EXCEPTION.create_without_context(TextComponent::text(format!(
"Could not find argument with name '{name}'"
)))
})?;
let dyn_ref = &*arg.result;
dyn_ref.downcast_ref::<T>().ok_or_else(|| {
DISPATCHER_PARSE_EXCEPTION.create_without_context(TextComponent::text(format!(
"Could not downcast argument '{name}'"
)))
})
}
}
/// Represents a linked chain of [`CommandContext`]s, where the previous links to the next as a child.
#[derive(Clone)]
pub struct ContextChain<'a> {
/// The modifiers of this context chain.
modifiers: Vec<Arc<CommandContext<'a>>>,
/// That specific [`CommandContext`] to execute.
execute: Arc<CommandContext<'a>>,
}
impl<'a> ContextChain<'a> {
/// Creates a new chain of contexts from a vector of them and one to execute.
///
/// # Panics
///
/// Panics if the `execute` given is non-executable.
#[must_use]
pub fn new(modifiers: Vec<Arc<CommandContext<'a>>>, execute: Arc<CommandContext<'a>>) -> Self {
assert!(
execute.command.is_some(),
"Expected last command in chain to be executable"
);
Self { modifiers, execute }
}
/// Tries to flatten a [`CommandContext`]. If no command
/// is available at the end of chain, [`None`] is returned.
#[must_use]
pub fn try_flatten(root: &CommandContext<'a>) -> Option<Self> {
let mut modifiers = Vec::new();
let mut current = root;
loop {
if let Some(child) = current.get_child() {
modifiers.push(child.clone());
current = child;
} else {
return current
.command
.is_some()
.then(|| Self::new(modifiers, Arc::new(current.clone())));
}
}
}
/// Runs the given modifier with provided details.
pub async fn run_modifier(
modifier: &CommandContext<'a>,
source: &Arc<CommandSource>,
result_consumer: &dyn ResultConsumer,
forked_mode: bool,
) -> Result<Vec<Arc<CommandSource>>, CommandSyntaxError> {
let source_modifier = &modifier.modifier;
if matches!(source_modifier, RedirectModifier::OneSource) {
return Ok(vec![source.clone()]);
}
let context_to_use = modifier.with_source(source.clone());
let mut result = source_modifier.sources(&context_to_use).await;
if result.is_err() {
result_consumer
.on_command_completion(&context_to_use, ReturnValue::Failure)
.await;
if forked_mode {
result = Ok(vec![]);
}
}
result
}
/// Runs the given executable, returning an [`i32`] on success.
///
/// # Panics
///
/// Panics if the `executable` provided cannot be executed.
pub async fn run_executable(
executable: &CommandContext<'a>,
source: &Arc<CommandSource>,
result_consumer: &dyn ResultConsumer,
forked_mode: bool,
) -> Result<i32, CommandSyntaxError> {
let context_to_use = executable.with_source(source.clone());
let mut result = match &executable.command {
None => panic!("Expected `executable` to be executable"),
Some(command) => command.execute(&context_to_use).await,
};
if let Ok(result) = result {
result_consumer
.on_command_completion(&context_to_use, ReturnValue::Success(result))
.await;
Ok(if forked_mode { 1 } else { result })
} else {
result_consumer
.on_command_completion(&context_to_use, ReturnValue::Failure)
.await;
if forked_mode {
result = Ok(0);
}
result
}
}
/// Executes all contexts in the chain, returning the ultimate result.
pub async fn execute_all(
&self,
source: &Arc<CommandSource>,
result_consumer: &dyn ResultConsumer,
) -> Result<i32, CommandSyntaxError> {
if self.modifiers.is_empty() {
return Self::run_executable(&self.execute, source, result_consumer, false).await;
}
let mut forked_mode = false;
let mut current_sources: Vec<Arc<CommandSource>> = vec![source.clone()];
for modifier in &self.modifiers {
forked_mode |= modifier.forks;
let mut next_sources = Vec::new();
for source in current_sources {
let mut to_add =
Self::run_modifier(modifier, &source, result_consumer, forked_mode).await?;
next_sources.append(&mut to_add);
}
if next_sources.is_empty() {
return Ok(0);
}
current_sources = next_sources;
}
let mut result = 0;
for execution_source in current_sources {
result += Self::run_executable(
&self.execute,
&execution_source,
result_consumer,
forked_mode,
)
.await?;
}
Ok(result)
}
/// Gets the current stage of this context.
#[must_use]
pub const fn get_stage(&self) -> Stage {
if self.modifiers.is_empty() {
Stage::EXECUTE
} else {
Stage::MODIFY
}
}
/// Gets a reference to the top context of this chain.
#[must_use]
pub fn get_top_context(&'_ self) -> &'_ Arc<CommandContext<'_>> {
if self.modifiers.is_empty() {
&self.execute
} else {
&self.modifiers[0]
}
}
/// Gets a mutable reference to the top context of this chain.
pub fn get_top_context_mut(&'_ mut self) -> &mut Arc<CommandContext<'a>> {
if self.modifiers.is_empty() {
&mut self.execute
} else {
&mut self.modifiers[0]
}
}
/// Gets the next stage of this chain.
#[must_use]
pub fn next_stage(&self) -> Option<Self> {
if self.modifiers.is_empty() {
None
} else {
Some(Self::new(
self.modifiers[1..].to_vec(),
self.execute.clone(),
))
}
}
}
/// A builder that helps to create a [`CommandContext`].
///
/// This builder's lifetime is bound to the dispatcher provided to it.
#[derive(Clone)]
pub struct CommandContextBuilder<'a> {
/// The dispatcher this builder is related to.
pub dispatcher: &'a CommandDispatcher,
/// The source running the commands.
pub source: Arc<CommandSource>,
/// Arguments which have been parsed and
/// can be fetched for command execution.
pub arguments: FxHashMap<String, Arc<ParsedArgument>>,
/// The root that this context will use, bound to the tree
/// Not necessarily the root node of the tree, however.
pub root: NodeId,
/// All the parsed nodes of the command.
pub nodes: Vec<ParsedNode>,
/// The string range of input.
pub range: StringRange,
/// The child context of this context.
pub child: Option<Box<Self>>,
/// The redirect modifier of this context.
pub modifier: RedirectModifier,
/// Whether this context forks or not.
pub forks: bool,
/// The command stored in this context which
/// is run to get a command result.
pub command: Option<Command>,
}
impl<'a> CommandContextBuilder<'a> {
/// Creates a new [`CommandContextBuilder`] from the properties required to initialize one.
///
/// Note that builder's lifetime is bound to the dispatcher provided to it.
#[must_use]
pub fn new(
dispatcher: &'a CommandDispatcher,
source: Arc<CommandSource>,
root: NodeId,
start: usize,
) -> Self {
CommandContextBuilder {
dispatcher,
source,
arguments: FxHashMap::default(),
root,
nodes: Vec::new(),
range: StringRange::at(start),
child: None,
modifier: RedirectModifier::OneSource,
forks: false,
command: None,
}
}
/// Builds the required [`CommandContext`], consuming itself in the process.
#[must_use]
pub fn build(self, input: &str) -> CommandContext<'a> {
CommandContext {
source: self.source,
input: input.to_string(),
arguments: self.arguments,
tree: &self.dispatcher.tree,
root: self.root,
nodes: self.nodes,
range: self.range,
child: self.child.map(|child| Arc::new(child.build(input))),
modifier: self.modifier,
forks: self.forks,
command: self.command,
}
}
/// Mutates itself with the new source set.
pub fn with_source(&mut self, source: Arc<CommandSource>) {
self.source = source;
}
/// Mutates itself with a new argument added.
pub fn with_argument(&mut self, name: String, argument: Arc<ParsedArgument>) {
self.arguments.insert(name, argument);
}
/// Mutates itself with the new command set.
pub fn with_command(&mut self, command: Option<Command>) {
self.command = command;
}
/// Mutates itself with a new node added to this builder.
pub fn with_node(&mut self, node: NodeId, range: StringRange) {
self.nodes.push(ParsedNode { node, range });
self.range = StringRange::encompass(self.range, range);
self.modifier = self.dispatcher.tree[node].modifier().clone();
self.forks = self.dispatcher.tree[node].forks();
}
/// Mutates itself with the new child set.
pub fn with_child(&mut self, child: Self) {
self.child = Some(Box::new(child));
}
/// Mutates the last child of this builder.
#[must_use]
pub fn last_child(&self) -> &Self {
let mut result = self;
while let Some(child) = &result.child {
result = child;
}
result
}
/// Creates a [`SuggestionContext`] from the provided cursor position.
///
/// # Panics
///
/// Panics if the node couldn't be found before the cursor.
#[must_use]
pub fn find_suggestion_context(&self, cursor: usize) -> SuggestionContext {
assert!(
self.range.start <= cursor,
"Could not find node before cursor"
);
if self.range.end < cursor {
self.child.as_ref().map_or_else(
|| {
self.nodes.last().as_ref().map_or_else(
|| SuggestionContext {
parent: self.root,
starting_position: self.range.start,
},
|last_node| SuggestionContext {
parent: last_node.node,
starting_position: last_node.range.end + 1,
},
)
},
|child| child.find_suggestion_context(cursor),
)
} else {
let mut previous = self.root;
for node in &self.nodes {
if (self.range.start..=self.range.end).contains(&cursor) {
return SuggestionContext {
parent: previous,
starting_position: self.range.start,
};
}
previous = node.node;
}
SuggestionContext {
parent: previous,
starting_position: self.range.start,
}
}
}
}
#[cfg(test)]
mod test {
use std::sync::Arc;
use crate::command::argument_builder::{ArgumentBuilder, CommandArgumentBuilder};
use crate::command::context::command_context::{
CommandContext, CommandContextBuilder, ContextChain, ParsedArgument, Stage,
};
use crate::command::context::command_source::CommandSource;
use crate::command::context::string_range::StringRange;
use crate::command::errors::command_syntax_error::CommandSyntaxError;
use crate::command::node::dispatcher::{CommandDispatcher, EmptyResultConsumer};
use crate::command::node::tree::ROOT_NODE_ID;
use crate::command::node::{CommandExecutor, CommandExecutorResult, Redirection};
struct TenExecutor;
impl CommandExecutor for TenExecutor {
fn execute<'a>(&'a self, _context: &'a CommandContext) -> CommandExecutorResult<'a> {
Box::pin(async move { Ok(10) })
}
}
// For testing purposes
fn builder(dispatcher: &'_ CommandDispatcher) -> CommandContextBuilder<'_> {
let mut builder = CommandContextBuilder::new(
dispatcher,
Arc::new(CommandSource::dummy()),
ROOT_NODE_ID,
0,
);
let parsed_argument = ParsedArgument::new(StringRange::between(0, 1), Box::new(6789i32));
builder.with_argument("foo".to_string(), Arc::new(parsed_argument));
builder
}
#[test]
fn get_argument() -> Result<(), CommandSyntaxError> {
let dispatcher = CommandDispatcher::new();
let builder = builder(&dispatcher);
let context = builder.build("6789");
assert_eq!(context.get_argument::<i32>("foo")?, &6789);
Ok(())
}
#[test]
fn get_nonexistent_argument() {
let dispatcher = CommandDispatcher::new();
let builder = builder(&dispatcher);
let context = builder.build("6789");
assert!(context.get_argument::<i32>("bar").is_err());
}
#[test]
fn get_different_type_argument() {
let dispatcher = CommandDispatcher::new();
let builder = builder(&dispatcher);
let context = builder.build("6789");
assert!(context.get_argument::<f32>("foo").is_err());
}
#[tokio::test]
async fn execute_single_command_chain() {
let mut dispatcher = CommandDispatcher::new();
dispatcher
.register(CommandArgumentBuilder::new("foo", "A test command").executes(TenExecutor));
let source = Arc::new(CommandSource::dummy());
let result = dispatcher.parse_input("foo", &source).await;
let top_context = result.context.build("foo");
let chain = ContextChain::try_flatten(&top_context)
.expect("The context should have properly flattened, as it has a command to execute");
assert_eq!(
chain.execute_all(&source, &EmptyResultConsumer).await,
Ok(10)
);
}
#[tokio::test]
async fn execute_redirected_command_chain() {
let mut dispatcher = CommandDispatcher::new();
dispatcher
.register(CommandArgumentBuilder::new("foo", "A test command").executes(TenExecutor));
dispatcher.register(
CommandArgumentBuilder::new("bar", "Another test command").redirect(Redirection::Root),
);
let source = Arc::new(CommandSource::dummy());
let result = dispatcher.parse_input("bar foo", &source).await;
let top_context = result.context.build("bar foo");
let chain = ContextChain::try_flatten(&top_context)
.expect("The context should have properly flattened, as it has a command to execute");
assert_eq!(
chain.execute_all(&source, &EmptyResultConsumer).await,
Ok(10)
);
}
#[tokio::test]
async fn single_stage_execution() {
let mut dispatcher = CommandDispatcher::new();
dispatcher
.register(CommandArgumentBuilder::new("foo", "A test command").executes(TenExecutor));
let source = Arc::new(CommandSource::dummy());
let result = dispatcher.parse_input("foo", &source).await;
let top_context = result.context.build("foo");
let chain = ContextChain::try_flatten(&top_context)
.expect("The context should have properly flattened, as it has a command to execute");
assert_eq!(chain.get_stage(), Stage::EXECUTE);
assert!(chain.next_stage().is_none());
}
#[tokio::test]
async fn multi_stage_execution() {
let mut dispatcher = CommandDispatcher::new();
dispatcher
.register(CommandArgumentBuilder::new("foo", "A test command").executes(TenExecutor));
dispatcher.register(
CommandArgumentBuilder::new("bar", "Another test command").redirect(Redirection::Root),
);
dispatcher.register(
CommandArgumentBuilder::new("qux", "Yet another test command")
.redirect(Redirection::Root),
);
let source = Arc::new(CommandSource::dummy());
let result = dispatcher.parse_input("bar qux foo", &source).await;
let top_context = result.context.build("bar qux foo");
let chain = ContextChain::try_flatten(&top_context)
.expect("The context should have properly flattened, as it has a command to execute");
assert_eq!(chain.get_stage(), Stage::MODIFY);
let chain2 = chain
.next_stage()
.expect("There should have been the next stage");
assert_eq!(chain2.get_stage(), Stage::MODIFY);
let chain3 = chain2
.next_stage()
.expect("There should have been the next stage");
assert_eq!(chain3.get_stage(), Stage::EXECUTE);
assert!(chain3.next_stage().is_none());
}
#[tokio::test]
async fn missing_command() {
let mut dispatcher = CommandDispatcher::new();
dispatcher.register(CommandArgumentBuilder::new("foo", "A test command"));
let source = Arc::new(CommandSource::dummy());
let result = dispatcher.parse_input("foo", &source).await;
let top_context = result.context.build("foo");
assert!(ContextChain::try_flatten(&top_context).is_none());
}
}

View File

@@ -0,0 +1,568 @@
use crate::command::CommandSender;
use crate::command::errors::command_syntax_error::CommandSyntaxError;
use crate::command::errors::error_types::CommandErrorType;
use crate::entity::EntityBase;
use crate::entity::player::Player;
use crate::server::Server;
use crate::world::World;
use pumpkin_data::translation;
use pumpkin_util::math::vector2::Vector2;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::math::wrap_degrees;
use pumpkin_util::text::TextComponent;
use pumpkin_util::text::color::{Color, NamedColor};
use std::pin::Pin;
use std::sync::Arc;
pub const REQUIRES_PLAYER: CommandErrorType<0> =
CommandErrorType::new(translation::PERMISSIONS_REQUIRES_PLAYER);
pub const REQUIRES_ENTITY: CommandErrorType<0> =
CommandErrorType::new(translation::PERMISSIONS_REQUIRES_ENTITY);
pub trait ReturnValueCallable: Send + Sync {
fn call(&self, value: ReturnValue) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
}
pub type ReturnValueCallback = Arc<dyn ReturnValueCallable>;
/// Represents a collection of 'return value callbacks'.
#[derive(Clone)]
pub struct ResultValueTaker(pub Vec<ReturnValueCallback>);
impl ResultValueTaker {
/// Merges two takers, returning one.
#[must_use]
pub fn merge(taker_1: &Self, taker_2: &Self) -> Self {
let mut takers = Vec::with_capacity(taker_1.0.len() + taker_2.0.len());
for taker in &taker_1.0 {
takers.push(taker.clone());
}
for taker in &taker_2.0 {
takers.push(taker.clone());
}
Self(takers)
}
/// Constructs a new, empty result value taker.
#[must_use]
pub fn new() -> Self {
Self(Vec::new())
}
/// Calls all the contained callbacks of this taker with the returned result.
#[must_use]
pub fn call(&self, return_value: ReturnValue) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
for callback in &self.0 {
callback.call(return_value).await;
}
})
}
}
impl Default for ResultValueTaker {
fn default() -> Self {
Self::new()
}
}
/// Represents a source of a command, which
/// contains its own state, which could keep track of its:
/// - position
/// - rotation
/// - world
/// - permissions
/// - name
/// - display name
/// - the internal server
/// - whether it is silent or not
/// - entity which it could represent
///
/// Not to be confused with [`CommandSender`], [`CommandSource`]
/// can be modified by commands to change how another
/// command works later in the command chain.
///
/// A source having a player and a particular position does
/// not necessarily mean that the player does have that position;
/// but rather is a state for more complex functionality.
#[derive(Clone)]
pub struct CommandSource {
pub output: CommandSender,
pub world: Option<Arc<World>>,
pub entity: Option<Arc<dyn EntityBase>>,
pub position: Vector3<f64>,
pub rotation: Vector2<f32>,
pub name: String,
pub display_name: TextComponent,
pub server: Option<Arc<Server>>,
pub silent: bool,
pub command_result_taker: ResultValueTaker,
pub entity_anchor: EntityAnchor,
}
impl CommandSource {
/// Creates a dummy [`CommandSource`], great for unit testing.
///
/// # Note
/// **This should only be used for unit tests!!!**
///
/// The returned [`CommandSource`] does not contain
/// a server or a world. If there is attempt to fetch the server or a world from
/// the returned source, there will be a panic!
#[must_use]
pub fn dummy() -> Self {
Self {
output: CommandSender::Dummy,
world: None,
entity: None,
position: Vector3::default(),
rotation: Vector2::default(),
name: String::new(),
display_name: TextComponent::text(""),
server: None,
silent: false,
command_result_taker: ResultValueTaker::new(),
entity_anchor: EntityAnchor::Feet,
}
}
/// Creates a usable [`CommandSource`] for running commands in an actual environment.
#[expect(clippy::too_many_arguments)]
pub fn new(
output: CommandSender,
world: Arc<World>,
entity: Option<Arc<dyn EntityBase>>,
position: Vector3<f64>,
rotation: Vector2<f32>,
name: String,
display_name: TextComponent,
server: Arc<Server>,
silent: bool,
command_result_taker: ResultValueTaker,
entity_anchor: EntityAnchor,
) -> Self {
Self {
output,
world: Some(world),
entity,
position,
rotation,
name,
display_name,
server: Some(server),
silent,
command_result_taker,
entity_anchor,
}
}
/// Returns a new [`CommandSource`] with the specified output and
/// everything else from the `source` provided.
#[must_use]
pub fn with_output(self, output: CommandSender) -> Self {
Self {
output,
world: self.world,
entity: self.entity,
position: self.position,
rotation: self.rotation,
name: self.name,
display_name: self.display_name,
server: self.server,
silent: self.silent,
command_result_taker: self.command_result_taker,
entity_anchor: self.entity_anchor,
}
}
/// Returns a new [`CommandSource`] with the specified world and
/// everything else from the `source` provided.
#[must_use]
pub fn with_world(self, world: Arc<World>) -> Self {
Self {
output: self.output,
world: Some(world),
entity: self.entity,
position: self.position,
rotation: self.rotation,
name: self.name,
display_name: self.display_name,
server: self.server,
silent: true,
command_result_taker: self.command_result_taker,
entity_anchor: self.entity_anchor,
}
}
/// Returns a new [`CommandSource`] with the specified entity and
/// everything else from the `source` provided.
#[must_use]
pub async fn with_entity(self, entity: Arc<dyn EntityBase>) -> Self {
let name = entity.get_name().get_text();
let display_name = entity.get_display_name().await;
Self {
output: self.output,
world: self.world,
entity: Some(entity),
position: self.position,
rotation: self.rotation,
name,
display_name,
server: self.server,
silent: self.silent,
command_result_taker: self.command_result_taker,
entity_anchor: self.entity_anchor,
}
}
/// Returns a new [`CommandSource`] with the specified position and
/// everything else from the `source` provided.
#[must_use]
pub fn with_position(self, position: Vector3<f64>) -> Self {
Self {
output: self.output,
world: self.world,
entity: self.entity,
position,
rotation: self.rotation,
name: self.name,
display_name: self.display_name,
server: self.server,
silent: self.silent,
command_result_taker: self.command_result_taker,
entity_anchor: self.entity_anchor,
}
}
/// Returns a new [`CommandSource`] with the specified rotation and
/// everything else from the `source` provided.
#[must_use]
pub fn with_rotation(self, rotation: Vector2<f32>) -> Self {
Self {
output: self.output,
world: self.world,
entity: self.entity,
position: self.position,
rotation,
name: self.name,
display_name: self.display_name,
server: self.server,
silent: self.silent,
command_result_taker: self.command_result_taker,
entity_anchor: self.entity_anchor,
}
}
/// Merges the given takers with this one, returning a new [`CommandSource`] with
/// the merged taker.
#[must_use]
pub fn merge_command_result_taker(self, command_result_taker: &ResultValueTaker) -> Self {
let merged = ResultValueTaker::merge(&self.command_result_taker, command_result_taker);
self.with_command_result_taker(merged)
}
/// Returns a new [`CommandSource`] with the specified silent state and
/// everything else from the `source` provided.
#[must_use]
pub fn with_silent(self) -> Self {
Self {
output: self.output,
world: self.world,
entity: self.entity,
position: self.position,
rotation: self.rotation,
name: self.name,
display_name: self.display_name,
server: self.server,
silent: true,
command_result_taker: self.command_result_taker,
entity_anchor: self.entity_anchor,
}
}
/// Returns a new [`CommandSource`] with the specified command result taker and
/// everything else from the `source` provided.
#[must_use]
pub fn with_command_result_taker(self, command_result_taker: ResultValueTaker) -> Self {
Self {
output: self.output,
world: self.world,
entity: self.entity,
position: self.position,
rotation: self.rotation,
name: self.name,
display_name: self.display_name,
server: self.server,
silent: self.silent,
command_result_taker,
entity_anchor: self.entity_anchor,
}
}
/// Returns a new [`CommandSource`] with the specified entity anchor and
/// everything else from the `source` provided.
#[must_use]
pub fn with_entity_anchor(self, entity_anchor: EntityAnchor) -> Self {
Self {
output: self.output,
world: self.world,
entity: self.entity,
position: self.position,
rotation: self.rotation,
name: self.name,
display_name: self.display_name,
server: self.server,
silent: true,
command_result_taker: self.command_result_taker,
entity_anchor,
}
}
/// Returns a new [`CommandSource`] with the rotation changed in such
/// a way that the source faces the anchor of the entity and
/// everything else from the `source` provided.
#[must_use]
pub fn with_looking_at_entity(
self,
entity: &Arc<dyn EntityBase>,
anchor: EntityAnchor,
) -> Self {
self.with_looking_at_pos(anchor.position_at_entity(entity))
}
/// Returns a new [`CommandSource`] with the rotation changed in such
/// a way that the source faces the provided position and
/// everything else from the `source` provided.
#[must_use]
pub fn with_looking_at_pos(self, pos: Vector3<f64>) -> Self {
let source_pos = self.entity_anchor.position_at_source(&self);
let delta = pos.sub(&source_pos);
let horizontal_len = delta.horizontal_length();
let pitch = -delta.y.atan2(horizontal_len).to_degrees();
let yaw = delta.z.atan2(delta.x).to_degrees() - 90.0;
self.with_rotation(Vector2::new(
wrap_degrees(pitch as f32),
wrap_degrees(yaw as f32),
))
}
/// Gets the entity as a result:
///
/// - If this source actually contains an entity, it returns that wrapped in an [`Ok`].
/// - If it doesn't, a command error is provided instead, wrapped in an [`Err`].
pub fn entity_or_err(&self) -> Result<Arc<dyn EntityBase>, CommandSyntaxError> {
self.entity
.clone()
.ok_or(REQUIRES_ENTITY.create_without_context())
}
/// Gets the world as a result:
///
/// - If this source actually contains a server, it returns that.
/// - If it doesn't, this function **panics**. Ideally, a source should contain a world, but it may not in a unit test.
#[must_use]
pub fn world(&self) -> Arc<World> {
self.world.clone().expect("Expected world to exist")
}
/// Gets the server as a result:
///
/// - If this source actually contains a server, it returns that.
/// - If it doesn't, this function **panics**. Ideally, a source should contain the server, but it may not in a unit test.
#[must_use]
pub fn server(&self) -> Arc<Server> {
self.server.clone().expect("Expected server to exist")
}
/// Gets the player as an option:
///
/// - If this source actually contains a player, it returns that wrapped in a [`Some`].
/// - If it doesn't, a [`None`] is returned instead.
#[must_use]
pub fn player_or_none(&self) -> Option<&Player> {
self.entity.as_ref().and_then(|entity| entity.get_player())
}
/// Gets the player as a result:
///
/// - If this source actually contains a player, it returns that wrapped in an [`Ok`].
/// - If it doesn't, a command error is provided instead, wrapped in an [`Err`].
pub fn player_or_err(&self) -> Result<&Player, CommandSyntaxError> {
self.player_or_none()
.ok_or(REQUIRES_PLAYER.create_without_context())
}
/// Returns if the command was executed by a player.
#[must_use]
pub fn executed_by_player(&self) -> bool {
self.player_or_none().is_some()
}
/// Sends a message to this source.
pub async fn send_message(&self, message: TextComponent) {
if !self.silent {
self.output.send_message(message).await;
}
}
/// Sends a message to all online operators.
async fn send_to_ops(&self, message: TextComponent) {
let text =
TextComponent::translate("chat.type.admin", &[self.display_name.clone(), message])
.color(Color::Named(NamedColor::Gray))
.italic();
let Some(server) = &self.server else {
return;
};
if server.level_info.load().game_rules.send_command_feedback {
let output_player = match &self.output {
CommandSender::Player(sender) => Some(sender),
_ => None,
};
for player in server.get_all_players() {
if output_player != Some(&player)
&& player.permission_lvl.load() >= server.basic_config.op_permission_level
{
player.send_system_message(&text).await;
}
}
}
}
/// Sends feedback to this source.
pub async fn send_feedback(&self, message: TextComponent, broadcast_to_ops: bool) {
if !self.silent {
let should_send_to_output = self.output.should_receive_feedback();
let should_send_to_ops =
broadcast_to_ops && self.output.should_broadcast_console_to_ops();
if should_send_to_output {
self.output.send_message(message.clone()).await;
}
if should_send_to_ops {
self.send_to_ops(message).await;
}
}
}
/// Sends an error message to the console.
///
/// # Note
/// Do not use this function if you want to report a [`CommandSyntaxError`].
/// Instead, wrap the error in an [`Err`] and return that (or use the `?` operator)
///
/// However, there are still use cases of this function to send an error
/// without reporting command failure directly.
pub async fn send_error(&self, error: TextComponent) {
if !self.silent && self.output.should_track_output() {
// TODO: Use `TextComponent::empty` instead of `TextComponent::text` when implemented
self.output
.send_message(
TextComponent::text("")
.add_child(error)
.color(Color::Named(NamedColor::Red)),
)
.await;
}
}
/// Returns whether this source has the permission provided.
///
/// # Panics
///
/// Panics if this source does not have a reference to the
/// server (i.e. this is a dummy [`CommandSource`].)
#[must_use]
pub async fn has_permission(&self, permission: &str) -> bool {
self.output.has_permission(&self.server(), permission).await
}
/// Returns whether this source has the permission provided.
///
/// # Panics
///
/// Panics **if, and only if** both the following conditions are met:
///
/// - permission is not [`None`].
/// - this source does not have a reference to the server (i.e. this is a dummy [`CommandSource`].)
#[must_use]
pub async fn has_permission_from_option(&self, permission: Option<&str>) -> bool {
match permission {
None => true,
Some(permission) => self.has_permission(permission).await,
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum EntityAnchor {
Feet,
Eyes,
}
// TODO: Move this to the /execute command when implemented.
impl EntityAnchor {
/// Gets the [`EntityAnchor`] whose identity is the ID provided.
#[must_use]
pub fn from_id(id: &str) -> Option<Self> {
match id {
"feet" => Some(Self::Feet),
"eyes" => Some(Self::Eyes),
_ => None,
}
}
/// Gets the ID of this [`EntityAnchor`]
#[must_use]
pub const fn id(self) -> &'static str {
match self {
Self::Feet => "feet",
Self::Eyes => "eyes",
}
}
/// Gets the position of an entity with respect to this anchor.
pub fn position_at_entity(self, entity: &Arc<dyn EntityBase>) -> Vector3<f64> {
let entity = entity.get_entity();
let mut pos = entity.pos.load();
pos.y = entity.get_entity().get_eye_y();
pos
}
/// Gets the position of a source with respect to this anchor.
#[must_use]
pub fn position_at_source(self, command_source: &CommandSource) -> Vector3<f64> {
command_source
.entity
.as_ref()
.map_or(command_source.position, |entity| {
self.position_at_entity(entity)
})
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum ReturnValue {
Success(i32),
Failure,
}
impl ReturnValue {
/// Get the success value of this return value.
#[must_use]
pub const fn success_value(self) -> bool {
match self {
Self::Success(_) => true,
Self::Failure => false,
}
}
/// Get the result integral value of this return value.
#[must_use]
pub const fn result_value(self) -> i32 {
match self {
Self::Success(value) => value,
Self::Failure => 0,
}
}
}

View File

@@ -0,0 +1,3 @@
pub mod command_context;
pub mod command_source;
pub mod string_range;

View File

@@ -0,0 +1,76 @@
use std::ops::Range;
use crate::command::string_reader::StringReader;
/// Indicates a range that is effectively
/// a substring of a string from its `start`
/// and `end` byte-indices.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct StringRange {
pub start: usize,
pub end: usize,
}
impl StringRange {
/// Constructs a new substring range, with indices
/// inclusive to `start`, but exclusive to `end`.
#[must_use]
pub const fn between(start: usize, end: usize) -> Self {
Self { start, end }
}
/// Constructs an empty range on the left side of
/// one character with index `pos`.
#[must_use]
pub const fn at(pos: usize) -> Self {
Self::between(pos, pos)
}
/// Constructs a new substring range encompassing two
/// [`StringRange`]s, returning a new one that covers
/// both the required ranges.
#[must_use]
pub fn encompass(a: Self, b: Self) -> Self {
Self::between(a.start.min(b.start), a.end.max(b.end))
}
/// Gets a [`str`] substring slice bound
/// to a [`StringReader`] from this range.
#[must_use]
pub fn slice_from_reader<'a>(&self, reader: &'a StringReader) -> &'a str {
&reader.string()[self.start..self.end]
}
/// Gets a [`str`] substring slice bound
/// to a [`String`] from this range.
#[must_use]
pub fn substring_slice<'a>(&self, string: &'a str) -> &'a str {
&string[self.start..self.end]
}
/// Returns if the length of this range is zero.
#[must_use]
#[inline]
pub const fn is_empty(&self) -> bool {
self.start == self.end
}
/// Returns the length of this range.
#[must_use]
#[inline]
pub const fn len(&self) -> usize {
self.end - self.start
}
}
impl From<Range<usize>> for StringRange {
fn from(value: Range<usize>) -> Self {
Self::between(value.start, value.end)
}
}
impl From<StringRange> for Range<usize> {
fn from(value: StringRange) -> Self {
value.start..value.end
}
}

View File

@@ -1,49 +1,62 @@
// These are akin to the translatable built-in exceptions in Minecraft.
use pumpkin_data::translation;
// These are akin to the translatable built-in exceptions in Minecraft.
pub const READER_EXPECTED_START_QUOTE: CommandErrorType<0> =
CommandErrorType::new("parsing.quote.expected.start");
CommandErrorType::new(translation::PARSING_QUOTE_EXPECTED_START);
pub const READER_EXPECTED_END_QUOTE: CommandErrorType<0> =
CommandErrorType::new("parsing.quote.expected.end");
CommandErrorType::new(translation::PARSING_QUOTE_EXPECTED_END);
pub const READER_INVALID_ESCAPE: CommandErrorType<1> =
CommandErrorType::new("parsing.quote.escape");
pub const READER_INVALID_BOOL: CommandErrorType<1> = CommandErrorType::new("parsing.bool.invalid");
CommandErrorType::new(translation::PARSING_QUOTE_ESCAPE);
pub const READER_INVALID_BOOL: CommandErrorType<1> =
CommandErrorType::new(translation::PARSING_BOOL_INVALID);
pub const READER_EXPECTED_BOOL: CommandErrorType<0> =
CommandErrorType::new("parsing.bool.expected");
pub const READER_INVALID_INT: CommandErrorType<1> = CommandErrorType::new("parsing.int.invalid");
pub const READER_EXPECTED_INT: CommandErrorType<0> = CommandErrorType::new("parsing.int.expected");
pub const READER_INVALID_LONG: CommandErrorType<1> = CommandErrorType::new("parsing.long.invalid");
CommandErrorType::new(translation::PARSING_BOOL_EXPECTED);
pub const READER_INVALID_INT: CommandErrorType<1> =
CommandErrorType::new(translation::PARSING_INT_INVALID);
pub const READER_EXPECTED_INT: CommandErrorType<0> =
CommandErrorType::new(translation::PARSING_INT_EXPECTED);
pub const READER_INVALID_LONG: CommandErrorType<1> =
CommandErrorType::new(translation::PARSING_LONG_INVALID);
pub const READER_EXPECTED_LONG: CommandErrorType<0> =
CommandErrorType::new("parsing.long.expected");
CommandErrorType::new(translation::PARSING_LONG_EXPECTED);
pub const READER_INVALID_DOUBLE: CommandErrorType<1> =
CommandErrorType::new("parsing.double.invalid");
CommandErrorType::new(translation::PARSING_DOUBLE_INVALID);
pub const READER_EXPECTED_DOUBLE: CommandErrorType<0> =
CommandErrorType::new("parsing.double.expected");
CommandErrorType::new(translation::PARSING_DOUBLE_EXPECTED);
pub const READER_INVALID_FLOAT: CommandErrorType<1> =
CommandErrorType::new("parsing.float.invalid");
CommandErrorType::new(translation::PARSING_FLOAT_INVALID);
pub const READER_EXPECTED_FLOAT: CommandErrorType<0> =
CommandErrorType::new("parsing.float.expected");
pub const READER_EXPECTED_SYMBOL: CommandErrorType<1> = CommandErrorType::new("parsing.expected");
CommandErrorType::new(translation::PARSING_FLOAT_EXPECTED);
pub const READER_EXPECTED_SYMBOL: CommandErrorType<1> =
CommandErrorType::new(translation::PARSING_EXPECTED);
pub const LITERAL_INCORRECT: CommandErrorType<1> =
CommandErrorType::new("argument.literal.incorrect");
CommandErrorType::new(translation::ARGUMENT_LITERAL_INCORRECT);
pub const DOUBLE_TOO_LOW: CommandErrorType<2> = CommandErrorType::new("argument.double.low");
pub const DOUBLE_TOO_HIGH: CommandErrorType<2> = CommandErrorType::new("argument.double.big");
pub const FLOAT_TOO_LOW: CommandErrorType<2> = CommandErrorType::new("argument.float.low");
pub const FLOAT_TOO_HIGH: CommandErrorType<2> = CommandErrorType::new("argument.float.big");
pub const INTEGER_TOO_LOW: CommandErrorType<2> = CommandErrorType::new("argument.integer.low");
pub const INTEGER_TOO_HIGH: CommandErrorType<2> = CommandErrorType::new("argument.integer.big");
pub const LONG_TOO_LOW: CommandErrorType<2> = CommandErrorType::new("argument.long.low");
pub const LONG_TOO_HIGH: CommandErrorType<2> = CommandErrorType::new("argument.long.big");
pub const DOUBLE_TOO_LOW: CommandErrorType<2> =
CommandErrorType::new(translation::ARGUMENT_DOUBLE_LOW);
pub const DOUBLE_TOO_HIGH: CommandErrorType<2> =
CommandErrorType::new(translation::ARGUMENT_DOUBLE_BIG);
pub const FLOAT_TOO_LOW: CommandErrorType<2> =
CommandErrorType::new(translation::ARGUMENT_FLOAT_LOW);
pub const FLOAT_TOO_HIGH: CommandErrorType<2> =
CommandErrorType::new(translation::ARGUMENT_FLOAT_BIG);
pub const INTEGER_TOO_LOW: CommandErrorType<2> =
CommandErrorType::new(translation::ARGUMENT_INTEGER_LOW);
pub const INTEGER_TOO_HIGH: CommandErrorType<2> =
CommandErrorType::new(translation::ARGUMENT_INTEGER_BIG);
pub const LONG_TOO_LOW: CommandErrorType<2> = CommandErrorType::new(translation::ARGUMENT_LONG_LOW);
pub const LONG_TOO_HIGH: CommandErrorType<2> =
CommandErrorType::new(translation::ARGUMENT_LONG_BIG);
pub const DISPATCHER_UNKNOWN_COMMAND: CommandErrorType<0> =
CommandErrorType::new("command.unknown.command");
CommandErrorType::new(translation::COMMAND_UNKNOWN_COMMAND);
pub const DISPATCHER_UNKNOWN_ARGUMENT: CommandErrorType<0> =
CommandErrorType::new("command.unknown.argument");
CommandErrorType::new(translation::COMMAND_UNKNOWN_ARGUMENT);
pub const DISPATCHER_EXPECTED_ARGUMENT_SEPARATOR: CommandErrorType<0> =
CommandErrorType::new("command.expected.separator");
CommandErrorType::new(translation::COMMAND_EXPECTED_SEPARATOR);
pub const DISPATCHER_PARSE_EXCEPTION: CommandErrorType<1> =
CommandErrorType::new("command.exception");
CommandErrorType::new(translation::COMMAND_EXCEPTION);
use crate::command::errors::{
command_syntax_error::{CommandSyntaxError, ContextProvider},
@@ -51,7 +64,21 @@ use crate::command::errors::{
};
use pumpkin_util::text::TextComponent;
/// Represents text which can be used as a template that is generated at
/// compile time and cannot change at runtime.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TemplateText {
/// This is first translated, then arguments are substituted (which are not constant)
/// when it needs to be displayed.
TranslationKey(&'static str),
/// Directly shows up to the user without any translation done.
Literal(&'static str),
}
/// A command error that requires **exactly** `N` translation arguments.
/// This takes a translation key. If you want the non-translatable version,
/// use [`LiteralCommandErrorType`].
///
/// **Comparison with Brigadier**:
/// - [`CommandErrorType<0>`] = `SimpleCommandExceptionType`
@@ -101,6 +128,41 @@ impl<const N: usize> CommandErrorType<N> {
}
}
/// A command error that is not translated, and cannot take any arguments.
/// This takes a constant string literal. If you want the translatable version,
/// use [`CommandErrorType`].
///
/// [`CommandErrorType`] should be preferred to this whenever possible.
///
/// Use this for custom error messages, which don't have any
/// translation in vanilla.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LiteralCommandErrorType {
pub literal: &'static str,
}
impl LiteralCommandErrorType {
/// Creates an error type from a given literal string.
#[must_use]
pub const fn new(literal: &'static str) -> Self {
Self { literal }
}
/// Creates an error without context from itself.
#[must_use]
pub fn create_without_context(&'static self) -> CommandSyntaxError {
CommandSyntaxError::create_without_context(self, TextComponent::text(self.literal))
}
/// Creates an error with context from itself.
pub fn create<C>(&'static self, context_provider: &C) -> CommandSyntaxError
where
C: ContextProvider,
{
CommandSyntaxError::create(self, TextComponent::text(self.literal), context_provider)
}
}
// Prevent other crates from using this trait
// Thus, we can effectively 'seal' our trait meant
// only for `CommandErrorType<N>`.
@@ -116,7 +178,7 @@ mod sealed {
/// key and the number of arguments (at runtime).
pub trait AnyCommandErrorType: Sealed + std::fmt::Debug {
/// Returns the underlying translation key of this specific error type.
fn translation_key(&self) -> &'static str;
fn text(&self) -> TemplateText;
/// Returns the number of arguments supported by this error type.
fn argument_count(&self) -> usize;
@@ -126,17 +188,21 @@ impl Eq for dyn AnyCommandErrorType {}
impl PartialEq for dyn AnyCommandErrorType {
fn eq(&self, other: &Self) -> bool {
self.translation_key() == other.translation_key()
&& self.argument_count() == other.argument_count()
self.text() == other.text() && self.argument_count() == other.argument_count()
}
}
impl<T: AnyCommandErrorType> PartialEq<T> for dyn AnyCommandErrorType {
fn eq(&self, other: &T) -> bool {
self.text() == other.text() && self.argument_count() == other.argument_count()
}
}
// Implement the private trait for our types.
impl<const N: usize> Sealed for CommandErrorType<N> {}
impl<const N: usize> AnyCommandErrorType for CommandErrorType<N> {
fn translation_key(&self) -> &'static str {
self.translation_key
fn text(&self) -> TemplateText {
TemplateText::TranslationKey(self.translation_key)
}
fn argument_count(&self) -> usize {
@@ -144,6 +210,17 @@ impl<const N: usize> AnyCommandErrorType for CommandErrorType<N> {
}
}
impl Sealed for LiteralCommandErrorType {}
impl AnyCommandErrorType for LiteralCommandErrorType {
fn text(&self) -> TemplateText {
TemplateText::Literal(self.literal)
}
fn argument_count(&self) -> usize {
0
}
}
// Ease-of-use Implementations:
/// Generates a specific implementation for `CommandErrorType<N>` with two methods to create
@@ -190,3 +267,69 @@ error_type_no_arg_slice_impl!(N = 1 => arg1 | "by taking 1 tra
error_type_no_arg_slice_impl!(N = 2 => arg1, arg2 | "by taking 2 translation arguments.");
error_type_no_arg_slice_impl!(N = 3 => arg1, arg2, arg3 | "by taking 3 translation arguments.");
error_type_no_arg_slice_impl!(N = 4 => arg1, arg2, arg3, arg4 | "by taking 4 translation arguments.");
#[cfg(test)]
mod test {
use crate::command::errors::error_types::{CommandErrorType, LiteralCommandErrorType};
use crate::command::string_reader::StringReader;
use pumpkin_util::text::TextComponent;
const TEST_LITERAL_ERROR_TYPE: LiteralCommandErrorType =
LiteralCommandErrorType::new("Test error");
const TEST_TRANSLATABLE_ERROR_TYPE: CommandErrorType<1> =
CommandErrorType::new("this.key.is.arbitrary");
#[test]
fn create_literal_error() {
let mut reader = StringReader::new("foo bar");
reader.set_cursor(4);
let error = TEST_LITERAL_ERROR_TYPE.create(&reader);
assert_eq!(error.error_type, &TEST_LITERAL_ERROR_TYPE);
assert_eq!(error.message, TextComponent::text("Test error"));
match &error.context {
Some(context) => {
assert_eq!(context.cursor, 4);
assert_eq!(context.input, "foo bar");
}
None => panic!("There should have been a context for the error"),
}
}
#[test]
fn create_literal_error_without_context() {
let error = TEST_LITERAL_ERROR_TYPE.create_without_context();
assert_eq!(error.error_type, &TEST_LITERAL_ERROR_TYPE);
assert_eq!(error.message, TextComponent::text("Test error"));
assert_eq!(error.context, None);
}
#[test]
fn create_translatable_error() {
let mut reader = StringReader::new("foo bar");
reader.set_cursor(4);
let error =
TEST_TRANSLATABLE_ERROR_TYPE.create(&reader, TextComponent::text("some argument"));
assert_eq!(error.error_type, &TEST_TRANSLATABLE_ERROR_TYPE);
assert_eq!(
error.message,
TextComponent::translate(
"this.key.is.arbitrary",
[TextComponent::text("some argument")]
)
);
match &error.context {
Some(context) => {
assert_eq!(context.cursor, 4);
assert_eq!(context.input, "foo bar");
}
None => panic!("There should have been a context for the error"),
}
}
}

View File

@@ -17,11 +17,16 @@ use pumpkin_world::block::entities::BlockEntity;
use pumpkin_world::block::entities::command_block::CommandBlockEntity;
pub mod args;
pub mod argument_builder;
pub mod argument_types;
pub mod client_suggestions;
pub mod commands;
pub mod context;
pub mod dispatcher;
pub mod errors;
pub mod node;
pub mod string_reader;
pub mod suggestion;
pub mod tree;
/// Represents the source of a command execution.
@@ -29,6 +34,7 @@ pub mod tree;
/// Different senders have different permissions, output targets, and
/// positions in the world. This enum abstracts those differences for the
/// command dispatcher.
#[derive(Clone)]
pub enum CommandSender {
/// A remote console connection via the RCON protocol.
///
@@ -49,7 +55,10 @@ pub enum CommandSender {
///
/// Contains the block entity responsible for the command and the
/// world context it exists in for coordinate-relative execution (e.g., `~ ~ ~`).
CommandBlock(Arc<dyn BlockEntity>, Arc<World>),
CommandBlock(Arc<CommandBlockEntity>, Arc<World>),
/// Nothingness. Anything sent to this sender is void.
/// Has the same permissions as that of `CommandBlock`.
Dummy,
}
impl fmt::Display for CommandSender {
@@ -62,6 +71,7 @@ impl fmt::Display for CommandSender {
Self::Rcon(_) => "Rcon",
Self::Player(p) => &p.gameprofile.name,
Self::CommandBlock(..) => "@",
Self::Dummy => "",
}
)
}
@@ -75,9 +85,7 @@ impl CommandSender {
Self::Player(c) => c.send_system_message(&text).await,
Self::Rcon(s) => s.lock().await.push(text.to_pretty_console()),
Self::CommandBlock(block_entity, _) => {
let command_entity: &CommandBlockEntity =
block_entity.as_any().downcast_ref().unwrap();
let mut last_output = command_entity.last_output.lock().await;
let mut last_output = block_entity.last_output.lock().await;
let now = time::OffsetDateTime::now_utc();
let format = time::macros::format_description!("[hour]:[minute]:[second]");
@@ -85,14 +93,13 @@ impl CommandSender {
*last_output = format!("[{}] {}", timestamp, text.get_text());
}
Self::Dummy => {}
}
}
pub fn set_success_count(&self, count: u32) {
if let Self::CommandBlock(c, _) = self {
let block: &CommandBlockEntity = c.as_any().downcast_ref().unwrap();
block
.success_count
c.success_count
.store(count, std::sync::atomic::Ordering::SeqCst);
}
}
@@ -120,7 +127,7 @@ impl CommandSender {
match self {
Self::Console | Self::Rcon(_) => PermissionLvl::Four,
Self::Player(p) => p.permission_lvl.load(),
Self::CommandBlock(..) => PermissionLvl::Two,
Self::CommandBlock(..) | Self::Dummy => PermissionLvl::Two,
}
}
@@ -129,7 +136,7 @@ impl CommandSender {
match self {
Self::Console | Self::Rcon(_) => true,
Self::Player(p) => p.permission_lvl.load().ge(&lvl),
Self::CommandBlock(..) => PermissionLvl::Two >= lvl,
Self::CommandBlock(..) | Self::Dummy => PermissionLvl::Two >= lvl,
}
}
@@ -138,7 +145,7 @@ impl CommandSender {
match self {
Self::Console | Self::Rcon(_) => true, // Console and RCON always have all permissions
Self::Player(p) => p.has_permission(server, node).await,
Self::CommandBlock(..) => {
Self::CommandBlock(..) | Self::Dummy => {
let perm_reg = server.permission_registry.read().await;
let Some(p) = perm_reg.get_permission(node) else {
return false;
@@ -155,7 +162,7 @@ impl CommandSender {
#[must_use]
pub fn position(&self) -> Option<Vector3<f64>> {
match self {
Self::Console | Self::Rcon(..) => None,
Self::Console | Self::Rcon(..) | Self::Dummy => None,
Self::Player(p) => Some(p.living_entity.entity.pos.load()),
Self::CommandBlock(c, _) => Some(c.get_position().to_centered_f64()),
}
@@ -165,7 +172,7 @@ impl CommandSender {
pub fn world(&self) -> Option<Arc<World>> {
match self {
// TODO: maybe return first world when console
Self::Console | Self::Rcon(..) => None,
Self::Console | Self::Rcon(..) | Self::Dummy => None,
Self::Player(p) => Some(p.living_entity.entity.world.load_full()),
Self::CommandBlock(_, w) => Some(w.clone()),
}
@@ -174,12 +181,49 @@ impl CommandSender {
#[must_use]
pub fn get_locale(&self) -> Locale {
match self {
Self::CommandBlock(..) | Self::Console | Self::Rcon(..) => Locale::EnUs, // Default locale for console and RCON
Self::CommandBlock(..) | Self::Console | Self::Rcon(..) | Self::Dummy => Locale::EnUs, // Default locale for console and RCON
Self::Player(player) => {
Locale::from_str(&player.config.load().locale).unwrap_or(Locale::EnUs)
}
}
}
#[must_use]
pub fn should_receive_feedback(&self) -> bool {
match self {
Self::CommandBlock(_, world) => {
world.level_info.load().game_rules.send_command_feedback
}
Self::Player(player) => {
player
.world()
.level_info
.load()
.game_rules
.send_command_feedback
}
Self::Console | Self::Rcon(_) => true,
Self::Dummy => false,
}
}
#[must_use]
pub fn should_broadcast_console_to_ops(&self) -> bool {
match self {
Self::CommandBlock(_, world) => world.level_info.load().game_rules.command_block_output,
// TODO: should Console and Rcon be decided by server config?
Self::Player(..) | Self::Console | Self::Rcon(_) => true,
Self::Dummy => false,
}
}
#[must_use]
pub const fn should_track_output(&self) -> bool {
match self {
Self::Dummy => false,
Self::Player(..) | Self::Console | Self::Rcon(_) | Self::CommandBlock(..) => true,
}
}
}
/// Represents the result of running a command after completion.

View File

@@ -0,0 +1,436 @@
use crate::command::context::string_range::StringRange;
use crate::command::errors::command_syntax_error::CommandSyntaxError;
use crate::command::errors::error_types::LITERAL_INCORRECT;
use crate::command::node::detached::GlobalNodeId;
use crate::command::node::tree::ROOT_NODE_ID;
use crate::command::node::{
ArgumentNodeMetadata, Command, CommandNodeMetadata, LiteralNodeMetadata, NodeMetadata,
OwnedNodeData, RedirectModifier, Redirection, Requirement,
};
use crate::command::string_reader::StringReader;
use pumpkin_util::text::TextComponent;
use rustc_hash::FxHashMap;
use std::borrow::Cow;
use std::num::NonZero;
/// Represents the unique integral number
/// of any node, with respect to a tree.
///
/// A [`NonZero<usize>`] is used internally in this
/// struct. This means [`Option<NodeId>`] carries the
/// same size as of [`NodeId`], but comes at the cost of
/// ID `0` being unassignable.
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
pub struct NodeId(pub NonZero<usize>);
/// Represents the unique integral number
/// of the root node, with respect to a tree.
///
/// This is unit-sized as it is constant.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct RootNodeId;
/// Represents the unique integral number
/// of a specific literal node, with respect to a tree.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct LiteralNodeId(pub NonZero<usize>);
/// Represents the unique integral number
/// of a specific command node, with respect to a tree.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct CommandNodeId(pub NonZero<usize>);
/// Represents the unique integral number
/// of a specific argument node, with respect to a tree.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct ArgumentNodeId(pub NonZero<usize>);
impl From<RootNodeId> for NodeId {
fn from(_id: RootNodeId) -> Self {
ROOT_NODE_ID
}
}
impl From<LiteralNodeId> for NodeId {
fn from(id: LiteralNodeId) -> Self {
Self(id.0)
}
}
impl From<CommandNodeId> for NodeId {
fn from(id: CommandNodeId) -> Self {
Self(id.0)
}
}
impl From<ArgumentNodeId> for NodeId {
fn from(id: ArgumentNodeId) -> Self {
Self(id.0)
}
}
/// Represents a node which has been attached as the root of a [`Tree`].
#[derive(Clone)]
pub struct RootAttachedNode {
pub owned: OwnedNodeData,
pub children: FxHashMap<String, NodeId>,
}
impl Default for RootAttachedNode {
fn default() -> Self {
Self::new()
}
}
impl RootAttachedNode {
#[must_use]
pub fn new() -> Self {
Self {
owned: OwnedNodeData {
global_id: GlobalNodeId::new(),
requirement: Requirement::AlwaysQualified,
modifier: RedirectModifier::OneSource,
permission: None,
forks: false,
command: None,
},
children: FxHashMap::default(),
}
}
}
/// Represents a literal, non-command node that has already been attached
/// to a [`Tree`].
#[derive(Clone)]
pub struct LiteralAttachedNode {
pub owned: OwnedNodeData,
pub children: FxHashMap<String, NodeId>,
pub redirect: Option<Redirection>,
pub meta: LiteralNodeMetadata,
}
/// Represents a literal, command node that has already been attached
/// to a [`Tree`].
#[derive(Clone)]
pub struct CommandAttachedNode {
pub owned: OwnedNodeData,
pub children: FxHashMap<String, NodeId>,
pub redirect: Option<Redirection>,
pub meta: CommandNodeMetadata,
}
/// Represents a node that accepts a specific type of argument that has already been attached
/// to a [`Tree`].
#[derive(Clone)]
pub struct ArgumentAttachedNode {
pub owned: OwnedNodeData,
pub children: FxHashMap<String, NodeId>,
pub redirect: Option<Redirection>,
pub meta: ArgumentNodeMetadata,
}
/// Allows a way to store the kind of node
/// without any actual cloning of [`NodeMetadata`].
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum NodeClassification {
Root,
Literal,
Command,
Argument,
}
/// Represents a node not attached to a [`Tree`] yet.
#[derive(Clone)]
pub enum AttachedNode {
Root(RootAttachedNode),
Literal(LiteralAttachedNode),
Command(CommandAttachedNode),
Argument(ArgumentAttachedNode),
}
/// Returned when a literal could not be parsed.
pub struct CouldNotParseLiteral;
impl AttachedNode {
/// Creates an [`AttachedNode`] from its properties allowing any [`NodeMetadata`].
#[must_use]
pub fn from_parts(
owned: OwnedNodeData,
children: FxHashMap<String, NodeId>,
redirect: Option<Redirection>,
meta: NodeMetadata,
) -> Self {
match meta {
NodeMetadata::Root => Self::Root(RootAttachedNode { owned, children }),
NodeMetadata::Literal(meta) => Self::Literal(LiteralAttachedNode {
owned,
children,
redirect,
meta,
}),
NodeMetadata::Command(meta) => Self::Command(CommandAttachedNode {
owned,
children,
redirect,
meta,
}),
NodeMetadata::Argument(meta) => Self::Argument(ArgumentAttachedNode {
owned,
children,
redirect,
meta,
}),
}
}
/// Gets the classification of this node.
/// This is a relatively cheap operation.
#[must_use]
pub const fn classification(&self) -> NodeClassification {
match self {
Self::Root(_) => NodeClassification::Root,
Self::Literal(_) => NodeClassification::Literal,
Self::Command(_) => NodeClassification::Command,
Self::Argument(_) => NodeClassification::Argument,
}
}
/// Gets the global ID from this node.
#[must_use]
pub const fn global_id(&self) -> GlobalNodeId {
self.owned_node_data_ref().global_id
}
/// Gets a reference to the owned data of this node.
#[must_use]
pub const fn owned_node_data_ref(&self) -> &OwnedNodeData {
match self {
Self::Root(node) => &node.owned,
Self::Literal(node) => &node.owned,
Self::Command(node) => &node.owned,
Self::Argument(node) => &node.owned,
}
}
/// Gets a mutable reference to the owned data of this node.
pub const fn owned_node_data_mut_ref(&mut self) -> &mut OwnedNodeData {
match self {
Self::Root(node) => &mut node.owned,
Self::Literal(node) => &mut node.owned,
Self::Command(node) => &mut node.owned,
Self::Argument(node) => &mut node.owned,
}
}
/// Gets a reference to the children IDs of this node.
#[must_use]
pub const fn children_ref(&self) -> &FxHashMap<String, NodeId> {
match self {
Self::Root(node) => &node.children,
Self::Literal(node) => &node.children,
Self::Command(node) => &node.children,
Self::Argument(node) => &node.children,
}
}
/// Gets a mutable reference to the children IDs of this node.
pub const fn children_mut_ref(&mut self) -> &mut FxHashMap<String, NodeId> {
match self {
Self::Root(node) => &mut node.children,
Self::Literal(node) => &mut node.children,
Self::Command(node) => &mut node.children,
Self::Argument(node) => &mut node.children,
}
}
/// Gets the name of this node.
#[must_use]
pub fn name(&self) -> String {
match self {
Self::Root(_) => String::new(),
Self::Literal(node) => node.meta.literal.to_string(),
Self::Command(node) => node.meta.literal.to_string(),
Self::Argument(node) => node.meta.name.to_string(),
}
}
/// Gets the redirection of this node.
#[must_use]
pub const fn redirect(&self) -> Option<Redirection> {
match self {
Self::Root(_) => None,
Self::Literal(node) => node.redirect,
Self::Command(node) => node.redirect,
Self::Argument(node) => node.redirect,
}
}
/// Gets an [`Option`] of a mutable reference to the redirection of this node.
pub const fn redirect_mut_ref(&mut self) -> Option<&mut Redirection> {
match self {
Self::Root(_) => None,
Self::Literal(node) => node.redirect.as_mut(),
Self::Command(node) => node.redirect.as_mut(),
Self::Argument(node) => node.redirect.as_mut(),
}
}
/// Gets the requirement for this node to be run.
///
/// Note that this does not account for the permission required; that is separately
/// stored. Use the [`permission`] method to index it.
///
/// [`permission`]: AttachedNode::permission
#[must_use]
pub const fn requirement(&self) -> &Requirement {
&self.owned_node_data_ref().requirement
}
/// Sets the requirement for this node to be run to a value.
///
/// Note that this does not account for the permission required; that is separately
/// stored. Use the [`set_permission`] method to set that field's value instead.
///
/// [`set_permission`]: AttachedNode::set_permission
pub fn set_requirement(&mut self, requirement: Requirement) {
self.owned_node_data_mut_ref().requirement = requirement;
}
/// Gets the permission required for this node to be run.
///
/// Note that this does not account for the extra requirement required; that is separately
/// stored. Use the [`requirement`] method to index it.
///
/// [`requirement`]: AttachedNode::requirement
#[must_use]
pub fn permission(&self) -> Option<&str> {
self.owned_node_data_ref().permission.as_deref()
}
/// Sets the permission required for this node to be run to a value.
///
/// Note that this does not account for the extra requirement required; that is separately
/// stored. Use the [`set_requirement`] method to set that field's value instead.
///
/// [`set_requirement`]: AttachedNode::set_requirement
pub fn set_permission<P>(&mut self, permission: Option<P>)
where
P: Into<Cow<'static, str>>,
{
self.owned_node_data_mut_ref().permission = permission.map(Into::into);
}
/// Gets the modifier for this node to be run.
#[must_use]
pub const fn modifier(&self) -> &RedirectModifier {
&self.owned_node_data_ref().modifier
}
/// Sets the modifier for this node to a value.
pub fn set_modifier(&mut self, modifier: RedirectModifier) {
self.owned_node_data_mut_ref().modifier = modifier;
}
/// Whether this node forks [`CommandSources`] or not.
#[must_use]
pub const fn forks(&self) -> bool {
self.owned_node_data_ref().forks
}
/// Sets whether this node forks [`CommandSources`] or not.
pub const fn set_forks(&mut self, forks: bool) {
self.owned_node_data_mut_ref().forks = forks;
}
/// Gets the executable command for this node.
#[must_use]
pub fn command(&self) -> &Option<Command> {
&self.owned_node_data_ref().command
}
/// Sets the executable command for this node.
pub fn set_command(&mut self, command: Option<Command>) {
self.owned_node_data_mut_ref().command = command;
}
/// Get the usage text of this node.
#[must_use]
pub fn usage_text(&self) -> String {
match self {
Self::Root(_) => String::new(),
Self::Literal(node) => node.meta.literal.to_string(),
Self::Command(node) => node.meta.literal.to_string(),
Self::Argument(node) => format!("<{}>", node.meta.name),
}
}
/// Checks if the given input is valid for this node.
#[must_use]
pub fn is_valid_input(&self, input: &str) -> bool {
match self {
Self::Root(_) => false,
Self::Literal(node) => {
let mut reader = StringReader::new(input);
Self::parse_literal(&mut reader, &node.meta.literal).is_ok()
}
Self::Command(node) => {
let mut reader = StringReader::new(input);
Self::parse_literal(&mut reader, &node.meta.literal).is_ok()
}
Self::Argument(node) => {
let mut reader = StringReader::new(input);
let parsed = node.meta.argument_type.parse(&mut reader);
if parsed.is_ok() {
matches!(reader.peek(), Some(' ') | None)
} else {
false
}
}
}
}
/// Parses the given input for this node.
/// Prefer using a [`CommandDispatcher`] over this function directly.
pub fn parse(
&self,
reader: &mut StringReader,
literal: &str,
) -> Result<StringRange, CommandSyntaxError> {
let start = reader.cursor();
Self::parse_literal(reader, literal).map_or_else(
|_| Err(LITERAL_INCORRECT.create(reader, TextComponent::text(literal.to_string()))),
|end| Ok(StringRange::between(start, end)),
)
}
/// Internal function to parse a literal. Used by [`Tree`].
pub fn parse_literal(
reader: &mut StringReader,
literal: &str,
) -> Result<usize, CouldNotParseLiteral> {
let start = reader.cursor();
let len = literal.len();
if reader.can_read_bytes(len) {
let end = start + len;
if &reader.string()[start..end] == literal {
reader.set_cursor(end);
if matches!(reader.peek(), Some(' ') | None) {
return Ok(end);
}
reader.set_cursor(start);
}
}
Err(CouldNotParseLiteral)
}
/// Gets examples accepted by this node.
#[must_use]
pub fn examples(&self) -> Vec<String> {
match self {
Self::Root(_) => Vec::new(),
Self::Literal(node) => vec![node.meta.literal.to_string()],
Self::Command(node) => vec![node.meta.literal.to_string()],
Self::Argument(node) => node.meta.argument_type.examples(),
}
}
}

View File

@@ -0,0 +1,255 @@
use crate::command::argument_types::argument_type::AnyArgumentType;
use crate::command::node::{
ArgumentNodeMetadata, Command, CommandNodeMetadata, LiteralNodeMetadata, NodeMetadata,
OwnedNodeData, RedirectModifier, Redirection, Requirement,
};
use rustc_hash::FxHashMap;
use std::borrow::Cow;
use std::num::NonZero;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT_DETACHED_NODE_ID: AtomicU64 = AtomicU64::new(1);
/// Represents a **global** integral number of
/// any type of node which is unique at runtime.
///
/// This is important for nodes not bound to a
/// tree.
#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug)]
pub struct GlobalNodeId(pub NonZero<u64>);
impl GlobalNodeId {
/// Generates an ID that is guaranteed
/// to be unique at runtime, by using atomics.
pub fn new() -> Self {
Self(
NonZero::new(NEXT_DETACHED_NODE_ID.fetch_add(1, Ordering::Relaxed))
.expect("expected a non-zero id"),
)
}
}
impl Default for GlobalNodeId {
fn default() -> Self {
Self::new()
}
}
/// Represents a literal, non-command node that has not been attached
/// to a tree yet.
///
/// If you want to start a command with this node, use [`CommandDetachedNode`] instead.
///
/// To be of any utility, this must be attached to a tree later.
pub struct LiteralDetachedNode {
pub owned: OwnedNodeData,
pub children: FxHashMap<String, DetachedNode>,
pub redirect: Option<Redirection>,
pub meta: LiteralNodeMetadata,
}
impl LiteralDetachedNode {
/// Creates a detached literal node from its properties,
/// without any children.
///
/// # Note
/// Prefer using the [`LiteralArgumentBuilder`] over this function.
#[expect(clippy::too_many_arguments)]
pub fn new<P>(
global_id: GlobalNodeId,
literal: impl Into<Cow<'static, str>>,
command: Option<Command>,
requirement: Requirement,
redirect: Option<Redirection>,
modifier: RedirectModifier,
permission: Option<P>,
forks: bool,
) -> Self
where
P: Into<Cow<'static, str>>,
{
Self {
owned: OwnedNodeData {
global_id,
requirement,
modifier,
forks,
command,
permission: permission.map(Into::into),
},
children: FxHashMap::default(),
redirect,
meta: LiteralNodeMetadata::new(literal),
}
}
}
/// Represents a literal, command node that has not been attached
/// to a tree yet.
///
/// If you don't want to start a command with this node, use [`LiteralDetachedNode`] instead.
///
/// To be of any utility, this must be attached to a tree later.
pub struct CommandDetachedNode {
pub owned: OwnedNodeData,
pub children: FxHashMap<String, DetachedNode>,
pub redirect: Option<Redirection>,
pub meta: CommandNodeMetadata,
}
impl CommandDetachedNode {
/// Creates a detached literal node from its properties,
/// without any children.
///
/// # Note
/// Prefer using the [`LiteralArgumentBuilder`] over this function.
#[expect(clippy::too_many_arguments)]
pub fn new<P>(
global_id: GlobalNodeId,
literal: impl Into<Cow<'static, str>>,
description: impl Into<Cow<'static, str>>,
command: Option<Command>,
requirement: Requirement,
redirect: Option<Redirection>,
modifier: RedirectModifier,
permission: Option<P>,
forks: bool,
) -> Self
where
P: Into<Cow<'static, str>>,
{
Self {
owned: OwnedNodeData {
global_id,
requirement,
modifier,
forks,
command,
permission: permission.map(Into::into),
},
children: FxHashMap::default(),
redirect,
meta: CommandNodeMetadata::new(literal, description),
}
}
}
/// Represents a node that accepts a specific type of argument.
///
/// To be of any utility, this must be attached to a tree later.
pub struct ArgumentDetachedNode {
pub owned: OwnedNodeData,
pub children: FxHashMap<String, DetachedNode>,
pub redirect: Option<Redirection>,
pub meta: ArgumentNodeMetadata,
}
impl ArgumentDetachedNode {
/// Creates a detached argument node from its properties,
/// without any children.
///
/// # Note
/// Prefer using the [`RequiredArgumentBuilder`] over this function.
#[expect(clippy::too_many_arguments)]
pub fn new<P>(
global_id: GlobalNodeId,
name: impl Into<Cow<'static, str>>,
argument_type: Arc<dyn AnyArgumentType>,
command: Option<Command>,
requirement: Requirement,
redirect: Option<Redirection>,
modifier: RedirectModifier,
permission: Option<P>,
forks: bool,
) -> Self
where
P: Into<Cow<'static, str>>,
{
Self {
owned: OwnedNodeData {
global_id,
requirement,
modifier,
forks,
command,
permission: permission.map(Into::into),
},
children: FxHashMap::default(),
redirect,
meta: ArgumentNodeMetadata::new(name, argument_type),
}
}
}
/// Represents a node not attached to a [`Tree`] yet.
pub enum DetachedNode {
Literal(LiteralDetachedNode),
Command(CommandDetachedNode),
Argument(ArgumentDetachedNode),
}
/// Represents a [`DetachedNode`] that has been irreversibly
/// decomposed into its elements so that it can be recast
/// into a new [`AttachedNode`].
pub struct DecomposedNode {
pub owned: OwnedNodeData,
pub children: FxHashMap<String, DetachedNode>,
pub redirect: Option<Redirection>,
pub meta: NodeMetadata,
}
impl From<LiteralDetachedNode> for DetachedNode {
fn from(node: LiteralDetachedNode) -> Self {
Self::Literal(node)
}
}
impl From<CommandDetachedNode> for DetachedNode {
fn from(node: CommandDetachedNode) -> Self {
Self::Command(node)
}
}
impl From<ArgumentDetachedNode> for DetachedNode {
fn from(node: ArgumentDetachedNode) -> Self {
Self::Argument(node)
}
}
impl DetachedNode {
/// Irreversibly decomposes this [`DetachedNode`] into its constituent elements.
/// This allows it to then be recast into a new [`AttachedNode`].
#[must_use]
pub fn decompose(self) -> DecomposedNode {
match self {
Self::Literal(node) => DecomposedNode {
owned: node.owned,
children: node.children,
redirect: node.redirect,
meta: NodeMetadata::Literal(node.meta),
},
Self::Command(node) => DecomposedNode {
owned: node.owned,
children: node.children,
redirect: node.redirect,
meta: NodeMetadata::Command(node.meta),
},
Self::Argument(node) => DecomposedNode {
owned: node.owned,
children: node.children,
redirect: node.redirect,
meta: NodeMetadata::Argument(node.meta),
},
}
}
#[must_use]
pub fn name(&self) -> String {
match self {
Self::Literal(node) => node.meta.literal.to_string(),
Self::Command(node) => node.meta.literal.to_string(),
Self::Argument(node) => node.meta.name.to_string(),
}
}
}

View File

@@ -0,0 +1,485 @@
use crate::command::context::command_context::{
CommandContext, CommandContextBuilder, ContextChain,
};
use crate::command::context::command_source::{CommandSource, ReturnValue};
use crate::command::errors::command_syntax_error::CommandSyntaxError;
use crate::command::errors::error_types::{
DISPATCHER_EXPECTED_ARGUMENT_SEPARATOR, DISPATCHER_UNKNOWN_ARGUMENT,
DISPATCHER_UNKNOWN_COMMAND, LiteralCommandErrorType,
};
use crate::command::node::attached::{CommandNodeId, NodeId};
use crate::command::node::detached::CommandDetachedNode;
use crate::command::node::tree::{ROOT_NODE_ID, Tree};
use crate::command::string_reader::StringReader;
use rustc_hash::FxHashMap;
use std::pin::Pin;
use std::sync::{Arc, LazyLock};
pub const ARG_SEPARATOR: &str = " ";
pub const ARG_SEPARATOR_CHAR: char = ' ';
pub const USAGE_OPTIONAL_OPEN: &str = "[";
pub const USAGE_OPTIONAL_CLOSE: &str = "]";
pub const USAGE_REQUIRED_OPEN: &str = "(";
pub const USAGE_REQUIRED_CLOSE: &str = ")";
pub const USAGE_OR: &str = "|";
/// Thrown when redirection could not be resolved.
/// This shouldn't happen, and only happens when the command is incorrectly configured.
pub const UNRESOLVED_REDIRECT: LiteralCommandErrorType =
LiteralCommandErrorType::new("Could not resolve redirect to node");
/// Represents the result of parsing.
pub struct ParsingResult<'a> {
pub context: CommandContextBuilder<'a>,
pub errors: FxHashMap<NodeId, CommandSyntaxError>,
pub reader: StringReader<'static>,
}
/// Structs implementing this trait are able to execute upon command completion.
pub trait ResultConsumer {
fn on_command_completion<'a>(
&'a self,
context: &'a CommandContext,
result: ReturnValue,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
}
/// A [`ResultConsumer`] which does nothing.
pub struct EmptyResultConsumer;
impl ResultConsumer for EmptyResultConsumer {
fn on_command_completion<'a>(
&self,
_context: &'a CommandContext,
_result: ReturnValue,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
Box::pin(async {})
}
}
pub static EMPTY_CONSUMER: LazyLock<Arc<EmptyResultConsumer>> =
LazyLock::new(|| Arc::new(EmptyResultConsumer));
/// A [`ResultConsumer`] which defers the given result to the source provided.
pub struct ResultDeferrer;
impl ResultConsumer for ResultDeferrer {
fn on_command_completion<'a>(
&self,
context: &'a CommandContext,
result: ReturnValue,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
Box::pin(async move {
context.source.command_result_taker.call(result).await;
})
}
}
pub static RESULT_DEFERRER: LazyLock<Arc<ResultDeferrer>> =
LazyLock::new(|| Arc::new(ResultDeferrer));
/// The core command dispatcher, used to register, parse and execute commands.
///
/// Internally, this dispatcher stores a [`Tree`]. Refer to its documentation
/// for more information about nodes.
pub struct CommandDispatcher {
pub tree: Tree,
pub consumer: Arc<dyn ResultConsumer>,
}
impl Default for CommandDispatcher {
fn default() -> Self {
Self::new()
}
}
impl CommandDispatcher {
/// Creates a new [`CommandDispatcher`] with a new [`Tree`].
#[must_use]
pub fn new() -> Self {
Self::from_existing_tree(Tree::new())
}
/// Creates this [`CommandDispatcher`] from a pre-existing tree.
pub fn from_existing_tree(tree: Tree) -> Self {
Self {
tree,
consumer: RESULT_DEFERRER.clone(),
}
}
/// Registers a command which can then be dispatched.
/// Returns the local ID of the node attached to the tree.
///
/// Note that, at least for now with this system, there is no way to
/// unregister a command. This is due to redirection to
/// potentially unregistered (freed) nodes.
pub fn register(&mut self, command_node: impl Into<CommandDetachedNode>) -> CommandNodeId {
self.tree.add_child_to_root(command_node)
}
/// Executes the given command with the provided source, returning a result of execution.
///
/// # Note
/// This does not cache parsed input.
pub async fn execute_input(
&self,
input: &str,
source: &CommandSource,
) -> Result<i32, CommandSyntaxError> {
let mut reader = StringReader::new(input);
self.execute_reader(&mut reader, source).await
}
/// Executes the given command in a [`StringReader`] with the provided source, returning a result of execution.
///
/// # Note
/// This does not cache parsed input.
pub async fn execute_reader(
&self,
reader: &mut StringReader<'_>,
source: &CommandSource,
) -> Result<i32, CommandSyntaxError> {
let parsed = self.parse(reader, source).await;
self.execute(parsed).await
}
/// Executes a given result that has already been parsed from an input.
pub async fn execute(&self, parsed: ParsingResult<'_>) -> Result<i32, CommandSyntaxError> {
if parsed.reader.peek().is_some() {
return if parsed.errors.len() == 1 {
Err(parsed.errors.values().next().unwrap().clone())
} else if parsed.context.range.is_empty() {
Err(DISPATCHER_UNKNOWN_COMMAND.create(&parsed.reader))
} else {
Err(DISPATCHER_UNKNOWN_ARGUMENT.create(&parsed.reader))
};
}
let command = parsed.reader.string();
let original_context = parsed.context.build(command);
match ContextChain::try_flatten(&original_context) {
None => {
self.consumer
.on_command_completion(&original_context, ReturnValue::Failure)
.await;
Err(DISPATCHER_UNKNOWN_COMMAND.create(&parsed.reader))
}
Some(flat_context) => {
flat_context
.execute_all(&original_context.source, self.consumer.as_ref())
.await
}
}
}
/// Only parses a given source with the specified source.
#[must_use]
pub async fn parse_input(&self, command: &str, source: &CommandSource) -> ParsingResult<'_> {
let mut reader = StringReader::new(command);
self.parse(&mut reader, source).await
}
/// Parses a command owned by a [`StringReader`] with the provided source.
pub async fn parse(
&self,
reader: &mut StringReader<'_>,
source: &CommandSource,
) -> ParsingResult<'_> {
let context = CommandContextBuilder::new(
self,
Arc::new(source.clone()),
ROOT_NODE_ID,
reader.cursor(),
);
self.parse_nodes(ROOT_NODE_ID, reader, &context).await
}
async fn parse_nodes<'a>(
&'a self,
node: NodeId,
original_reader: &mut StringReader<'_>,
context_so_far: &CommandContextBuilder<'a>,
) -> ParsingResult<'a> {
let source = context_so_far.source.clone();
let mut errors: FxHashMap<NodeId, CommandSyntaxError> = FxHashMap::default();
let mut potentials: Vec<ParsingResult> = Vec::new();
let cursor = original_reader.cursor();
for child in self.tree.get_relevant_nodes(original_reader, node) {
if !self.tree.can_use(child, &source).await {
continue;
}
let mut context = context_so_far.clone();
let mut reader = original_reader.clone();
let parse_result = {
if let Err(error) = self.tree.parse(child, &mut reader, &mut context) {
Err(error)
} else {
let peek = reader.peek();
if peek.is_some() && peek != Some(ARG_SEPARATOR_CHAR) {
Err(DISPATCHER_EXPECTED_ARGUMENT_SEPARATOR.create(&reader))
} else {
Ok(())
}
}
};
if let Err(parse_error) = parse_result {
errors.insert(child, parse_error);
reader.set_cursor(cursor);
continue;
}
let child_node = &self.tree[child];
context.with_command(child_node.command().clone());
let redirect = self.tree[child].redirect();
if reader.can_read_chars(if redirect.is_some() { 2 } else { 1 }) {
reader.skip();
if let Some(redirect) = redirect {
let Some(redirect) = self.tree.resolve(redirect) else {
errors.insert(child, UNRESOLVED_REDIRECT.create(&reader));
reader.set_cursor(cursor);
continue;
};
let child_context =
CommandContextBuilder::new(self, source, redirect, reader.cursor());
let parsed =
Box::pin(self.parse_nodes(redirect, &mut reader, &child_context)).await;
context.with_child(parsed.context);
return ParsingResult {
context,
errors: parsed.errors,
reader: parsed.reader,
};
}
let parsed = Box::pin(self.parse_nodes(child, &mut reader, &context)).await;
potentials.push(parsed);
} else {
potentials.push(ParsingResult {
context,
errors: FxHashMap::default(),
reader: reader.clone_into_owned(),
});
}
}
if potentials.is_empty() {
ParsingResult {
context: context_so_far.clone(),
errors,
reader: original_reader.clone_into_owned(),
}
} else {
potentials
.into_iter()
.min_by(|a, b| {
let a_reader_remaining = a.reader.peek().is_some();
let b_reader_remaining = b.reader.peek().is_some();
let a_has_errors = !a.errors.is_empty();
let b_has_errors = !b.errors.is_empty();
(a_reader_remaining, a_has_errors).cmp(&(b_reader_remaining, b_has_errors))
})
.unwrap()
}
}
}
#[cfg(test)]
mod test {
use crate::command::argument_builder::{
ArgumentBuilder, CommandArgumentBuilder, LiteralArgumentBuilder, RequiredArgumentBuilder,
};
use crate::command::argument_types::core::integer::IntegerArgumentType;
use crate::command::context::command_context::CommandContext;
use crate::command::context::command_source::CommandSource;
use crate::command::errors::error_types::DISPATCHER_UNKNOWN_COMMAND;
use crate::command::node::dispatcher::CommandDispatcher;
use crate::command::node::{CommandExecutor, CommandExecutorResult};
#[tokio::test]
async fn unknown_command() {
let mut dispatcher = CommandDispatcher::new();
dispatcher.register(
CommandArgumentBuilder::new("unknown", "A command without an executor").build(),
);
let source = CommandSource::dummy();
let result = dispatcher.execute_input("unknown", &source).await;
assert!(result.is_err_and(|error| error.error_type == &DISPATCHER_UNKNOWN_COMMAND));
}
#[tokio::test]
async fn simple_command() {
let mut dispatcher = CommandDispatcher::new();
let executor: for<'c> fn(&'c CommandContext) -> CommandExecutorResult<'c> =
|_| Box::pin(async move { Ok(1) });
dispatcher
.register(CommandArgumentBuilder::new("simple", "A simple command").executes(executor));
let source = CommandSource::dummy();
let result = dispatcher.execute_input("simple", &source).await;
assert_eq!(result, Ok(1));
}
#[tokio::test]
async fn arithmetic_command() {
enum Operation {
Add,
Subtract,
Multiply,
Divide,
}
struct Executor(Operation);
impl CommandExecutor for Executor {
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
Box::pin(async move {
let operand1: i32 = *context.get_argument("operand1")?;
let operand2: i32 = *context.get_argument("operand2")?;
Ok(match self.0 {
Operation::Add => operand1 + operand2,
Operation::Subtract => operand1 - operand2,
Operation::Multiply => operand1 * operand2,
Operation::Divide => operand1 / operand2,
})
})
}
}
let mut dispatcher = CommandDispatcher::new();
dispatcher.register(
CommandArgumentBuilder::new(
"arithmetic",
"A command which adds two integers, returning the result",
)
.then(
RequiredArgumentBuilder::new("operand1", IntegerArgumentType::any())
.then(
LiteralArgumentBuilder::new("+").then(
RequiredArgumentBuilder::new("operand2", IntegerArgumentType::any())
.executes(Executor(Operation::Add)),
),
)
.then(
LiteralArgumentBuilder::new("-").then(
RequiredArgumentBuilder::new("operand2", IntegerArgumentType::any())
.executes(Executor(Operation::Subtract)),
),
)
.then(
LiteralArgumentBuilder::new("*").then(
RequiredArgumentBuilder::new("operand2", IntegerArgumentType::any())
.executes(Executor(Operation::Multiply)),
),
)
.then(
LiteralArgumentBuilder::new("/").then(
RequiredArgumentBuilder::new("operand2", IntegerArgumentType::any())
.executes(Executor(Operation::Divide)),
),
),
),
);
let source = CommandSource::dummy();
assert_eq!(
dispatcher.execute_input("arithmetic 3 + -7", &source).await,
Ok(-4)
);
assert_eq!(
dispatcher.execute_input("arithmetic 4 - -8", &source).await,
Ok(12)
);
assert_eq!(
dispatcher.execute_input("arithmetic 2 * 9", &source).await,
Ok(18)
);
assert_eq!(
dispatcher.execute_input("arithmetic 9 / 2", &source).await,
Ok(4)
);
}
#[tokio::test]
async fn alias_simple() {
let mut dispatcher = CommandDispatcher::new();
let executor: for<'c> fn(&'c CommandContext) -> CommandExecutorResult<'c> =
|_| Box::pin(async move { Ok(1) });
dispatcher.register(CommandArgumentBuilder::new("a", "A command").executes(executor));
// Note that we CANNOT use redirect here as node itself needs to execute the command,
// not its 'children'.
dispatcher.register(CommandArgumentBuilder::new("b", "An alias for /a").executes(executor));
let source = CommandSource::dummy();
assert_eq!(dispatcher.execute_input("a", &source).await, Ok(1));
assert_eq!(dispatcher.execute_input("b", &source).await, Ok(1));
}
#[tokio::test]
async fn alias_complex() {
struct Executor;
impl CommandExecutor for Executor {
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
Box::pin(async move { Ok(*context.get_argument("result")?) })
}
}
let mut dispatcher = CommandDispatcher::new();
let a = dispatcher.register(CommandArgumentBuilder::new("a", "A command").then(
RequiredArgumentBuilder::new("result", IntegerArgumentType::any()).executes(Executor),
));
// Note that this time, we SHOULD use redirect - it is leading to another node having `command`.
dispatcher.register(CommandArgumentBuilder::new("b", "An alias for /a").redirect(a));
let source = CommandSource::dummy();
assert_eq!(dispatcher.execute_input("a 5", &source).await, Ok(5));
assert_eq!(dispatcher.execute_input("b 7", &source).await, Ok(7));
}
#[tokio::test]
async fn recurse() {
struct Executor;
impl CommandExecutor for Executor {
fn execute<'a>(&'a self, _context: &'a CommandContext) -> CommandExecutorResult<'a> {
Box::pin(async move { Ok(1) })
}
}
let mut dispatcher = CommandDispatcher::new();
let mut builder = CommandArgumentBuilder::new(
"recurse",
"Recurses itself, doing nothing with the numbers provided",
)
.executes(Executor);
let id = builder.id();
builder = builder.then(
RequiredArgumentBuilder::new("value", IntegerArgumentType::any())
.executes(Executor)
.redirect(id),
);
dispatcher.register(builder);
let source = CommandSource::dummy();
assert_eq!(dispatcher.execute_input("recurse", &source).await, Ok(1));
assert_eq!(dispatcher.execute_input("recurse 4", &source).await, Ok(1));
assert_eq!(
dispatcher.execute_input("recurse 9 -1", &source).await,
Ok(1)
);
assert_eq!(
dispatcher
.execute_input("recurse 9 7 -6 5 -4", &source)
.await,
Ok(1)
);
assert_eq!(
dispatcher
.execute_input("recurse 1 2 4 8 16 32 64 128 256 512", &source)
.await,
Ok(1)
);
}
}

View File

@@ -0,0 +1,205 @@
pub mod attached;
pub mod detached;
pub mod dispatcher;
pub mod tree;
use crate::command::argument_types::argument_type::AnyArgumentType;
use crate::command::context::command_context::CommandContext;
use crate::command::context::command_source::CommandSource;
use crate::command::errors::command_syntax_error::CommandSyntaxError;
use crate::command::node::attached::NodeId;
use crate::command::node::detached::GlobalNodeId;
use std::borrow::Cow;
use std::pin::Pin;
use std::sync::Arc;
/// Represents a [`CommandExecutor`]'s result.
pub type CommandExecutorResult<'a> =
Pin<Box<dyn Future<Output = Result<i32, CommandSyntaxError>> + Send + 'a>>;
/// A struct implementing this trait is able to run with a given context.
pub trait CommandExecutor: Sync + Send {
/// Executes this executor for a command.
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a>;
}
impl<F> CommandExecutor for F
where
F: for<'c> Fn(&'c CommandContext) -> CommandExecutorResult<'c> + Send + Sync,
{
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
self(context)
}
}
/// A function that takes a context and returns a command result.
pub type Command = Arc<dyn CommandExecutor>;
/// Represents the result of [`Arc<CommandSource>`]s from a [`CommandContext`].
pub type RedirectModifierResult<'a> =
Pin<Box<dyn Future<Output = Result<Vec<Arc<CommandSource>>, CommandSyntaxError>> + Send + 'a>>;
/// A function that performs the required modification.
pub type RedirectModifierExecutor =
dyn for<'c> Fn(&'c CommandContext) -> RedirectModifierResult<'c> + Send + Sync;
/// A function that returns a new collection of sources from a given context.
#[derive(Clone)]
pub enum RedirectModifier {
/// Always returns only the source from the given context.
OneSource,
/// Returns multiple [`CommandSource`]s from one context via
/// custom behavior.
Custom(Arc<RedirectModifierExecutor>),
}
impl RedirectModifier {
/// Tries to provide a [`Vec`] of [`Arc<CommandSource>`] from a
/// given [`CommandContext`].
#[must_use]
pub fn sources<'c>(&self, command_context: &'c CommandContext) -> RedirectModifierResult<'c> {
match self {
Self::OneSource => Box::pin(async move { Ok(vec![command_context.source.clone()]) }),
Self::Custom(function) => function(command_context),
}
}
}
/// Represents the result of a node requirement as a pinned boxed [`Future`].
pub type RequirementResult<'a> = Pin<Box<dyn Future<Output = bool> + Send + 'a>>;
/// A structure that returns if the source is qualified enough to run the command.
#[derive(Clone)]
pub enum Requirement {
/// Always returns `true`, i.e. no matter the source,
/// it will always be qualified enough to run the command,
/// according to this requirement.
AlwaysQualified,
/// The given source must satisfy the condition to
/// be allowed to run the command.
Condition(Arc<dyn Fn(&CommandSource) -> RequirementResult<'_> + Send + Sync>),
}
impl Requirement {
/// Evaluates the given condition, returning whether the
/// given [`CommandSource`] satisfies this requirement.
#[must_use]
pub fn evaluate<'a>(&'a self, command_source: &'a CommandSource) -> RequirementResult<'a> {
match self {
Self::AlwaysQualified => Box::pin(async { true }),
Self::Condition(condition) => condition(command_source),
}
}
}
/// Stores common owned data for a node.
#[derive(Clone)]
pub struct OwnedNodeData {
pub global_id: GlobalNodeId,
pub requirement: Requirement,
pub modifier: RedirectModifier,
pub forks: bool,
pub command: Option<Command>,
pub permission: Option<Cow<'static, str>>,
}
/// Represents the extra metadata of a node storing a literal.
#[derive(Clone)]
pub struct LiteralNodeMetadata {
pub literal: Cow<'static, str>,
pub literal_lowercase: String,
}
impl LiteralNodeMetadata {
pub fn new(literal: impl Into<Cow<'static, str>>) -> Self {
let literal = literal.into();
Self {
literal: literal.clone(),
literal_lowercase: literal.to_lowercase(),
}
}
}
/// A special type of [`LiteralNodeMetadata`], containing
/// a description for the command as well.
#[derive(Clone)]
pub struct CommandNodeMetadata {
pub literal: Cow<'static, str>,
pub literal_lowercase: String,
pub description: Cow<'static, str>,
}
impl CommandNodeMetadata {
pub fn new(
literal: impl Into<Cow<'static, str>>,
description: impl Into<Cow<'static, str>>,
) -> Self {
let literal = literal.into();
Self {
literal: literal.clone(),
literal_lowercase: literal.to_lowercase(),
description: description.into(),
}
}
}
/// Represents the extra metadata of an argument of any type.
#[derive(Clone)]
pub struct ArgumentNodeMetadata {
pub name: Cow<'static, str>,
pub argument_type: Arc<dyn AnyArgumentType>,
}
impl ArgumentNodeMetadata {
pub fn new(
name: impl Into<Cow<'static, str>>,
argument_type: Arc<dyn AnyArgumentType>,
) -> Self {
Self {
name: name.into(),
argument_type,
}
}
}
/// Represents the extra metadata for nodes of different types. Can be of the root, a literal, command or an argument.
pub enum NodeMetadata {
/// Metadata of the root node.
Root,
/// Metadata of a literal node that doesn't start a command.
Literal(LiteralNodeMetadata),
/// Metadata of a literal node that starts a command.
Command(CommandNodeMetadata),
/// Metadata of an argument node.
Argument(ArgumentNodeMetadata),
}
/// Stores where this redirection would lead to.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum Redirection {
/// Leads to the root of the tree.
Root,
/// Leads to a node in the tree from its tree-local ID.
Local(NodeId),
/// Leads to a node in the tree from its global ID.
Global(GlobalNodeId),
}
impl<T: Into<NodeId>> From<T> for Redirection {
fn from(value: T) -> Self {
Self::Local(value.into())
}
}
impl From<GlobalNodeId> for Redirection {
fn from(value: GlobalNodeId) -> Self {
Self::Global(value)
}
}

View File

@@ -0,0 +1,524 @@
use crate::command::context::command_context::{CommandContextBuilder, ParsedArgument};
use crate::command::context::command_source::CommandSource;
use crate::command::context::string_range::StringRange;
use crate::command::errors::command_syntax_error::CommandSyntaxError;
use crate::command::errors::error_types::LITERAL_INCORRECT;
use crate::command::node::Redirection;
use crate::command::node::attached::{
ArgumentAttachedNode, ArgumentNodeId, AttachedNode, CommandAttachedNode, CommandNodeId,
LiteralAttachedNode, LiteralNodeId, NodeClassification, NodeId, RootAttachedNode,
};
use crate::command::node::detached::{CommandDetachedNode, DetachedNode, GlobalNodeId};
use crate::command::string_reader::StringReader;
use pumpkin_util::text::TextComponent;
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use std::num::NonZero;
use std::ops::{Index, IndexMut};
use std::sync::Arc;
/// The constant local ID occupied by the root node.
pub const ROOT_NODE_ID: NodeId = NodeId(NonZero::new(1).unwrap());
/// A consumer which takes ambiguity of input (when two or more nodes are satisfied)
pub trait AmbiguityConsumer {
fn ambiguous(
&mut self,
tree: &Tree,
parent: NodeId,
child: NodeId,
sibling: NodeId,
inputs: Vec<String>,
);
}
/// Allows a way to store the kind of node
/// along with its ID.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum NodeIdClassification {
Root,
Literal(LiteralNodeId),
Command(CommandNodeId),
Argument(ArgumentNodeId),
}
/// Represents an entire tree of nodes.
/// It all starts from the root node, which arise
/// to children nodes, which arise to their children,
/// and so on.
///
/// This allows redirection and forking
/// between two nodes, even if from different commands.
///
/// This tree can be indexed like an array but with [`NodeId`].
///
/// # Hierarchy
/// This tree can have four different types of nodes, which are the following:
///
/// - **Root**:
/// Does not have a parent. Exactly one instance of this type of node
/// exists per [`Tree`]. Always identifiable by [`ROOT_NODE_ID`].
///
/// - **Command**:
/// Its parent must be the root node, and specifies the start of a
/// command definition.
///
/// - **Literal**:
/// Accepts a particular constant word.
///
/// - **Argument**:
/// Parses and accepts a specific type of value. This is very dynamic.
#[derive(Clone)]
pub struct Tree {
/// All the nodes stored in this tree.
///
/// In this vector, indices starting at 0 indicates the first node (ID = 1),
/// 1 indicates the second node (ID = 2) and so on.
nodes: Vec<AttachedNode>,
/// Keys linking [`GlobalNodeId`] to the [`NodeId`] for this tree.
/// Useful for redirecting.
ids_map: FxHashMap<GlobalNodeId, NodeId>,
}
impl Default for Tree {
fn default() -> Self {
Self::new()
}
}
impl Tree {
/// Constructs a new tree, containing a new root node without children.
#[must_use]
pub fn new() -> Self {
let node = RootAttachedNode::new();
let mut ids_map = FxHashMap::default();
ids_map.insert(node.owned.global_id, ROOT_NODE_ID);
Self {
nodes: vec![AttachedNode::Root(node)],
ids_map,
}
}
/// Allocates a new [`NodeId`] by creating a new unique one.
const fn alloc(&self) -> NodeId {
NodeId(NonZero::new(self.nodes.len() + 1).expect("expected a non-zero id"))
}
/// Helper to attach a given [`AttachedNode`], returning
/// its [`NodeId`].
fn add(&mut self, node: AttachedNode) -> NodeId {
let global_id = node.global_id();
let local_id = self.alloc();
// Update state variables.
self.nodes.push(node);
self.ids_map.insert(global_id, local_id);
local_id
}
/// Helper to attach a [`DetachedNode`] irreversibly
/// into this [`Tree`], returning the ID of the now attached
/// node.
fn attach(&mut self, node: DetachedNode) -> NodeId {
// First, we decompose this node.
let node = node.decompose();
// Add its children to this tree.
let mut children = FxHashMap::with_capacity_and_hasher(node.children.len(), FxBuildHasher);
for (child_name, child) in node.children {
let child_id = self.attach(child);
children.insert(child_name, child_id);
}
// Now create the node to be 'attached'.
let node = AttachedNode::from_parts(node.owned, children, node.redirect, node.meta);
self.add(node)
}
/// Gets the size of this [`Tree`], which is the number of nodes this tree contains.
#[must_use]
pub const fn size(&self) -> usize {
self.nodes.len()
}
/// Gets the size of this [`Tree`], which is the number of nodes this tree contains.
#[must_use]
pub fn size_nonzero(&self) -> NonZero<usize> {
self.nodes
.len()
.try_into()
.expect("Expected non-zero size, but Tree was somehow zero-sized")
}
/// Adds a [`CommandDetachedNode`] to the root node of this tree.
pub fn add_child_to_root(&mut self, node: impl Into<CommandDetachedNode>) -> CommandNodeId {
// First, attach the node to this tree.
let node = self.attach(node.into().into());
self.add_attached_child(ROOT_NODE_ID, node);
// This is safe as the node ID now points to a `CommandAttachedNode`.
CommandNodeId(node.0)
}
/// Adds a child to a given node.
///
/// # Panics
///
/// Panics if the node to be added to a non-root node is a [`CommandDetachedNode`].
///
/// Essentially, this means that a [`CommandDetachedNode`] must have the root node
/// of the tree *as its parent*, to be attached to the tree.
pub fn add_child(&mut self, parent: NodeId, node: impl Into<DetachedNode>) -> NodeId {
let node = node.into();
assert!(
parent == ROOT_NODE_ID || !matches!(node, DetachedNode::Command(_)),
"Cannot add a CommandDetachedNode as a child of a non-root node"
);
// First, attach the node to this tree.
let node = self.attach(node);
self.add_attached_child(parent, node);
node
}
/// Adds an already-attached child to a given node.
///
/// # Panics
///
/// Panics if the node to be added to a non-root node is a [`CommandAttachedNode`],
/// or if the node to be added to a node is a [`RootAttachedNode`],
///
/// Essentially, this means that a [`CommandAttachedNode`] must have the root node
/// of the tree *as its parent*, and [`RootAttachedNode`] cannot have a parent.
fn add_attached_child(&mut self, parent: NodeId, node: NodeId) {
assert!(
parent == ROOT_NODE_ID || self[node].classification() != NodeClassification::Command,
"Cannot add a CommandAttachedNode as a child of a non-root node"
);
let node_name = self[node].name();
let child = self[parent].children_ref().get(&node_name);
if let Some(child) = child {
let node_command = self[node].command().clone();
let node_children: Vec<NodeId> = self[node].children_ref().values().copied().collect();
let child = *child;
// Merge onto the child.
if let Some(command) = node_command {
self[child].set_command(Some(command));
}
for grandchild in node_children {
self.add_attached_child(child, grandchild);
}
} else {
self[parent].children_mut_ref().insert(node_name, node);
}
}
/// Gets the children of a given node in the tree.
#[must_use]
pub fn get_children(&self, node: NodeId) -> Vec<NodeId> {
self[node].children_ref().values().copied().collect()
}
/// Returns whether the given node is able to be used by a given source.
#[must_use]
pub async fn can_use(&self, node: NodeId, source: &CommandSource) -> bool {
source
.has_permission_from_option(self[node].permission())
.await
&& self[node].requirement().evaluate(source).await
}
/// Finds ambiguities of input and gives them to the [`AmbiguityConsumer`].
pub fn find_ambiguities(&self, node: NodeId, consumer: &mut impl AmbiguityConsumer) {
let mut matches: FxHashSet<String> = FxHashSet::default();
for child in self.get_children(node) {
for sibling in self.get_children(node) {
if child == sibling {
continue;
}
for input in self[child].examples() {
if self[sibling].is_valid_input(&input) {
matches.insert(input.clone());
}
}
if !matches.is_empty() {
consumer.ambiguous(self, node, child, sibling, matches.drain().collect());
}
}
self.find_ambiguities(child, consumer);
}
}
/// Classifies a given node to a typed ID.
#[must_use]
pub fn classify_id(&self, node: NodeId) -> NodeIdClassification {
match self[node].classification() {
NodeClassification::Root => NodeIdClassification::Root,
NodeClassification::Literal => NodeIdClassification::Literal(LiteralNodeId(node.0)),
NodeClassification::Command => NodeIdClassification::Command(CommandNodeId(node.0)),
NodeClassification::Argument => NodeIdClassification::Argument(ArgumentNodeId(node.0)),
}
}
pub fn get_relevant_nodes(&self, reader: &mut StringReader, node: NodeId) -> Vec<NodeId> {
// TODO: Determine whether this function should be optimized or not.
let children = self.get_children(node);
let mut literals = Vec::new();
let mut commands = Vec::new();
let mut arguments = Vec::new();
for child in children {
let id = self.classify_id(child);
match id {
NodeIdClassification::Root => {}
NodeIdClassification::Literal(literal) => literals.push(literal),
NodeIdClassification::Command(command) => commands.push(command),
NodeIdClassification::Argument(arg) => arguments.push(arg),
}
}
// Priority order:
// 1. Commands > Literals
// 2. Arguments
if !literals.is_empty() || !commands.is_empty() {
let cursor = reader.cursor();
while !matches!(reader.peek(), None | Some(' ')) {
reader.skip();
}
let new_cursor = reader.cursor();
reader.set_cursor(cursor);
let text = &reader.string()[cursor..new_cursor];
for command in commands {
if self[command].meta.literal == text {
return vec![command.into()];
}
}
for literal in literals {
if self[literal].meta.literal == text {
return vec![literal.into()];
}
}
}
arguments.into_iter().map(ArgumentNodeId::into).collect()
}
/// Parses the given node, returning an error on failure.
pub fn parse(
&self,
node_id: NodeId,
reader: &mut StringReader,
command_context_builder: &mut CommandContextBuilder,
) -> Result<(), CommandSyntaxError> {
match &self[node_id] {
AttachedNode::Root(_) => {}
AttachedNode::Literal(node) => {
let start = reader.cursor();
let Ok(end) = AttachedNode::parse_literal(reader, &node.meta.literal) else {
return Err(LITERAL_INCORRECT
.create(reader, TextComponent::text(node.meta.literal.to_string())));
};
command_context_builder.with_node(node_id, StringRange::between(start, end));
}
AttachedNode::Command(node) => {
let start = reader.cursor();
let Ok(end) = AttachedNode::parse_literal(reader, &node.meta.literal) else {
return Err(LITERAL_INCORRECT
.create(reader, TextComponent::text(node.meta.literal.to_string())));
};
command_context_builder.with_node(node_id, StringRange::between(start, end));
}
AttachedNode::Argument(node) => {
let start = reader.cursor();
let result = node.meta.argument_type.parse(reader)?;
let range = StringRange::between(start, reader.cursor());
let parsed = ParsedArgument::new(range, result);
command_context_builder.with_argument(node.meta.name.to_string(), Arc::new(parsed));
command_context_builder.with_node(node_id, range);
}
}
Ok(())
}
/// Resolves the given redirection with respect to this tree, which is the node from
/// which redirection takes place.
///
/// Returns [`Some`] if the node required could be found, and
/// returns [`None`] otherwise.
#[must_use]
pub fn resolve(&self, redirect: Redirection) -> Option<NodeId> {
match redirect {
Redirection::Root => Some(ROOT_NODE_ID),
Redirection::Global(id) => self.ids_map.get(&id).copied(),
Redirection::Local(id) => (id.0 < self.size_nonzero()).then_some(id),
}
}
}
impl Index<NodeId> for Tree {
type Output = AttachedNode;
fn index(&self, index: NodeId) -> &Self::Output {
&self.nodes[index.0.get() - 1]
}
}
impl IndexMut<NodeId> for Tree {
fn index_mut(&mut self, index: NodeId) -> &mut Self::Output {
&mut self.nodes[index.0.get() - 1]
}
}
/// Macro helper to create [`Index`] and [`IndexMut`] for [`Tree`] with typed IDs.
macro_rules! impl_index_index_mut {
($node_id: ident -> AttachedNode::$attached_node_enum: ident($attached_node: ident)) => {
impl Index<$node_id> for Tree {
type Output = $attached_node;
fn index(&self, index: $node_id) -> &Self::Output {
if let AttachedNode::$attached_node_enum(node) = &self.nodes[index.0.get() - 1] {
node
} else {
unreachable!(
"Node should have been AttachedNode::{}",
stringify!($attached_node_enum)
)
}
}
}
impl IndexMut<$node_id> for Tree {
fn index_mut(&mut self, index: $node_id) -> &mut Self::Output {
if let AttachedNode::$attached_node_enum(node) = &mut self.nodes[index.0.get() - 1]
{
node
} else {
unreachable!(
"Node should have been AttachedNode::{}",
stringify!($attached_node_enum)
)
}
}
}
};
}
impl_index_index_mut!(LiteralNodeId -> AttachedNode::Literal(LiteralAttachedNode));
impl_index_index_mut!(CommandNodeId -> AttachedNode::Command(CommandAttachedNode));
impl_index_index_mut!(ArgumentNodeId -> AttachedNode::Argument(ArgumentAttachedNode));
#[cfg(test)]
mod test {
use crate::command::argument_builder::{
ArgumentBuilder, CommandArgumentBuilder, LiteralArgumentBuilder, RequiredArgumentBuilder,
};
use crate::command::argument_types::core::string::StringArgumentType;
use crate::command::node::attached::NodeId;
use crate::command::node::tree::{AmbiguityConsumer, Tree};
#[test]
fn adding_nodes() {
// New tree (containing only one root node)
let mut tree = Tree::new();
assert_eq!(tree.size(), 1);
// Adding one node.
tree.add_child_to_root(CommandArgumentBuilder::new("foo", "A test command"));
assert_eq!(tree.size(), 2);
// Adding a node with children.
tree.add_child_to_root(
// Each subcommand is a child.
CommandArgumentBuilder::new("bar", "Another test command")
.then(LiteralArgumentBuilder::new("baz"))
.then(LiteralArgumentBuilder::new("qux")),
);
assert_eq!(tree.size(), 5);
}
#[test]
fn adding_children_to_attached_node() {
let mut tree = Tree::new();
let parent: NodeId = tree
.add_child_to_root(CommandArgumentBuilder::new("foo", "A test command"))
.into();
tree.add_child(parent, LiteralArgumentBuilder::new("baz"));
tree.add_child(parent, LiteralArgumentBuilder::new("qux"));
assert_eq!(tree.size(), 4);
assert_eq!(tree.get_children(parent).len(), 2);
}
#[test]
#[should_panic = "Cannot add a CommandDetachedNode as a child of a non-root node"]
fn adding_command_node_to_non_root_node() {
let mut tree = Tree::new();
let parent: NodeId = tree
.add_child_to_root(CommandArgumentBuilder::new("foo", "A test command"))
.into();
tree.add_child(
parent,
CommandArgumentBuilder::new("bar", "Another test command"),
);
}
#[test]
fn finding_ambiguities() {
struct Consumer {
inputs_received: usize,
expected_parent: NodeId,
expected_sibling: NodeId,
}
impl AmbiguityConsumer for Consumer {
fn ambiguous(
&mut self,
_tree: &Tree,
parent: NodeId,
_child: NodeId,
sibling: NodeId,
inputs: Vec<String>,
) {
self.inputs_received += inputs.len();
assert_eq!(self.expected_parent, parent);
assert_eq!(self.expected_sibling, sibling);
}
}
let mut tree = Tree::new();
let parent: NodeId = tree
.add_child_to_root(CommandArgumentBuilder::new("foo", "A test command"))
.into();
tree.add_child(parent, LiteralArgumentBuilder::new("hello"));
tree.add_child(parent, LiteralArgumentBuilder::new("bye"));
let sibling = tree.add_child(
parent,
RequiredArgumentBuilder::new("string", StringArgumentType::SingleWord),
);
let mut consumer = Consumer {
inputs_received: 0,
expected_parent: parent,
expected_sibling: sibling,
};
tree.find_ambiguities(parent, &mut consumer);
assert_eq!(consumer.inputs_received, 2);
}
}

View File

@@ -317,6 +317,26 @@ impl<'a> StringReader<'a> {
.create(self, TextComponent::text(c.to_string())))
}
}
/// Converts this reader into a `'static` form, which
/// is useful for snapshotting the reader.
#[must_use]
pub fn into_owned(self) -> StringReader<'static> {
StringReader {
string: Cow::Owned(self.string.into_owned()),
byte_cursor: self.byte_cursor,
}
}
/// Clones this reader into a `'static` form, which
/// is useful for snapshotting the reader.
#[must_use]
pub fn clone_into_owned(&self) -> StringReader<'static> {
StringReader {
string: Cow::Owned(self.string.to_string()),
byte_cursor: self.byte_cursor,
}
}
}
impl ContextProvider for StringReader<'_> {

View File

@@ -0,0 +1,478 @@
pub mod suggestions;
use pumpkin_util::text::TextComponent;
use std::fmt::Debug;
use std::hash::Hash;
use crate::command::context::string_range::StringRange;
/// A structure that describes the text of a suggestion.
/// It's actual value can either be a [`String`], or an [`i32`].
///
/// Use the [`new`] method to create new [`SuggestionType`]s.
///
/// If you want to use an `i32` for a suggestion's text,
/// go with [`SuggestionType::Integer`]. In all other cases,
/// go with [`SuggestionType::Text`].
///
/// # Invariant
/// A [`SuggestionText::Text`] **shall not exist** if it can instead
/// be fully expressed as a [`SuggestionText::Integer`] (no leading zeros).
/// This is important to establish proper ordering.
/// ```
/// use pumpkin::command::suggestion::SuggestionText;
///
/// let five_suggestion_1 = SuggestionText::new(5);
/// let five_suggestion_2 = SuggestionText::new("5");
/// let zero_five_suggestion = SuggestionText::new("05");
///
/// // `five_suggestion_1` and `five_suggestion_2`
/// // are both instances of `SuggestionText::Integer`,
/// // as guaranteed by the invariant, both having
/// // the same integer `5`.
/// assert_eq!(five_suggestion_1, five_suggestion_2);
///
/// // `zero_five_suggestion` contains a leading zero,
/// // and hence does not have an integer representing it
/// // fully, so it is an instance of `SuggestionText::Text`.
/// assert_ne!(five_suggestion_1, zero_five_suggestion);
/// ```
/// Violating this invariant is a logic error.
///
/// [`new`]: SuggestionText::new
#[derive(Debug, Clone)]
pub enum SuggestionText {
/// The normal one to use. Stores a [`String`].
Text(String),
/// The one to use for integral suggestions. Stores an [`i32`].
/// Note that a cached [`String`] is stored inside this value
/// so that [`String`] allocations don't occur when this object is compared.
Integer { cached_text: String, value: i32 },
}
impl From<String> for SuggestionText {
fn from(text: String) -> Self {
if let Ok(integer) = text.parse::<i32>()
&& integer.to_string() == text
{
Self::Integer {
cached_text: text,
value: integer,
}
} else {
Self::Text(text)
}
}
}
impl From<&str> for SuggestionText {
fn from(text: &str) -> Self {
text.to_owned().into()
}
}
impl From<i32> for SuggestionText {
fn from(text: i32) -> Self {
Self::Integer {
cached_text: text.to_string(),
value: text,
}
}
}
impl SuggestionText {
/// Provides the internally cached text: this is important so that
/// we don't allocate a new string every time we want to
/// compare two [`SuggestionText`]s.
#[must_use]
const fn cached_text(&self) -> &String {
match self {
Self::Text(text) => text,
Self::Integer { cached_text, .. } => cached_text,
}
}
/// Creates a new [`SuggestionText`] from a usable value.
/// This value can be a `&str`, a [`String`], or an `i32`.
pub fn new(value: impl Into<Self>) -> Self {
value.into()
}
}
impl Eq for SuggestionText {}
impl PartialEq for SuggestionText {
fn eq(&self, other: &Self) -> bool {
self.cached_text() == other.cached_text()
}
}
impl Hash for SuggestionText {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.cached_text().hash(state);
}
}
/// A structure that describes a suggestion
/// that may be applied to a string or
/// expanded using a command and range.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Suggestion {
pub range: StringRange,
pub text: SuggestionText,
pub tooltip: Option<TextComponent>,
}
impl Suggestion {
/// Constructs a [`Suggestion`] from its range and text (which can either be a
/// [`String`] or an [`i32`]).
#[must_use]
pub fn without_tooltip<T>(range: StringRange, text: T) -> Self
where
T: Into<SuggestionText>,
{
Self {
range,
text: text.into(),
tooltip: None,
}
}
/// Constructs a [`Suggestion`] from its range, text (which can either be a
/// [`String`] or an [`i32`]), and a tooltip component.
#[must_use]
pub fn with_tooltip<T>(range: StringRange, text: T, tooltip: TextComponent) -> Self
where
T: Into<SuggestionText>,
{
Self {
range,
text: text.into(),
tooltip: Some(tooltip),
}
}
/// Constructs a [`Suggestion`] from its range, text (which can either be a
/// [`String`] or an [`i32`]), and an [`Option`] of [`TextComponent`].
#[must_use]
pub fn new<T>(range: StringRange, text: T, tooltip: Option<TextComponent>) -> Self
where
T: Into<SuggestionText>,
{
Self {
range,
text: text.into(),
tooltip,
}
}
/// Gets the internal [`SuggestionText`] that represents the text of this suggestion,
/// but as a String cloned from the cache.
#[must_use]
pub fn text_as_string(&self) -> String {
self.text_as_string_ref().clone()
}
/// Gets the internal [`SuggestionText`] that represents the text of this suggestion,
/// but as a reference of a String taken directly from the cache without any cloning.
#[must_use]
pub const fn text_as_string_ref(&self) -> &String {
self.text.cached_text()
}
/// Gets the internal [`SuggestionText`] that represents the text of this suggestion,
/// but as a `&str` taken directly from the cache without any cloning.
#[must_use]
pub const fn text_as_str(&self) -> &str {
self.text.cached_text().as_str()
}
/// Applies this [`Suggestion`] into a string,
/// returning a new [`String`] with the applied suggestion.
#[must_use]
pub fn apply(&self, input: &str) -> String {
let text_string = self.text_as_string_ref();
if self.range.start == 0 && self.range.end == input.len() {
return text_string.clone();
}
let mut result: String =
String::with_capacity(input.len() - self.range.len() + text_string.len());
result.push_str(&input[0..self.range.start]); // usize >= 0
result.push_str(text_string);
if self.range.end < input.len() {
result.push_str(&input[self.range.end..]);
}
result
}
/// Expands this [`Suggestion`] onto a command with a [`StringRange`],
/// returning a new [`Suggestion`].
#[must_use]
pub fn expand(&self, command: &str, range: StringRange) -> Self {
if self.range == range {
return Self::new(self.range, self.text.clone(), self.tooltip.clone());
}
let mut result = String::new();
if range.start < self.range.start {
result.push_str(&command[range.start..self.range.start]);
}
result.push_str(&self.text_as_string());
if range.end > self.range.end {
result.push_str(&command[self.range.end..range.end]);
}
Self::new(range, result, self.tooltip.clone())
}
}
#[cfg(test)]
mod test {
use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder};
use crate::command::{context::string_range::StringRange, suggestion::Suggestion};
use std::slice;
#[test]
fn apply_insertion_start() {
let suggestion = Suggestion::without_tooltip(StringRange::at(0), "Pumpkin once said: ");
assert_eq!(
suggestion.apply("'Server is now running'"),
"Pumpkin once said: 'Server is now running'".to_owned()
);
}
#[test]
fn apply_insertion_middle() {
let suggestion = Suggestion::without_tooltip(StringRange::at(6), "Efficient, ");
assert_eq!(
suggestion.apply("Fast, and User-Friendly"),
"Fast, Efficient, and User-Friendly".to_owned()
);
}
#[test]
fn apply_insertion_end() {
let suggestion = Suggestion::without_tooltip(StringRange::at(10), " has stopped");
assert_eq!(
suggestion.apply("The server"),
"The server has stopped".to_owned()
);
}
#[test]
fn apply_replacement_start() {
let suggestion = Suggestion::without_tooltip(StringRange::between(0, 5), "Goodbye");
assert_eq!(
suggestion.apply("Hello world!"),
"Goodbye world!".to_owned()
);
}
#[test]
fn apply_replacement_middle() {
let suggestion = Suggestion::without_tooltip(StringRange::between(6, 11), "melon");
assert_eq!(suggestion.apply("Hello world!"), "Hello melon!".to_owned());
}
#[test]
fn apply_replacement_end() {
let suggestion = Suggestion::without_tooltip(StringRange::between(13, 23), "fruit.");
assert_eq!(
suggestion.apply("Pumpkin is a vegetable."),
"Pumpkin is a fruit.".to_owned()
);
}
#[test]
fn apply_replacement_everything() {
let suggestion =
Suggestion::without_tooltip(StringRange::between(0, 36), "This is a phrase.");
assert_eq!(
suggestion.apply("I'm not related to the other phrase."),
"This is a phrase.".to_owned()
);
}
#[test]
fn expand_unchanged() {
let suggestion = Suggestion::without_tooltip(StringRange::at(1), "oo");
assert_eq!(suggestion.expand("f", StringRange::at(1)), suggestion);
}
#[test]
fn expand_left() {
let suggestion = Suggestion::without_tooltip(StringRange::at(1), "oo");
assert_eq!(
suggestion.expand("f", StringRange::between(0, 1)),
Suggestion::without_tooltip(StringRange::between(0, 1), "foo")
);
}
#[test]
fn expand_right() {
let suggestion = Suggestion::without_tooltip(StringRange::at(0), "ba");
assert_eq!(
suggestion.expand("r", StringRange::between(0, 1)),
Suggestion::without_tooltip(StringRange::between(0, 1), "bar")
);
}
#[test]
fn expand_both() {
let suggestion = Suggestion::without_tooltip(
StringRange::at(30),
"sheared to make a Carved Pumpkin and can be ",
);
assert_eq!(
suggestion.expand(
"A block called Pumpkin can be crafted into its seeds which can be planted",
StringRange::between(0, 52)
),
Suggestion::without_tooltip(
StringRange::between(0, 52),
"A block called Pumpkin can be sheared to make a Carved Pumpkin and can be crafted into its seeds"
)
);
}
#[test]
fn expand_replacement() {
let suggestion = Suggestion::without_tooltip(StringRange::between(6, 11), "everyone");
assert_eq!(
suggestion.expand("Hello world!", StringRange::between(0, 12)),
Suggestion::without_tooltip(StringRange::between(0, 12), "Hello everyone!")
);
}
#[test]
fn merge_empty() {
let merged = Suggestions::merge("foo b", &[]);
assert!(merged.is_empty());
}
#[test]
fn merge_single() {
let suggestions = Suggestions::new(
StringRange::at(5),
vec![Suggestion::without_tooltip(StringRange::at(5), "ar")],
);
let merged = Suggestions::merge("foo b", slice::from_ref(&suggestions));
assert_eq!(merged, suggestions);
}
#[test]
fn merge_multiple() {
let a = Suggestions::new(
StringRange::at(5),
vec![
Suggestion::without_tooltip(StringRange::at(5), "ar"),
Suggestion::without_tooltip(StringRange::at(5), "az"),
Suggestion::without_tooltip(StringRange::at(5), "ars"),
],
);
let b = Suggestions::new(
StringRange::between(4, 5),
vec![
Suggestion::without_tooltip(StringRange::between(4, 5), "foo"),
Suggestion::without_tooltip(StringRange::between(4, 5), "qux"),
Suggestion::without_tooltip(StringRange::between(4, 5), "BAR"),
],
);
let merged = Suggestions::merge("foo b", &[a, b]);
assert_eq!(
&merged.suggestions,
&[
Suggestion::without_tooltip(StringRange::between(4, 5), "BAR"),
Suggestion::without_tooltip(StringRange::between(4, 5), "bar"),
Suggestion::without_tooltip(StringRange::between(4, 5), "bars"),
Suggestion::without_tooltip(StringRange::between(4, 5), "baz"),
Suggestion::without_tooltip(StringRange::between(4, 5), "foo"),
Suggestion::without_tooltip(StringRange::between(4, 5), "qux"),
]
);
}
#[test]
fn suggest_append() {
let suggestions = SuggestionsBuilder::new("Hello w", 6)
.suggest("world!")
.build();
assert_eq!(
suggestions.suggestions,
vec![Suggestion::without_tooltip(
StringRange::between(6, 7),
"world!"
)]
);
assert_eq!(suggestions.range, StringRange::between(6, 7));
}
#[test]
fn suggest_replace() {
let suggestions = SuggestionsBuilder::new("Hello w", 6)
.suggest("everyone!")
.build();
assert_eq!(
suggestions.suggestions,
vec![Suggestion::without_tooltip(
StringRange::between(6, 7),
"everyone!"
)]
);
assert_eq!(suggestions.range, StringRange::between(6, 7));
}
#[test]
fn suggest_noop() {
let suggestions = SuggestionsBuilder::new("hello", 6).build();
assert!(suggestions.is_empty());
}
#[test]
fn suggest_multiple() {
let suggestions = SuggestionsBuilder::new("Cut a b", 6)
.suggest("banana")
.suggest("plum")
.suggest("tomato")
.build();
assert_eq!(
suggestions.suggestions,
vec![
Suggestion::without_tooltip(StringRange::between(6, 7), "banana"),
Suggestion::without_tooltip(StringRange::between(6, 7), "plum"),
Suggestion::without_tooltip(StringRange::between(6, 7), "tomato")
]
);
assert_eq!(suggestions.range, StringRange::between(6, 7));
}
#[test]
fn sort() {
let suggestions = SuggestionsBuilder::new("A random thing to say is foobar", 25)
.suggest("1")
.suggest(9)
.suggest("4")
.suggest(6)
.suggest("05")
.suggest(533)
.suggest("x8")
.suggest("a")
.suggest("x")
.suggest("6x")
.build();
let internal_sorted_repr: Vec<String> = suggestions
.suggestions
.into_iter()
.map(|suggestion| suggestion.text_as_string())
.collect();
assert_eq!(
internal_sorted_repr,
vec!["05", "1", "4", "6", "6x", "9", "533", "a", "x", "x8"]
);
}
}

View File

@@ -0,0 +1,272 @@
use crate::command::context::string_range::StringRange;
use crate::command::suggestion::{Suggestion, SuggestionText};
use pumpkin_util::text::TextComponent;
use std::borrow::Borrow;
use std::cmp::Ordering;
use std::collections::HashSet;
/// Represents a builder of [`Suggestion`]s.
pub struct SuggestionsBuilder {
/// Represents the starting position of the [`SuggestionsBuilder`]
/// from the start of the input string.
pub start: usize,
/// Represents the input of the [`SuggestionsBuilder`].
pub input: String,
/// Represents the lowercase version of the input of the [`SuggestionsBuilder`].
pub input_lowercase: String,
/// The eventual result of this [`SuggestionsBuilder`].
pub result: Vec<Suggestion>,
}
impl SuggestionsBuilder {
/// Constructs a new [`SuggestionsBuilder`] from the given
/// input string and a starting position relative to it.
#[must_use]
pub fn new(input: &str, start: usize) -> Self {
Self {
input: input.to_string(),
input_lowercase: input.to_lowercase(),
start,
result: Vec::new(),
}
}
/// Gets the remaining substring of the underlying input string.
#[must_use]
pub fn remaining(&self) -> &str {
&self.input[self.start..]
}
/// Gets the remaining substring of the underlying lowercased input string.
#[must_use]
pub fn remaining_lowercase(&self) -> &str {
&self.input_lowercase[self.start..]
}
/// Builds the [`Suggestions`] object, consuming itself in the process.
#[must_use]
pub fn build(self) -> Suggestions {
Suggestions::create(&self.input, self.result)
}
/// Adds a suggestion without a tooltip to this builder.
#[must_use]
pub fn suggest<T>(mut self, text: T) -> Self
where
T: Into<SuggestionText>,
{
let text = text.into();
if text.cached_text() != self.remaining() {
self.result.push(Suggestion::without_tooltip(
StringRange::between(self.start, self.input.len()),
text,
));
}
self
}
/// Adds a suggestion with a tooltip to this builder.
#[must_use]
pub fn suggest_with_tooltip<T>(mut self, text: T, tooltip: TextComponent) -> Self
where
T: Into<SuggestionText>,
{
let text = text.into();
if text.cached_text() != self.remaining() {
self.result.push(Suggestion::with_tooltip(
StringRange::between(self.start, self.input.len()),
text,
tooltip,
));
}
self
}
/// Adds all suggestions from another [`SuggestionsBuilder`] to this one.
#[must_use]
pub fn append(mut self, other: &Self) -> Self {
for suggestion in &other.result {
self.result.push(suggestion.clone());
}
self
}
/// Creates another [`SuggestionsBuilder`] from this one
/// by copying the input and taking the starting position.
#[must_use]
pub fn create_offset(&self, start: usize) -> Self {
Self {
input: self.input.clone(),
input_lowercase: self.input_lowercase.clone(),
start,
result: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Suggestions {
pub range: StringRange,
pub suggestions: Vec<Suggestion>,
}
impl Suggestions {
/// Constructs a new [`Suggestions`] structure from
/// a range and [`Suggestion`]s.
#[must_use]
pub const fn new(range: StringRange, suggestions: Vec<Suggestion>) -> Self {
Self { range, suggestions }
}
/// Constructs a new [`Suggestions`] of zero size and no range.
#[must_use]
pub const fn empty() -> Self {
Self::new(StringRange::at(0), vec![])
}
/// Returns whether this [`Suggestions`] *is* of zero size.
#[must_use]
pub const fn is_empty(&self) -> bool {
self.suggestions.is_empty()
}
/// Merges all [`Suggestions`] provided with a command into a single [`Suggestions`].
#[must_use]
pub fn merge<I, S>(command: &str, input: I) -> Self
where
I: IntoIterator<Item = S>,
S: Borrow<Self>,
{
let input: Vec<S> = input.into_iter().collect();
if input.is_empty() {
return Self::empty();
} else if input.len() == 1 {
return input[0].borrow().clone();
}
let mut texts = HashSet::new();
for suggestions in &input {
for suggestion in &suggestions.borrow().suggestions {
texts.insert(suggestion);
}
}
Self::create(command, texts)
}
/// Creates a single [`Suggestions`] structure from
/// many [`Suggestion`]s and a command.
#[must_use]
pub fn create<I, S>(command: &str, suggestions: I) -> Self
where
I: IntoIterator<Item = S>,
S: Borrow<Suggestion>,
{
let suggestions: Vec<S> = suggestions.into_iter().collect();
if suggestions.is_empty() {
return Self::empty();
}
// First, we figure out the range encompassing all suggestions provided.
let range = suggestions
.iter()
.map(|s| s.borrow().range)
.reduce(StringRange::encompass)
.unwrap();
let mut texts: HashSet<Suggestion> = HashSet::new();
for suggestion in &suggestions {
texts.insert(suggestion.borrow().expand(command, range));
}
Self::new(range, Self::sort(texts))
}
/// Sorts a set of [`Suggestion`]s, in the following precedence:
///
/// 1. If both suggestions are integers, their integral value is compared.
/// 2. Otherwise, compare their text lexicographically.
fn sort(suggestions: HashSet<Suggestion>) -> Vec<Suggestion> {
enum PushSide {
Text,
Integer,
Break,
}
let mut text_suggestions = Vec::new();
let mut integer_suggestions = Vec::new();
let len = suggestions.len();
for suggestion in suggestions {
match suggestion.text {
SuggestionText::Text(text) => {
text_suggestions.push((text, suggestion.tooltip, suggestion.range));
}
SuggestionText::Integer { cached_text, value } => integer_suggestions.push((
cached_text,
value,
suggestion.tooltip,
suggestion.range,
)),
}
}
// We need not preserve the original order as
// there cannot be two or more equivalent suggestions in a set.
text_suggestions.sort_unstable_by(|a, b| a.0.cmp(&b.0));
integer_suggestions.sort_unstable_by_key(|x| x.1);
let mut text_iter = text_suggestions.into_iter().peekable();
let mut integer_iter = integer_suggestions.into_iter().peekable();
let mut suggestions = Vec::with_capacity(len);
loop {
let text = text_iter.peek();
let integer = integer_iter.peek();
let side = match (text, integer) {
(Some(text), Some(integer)) => match text.0.cmp(&integer.0) {
Ordering::Less => PushSide::Text,
Ordering::Greater => PushSide::Integer,
Ordering::Equal => unreachable!(),
},
(Some(_), None) => PushSide::Text,
(None, Some(_)) => PushSide::Integer,
(None, None) => PushSide::Break,
};
match side {
PushSide::Text => {
let text = text_iter.next().unwrap();
suggestions.push(Suggestion {
text: SuggestionText::Text(text.0),
tooltip: text.1,
range: text.2,
});
}
PushSide::Integer => {
let text = integer_iter.next().unwrap();
suggestions.push(Suggestion {
text: SuggestionText::Integer {
cached_text: text.0,
value: text.1,
},
tooltip: text.2,
range: text.3,
});
}
PushSide::Break => break,
}
}
suggestions
}
}