mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
513
pumpkin/src/command/argument_builder.rs
Normal file
513
pumpkin/src/command/argument_builder.rs
Normal 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"));
|
||||
}
|
||||
}
|
||||
139
pumpkin/src/command/argument_types/argument_type.rs
Normal file
139
pumpkin/src/command/argument_types/argument_type.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
45
pumpkin/src/command/argument_types/core/bool.rs
Normal file
45
pumpkin/src/command/argument_types/core/bool.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
106
pumpkin/src/command/argument_types/core/double.rs
Normal file
106
pumpkin/src/command/argument_types/core/double.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
106
pumpkin/src/command/argument_types/core/float.rs
Normal file
106
pumpkin/src/command/argument_types/core/float.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
117
pumpkin/src/command/argument_types/core/integer.rs
Normal file
117
pumpkin/src/command/argument_types/core/integer.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
121
pumpkin/src/command/argument_types/core/long.rs
Normal file
121
pumpkin/src/command/argument_types/core/long.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
46
pumpkin/src/command/argument_types/core/mod.rs
Normal file
46
pumpkin/src/command/argument_types/core/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
128
pumpkin/src/command/argument_types/core/string.rs
Normal file
128
pumpkin/src/command/argument_types/core/string.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
45
pumpkin/src/command/argument_types/mod.rs
Normal file
45
pumpkin/src/command/argument_types/mod.rs
Normal 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;
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
|
||||
698
pumpkin/src/command/context/command_context.rs
Normal file
698
pumpkin/src/command/context/command_context.rs
Normal 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) = ¤t_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());
|
||||
}
|
||||
}
|
||||
568
pumpkin/src/command/context/command_source.rs
Normal file
568
pumpkin/src/command/context/command_source.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
3
pumpkin/src/command/context/mod.rs
Normal file
3
pumpkin/src/command/context/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod command_context;
|
||||
pub mod command_source;
|
||||
pub mod string_range;
|
||||
76
pumpkin/src/command/context/string_range.rs
Normal file
76
pumpkin/src/command/context/string_range.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
436
pumpkin/src/command/node/attached.rs
Normal file
436
pumpkin/src/command/node/attached.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
255
pumpkin/src/command/node/detached.rs
Normal file
255
pumpkin/src/command/node/detached.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
485
pumpkin/src/command/node/dispatcher.rs
Normal file
485
pumpkin/src/command/node/dispatcher.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
205
pumpkin/src/command/node/mod.rs
Normal file
205
pumpkin/src/command/node/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
524
pumpkin/src/command/node/tree.rs
Normal file
524
pumpkin/src/command/node/tree.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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<'_> {
|
||||
|
||||
478
pumpkin/src/command/suggestion/mod.rs
Normal file
478
pumpkin/src/command/suggestion/mod.rs
Normal 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"]
|
||||
);
|
||||
}
|
||||
}
|
||||
272
pumpkin/src/command/suggestion/suggestions.rs
Normal file
272
pumpkin/src/command/suggestion/suggestions.rs
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user