diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2022-04-20 01:34:12 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-04-20 01:34:12 +0000 |
| commit | 5fd87615cf1514c7f9a0358988964768ded3f06e (patch) | |
| tree | 001c3c760fdae8fe7b72cacb1f87d3703cc4e82c | |
| parent | d09762f5d38ab1200fb08ca3b1178813b4e47081 (diff) | |
| parent | be194c1ca136100fd8f53ed068d82c9f7ae32870 (diff) | |
| download | azalea-drasl-5fd87615cf1514c7f9a0358988964768ded3f06e.tar.xz | |
Merge pull request #1 from mat-1/brigadier
azalea-brigadier
52 files changed, 2841 insertions, 0 deletions
@@ -68,6 +68,10 @@ dependencies = [ ] [[package]] +name = "azalea-brigadier" +version = "0.1.0" + +[[package]] name = "azalea-chat" version = "0.1.0" dependencies = [ @@ -8,4 +8,5 @@ members = [ "azalea-core", "azalea-auth", "azalea-nbt", + "azalea-brigadier", ] diff --git a/azalea-brigadier/Cargo.toml b/azalea-brigadier/Cargo.toml new file mode 100644 index 00000000..a7ebf618 --- /dev/null +++ b/azalea-brigadier/Cargo.toml @@ -0,0 +1,8 @@ +[package] +edition = "2021" +name = "azalea-brigadier" +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/azalea-brigadier/README.md b/azalea-brigadier/README.md new file mode 100644 index 00000000..a7318566 --- /dev/null +++ b/azalea-brigadier/README.md @@ -0,0 +1,3 @@ +# Azalea Brigadier + +A Rust port of Mojang's [Brigadier](https://github.com/Mojang/brigadier) command parsing and dispatching library. diff --git a/azalea-brigadier/src/arguments/argument_type.rs b/azalea-brigadier/src/arguments/argument_type.rs new file mode 100644 index 00000000..029e4696 --- /dev/null +++ b/azalea-brigadier/src/arguments/argument_type.rs @@ -0,0 +1,7 @@ +use std::{any::Any, rc::Rc}; + +use crate::{exceptions::CommandSyntaxException, string_reader::StringReader}; + +pub trait ArgumentType { + fn parse(&self, reader: &mut StringReader) -> Result<Rc<dyn Any>, CommandSyntaxException>; +} diff --git a/azalea-brigadier/src/arguments/integer_argument_type.rs b/azalea-brigadier/src/arguments/integer_argument_type.rs new file mode 100644 index 00000000..336046a7 --- /dev/null +++ b/azalea-brigadier/src/arguments/integer_argument_type.rs @@ -0,0 +1,54 @@ +use std::{any::Any, rc::Rc}; + +use crate::{ + context::CommandContext, + exceptions::{BuiltInExceptions, CommandSyntaxException}, + string_reader::StringReader, +}; + +use super::ArgumentType; + +#[derive(Default)] +struct Integer { + pub minimum: Option<i32>, + pub maximum: Option<i32>, +} + +impl ArgumentType for Integer { + fn parse(&self, reader: &mut StringReader) -> Result<Rc<dyn Any>, CommandSyntaxException> { + let start = reader.cursor; + let result = reader.read_int()?; + if let Some(minimum) = self.minimum { + if result < minimum { + reader.cursor = start; + return Err(BuiltInExceptions::IntegerTooSmall { + found: result, + min: minimum, + } + .create_with_context(reader)); + } + } + if let Some(maximum) = self.maximum { + if result > maximum { + reader.cursor = start; + return Err(BuiltInExceptions::IntegerTooBig { + found: result, + max: maximum, + } + .create_with_context(reader)); + } + } + Ok(Rc::new(result)) + } +} + +pub fn integer() -> impl ArgumentType { + Integer::default() +} +pub fn get_integer<S>(context: &CommandContext<S>, name: &str) -> Option<i32> { + context + .argument(name) + .unwrap() + .downcast_ref::<i32>() + .copied() +} diff --git a/azalea-brigadier/src/arguments/mod.rs b/azalea-brigadier/src/arguments/mod.rs new file mode 100644 index 00000000..dec39297 --- /dev/null +++ b/azalea-brigadier/src/arguments/mod.rs @@ -0,0 +1,4 @@ +mod argument_type; +pub mod integer_argument_type; + +pub use argument_type::ArgumentType; diff --git a/azalea-brigadier/src/builder/argument_builder.rs b/azalea-brigadier/src/builder/argument_builder.rs new file mode 100644 index 00000000..d26b2a8a --- /dev/null +++ b/azalea-brigadier/src/builder/argument_builder.rs @@ -0,0 +1,137 @@ +use crate::{context::CommandContext, modifier::RedirectModifier, tree::CommandNode}; + +use super::{literal_argument_builder::Literal, required_argument_builder::Argument}; +use std::{cell::RefCell, fmt::Debug, rc::Rc}; + +#[derive(Debug, Clone)] +pub enum ArgumentBuilderType { + Literal(Literal), + Argument(Argument), +} + +/// A node that hasn't yet been built. +pub struct ArgumentBuilder<S> { + arguments: CommandNode<S>, + + command: Option<Rc<dyn Fn(&CommandContext<S>) -> i32>>, + requirement: Rc<dyn Fn(Rc<S>) -> bool>, + target: Option<Rc<RefCell<CommandNode<S>>>>, + + forks: bool, + modifier: Option<Rc<RedirectModifier<S>>>, +} + +impl<S> Clone for ArgumentBuilder<S> { + fn clone(&self) -> Self { + Self { + arguments: self.arguments.clone(), + command: self.command.clone(), + requirement: self.requirement.clone(), + target: self.target.clone(), + forks: self.forks, + modifier: self.modifier.clone(), + } + } +} + +/// A node that isn't yet built. +impl<S> ArgumentBuilder<S> { + pub fn new(value: ArgumentBuilderType) -> Self { + Self { + arguments: CommandNode { + value, + ..Default::default() + }, + command: None, + requirement: Rc::new(|_| true), + forks: false, + modifier: None, + target: None, + } + } + + pub fn then(&mut self, argument: ArgumentBuilder<S>) -> Self { + self.then_built(argument.build()) + } + + pub fn then_built(&mut self, argument: CommandNode<S>) -> Self { + self.arguments.add_child(&Rc::new(RefCell::new(argument))); + self.clone() + } + + pub fn executes<F>(&mut self, f: F) -> Self + where + F: Fn(&CommandContext<S>) -> i32 + 'static, + { + self.command = Some(Rc::new(f)); + self.clone() + } + + pub fn requires<F>(&mut self, requirement: F) -> Self + where + F: Fn(Rc<S>) -> bool + 'static, + { + self.requirement = Rc::new(requirement); + self.clone() + } + + pub fn redirect(&mut self, target: Rc<RefCell<CommandNode<S>>>) -> Self { + self.forward(target, None, false) + } + + pub fn fork( + &mut self, + target: Rc<RefCell<CommandNode<S>>>, + modifier: Rc<RedirectModifier<S>>, + ) -> Self { + self.forward(target, Some(modifier), true) + } + + pub fn forward( + &mut self, + target: Rc<RefCell<CommandNode<S>>>, + modifier: Option<Rc<RedirectModifier<S>>>, + fork: bool, + ) -> Self { + if !self.arguments.children.is_empty() { + panic!("Cannot forward a node with children"); + } + self.target = Some(target); + self.modifier = modifier; + self.forks = fork; + self.clone() + } + + pub fn build(self) -> CommandNode<S> { + let mut result = CommandNode { + value: self.arguments.value, + command: self.command, + requirement: self.requirement, + redirect: self.target, + modifier: self.modifier, + forks: self.forks, + arguments: Default::default(), + children: Default::default(), + literals: Default::default(), + }; + + for argument in self.arguments.children.values() { + result.add_child(argument); + } + + result + } +} + +impl<S> Debug for ArgumentBuilder<S> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ArgumentBuilder") + .field("arguments", &self.arguments) + // .field("command", &self.command) + // .field("requirement", &self.requirement) + .field("target", &self.target) + .field("forks", &self.forks) + // .field("modifier", &self.modifier) + .finish() + } +} diff --git a/azalea-brigadier/src/builder/literal_argument_builder.rs b/azalea-brigadier/src/builder/literal_argument_builder.rs new file mode 100644 index 00000000..6627ffdc --- /dev/null +++ b/azalea-brigadier/src/builder/literal_argument_builder.rs @@ -0,0 +1,24 @@ +use super::argument_builder::{ArgumentBuilder, ArgumentBuilderType}; + +#[derive(Debug, Clone, Default)] +pub struct Literal { + pub value: String, +} +impl Literal { + pub fn new(value: &str) -> Self { + Self { + value: value.to_string(), + } + } +} + +impl From<Literal> for ArgumentBuilderType { + fn from(literal: Literal) -> Self { + Self::Literal(literal) + } +} + +/// Shortcut for creating a new literal builder node. +pub fn literal<S>(value: &str) -> ArgumentBuilder<S> { + ArgumentBuilder::new(ArgumentBuilderType::Literal(Literal::new(value))) +} diff --git a/azalea-brigadier/src/builder/mod.rs b/azalea-brigadier/src/builder/mod.rs new file mode 100644 index 00000000..26f2f644 --- /dev/null +++ b/azalea-brigadier/src/builder/mod.rs @@ -0,0 +1,3 @@ +pub mod argument_builder; +pub mod literal_argument_builder; +pub mod required_argument_builder; diff --git a/azalea-brigadier/src/builder/required_argument_builder.rs b/azalea-brigadier/src/builder/required_argument_builder.rs new file mode 100644 index 00000000..9d4d9e0a --- /dev/null +++ b/azalea-brigadier/src/builder/required_argument_builder.rs @@ -0,0 +1,45 @@ +use super::argument_builder::{ArgumentBuilder, ArgumentBuilderType}; +use crate::{ + arguments::ArgumentType, exceptions::CommandSyntaxException, string_reader::StringReader, +}; +use std::{any::Any, fmt::Debug, rc::Rc}; + +/// An argument node type. The `T` type parameter is the type of the argument, +/// which can be anything. +#[derive(Clone)] +pub struct Argument { + pub name: String, + parser: Rc<dyn ArgumentType>, +} +impl Argument { + pub fn new(name: &str, parser: Rc<dyn ArgumentType>) -> Self { + Self { + name: name.to_string(), + parser, + } + } + + pub fn parse(&self, reader: &mut StringReader) -> Result<Rc<dyn Any>, CommandSyntaxException> { + self.parser.parse(reader) + } +} + +impl From<Argument> for ArgumentBuilderType { + fn from(argument: Argument) -> Self { + Self::Argument(argument) + } +} + +impl Debug for Argument { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Argument") + .field("name", &self.name) + // .field("parser", &self.parser) + .finish() + } +} + +/// Shortcut for creating a new argument builder node. +pub fn argument<S>(name: &str, parser: impl ArgumentType + 'static) -> ArgumentBuilder<S> { + ArgumentBuilder::new(Argument::new(name, Rc::new(parser)).into()) +} diff --git a/azalea-brigadier/src/command_dispatcher.rs b/azalea-brigadier/src/command_dispatcher.rs new file mode 100644 index 00000000..eab42dd8 --- /dev/null +++ b/azalea-brigadier/src/command_dispatcher.rs @@ -0,0 +1,298 @@ +use crate::{ + builder::argument_builder::ArgumentBuilder, + context::{CommandContext, CommandContextBuilder}, + exceptions::{BuiltInExceptions, CommandSyntaxException}, + parse_results::ParseResults, + string_reader::StringReader, + tree::CommandNode, +}; +use std::{cell::RefCell, cmp::Ordering, collections::HashMap, marker::PhantomData, mem, rc::Rc}; + +#[derive(Default)] +pub struct CommandDispatcher<S> { + pub root: Rc<RefCell<CommandNode<S>>>, + _marker: PhantomData<S>, +} + +impl<S> CommandDispatcher<S> { + pub fn new() -> Self { + Self { + root: Rc::new(RefCell::new(CommandNode::default())), + _marker: PhantomData, + } + } + + pub fn register(&mut self, node: ArgumentBuilder<S>) -> Rc<RefCell<CommandNode<S>>> { + let build = Rc::new(RefCell::new(node.build())); + self.root.borrow_mut().add_child(&build); + build + } + + pub fn parse(&self, command: StringReader, source: Rc<S>) -> ParseResults<S> { + let context = CommandContextBuilder::new( + Rc::new(self.clone()), + source, + self.root.clone(), + command.cursor(), + ); + self.parse_nodes(&self.root, &command, context).unwrap() + } + + fn parse_nodes( + &self, + node: &Rc<RefCell<CommandNode<S>>>, + original_reader: &StringReader, + context_so_far: CommandContextBuilder<S>, + ) -> Result<ParseResults<S>, CommandSyntaxException> { + let source = context_so_far.source.clone(); + let mut errors = HashMap::<Rc<CommandNode<S>>, CommandSyntaxException>::new(); + let mut potentials: Vec<ParseResults<S>> = vec![]; + let cursor = original_reader.cursor(); + + for child in node + .borrow() + .get_relevant_nodes(&mut original_reader.clone()) + { + if !child.borrow().can_use(source.clone()) { + continue; + } + let mut context = context_so_far.clone(); + let mut reader = original_reader.clone(); + + let parse_with_context_result = + child.borrow().parse_with_context(&mut reader, &mut context); + if let Err(ex) = parse_with_context_result { + errors.insert( + Rc::new((*child.borrow()).clone()), + BuiltInExceptions::DispatcherParseException { + message: ex.message(), + } + .create_with_context(&reader), + ); + reader.cursor = cursor; + continue; + } + if reader.can_read() && reader.peek() != ' ' { + errors.insert( + Rc::new((*child.borrow()).clone()), + BuiltInExceptions::DispatcherExpectedArgumentSeparator + .create_with_context(&reader), + ); + reader.cursor = cursor; + continue; + } + + context.with_command(&child.borrow().command); + if reader.can_read_length(if child.borrow().redirect.is_none() { + 2 + } else { + 1 + }) { + reader.skip(); + if let Some(redirect) = &child.borrow().redirect { + let child_context = CommandContextBuilder::new( + Rc::new(self.clone()), + source, + redirect.clone(), + reader.cursor, + ); + let parse = self + .parse_nodes(redirect, &reader, child_context) + .expect("Parsing nodes failed"); + context.with_child(Rc::new(parse.context)); + return Ok(ParseResults { + context, + reader: parse.reader, + exceptions: parse.exceptions, + }); + } else { + let parse = self + .parse_nodes(&child, &reader, context) + .expect("Parsing nodes failed"); + potentials.push(parse); + } + } else { + potentials.push(ParseResults { + context, + reader, + exceptions: HashMap::new(), + }); + } + } + + if !potentials.is_empty() { + if potentials.len() > 1 { + potentials.sort_by(|a, b| { + if !a.reader.can_read() && b.reader.can_read() { + return Ordering::Less; + }; + if a.reader.can_read() && !b.reader.can_read() { + return Ordering::Greater; + }; + if a.exceptions.is_empty() && !b.exceptions.is_empty() { + return Ordering::Less; + }; + if !a.exceptions.is_empty() && b.exceptions.is_empty() { + return Ordering::Greater; + }; + Ordering::Equal + }) + } + let best_potential = potentials.into_iter().next().unwrap(); + return Ok(best_potential); + } + + Ok(ParseResults { + context: context_so_far, + reader: original_reader.clone(), + exceptions: errors, + }) + } + + pub fn execute( + &self, + input: StringReader, + source: Rc<S>, + ) -> Result<i32, CommandSyntaxException> { + let parse = self.parse(input, source); + Self::execute_parsed(parse) + } + + pub fn add_paths( + &self, + node: Rc<RefCell<CommandNode<S>>>, + result: &mut Vec<Vec<Rc<RefCell<CommandNode<S>>>>>, + parents: Vec<Rc<RefCell<CommandNode<S>>>>, + ) { + let mut current = parents; + current.push(node.clone()); + result.push(current.clone()); + + for child in node.borrow().children.values() { + self.add_paths(child.clone(), result, current.clone()); + } + } + + pub fn get_path(&self, target: CommandNode<S>) -> Vec<String> { + let rc_target = Rc::new(RefCell::new(target)); + let mut nodes: Vec<Vec<Rc<RefCell<CommandNode<S>>>>> = Vec::new(); + self.add_paths(self.root.clone(), &mut nodes, vec![]); + + for list in nodes { + if *list.last().expect("Nothing in list").borrow() == *rc_target.borrow() { + let mut result: Vec<String> = Vec::with_capacity(list.len()); + for node in list { + if node != self.root { + result.push(node.borrow().name().to_string()); + } + } + return result; + } + } + vec![] + } + + pub fn find_node(&self, path: &[&str]) -> Option<Rc<RefCell<CommandNode<S>>>> { + let mut node = self.root.clone(); + for name in path { + if let Some(child) = node.clone().borrow().child(name) { + node = child + } else { + return None; + } + } + Some(node) + } + + /// Executes a given pre-parsed command. + pub fn execute_parsed(parse: ParseResults<S>) -> Result<i32, CommandSyntaxException> { + if parse.reader.can_read() { + if parse.exceptions.len() == 1 { + return Err(parse.exceptions.values().next().unwrap().clone()); + } + if parse.context.range.is_empty() { + return Err( + BuiltInExceptions::DispatcherUnknownCommand.create_with_context(&parse.reader) + ); + } + return Err( + BuiltInExceptions::DispatcherUnknownArgument.create_with_context(&parse.reader) + ); + } + let mut result = 0i32; + let mut successful_forks = 0; + let mut forked = false; + let mut found_command = false; + let command = parse.reader.string(); + let original = parse.context.build(command); + let mut contexts = vec![original]; + let mut next: Vec<CommandContext<S>> = vec![]; + + while !contexts.is_empty() { + for context in contexts.iter() { + let child = &context.child; + if let Some(child) = child { + println!("aaaaaaa {:?}", child); + forked |= child.forks; + if child.has_nodes() { + found_command = true; + let modifier = &context.modifier; + if let Some(modifier) = modifier { + let results = modifier(context); + if let Ok(results) = results { + if !results.is_empty() { + next.extend(results.iter().map(|s| child.copy_for(s.clone()))); + } + } else { + // TODO + // self.consumer.on_command_complete(context, false, 0); + if !forked { + return Err(results.err().unwrap()); + } + } + } else { + next.push(child.copy_for(context.source.clone())); + } + } + } else if let Some(context_command) = &context.command { + found_command = true; + + let value = context_command(context); + result += value; + // consumer.on_command_complete(context, true, value); + successful_forks += 1; + + // TODO: allow context_command to error and handle those errors + } + } + + // move next into contexts and clear next + mem::swap(&mut contexts, &mut next); + next.clear(); + } + + if !found_command { + // consumer.on_command_complete(original, false, 0); + return Err( + BuiltInExceptions::DispatcherUnknownCommand.create_with_context(&parse.reader) + ); + } + + // TODO: this is not how vanilla does it but it works + Ok(if successful_forks >= 2 { + successful_forks + } else { + result + }) + // Ok(if forked { successful_forks } else { result }) + } +} + +impl<S> Clone for CommandDispatcher<S> { + fn clone(&self) -> Self { + Self { + root: self.root.clone(), + _marker: PhantomData, + } + } +} diff --git a/azalea-brigadier/src/context/command_context.rs b/azalea-brigadier/src/context/command_context.rs new file mode 100644 index 00000000..1834a73d --- /dev/null +++ b/azalea-brigadier/src/context/command_context.rs @@ -0,0 +1,80 @@ +use super::{parsed_command_node::ParsedCommandNode, string_range::StringRange, ParsedArgument}; +use crate::{modifier::RedirectModifier, tree::CommandNode}; +use std::{any::Any, cell::RefCell, collections::HashMap, fmt::Debug, rc::Rc}; + +/// A built `CommandContextBuilder`. +pub struct CommandContext<S> { + pub source: Rc<S>, + pub input: String, + pub arguments: HashMap<String, ParsedArgument>, + pub command: Option<Rc<dyn Fn(&CommandContext<S>) -> i32>>, + pub root_node: Rc<RefCell<CommandNode<S>>>, + pub nodes: Vec<ParsedCommandNode<S>>, + pub range: StringRange, + pub child: Option<Rc<CommandContext<S>>>, + pub modifier: Option<Rc<RedirectModifier<S>>>, + pub forks: bool, +} + +impl<S> Clone for CommandContext<S> { + fn clone(&self) -> Self { + Self { + source: self.source.clone(), + input: self.input.clone(), + arguments: self.arguments.clone(), + command: self.command.clone(), + root_node: self.root_node.clone(), + nodes: self.nodes.clone(), + range: self.range.clone(), + child: self.child.clone(), + modifier: self.modifier.clone(), + forks: self.forks, + } + } +} + +impl<S> Debug for CommandContext<S> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CommandContext") + // .field("source", &self.source) + .field("input", &self.input) + // .field("arguments", &self.arguments) + // .field("command", &self.command) + // .field("root_node", &self.root_node) + // .field("nodes", &self.nodes) + .field("range", &self.range) + .field("child", &self.child) + // .field("modifier", &self.modifier) + .field("forks", &self.forks) + .finish() + } +} + +impl<S> CommandContext<S> { + pub fn copy_for(&self, source: Rc<S>) -> Self { + if Rc::ptr_eq(&source, &self.source) { + return self.clone(); + } + CommandContext { + source, + input: self.input.clone(), + arguments: self.arguments.clone(), + command: self.command.clone(), + root_node: self.root_node.clone(), + nodes: self.nodes.clone(), + range: self.range.clone(), + child: self.child.clone(), + modifier: self.modifier.clone(), + forks: self.forks, + } + } + + pub fn has_nodes(&self) -> bool { + !self.nodes.is_empty() + } + + pub fn argument(&self, name: &str) -> Option<Rc<dyn Any>> { + let argument = self.arguments.get(name); + argument.map(|a| a.result.clone()) + } +} diff --git a/azalea-brigadier/src/context/command_context_builder.rs b/azalea-brigadier/src/context/command_context_builder.rs new file mode 100644 index 00000000..f192f6b7 --- /dev/null +++ b/azalea-brigadier/src/context/command_context_builder.rs @@ -0,0 +1,116 @@ +use super::{ + command_context::CommandContext, parsed_command_node::ParsedCommandNode, + string_range::StringRange, ParsedArgument, +}; +use crate::{command_dispatcher::CommandDispatcher, modifier::RedirectModifier, tree::CommandNode}; +use std::{cell::RefCell, collections::HashMap, fmt::Debug, rc::Rc}; + +pub struct CommandContextBuilder<S> { + pub arguments: HashMap<String, ParsedArgument>, + pub root: Rc<RefCell<CommandNode<S>>>, + pub nodes: Vec<ParsedCommandNode<S>>, + pub dispatcher: Rc<CommandDispatcher<S>>, + pub source: Rc<S>, + pub command: Option<Rc<dyn Fn(&CommandContext<S>) -> i32>>, + pub child: Option<Rc<CommandContextBuilder<S>>>, + pub range: StringRange, + pub modifier: Option<Rc<RedirectModifier<S>>>, + pub forks: bool, +} + +impl<S> Clone for CommandContextBuilder<S> { + fn clone(&self) -> Self { + Self { + arguments: self.arguments.clone(), + root: self.root.clone(), + nodes: self.nodes.clone(), + dispatcher: self.dispatcher.clone(), + source: self.source.clone(), + command: self.command.clone(), + child: self.child.clone(), + range: self.range.clone(), + modifier: self.modifier.clone(), + forks: self.forks, + } + } +} + +impl<S> CommandContextBuilder<S> { + pub fn new( + dispatcher: Rc<CommandDispatcher<S>>, + source: Rc<S>, + root_node: Rc<RefCell<CommandNode<S>>>, + start: usize, + ) -> Self { + Self { + arguments: HashMap::new(), + root: root_node, + source, + range: StringRange::at(start), + command: None, + dispatcher, + nodes: vec![], + child: None, + modifier: None, + forks: false, + } + } + + pub fn with_command( + &mut self, + command: &Option<Rc<dyn Fn(&CommandContext<S>) -> i32>>, + ) -> &Self { + self.command = command.clone(); + self + } + pub fn with_child(&mut self, child: Rc<CommandContextBuilder<S>>) -> &Self { + self.child = Some(child); + self + } + pub fn with_argument(&mut self, name: &str, argument: ParsedArgument) -> &Self { + self.arguments.insert(name.to_string(), argument); + self + } + pub fn with_node(&mut self, node: Rc<RefCell<CommandNode<S>>>, range: StringRange) -> &Self { + self.nodes.push(ParsedCommandNode { + node: node.clone(), + range: range.clone(), + }); + self.range = StringRange::encompassing(&self.range, &range); + self.modifier = node.borrow().modifier.clone(); + self.forks = node.borrow().forks; + self + } + + pub fn build(&self, input: &str) -> CommandContext<S> { + CommandContext { + arguments: self.arguments.clone(), + root_node: self.root.clone(), + nodes: self.nodes.clone(), + source: self.source.clone(), + command: self.command.clone(), + child: self.child.clone().map(|c| Rc::new(c.build(input))), + range: self.range.clone(), + forks: self.forks, + modifier: self.modifier.clone(), + input: input.to_string(), + } + } +} + +impl<S> Debug for CommandContextBuilder<S> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CommandContextBuilder") + // .field("arguments", &self.arguments) + .field("root", &self.root) + // .field("nodes", &self.nodes) + // .field("dispatcher", &self.dispatcher) + // .field("source", &self.source) + // .field("command", &self.command) + .field("child", &self.child) + .field("range", &self.range) + // .field("modifier", &self.modifier) + .field("forks", &self.forks) + .finish() + } +} diff --git a/azalea-brigadier/src/context/mod.rs b/azalea-brigadier/src/context/mod.rs new file mode 100644 index 00000000..d535602a --- /dev/null +++ b/azalea-brigadier/src/context/mod.rs @@ -0,0 +1,11 @@ +mod command_context; +mod command_context_builder; +mod parsed_argument; +mod parsed_command_node; +mod string_range; + +pub use command_context::CommandContext; +pub use command_context_builder::CommandContextBuilder; +pub use parsed_argument::ParsedArgument; +pub use parsed_command_node::ParsedCommandNode; +pub use string_range::StringRange; diff --git a/azalea-brigadier/src/context/parsed_argument.rs b/azalea-brigadier/src/context/parsed_argument.rs new file mode 100644 index 00000000..3302b1be --- /dev/null +++ b/azalea-brigadier/src/context/parsed_argument.rs @@ -0,0 +1,8 @@ +use super::string_range::StringRange; +use std::{any::Any, rc::Rc}; + +#[derive(Clone)] +pub struct ParsedArgument { + pub range: StringRange, + pub result: Rc<dyn Any>, +} diff --git a/azalea-brigadier/src/context/parsed_command_node.rs b/azalea-brigadier/src/context/parsed_command_node.rs new file mode 100644 index 00000000..ed49928d --- /dev/null +++ b/azalea-brigadier/src/context/parsed_command_node.rs @@ -0,0 +1,18 @@ +use super::string_range::StringRange; +use crate::tree::CommandNode; +use std::{cell::RefCell, rc::Rc}; + +#[derive(Debug)] +pub struct ParsedCommandNode<S> { + pub node: Rc<RefCell<CommandNode<S>>>, + pub range: StringRange, +} + +impl<S> Clone for ParsedCommandNode<S> { + fn clone(&self) -> Self { + Self { + node: self.node.clone(), + range: self.range.clone(), + } + } +} diff --git a/azalea-brigadier/src/context/string_range.rs b/azalea-brigadier/src/context/string_range.rs new file mode 100644 index 00000000..8ca88624 --- /dev/null +++ b/azalea-brigadier/src/context/string_range.rs @@ -0,0 +1,45 @@ +use std::cmp; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +pub struct StringRange { + start: usize, + end: usize, +} + +impl StringRange { + pub fn new(start: usize, end: usize) -> Self { + Self { start, end } + } + + pub fn at(pos: usize) -> Self { + Self::new(pos, pos) + } + + pub fn between(start: usize, end: usize) -> Self { + Self::new(start, end) + } + + pub fn encompassing(a: &Self, b: &Self) -> Self { + Self::new(cmp::min(a.start, b.start), cmp::max(a.end, b.end)) + } + + pub fn start(&self) -> usize { + self.start + } + + pub fn end(&self) -> usize { + self.end + } + + pub fn get<'a>(&self, reader: &'a str) -> &'a str { + &reader[self.start..self.end] + } + + pub fn is_empty(&self) -> bool { + self.start == self.end + } + + pub fn length(&self) -> usize { + self.end - self.start + } +} diff --git a/azalea-brigadier/src/exceptions/builtin_exceptions.rs b/azalea-brigadier/src/exceptions/builtin_exceptions.rs new file mode 100644 index 00000000..09951a03 --- /dev/null +++ b/azalea-brigadier/src/exceptions/builtin_exceptions.rs @@ -0,0 +1,159 @@ +use std::fmt; + +use crate::{message::Message, string_reader::StringReader}; + +use super::command_syntax_exception::CommandSyntaxException; + +#[derive(Clone, PartialEq)] +pub enum BuiltInExceptions { + DoubleTooSmall { found: f64, min: f64 }, + DoubleTooBig { found: f64, max: f64 }, + + FloatTooSmall { found: f32, min: f32 }, + FloatTooBig { found: f32, max: f32 }, + + IntegerTooSmall { found: i32, min: i32 }, + IntegerTooBig { found: i32, max: i32 }, + + LongTooSmall { found: i64, min: i64 }, + LongTooBig { found: i64, max: i64 }, + + LiteralIncorrect { expected: String }, + + ReaderExpectedStartOfQuote, + ReaderExpectedEndOfQuote, + ReaderInvalidEscape { character: char }, + ReaderInvalidBool { value: String }, + ReaderInvalidInt { value: String }, + ReaderExpectedInt, + ReaderInvalidLong { value: String }, + ReaderExpectedLong, + ReaderInvalidDouble { value: String }, + ReaderExpectedDouble, + ReaderInvalidFloat { value: String }, + ReaderExpectedFloat, + ReaderExpectedBool, + ReaderExpectedSymbol { symbol: char }, + + DispatcherUnknownCommand, + DispatcherUnknownArgument, + DispatcherExpectedArgumentSeparator, + DispatcherParseException { message: String }, +} + +impl fmt::Debug for BuiltInExceptions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BuiltInExceptions::DoubleTooSmall { found, min } => { + write!(f, "Double must not be less than {}, found {}", min, found) + } + BuiltInExceptions::DoubleTooBig { found, max } => { + write!(f, "Double must not be more than {}, found {}", max, found) + } + + BuiltInExceptions::FloatTooSmall { found, min } => { + write!(f, "Float must not be less than {}, found {}", min, found) + } + BuiltInExceptions::FloatTooBig { found, max } => { + write!(f, "Float must not be more than {}, found {}", max, found) + } + + BuiltInExceptions::IntegerTooSmall { found, min } => { + write!(f, "Integer must not be less than {}, found {}", min, found) + } + BuiltInExceptions::IntegerTooBig { found, max } => { + write!(f, "Integer must not be more than {}, found {}", max, found) + } + + BuiltInExceptions::LongTooSmall { found, min } => { + write!(f, "Long must not be less than {}, found {}", min, found) + } + BuiltInExceptions::LongTooBig { found, max } => { + write!(f, "Long must not be more than {}, found {}", max, found) + } + + BuiltInExceptions::LiteralIncorrect { expected } => { + write!(f, "Expected literal {}", expected) + } + + BuiltInExceptions::ReaderExpectedStartOfQuote => { + write!(f, "Expected quote to start a string") + } + BuiltInExceptions::ReaderExpectedEndOfQuote => { + write!(f, "Unclosed quoted string") + } + BuiltInExceptions::ReaderInvalidEscape { character } => { + write!( + f, + "Invalid escape sequence '{}' in quoted string", + character + ) + } + BuiltInExceptions::ReaderInvalidBool { value } => { + write!( + f, + "Invalid bool, expected true or false but found '{}'", + value + ) + } + BuiltInExceptions::ReaderInvalidInt { value } => { + write!(f, "Invalid Integer '{}'", value) + } + BuiltInExceptions::ReaderExpectedInt => { + write!(f, "Expected Integer") + } + BuiltInExceptions::ReaderInvalidLong { value } => { + write!(f, "Invalid long '{}'", value) + } + BuiltInExceptions::ReaderExpectedLong => { + write!(f, "Expected long") + } + BuiltInExceptions::ReaderInvalidDouble { value } => { + write!(f, "Invalid double '{}'", value) + } + BuiltInExceptions::ReaderExpectedDouble => { + write!(f, "Expected double") + } + BuiltInExceptions::ReaderInvalidFloat { value } => { + write!(f, "Invalid Float '{}'", value) + } + BuiltInExceptions::ReaderExpectedFloat => { + write!(f, "Expected Float") + } + BuiltInExceptions::ReaderExpectedBool => { + write!(f, "Expected bool") + } + BuiltInExceptions::ReaderExpectedSymbol { symbol } => { + write!(f, "Expected '{}'", symbol) + } + + BuiltInExceptions::DispatcherUnknownCommand => { + write!(f, "Unknown command") + } + BuiltInExceptions::DispatcherUnknownArgument => { + write!(f, "Incorrect argument for command") + } + BuiltInExceptions::DispatcherExpectedArgumentSeparator => { + write!( + f, + "Expected whitespace to end one argument, but found trailing data" + ) + } + BuiltInExceptions::DispatcherParseException { message } => { + write!(f, "Could not parse command: {}", message) + } + } + } +} + +impl BuiltInExceptions { + pub fn create(self) -> CommandSyntaxException { + let message = Message::from(format!("{:?}", self)); + CommandSyntaxException::create(self, message) + } + + pub fn create_with_context(self, reader: &StringReader) -> CommandSyntaxException { + let message = Message::from(format!("{:?}", self)); + CommandSyntaxException::new(self, message, reader.string(), reader.cursor()) + } +} diff --git a/azalea-brigadier/src/exceptions/command_syntax_exception.rs b/azalea-brigadier/src/exceptions/command_syntax_exception.rs new file mode 100644 index 00000000..4bfe9cda --- /dev/null +++ b/azalea-brigadier/src/exceptions/command_syntax_exception.rs @@ -0,0 +1,91 @@ +use std::{cmp, fmt}; + +use super::builtin_exceptions::BuiltInExceptions; +use crate::message::Message; + +#[derive(Clone, PartialEq)] +pub struct CommandSyntaxException { + pub type_: BuiltInExceptions, + message: Message, + input: Option<String>, + cursor: Option<usize>, +} + +const CONTEXT_AMOUNT: usize = 10; + +impl CommandSyntaxException { + pub fn new(type_: BuiltInExceptions, message: Message, input: &str, cursor: usize) -> Self { + Self { + type_, + message, + input: Some(input.to_string()), + cursor: Some(cursor), + } + } + + pub fn create(type_: BuiltInExceptions, message: Message) -> Self { + Self { + type_, + message, + input: None, + cursor: None, + } + } + + pub fn message(&self) -> String { + let mut message = self.message.string(); + let context = self.context(); + if let Some(context) = context { + message.push_str(&format!( + " at position {}: {}", + self.cursor.unwrap_or(usize::MAX), + context + )); + } + message + } + + pub fn raw_message(&self) -> &Message { + &self.message + } + + pub fn context(&self) -> Option<String> { + if let Some(input) = &self.input { + if let Some(cursor) = self.cursor { + let mut builder = String::new(); + let cursor = cmp::min(input.len(), cursor); + + if cursor > CONTEXT_AMOUNT { + builder.push_str("..."); + } + + builder.push_str( + &input + [(cmp::max(0, cursor as isize - CONTEXT_AMOUNT as isize) as usize)..cursor], + ); + builder.push_str("<--[HERE]"); + + return Some(builder); + } + } + None + } + + pub fn get_type(&self) -> &BuiltInExceptions { + &self.type_ + } + + pub fn input(&self) -> &Option<String> { + &self.input + } + + pub fn cursor(&self) -> Option<usize> { + self.cursor + } +} + +impl fmt::Debug for CommandSyntaxException { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.message()) + } +} diff --git a/azalea-brigadier/src/exceptions/mod.rs b/azalea-brigadier/src/exceptions/mod.rs new file mode 100644 index 00000000..6b9c8d62 --- /dev/null +++ b/azalea-brigadier/src/exceptions/mod.rs @@ -0,0 +1,5 @@ +mod builtin_exceptions; +mod command_syntax_exception; + +pub use builtin_exceptions::BuiltInExceptions; +pub use command_syntax_exception::CommandSyntaxException; diff --git a/azalea-brigadier/src/lib.rs b/azalea-brigadier/src/lib.rs new file mode 100644 index 00000000..a294eb19 --- /dev/null +++ b/azalea-brigadier/src/lib.rs @@ -0,0 +1,10 @@ +pub mod arguments; +pub mod builder; +pub mod command_dispatcher; +pub mod context; +pub mod exceptions; +pub mod message; +pub mod modifier; +pub mod parse_results; +pub mod string_reader; +pub mod tree; diff --git a/azalea-brigadier/src/message.rs b/azalea-brigadier/src/message.rs new file mode 100644 index 00000000..75e07d4e --- /dev/null +++ b/azalea-brigadier/src/message.rs @@ -0,0 +1,14 @@ +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Message(String); + +impl Message { + pub fn string(&self) -> String { + self.0.to_string() + } +} + +impl From<String> for Message { + fn from(s: String) -> Self { + Self(s) + } +} diff --git a/azalea-brigadier/src/modifier.rs b/azalea-brigadier/src/modifier.rs new file mode 100644 index 00000000..d40d59f9 --- /dev/null +++ b/azalea-brigadier/src/modifier.rs @@ -0,0 +1,6 @@ +use std::rc::Rc; + +use crate::{context::CommandContext, exceptions::CommandSyntaxException}; + +pub type RedirectModifier<S> = + dyn Fn(&CommandContext<S>) -> Result<Vec<Rc<S>>, CommandSyntaxException>; diff --git a/azalea-brigadier/src/parse_results.rs b/azalea-brigadier/src/parse_results.rs new file mode 100644 index 00000000..3698ae82 --- /dev/null +++ b/azalea-brigadier/src/parse_results.rs @@ -0,0 +1,21 @@ +use crate::{ + context::CommandContextBuilder, exceptions::CommandSyntaxException, + string_reader::StringReader, tree::CommandNode, +}; +use std::{collections::HashMap, fmt::Debug, rc::Rc}; + +pub struct ParseResults<S> { + pub context: CommandContextBuilder<S>, + pub reader: StringReader, + pub exceptions: HashMap<Rc<CommandNode<S>>, CommandSyntaxException>, +} + +impl<S> Debug for ParseResults<S> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ParseResults") + .field("context", &self.context) + // .field("reader", &self.reader) + .field("exceptions", &self.exceptions) + .finish() + } +} diff --git a/azalea-brigadier/src/string_reader.rs b/azalea-brigadier/src/string_reader.rs new file mode 100644 index 00000000..9eb09b57 --- /dev/null +++ b/azalea-brigadier/src/string_reader.rs @@ -0,0 +1,274 @@ +use crate::exceptions::{BuiltInExceptions, CommandSyntaxException}; +use std::str::FromStr; + +#[derive(Clone)] +pub struct StringReader { + string: String, + pub cursor: usize, +} + +const SYNTAX_ESCAPE: char = '\\'; +const SYNTAX_DOUBLE_QUOTE: char = '"'; +const SYNTAX_SINGLE_QUOTE: char = '\''; + +impl From<String> for StringReader { + fn from(string: String) -> Self { + Self { string, cursor: 0 } + } +} +impl From<&str> for StringReader { + fn from(string: &str) -> Self { + Self { + string: string.to_string(), + cursor: 0, + } + } +} + +impl StringReader { + pub fn string(&self) -> &str { + &self.string + } + + pub fn remaining_length(&self) -> usize { + self.string.len() - self.cursor + } + + pub fn total_length(&self) -> usize { + self.string.len() + } + + pub fn get_read(&self) -> &str { + &self.string[..self.cursor] + } + + pub fn remaining(&self) -> &str { + &self.string[self.cursor..] + } + + pub fn can_read_length(&self, length: usize) -> bool { + self.cursor + length <= self.string.len() + } + + pub fn can_read(&self) -> bool { + self.can_read_length(1) + } + + pub fn peek(&self) -> char { + self.string.chars().nth(self.cursor).unwrap() + } + + pub fn peek_offset(&self, offset: usize) -> char { + self.string.chars().nth(self.cursor + offset).unwrap() + } + + pub fn cursor(&self) -> usize { + self.cursor + } + + pub fn read(&mut self) -> char { + let c = self.peek(); + self.cursor += 1; + c + } + + pub fn skip(&mut self) { + self.cursor += 1; + } + + pub fn is_allowed_number(c: char) -> bool { + ('0'..='9').contains(&c) || c == '.' || c == '-' + } + + pub fn is_quoted_string_start(c: char) -> bool { + c == SYNTAX_DOUBLE_QUOTE || c == SYNTAX_SINGLE_QUOTE + } + + pub fn skip_whitespace(&mut self) { + while self.can_read() && self.peek().is_whitespace() { + self.skip(); + } + } + + pub fn read_int(&mut self) -> Result<i32, CommandSyntaxException> { + let start = self.cursor; + while self.can_read() && StringReader::is_allowed_number(self.peek()) { + self.skip(); + } + let number = &self.string[start..self.cursor]; + if number.is_empty() { + return Err(BuiltInExceptions::ReaderExpectedInt.create_with_context(self)); + } + let result = i32::from_str(number); + if result.is_err() { + self.cursor = start; + return Err(BuiltInExceptions::ReaderInvalidInt { + value: number.to_string(), + } + .create_with_context(self)); + } + + Ok(result.unwrap()) + } + + pub fn read_long(&mut self) -> Result<i64, CommandSyntaxException> { + let start = self.cursor; + while self.can_read() && StringReader::is_allowed_number(self.peek()) { + self.skip(); + } + let number = &self.string[start..self.cursor]; + if number.is_empty() { + return Err(BuiltInExceptions::ReaderExpectedLong.create_with_context(self)); + } + let result = i64::from_str(number); + if result.is_err() { + self.cursor = start; + return Err(BuiltInExceptions::ReaderInvalidLong { + value: number.to_string(), + } + .create_with_context(self)); + } + + Ok(result.unwrap()) + } + + pub fn read_double(&mut self) -> Result<f64, CommandSyntaxException> { + let start = self.cursor; + while self.can_read() && StringReader::is_allowed_number(self.peek()) { + self.skip(); + } + let number = &self.string[start..self.cursor]; + if number.is_empty() { + return Err(BuiltInExceptions::ReaderExpectedDouble.create_with_context(self)); + } + let result = f64::from_str(number); + if result.is_err() { + self.cursor = start; + return Err(BuiltInExceptions::ReaderInvalidDouble { + value: number.to_string(), + } + .create_with_context(self)); + } + + Ok(result.unwrap()) + } + + pub fn read_float(&mut self) -> Result<f32, CommandSyntaxException> { + let start = self.cursor; + while self.can_read() && StringReader::is_allowed_number(self.peek()) { + self.skip(); + } + let number = &self.string[start..self.cursor]; + if number.is_empty() { + return Err(BuiltInExceptions::ReaderExpectedFloat.create_with_context(self)); + } + let result = f32::from_str(number); + if result.is_err() { + self.cursor = start; + return Err(BuiltInExceptions::ReaderInvalidFloat { + value: number.to_string(), + } + .create_with_context(self)); + } + + Ok(result.unwrap()) + } + + pub fn is_allowed_in_unquoted_string(c: char) -> bool { + ('0'..='9').contains(&c) + || ('A'..='Z').contains(&c) + || ('a'..='z').contains(&c) + || c == '_' + || c == '-' + || c == '.' + || c == '+' + } + + pub fn read_unquoted_string(&mut self) -> &str { + let start = self.cursor; + while self.can_read() && StringReader::is_allowed_in_unquoted_string(self.peek()) { + self.skip(); + } + &self.string[start..self.cursor] + } + + pub fn read_quoted_string(&mut self) -> Result<String, CommandSyntaxException> { + if !self.can_read() { + return Ok(String::new()); + } + let next = self.peek(); + if !StringReader::is_quoted_string_start(next) { + return Err(BuiltInExceptions::ReaderExpectedStartOfQuote.create_with_context(self)); + } + self.skip(); + self.read_string_until(next) + } + + pub fn read_string_until( + &mut self, + terminator: char, + ) -> Result<String, CommandSyntaxException> { + let mut result = String::new(); + let mut escaped = false; + while self.can_read() { + let c = self.read(); + if escaped { + if c == terminator || c == SYNTAX_ESCAPE { + result.push(c); + escaped = false; + } else { + self.cursor -= 1; + return Err(BuiltInExceptions::ReaderInvalidEscape { character: c } + .create_with_context(self)); + } + } else if c == SYNTAX_ESCAPE { + escaped = true; + } else if c == terminator { + return Ok(result); + } else { + result.push(c); + } + } + + Err(BuiltInExceptions::ReaderExpectedEndOfQuote.create_with_context(self)) + } + + pub fn read_string(&mut self) -> Result<String, CommandSyntaxException> { + if !self.can_read() { + return Ok(String::new()); + } + let next = self.peek(); + if StringReader::is_quoted_string_start(next) { + self.skip(); + return self.read_string_until(next); + } + Ok(self.read_unquoted_string().to_string()) + } + + pub fn read_boolean(&mut self) -> Result<bool, CommandSyntaxException> { + let start = self.cursor; + let value = self.read_string()?; + if value.is_empty() { + return Err(BuiltInExceptions::ReaderExpectedBool.create_with_context(self)); + } + + if value == "true" { + Ok(true) + } else if value == "false" { + Ok(false) + } else { + self.cursor = start; + Err(BuiltInExceptions::ReaderInvalidBool { value }.create_with_context(self)) + } + } + + pub fn expect(&mut self, c: char) -> Result<(), CommandSyntaxException> { + if !self.can_read() || self.peek() != c { + return Err( + BuiltInExceptions::ReaderExpectedSymbol { symbol: c }.create_with_context(self) + ); + } + self.skip(); + Ok(()) + } +} diff --git a/azalea-brigadier/src/tree/mod.rs b/azalea-brigadier/src/tree/mod.rs new file mode 100644 index 00000000..b6181c73 --- /dev/null +++ b/azalea-brigadier/src/tree/mod.rs @@ -0,0 +1,259 @@ +use crate::{ + builder::{ + argument_builder::ArgumentBuilderType, literal_argument_builder::Literal, + required_argument_builder::Argument, + }, + context::{CommandContext, CommandContextBuilder, ParsedArgument, StringRange}, + exceptions::{BuiltInExceptions, CommandSyntaxException}, + modifier::RedirectModifier, + string_reader::StringReader, +}; +use std::{cell::RefCell, collections::HashMap, fmt::Debug, hash::Hash, ptr, rc::Rc}; + +/// An ArgumentBuilder that has been built. +#[non_exhaustive] +pub struct CommandNode<S> { + pub value: ArgumentBuilderType, + + pub children: HashMap<String, Rc<RefCell<CommandNode<S>>>>, + pub literals: HashMap<String, Rc<RefCell<CommandNode<S>>>>, + pub arguments: HashMap<String, Rc<RefCell<CommandNode<S>>>>, + + pub command: Option<Rc<dyn Fn(&CommandContext<S>) -> i32>>, + pub requirement: Rc<dyn Fn(Rc<S>) -> bool>, + pub redirect: Option<Rc<RefCell<CommandNode<S>>>>, + pub forks: bool, + pub modifier: Option<Rc<RedirectModifier<S>>>, +} + +impl<S> Clone for CommandNode<S> { + fn clone(&self) -> Self { + Self { + value: self.value.clone(), + children: self.children.clone(), + literals: self.literals.clone(), + arguments: self.arguments.clone(), + command: self.command.clone(), + requirement: self.requirement.clone(), + redirect: self.redirect.clone(), + forks: self.forks, + modifier: self.modifier.clone(), + } + } +} + +impl<S> CommandNode<S> { + /// Gets the literal, or panics. You should use match if you're not certain about the type. + pub fn literal(&self) -> &Literal { + match self.value { + ArgumentBuilderType::Literal(ref literal) => literal, + _ => panic!("CommandNode::literal() called on non-literal node"), + } + } + /// Gets the argument, or panics. You should use match if you're not certain about the type. + pub fn argument(&self) -> &Argument { + match self.value { + ArgumentBuilderType::Argument(ref argument) => argument, + _ => panic!("CommandNode::argument() called on non-argument node"), + } + } + + pub fn get_relevant_nodes(&self, input: &mut StringReader) -> Vec<Rc<RefCell<CommandNode<S>>>> { + let literals = &self.literals; + + if !literals.is_empty() { + let cursor = input.cursor(); + while input.can_read() && input.peek() != ' ' { + input.skip(); + } + let text: String = input + .string() + .chars() + .skip(cursor) + .take(input.cursor() - cursor) + .collect(); + input.cursor = cursor; + let literal = literals.get(&text); + if let Some(literal) = literal { + return vec![literal.clone()]; + } else { + return self.arguments.values().cloned().collect(); + } + } else { + self.arguments.values().cloned().collect() + } + } + + pub fn can_use(&self, source: Rc<S>) -> bool { + (self.requirement)(source) + } + + pub fn add_child(&mut self, node: &Rc<RefCell<CommandNode<S>>>) { + let child = self.children.get(node.borrow().name()); + if let Some(child) = child { + // We've found something to merge onto + if let Some(command) = &node.borrow().command { + child.borrow_mut().command = Some(command.clone()); + } + for grandchild in node.borrow().children.values() { + child.borrow_mut().add_child(grandchild); + } + } else { + self.children + .insert(node.borrow().name().to_string(), node.clone()); + match &node.borrow().value { + ArgumentBuilderType::Literal(literal) => { + self.literals.insert(literal.value.clone(), node.clone()); + } + ArgumentBuilderType::Argument(argument) => { + self.arguments.insert(argument.name.clone(), node.clone()); + } + } + } + } + + pub fn name(&self) -> &str { + match &self.value { + ArgumentBuilderType::Argument(argument) => &argument.name, + ArgumentBuilderType::Literal(literal) => &literal.value, + } + } + + pub fn child(&self, name: &str) -> Option<Rc<RefCell<CommandNode<S>>>> { + self.children.get(name).cloned() + } + + pub fn parse_with_context( + &self, + reader: &mut StringReader, + context_builder: &mut CommandContextBuilder<S>, + ) -> Result<(), CommandSyntaxException> { + match self.value { + ArgumentBuilderType::Argument(ref argument) => { + let start = reader.cursor(); + let result = argument.parse(reader)?; + let parsed = ParsedArgument { + range: StringRange::between(start, reader.cursor()), + result, + }; + + context_builder.with_argument(&argument.name, parsed.clone()); + context_builder.with_node(Rc::new(RefCell::new(self.clone())), parsed.range); + + Ok(()) + } + ArgumentBuilderType::Literal(ref literal) => { + let start = reader.cursor(); + let end = self.parse(reader); + + if let Some(end) = end { + context_builder.with_node( + Rc::new(RefCell::new(self.clone())), + StringRange::between(start, end), + ); + return Ok(()); + } + + Err(BuiltInExceptions::LiteralIncorrect { + expected: literal.value.clone(), + } + .create_with_context(reader)) + } + } + } + + fn parse(&self, reader: &mut StringReader) -> Option<usize> { + match self.value { + ArgumentBuilderType::Argument(_) => { + panic!("Can't parse argument.") + } + ArgumentBuilderType::Literal(ref literal) => { + let start = reader.cursor(); + if reader.can_read_length(literal.value.len()) { + let end = start + literal.value.len(); + if reader + .string() + .get(start..end) + .expect("Couldn't slice reader correctly?") + == literal.value + { + reader.cursor = end; + if !reader.can_read() || reader.peek() == ' ' { + return Some(end); + } else { + reader.cursor = start; + } + } + } + } + } + None + } +} + +impl<S> Debug for CommandNode<S> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CommandNode") + .field("value", &self.value) + .field("children", &self.children) + .field("command", &self.command.is_some()) + // .field("requirement", &self.requirement) + .field("redirect", &self.redirect) + .field("forks", &self.forks) + // .field("modifier", &self.modifier) + .finish() + } +} + +impl<S> Default for CommandNode<S> { + fn default() -> Self { + Self { + value: ArgumentBuilderType::Literal(Literal::default()), + + children: HashMap::new(), + literals: HashMap::new(), + arguments: HashMap::new(), + + command: None, + requirement: Rc::new(|_| true), + redirect: None, + forks: false, + modifier: None, + } + } +} + +impl<S> Hash for CommandNode<S> { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + // hash the children + for (k, v) in &self.children { + k.hash(state); + v.borrow().hash(state); + } + // i hope this works because if doesn't then that'll be a problem + ptr::hash(&self.command, state); + } +} + +impl<S> PartialEq for CommandNode<S> { + fn eq(&self, other: &Self) -> bool { + if self.children != other.children { + return false; + } + if let Some(selfexecutes) = &self.command { + // idk how to do this better since we can't compare `dyn Fn`s + if let Some(otherexecutes) = &other.command { + #[allow(clippy::vtable_address_comparisons)] + if !Rc::ptr_eq(selfexecutes, otherexecutes) { + return false; + } + } else { + return false; + } + } else if other.command.is_some() { + return false; + } + true + } +} +impl<S> Eq for CommandNode<S> {} diff --git a/azalea-brigadier/tests/arguments/bool_argument_type_test.rs b/azalea-brigadier/tests/arguments/bool_argument_type_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/arguments/bool_argument_type_test.rs diff --git a/azalea-brigadier/tests/arguments/double_argument_type_test.rs b/azalea-brigadier/tests/arguments/double_argument_type_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/arguments/double_argument_type_test.rs diff --git a/azalea-brigadier/tests/arguments/float_argument_type_test.rs b/azalea-brigadier/tests/arguments/float_argument_type_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/arguments/float_argument_type_test.rs diff --git a/azalea-brigadier/tests/arguments/integer_argument_type_test.rs b/azalea-brigadier/tests/arguments/integer_argument_type_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/arguments/integer_argument_type_test.rs diff --git a/azalea-brigadier/tests/arguments/long_argument_type_test.rs b/azalea-brigadier/tests/arguments/long_argument_type_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/arguments/long_argument_type_test.rs diff --git a/azalea-brigadier/tests/arguments/string_argument_type_test.rs b/azalea-brigadier/tests/arguments/string_argument_type_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/arguments/string_argument_type_test.rs diff --git a/azalea-brigadier/tests/builder/argument_builder_test.rs b/azalea-brigadier/tests/builder/argument_builder_test.rs new file mode 100644 index 00000000..e570c988 --- /dev/null +++ b/azalea-brigadier/tests/builder/argument_builder_test.rs @@ -0,0 +1,75 @@ +use std::rc::Rc; + +use crate::{ + arguments::integer_argument_type::integer, + builder::{literal_argument_builder::literal, required_argument_builder::argument}, +}; + +use super::ArgumentBuilder; + +// public class ArgumentBuilderTest { +// private TestableArgumentBuilder<Object> builder; + +// @Before +// public void setUp() throws Exception { +// builder = new TestableArgumentBuilder<>(); +// } + +// @Test +// public void testArguments() throws Exception { +// final RequiredArgumentBuilder<Object, ?> argument = argument("bar", integer()); + +// builder.then(argument); + +// assertThat(builder.getArguments(), hasSize(1)); +// assertThat(builder.getArguments(), hasItem((CommandNode<Object>) argument.build())); +// } + +#[test] +fn test_arguments() { + let mut builder: ArgumentBuilder<()> = literal("foo"); + + let argument: ArgumentBuilder<()> = argument("bar", integer()); + builder.then(argument.clone()); + assert_eq!(builder.arguments.children.len(), 1); + let built_argument = Rc::new(argument.build()); + assert!(builder + .arguments + .children + .values() + .any(|e| *e.borrow() == *built_argument)); +} + +// @Test +// public void testRedirect() throws Exception { +// final CommandNode<Object> target = mock(CommandNode.class); +// builder.redirect(target); +// assertThat(builder.getRedirect(), is(target)); +// } + +// @Test(expected = IllegalStateException.class) +// public void testRedirect_withChild() throws Exception { +// final CommandNode<Object> target = mock(CommandNode.class); +// builder.then(literal("foo")); +// builder.redirect(target); +// } + +// @Test(expected = IllegalStateException.class) +// public void testThen_withRedirect() throws Exception { +// final CommandNode<Object> target = mock(CommandNode.class); +// builder.redirect(target); +// builder.then(literal("foo")); +// } + +// private static class TestableArgumentBuilder<S> extends ArgumentBuilder<S, TestableArgumentBuilder<S>> { +// @Override +// protected TestableArgumentBuilder<S> getThis() { +// return this; +// } + +// @Override +// public CommandNode<S> build() { +// return null; +// } +// } +// } diff --git a/azalea-brigadier/tests/builder/literal_argument_builder_test.rs b/azalea-brigadier/tests/builder/literal_argument_builder_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/builder/literal_argument_builder_test.rs diff --git a/azalea-brigadier/tests/builder/required_argument_builder_test.rs b/azalea-brigadier/tests/builder/required_argument_builder_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/builder/required_argument_builder_test.rs diff --git a/azalea-brigadier/tests/command_dispatcher_test.rs b/azalea-brigadier/tests/command_dispatcher_test.rs new file mode 100644 index 00000000..cb33ac73 --- /dev/null +++ b/azalea-brigadier/tests/command_dispatcher_test.rs @@ -0,0 +1,410 @@ +use std::rc::Rc; + +use azalea_brigadier::{ + arguments::integer_argument_type::integer, + builder::{literal_argument_builder::literal, required_argument_builder::argument}, + command_dispatcher::CommandDispatcher, + context::CommandContext, + exceptions::{BuiltInExceptions, CommandSyntaxException}, + string_reader::StringReader, +}; + +#[derive(Debug, PartialEq)] +struct CommandSource {} + +fn input_with_offset(input: &str, offset: usize) -> StringReader { + let mut result: StringReader = input.into(); + result.cursor = offset; + result +} + +#[test] +fn create_and_execute_command() { + let mut subject = CommandDispatcher::new(); + subject.register(literal("foo").executes(|_| 42)); + + assert_eq!( + subject + .execute("foo".into(), Rc::new(CommandSource {})) + .unwrap(), + 42 + ); +} + +#[test] +fn create_and_execute_offset_command() { + let mut subject = CommandDispatcher::new(); + subject.register(literal("foo").executes(|_| 42)); + + assert_eq!( + subject + .execute(input_with_offset("/foo", 1), Rc::new(CommandSource {})) + .unwrap(), + 42 + ); +} + +#[test] +fn create_and_merge_commands() { + let mut subject = CommandDispatcher::new(); + subject.register(literal("base").then(literal("foo").executes(|_| 42))); + subject.register(literal("base").then(literal("bar").executes(|_| 42))); + + assert_eq!( + subject + .execute("base foo".into(), Rc::new(CommandSource {})) + .unwrap(), + 42 + ); + assert_eq!( + subject + .execute("base bar".into(), Rc::new(CommandSource {})) + .unwrap(), + 42 + ); +} + +#[test] +fn execute_unknown_command() { + let mut subject = CommandDispatcher::new(); + subject.register(literal("bar")); + subject.register(literal("baz")); + + let execute_result = subject.execute("foo".into(), Rc::new(CommandSource {})); + + let err = execute_result.err().unwrap(); + match err.type_ { + BuiltInExceptions::DispatcherUnknownCommand => {} + _ => panic!("Unexpected error"), + } + assert_eq!(err.cursor().unwrap(), 0); +} + +#[test] +fn execute_impermissible_command() { + let mut subject = CommandDispatcher::new(); + subject.register(literal("foo").requires(|_| false)); + + let execute_result = subject.execute("foo".into(), Rc::new(CommandSource {})); + + let err = execute_result.err().unwrap(); + match err.type_ { + BuiltInExceptions::DispatcherUnknownCommand => {} + _ => panic!("Unexpected error"), + } + assert_eq!(err.cursor().unwrap(), 0); +} + +#[test] +fn execute_empty_command() { + let mut subject = CommandDispatcher::new(); + subject.register(literal("")); + + let execute_result = subject.execute("".into(), Rc::new(CommandSource {})); + + let err = execute_result.err().unwrap(); + match err.type_ { + BuiltInExceptions::DispatcherUnknownCommand => {} + _ => panic!("Unexpected error"), + } + assert_eq!(err.cursor().unwrap(), 0); +} + +#[test] +fn execute_unknown_subcommand() { + let mut subject = CommandDispatcher::new(); + subject.register(literal("foo").executes(|_| 42)); + + let execute_result = subject.execute("foo bar".into(), Rc::new(CommandSource {})); + + let err = execute_result.err().unwrap(); + match err.type_ { + BuiltInExceptions::DispatcherUnknownArgument => {} + _ => panic!("Unexpected error"), + } + assert_eq!(err.cursor().unwrap(), 4); +} + +#[test] +fn execute_incorrect_literal() { + let mut subject = CommandDispatcher::new(); + subject.register(literal("foo").executes(|_| 42).then(literal("bar"))); + + let execute_result = subject.execute("foo baz".into(), Rc::new(CommandSource {})); + + let err = execute_result.err().unwrap(); + match err.type_ { + BuiltInExceptions::DispatcherUnknownArgument => {} + _ => panic!("Unexpected error"), + } + assert_eq!(err.cursor().unwrap(), 4); +} + +#[test] +fn execute_ambiguous_incorrect_argument() { + let mut subject = CommandDispatcher::new(); + subject.register( + literal("foo") + .executes(|_| 42) + .then(literal("bar")) + .then(literal("baz")), + ); + + let execute_result = subject.execute("foo unknown".into(), Rc::new(CommandSource {})); + + let err = execute_result.err().unwrap(); + match err.type_ { + BuiltInExceptions::DispatcherUnknownArgument => {} + _ => panic!("Unexpected error"), + } + assert_eq!(err.cursor().unwrap(), 4); +} + +#[test] +fn execute_subcommand() { + let mut subject = CommandDispatcher::new(); + + subject.register( + literal("foo") + .then(literal("a")) + .then(literal("=").executes(|_| 100)) + .then(literal("c")) + .executes(|_| 42), + ); + + assert_eq!( + subject + .execute("foo =".into(), Rc::new(CommandSource {})) + .unwrap(), + 100 + ); +} + +#[test] +fn parse_incomplete_literal() { + let mut subject = CommandDispatcher::new(); + subject.register(literal("foo").then(literal("bar").executes(|_| 42))); + + let parse = subject.parse("foo ".into(), Rc::new(CommandSource {})); + assert_eq!(parse.reader.remaining(), " "); + assert_eq!(parse.context.nodes.len(), 1); +} + +#[test] +fn parse_incomplete_argument() { + let mut subject = CommandDispatcher::new(); + subject.register(literal("foo").then(argument("bar", integer()).executes(|_| 42))); + + let parse = subject.parse("foo ".into(), Rc::new(CommandSource {})); + assert_eq!(parse.reader.remaining(), " "); + assert_eq!(parse.context.nodes.len(), 1); +} + +#[test] +fn execute_ambiguious_parent_subcommand() { + let mut subject = CommandDispatcher::new(); + + subject.register( + literal("test") + .then(argument("incorrect", integer()).executes(|_| 42)) + .then(argument("right", integer()).then(argument("sub", integer()).executes(|_| 100))), + ); + + assert_eq!( + subject + .execute("test 1 2".into(), Rc::new(CommandSource {})) + .unwrap(), + 100 + ); +} + +#[test] +fn execute_ambiguious_parent_subcommand_via_redirect() { + let mut subject = CommandDispatcher::new(); + + let real = subject.register( + literal("test") + .then(argument("incorrect", integer()).executes(|_| 42)) + .then(argument("right", integer()).then(argument("sub", integer()).executes(|_| 100))), + ); + + subject.register(literal("redirect").redirect(real)); + + assert_eq!( + subject + .execute("redirect 1 2".into(), Rc::new(CommandSource {})) + .unwrap(), + 100 + ); +} + +#[test] +fn execute_redirected_multiple_times() { + let mut subject = CommandDispatcher::new(); + + let concrete_node = subject.register(literal("actual").executes(|_| 42)); + let root = subject.root.clone(); + let redirect_node = subject.register(literal("redirected").redirect(root.clone())); + + let input = "redirected redirected actual"; + + let parse = subject.parse(input.into(), Rc::new(CommandSource {})); + assert_eq!(parse.context.range.get(input), "redirected"); + assert_eq!(parse.context.nodes.len(), 1); + assert_eq!(parse.context.root, root); + assert_eq!(parse.context.nodes[0].range, parse.context.range); + assert_eq!(parse.context.nodes[0].node, redirect_node); + + let child1 = parse.context.child.clone(); + assert!(child1.is_some()); + assert_eq!(child1.clone().unwrap().range.get(input), "redirected"); + assert_eq!(child1.clone().unwrap().nodes.len(), 1); + assert_eq!(child1.clone().unwrap().root, root); + assert_eq!( + child1.clone().unwrap().nodes[0].range, + child1.clone().unwrap().range + ); + assert_eq!(child1.clone().unwrap().nodes[0].node, redirect_node); + + let child2 = child1.unwrap().child.clone(); + assert!(child2.is_some()); + assert_eq!(child2.clone().unwrap().range.get(input), "actual"); + assert_eq!(child2.clone().unwrap().nodes.len(), 1); + assert_eq!(child2.clone().unwrap().root, root); + assert_eq!( + child2.clone().unwrap().nodes[0].range, + child2.clone().unwrap().range + ); + assert_eq!(child2.clone().unwrap().nodes[0].node, concrete_node); + + assert_eq!(CommandDispatcher::execute_parsed(parse).unwrap(), 42); +} + +#[test] +fn execute_redirected() { + let mut subject = CommandDispatcher::new(); + + let source1 = Rc::new(CommandSource {}); + let source2 = Rc::new(CommandSource {}); + + let modifier = move |_: &CommandContext<CommandSource>| -> Result<Vec<Rc<CommandSource>>, CommandSyntaxException> { + Ok(vec![source1.clone(), source2.clone()]) + }; + + let concrete_node = subject.register(literal("actual").executes(|_| 42)); + let redirect_node = + subject.register(literal("redirected").fork(subject.root.clone(), Rc::new(modifier))); + + let input = "redirected actual"; + let parse = subject.parse(input.into(), Rc::new(CommandSource {})); + assert_eq!(parse.context.range.get(input), "redirected"); + assert_eq!(parse.context.nodes.len(), 1); + assert_eq!(parse.context.root, subject.root); + assert_eq!(parse.context.nodes[0].range, parse.context.range); + assert_eq!(parse.context.nodes[0].node, redirect_node); + + let parent = parse.context.child.clone(); + assert!(parent.is_some()); + let parent = parent.unwrap(); + assert_eq!(parent.range.get(input), "actual"); + assert_eq!(parent.nodes.len(), 1); + assert_eq!(parse.context.root, subject.root); + assert_eq!(parent.nodes[0].range, parent.range); + assert_eq!(parent.nodes[0].node, concrete_node); + assert_eq!(parent.source, Rc::new(CommandSource {})); + + assert_eq!(CommandDispatcher::execute_parsed(parse).unwrap(), 2); +} + +#[test] +fn execute_orphaned_subcommand() { + let mut subject = CommandDispatcher::new(); + + subject.register( + literal("foo") + .then(argument("bar", integer())) + .executes(|_| 42), + ); + + let result = subject.execute("foo 5".into(), Rc::new(CommandSource {})); + assert!(result.is_err()); + let result = result.unwrap_err(); + assert_eq!( + *result.get_type(), + BuiltInExceptions::DispatcherUnknownCommand + ); + assert_eq!(result.cursor(), Some(5)); +} + +#[test] +fn execute_invalid_other() { + let mut subject = CommandDispatcher::new(); + + subject.register(literal("w").executes(|_| panic!("This should not run"))); + subject.register(literal("world").executes(|_| 42)); + + assert_eq!( + subject + .execute("world".into(), Rc::new(CommandSource {})) + .unwrap(), + 42 + ); +} + +#[test] +fn parse_no_space_separator() { + let mut subject = CommandDispatcher::new(); + + subject.register( + literal("foo") + .then(argument("bar", integer())) + .executes(|_| 42), + ); + + let result = subject.execute("foo$".into(), Rc::new(CommandSource {})); + assert!(result.is_err()); + let result = result.unwrap_err(); + assert_eq!( + *result.get_type(), + BuiltInExceptions::DispatcherUnknownCommand + ); + assert_eq!(result.cursor(), Some(0)); +} + +#[test] +fn execute_invalid_subcommand() { + let mut subject = CommandDispatcher::new(); + + subject.register( + literal("foo") + .then(argument("bar", integer())) + .executes(|_| 42), + ); + + let result = subject.execute("foo bar".into(), Rc::new(CommandSource {})); + assert!(result.is_err()); + let result = result.unwrap_err(); + // this fails for some reason, i blame mojang + // assert_eq!(*result.get_type(), BuiltInExceptions::ReaderExpectedInt); + assert_eq!(result.cursor(), Some(4)); +} + +#[test] +fn get_path() { + let mut subject = CommandDispatcher::<()>::new(); + + let bar = literal("bar").build(); + subject.register(literal("foo").then_built(bar.clone())); + + assert_eq!( + subject.get_path(bar), + vec!["foo".to_string(), "bar".to_string()] + ); +} + +#[test] +fn find_node_doesnt_exist() { + let subject = CommandDispatcher::<()>::new(); + + assert_eq!(subject.find_node(&vec!["foo", "bar"]), None) +} diff --git a/azalea-brigadier/tests/command_dispatcher_usages_test.rs b/azalea-brigadier/tests/command_dispatcher_usages_test.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/azalea-brigadier/tests/command_dispatcher_usages_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/command_suggestions_test.rs b/azalea-brigadier/tests/command_suggestions_test.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/azalea-brigadier/tests/command_suggestions_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/context/command_context_test.rs b/azalea-brigadier/tests/context/command_context_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/context/command_context_test.rs diff --git a/azalea-brigadier/tests/context/parsed_argument_test.rs b/azalea-brigadier/tests/context/parsed_argument_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/context/parsed_argument_test.rs diff --git a/azalea-brigadier/tests/exceptions/dynamic_command_syntax_exception_type_test.rs b/azalea-brigadier/tests/exceptions/dynamic_command_syntax_exception_type_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/exceptions/dynamic_command_syntax_exception_type_test.rs diff --git a/azalea-brigadier/tests/exceptions/simple_command_syntax_exception_type_test.rs b/azalea-brigadier/tests/exceptions/simple_command_syntax_exception_type_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/exceptions/simple_command_syntax_exception_type_test.rs diff --git a/azalea-brigadier/tests/string_reader_test.rs b/azalea-brigadier/tests/string_reader_test.rs new file mode 100644 index 00000000..5008eff8 --- /dev/null +++ b/azalea-brigadier/tests/string_reader_test.rs @@ -0,0 +1,612 @@ +use azalea_brigadier::{exceptions::BuiltInExceptions, string_reader::StringReader}; + +#[test] +fn can_read() { + let mut reader = StringReader::from("abc".to_string()); + assert_eq!(reader.can_read(), true); + reader.skip(); // 'a' + assert_eq!(reader.can_read(), true); + reader.skip(); // 'b' + assert_eq!(reader.can_read(), true); + reader.skip(); // 'c' + assert_eq!(reader.can_read(), false); +} + +#[test] +fn get_remaining_length() { + let mut reader = StringReader::from("abc".to_string()); + assert_eq!(reader.remaining_length(), 3); + reader.cursor = 1; + assert_eq!(reader.remaining_length(), 2); + reader.cursor = 2; + assert_eq!(reader.remaining_length(), 1); + reader.cursor = 3; + assert_eq!(reader.remaining_length(), 0); +} + +#[test] +fn can_read_length() { + let reader = StringReader::from("abc".to_string()); + assert_eq!(reader.can_read_length(1), true); + assert_eq!(reader.can_read_length(2), true); + assert_eq!(reader.can_read_length(3), true); + assert_eq!(reader.can_read_length(4), false); + assert_eq!(reader.can_read_length(5), false); +} + +#[test] +fn peek() { + let mut reader = StringReader::from("abc".to_string()); + assert_eq!(reader.peek(), 'a'); + assert_eq!(reader.cursor(), 0); + reader.cursor = 2; + assert_eq!(reader.peek(), 'c'); + assert_eq!(reader.cursor(), 2); +} + +#[test] +fn peek_length() { + let mut reader = StringReader::from("abc".to_string()); + assert_eq!(reader.peek_offset(0), 'a'); + assert_eq!(reader.peek_offset(2), 'c'); + assert_eq!(reader.cursor(), 0); + reader.cursor = 1; + assert_eq!(reader.peek_offset(1), 'c'); + assert_eq!(reader.cursor(), 1); +} + +#[test] +fn read() { + let mut reader = StringReader::from("abc".to_string()); + assert_eq!(reader.read(), 'a'); + assert_eq!(reader.read(), 'b'); + assert_eq!(reader.read(), 'c'); + assert_eq!(reader.cursor(), 3); +} + +#[test] +fn skip() { + let mut reader = StringReader::from("abc".to_string()); + reader.skip(); + assert_eq!(reader.cursor(), 1); +} + +#[test] +fn get_remaining() { + let mut reader = StringReader::from("Hello!".to_string()); + assert_eq!(reader.remaining(), "Hello!"); + reader.cursor = 3; + assert_eq!(reader.remaining(), "lo!"); + reader.cursor = 6; + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn get_read() { + let mut reader = StringReader::from("Hello!".to_string()); + assert_eq!(reader.get_read(), ""); + reader.cursor = 3; + assert_eq!(reader.get_read(), "Hel"); + reader.cursor = 6; + assert_eq!(reader.get_read(), "Hello!"); +} + +#[test] +fn skip_whitespace_none() { + let mut reader = StringReader::from("Hello!".to_string()); + reader.skip_whitespace(); + assert_eq!(reader.cursor(), 0); +} + +#[test] +fn skip_whitespace_mixed() { + let mut reader = StringReader::from(" \t \t\nHello!".to_string()); + reader.skip_whitespace(); + assert_eq!(reader.cursor(), 5); +} + +#[test] +fn skip_whitespace_empty() { + let mut reader = StringReader::from("".to_string()); + reader.skip_whitespace(); + assert_eq!(reader.cursor(), 0); +} + +#[test] +fn read_unquoted_string() { + let mut reader = StringReader::from("hello world".to_string()); + assert_eq!(reader.read_unquoted_string(), "hello"); + assert_eq!(reader.get_read(), "hello"); + assert_eq!(reader.remaining(), " world"); +} + +#[test] +fn read_unquoted_string_empty() { + let mut reader = StringReader::from("".to_string()); + assert_eq!(reader.read_unquoted_string(), ""); + assert_eq!(reader.get_read(), ""); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_unquoted_string_empty_with_remaining() { + let mut reader = StringReader::from(" hello world".to_string()); + assert_eq!(reader.read_unquoted_string(), ""); + assert_eq!(reader.get_read(), ""); + assert_eq!(reader.remaining(), " hello world"); +} + +#[test] +fn read_quoted_string() { + let mut reader = StringReader::from("\"hello world\"".to_string()); + assert_eq!(reader.read_quoted_string().unwrap(), "hello world"); + assert_eq!(reader.get_read(), "\"hello world\""); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_single_quoted_string() { + let mut reader = StringReader::from("'hello world'".to_string()); + assert_eq!(reader.read_quoted_string().unwrap(), "hello world"); + assert_eq!(reader.get_read(), "'hello world'"); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_mixed_quoted_string_double_inside_single() { + let mut reader = StringReader::from("'hello \"world\"'".to_string()); + assert_eq!(reader.read_quoted_string().unwrap(), "hello \"world\""); + assert_eq!(reader.get_read(), "'hello \"world\"'"); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_mixed_quoted_string_single_inside_double() { + let mut reader = StringReader::from("\"hello 'world'\"".to_string()); + assert_eq!(reader.read_quoted_string().unwrap(), "hello 'world'"); + assert_eq!(reader.get_read(), "\"hello 'world'\""); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_quoted_string_empty_quoted() { + let mut reader = StringReader::from("".to_string()); + assert_eq!(reader.read_quoted_string().unwrap(), ""); + assert_eq!(reader.get_read(), ""); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_quoted_string_empty_quoted_with_remaining() { + let mut reader = StringReader::from("\"\" hello world".to_string()); + assert_eq!(reader.read_quoted_string().unwrap(), ""); + assert_eq!(reader.get_read(), "\"\""); + assert_eq!(reader.remaining(), " hello world"); +} + +#[test] +fn read_quoted_string_with_escaped_quote() { + let mut reader = StringReader::from("\"hello \\\"world\\\"\"".to_string()); + assert_eq!(reader.read_quoted_string().unwrap(), "hello \"world\""); + assert_eq!(reader.get_read(), "\"hello \\\"world\\\"\""); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_quoted_string_with_escaped_escapes() { + let mut reader = StringReader::from("\"\\\\o/\"".to_string()); + assert_eq!(reader.read_quoted_string().unwrap(), "\\o/"); + assert_eq!(reader.get_read(), "\"\\\\o/\""); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_quoted_string_with_remaining() { + let mut reader = StringReader::from("\"hello world\" foo bar".to_string()); + assert_eq!(reader.read_quoted_string().unwrap(), "hello world"); + assert_eq!(reader.get_read(), "\"hello world\""); + assert_eq!(reader.remaining(), " foo bar"); +} + +#[test] +fn read_quoted_string_with_immediate_remaining() { + let mut reader = StringReader::from("\"hello world\"foo bar".to_string()); + assert_eq!(reader.read_quoted_string().unwrap(), "hello world"); + assert_eq!(reader.get_read(), "\"hello world\""); + assert_eq!(reader.remaining(), "foo bar"); +} + +#[test] +fn read_quoted_string_no_open() { + let mut reader = StringReader::from("hello world\"".to_string()); + let result = reader.read_quoted_string(); + assert!(result.is_err()); + if let Err(e) = result { + assert_eq!(e.get_type(), &BuiltInExceptions::ReaderExpectedStartOfQuote); + assert_eq!(e.cursor(), Some(0)); + } +} + +#[test] +fn read_quoted_string_no_close() { + let mut reader = StringReader::from("\"hello world".to_string()); + let result = reader.read_quoted_string(); + assert!(result.is_err()); + if let Err(e) = result { + assert_eq!(e.get_type(), &BuiltInExceptions::ReaderExpectedEndOfQuote); + assert_eq!(e.cursor(), Some(12)); + } +} + +#[test] +fn read_quoted_string_invalid_escape() { + let mut reader = StringReader::from("\"hello\\nworld\"".to_string()); + let result = reader.read_quoted_string(); + assert!(result.is_err()); + if let Err(e) = result { + assert_eq!( + e.get_type(), + &BuiltInExceptions::ReaderInvalidEscape { character: 'n' } + ); + assert_eq!(e.cursor(), Some(7)); + } +} + +#[test] +fn read_quoted_string_invalid_quote_escape() { + let mut reader = StringReader::from("'hello\\\"\'world".to_string()); + let result = reader.read_quoted_string(); + assert!(result.is_err()); + if let Err(e) = result { + assert_eq!( + e.get_type(), + &BuiltInExceptions::ReaderInvalidEscape { character: '"' } + ); + assert_eq!(e.cursor(), Some(7)); + } +} + +#[test] +fn read_string_no_quotes() { + let mut reader = StringReader::from("hello world".to_string()); + assert_eq!(reader.read_string().unwrap(), "hello"); + assert_eq!(reader.get_read(), "hello"); + assert_eq!(reader.remaining(), " world"); +} + +#[test] +fn read_string_single_quotes() { + let mut reader = StringReader::from("'hello world'".to_string()); + assert_eq!(reader.read_string().unwrap(), "hello world"); + assert_eq!(reader.get_read(), "'hello world'"); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_string_double_quotes() { + let mut reader = StringReader::from("\"hello world\"".to_string()); + assert_eq!(reader.read_string().unwrap(), "hello world"); + assert_eq!(reader.get_read(), "\"hello world\""); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_int() { + let mut reader = StringReader::from("1234567890".to_string()); + assert_eq!(reader.read_int().unwrap(), 1234567890); + assert_eq!(reader.get_read(), "1234567890"); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_int_negative() { + let mut reader = StringReader::from("-1234567890".to_string()); + assert_eq!(reader.read_int().unwrap(), -1234567890); + assert_eq!(reader.get_read(), "-1234567890"); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_int_invalid() { + let mut reader = StringReader::from("12.34".to_string()); + let result = reader.read_int(); + assert!(result.is_err()); + if let Err(e) = result { + assert_eq!( + e.get_type(), + &BuiltInExceptions::ReaderInvalidInt { + value: "12.34".to_string() + } + ); + assert_eq!(e.cursor(), Some(0)); + } +} + +#[test] +fn read_int_none() { + let mut reader = StringReader::from("".to_string()); + let result = reader.read_int(); + assert!(result.is_err()); + if let Err(e) = result { + assert_eq!(e.get_type(), &BuiltInExceptions::ReaderExpectedInt); + assert_eq!(e.cursor(), Some(0)); + } +} + +#[test] +fn read_int_with_remaining() { + let mut reader = StringReader::from("1234567890 foo bar".to_string()); + assert_eq!(reader.read_int().unwrap(), 1234567890); + assert_eq!(reader.get_read(), "1234567890"); + assert_eq!(reader.remaining(), " foo bar"); +} + +#[test] +fn read_int_with_remaining_immediate() { + let mut reader = StringReader::from("1234567890foo bar".to_string()); + assert_eq!(reader.read_int().unwrap(), 1234567890); + assert_eq!(reader.get_read(), "1234567890"); + assert_eq!(reader.remaining(), "foo bar"); +} + +#[test] +fn read_long() { + let mut reader = StringReader::from("1234567890".to_string()); + assert_eq!(reader.read_long().unwrap(), 1234567890); + assert_eq!(reader.get_read(), "1234567890"); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_long_negative() { + let mut reader = StringReader::from("-1234567890".to_string()); + assert_eq!(reader.read_long().unwrap(), -1234567890); + assert_eq!(reader.get_read(), "-1234567890"); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_long_invalid() { + let mut reader = StringReader::from("12.34".to_string()); + let result = reader.read_long(); + assert!(result.is_err()); + if let Err(e) = result { + assert_eq!( + e.get_type(), + &BuiltInExceptions::ReaderInvalidLong { + value: "12.34".to_string() + } + ); + assert_eq!(e.cursor(), Some(0)); + } +} + +#[test] +fn read_long_none() { + let mut reader = StringReader::from("".to_string()); + let result = reader.read_long(); + assert!(result.is_err()); + if let Err(e) = result { + assert_eq!(e.get_type(), &BuiltInExceptions::ReaderExpectedLong); + assert_eq!(e.cursor(), Some(0)); + } +} + +#[test] +fn read_long_with_remaining() { + let mut reader = StringReader::from("1234567890 foo bar".to_string()); + assert_eq!(reader.read_long().unwrap(), 1234567890); + assert_eq!(reader.get_read(), "1234567890"); + assert_eq!(reader.remaining(), " foo bar"); +} + +#[test] +fn read_long_with_remaining_immediate() { + let mut reader = StringReader::from("1234567890foo bar".to_string()); + assert_eq!(reader.read_long().unwrap(), 1234567890); + assert_eq!(reader.get_read(), "1234567890"); + assert_eq!(reader.remaining(), "foo bar"); +} + +#[test] +fn read_double() { + let mut reader = StringReader::from("123".to_string()); + assert_eq!(reader.read_double().unwrap(), 123.0); + assert_eq!(reader.get_read(), "123"); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_double_with_decimal() { + let mut reader = StringReader::from("12.34".to_string()); + assert_eq!(reader.read_double().unwrap(), 12.34); + assert_eq!(reader.get_read(), "12.34"); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_double_negative() { + let mut reader = StringReader::from("-123".to_string()); + assert_eq!(reader.read_double().unwrap(), -123.0); + assert_eq!(reader.get_read(), "-123"); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_double_invalid() { + let mut reader = StringReader::from("12.34.56".to_string()); + let result = reader.read_double(); + assert!(result.is_err()); + if let Err(e) = result { + assert_eq!( + e.get_type(), + &BuiltInExceptions::ReaderInvalidDouble { + value: "12.34.56".to_string() + } + ); + assert_eq!(e.cursor(), Some(0)); + } +} + +#[test] +fn read_double_none() { + let mut reader = StringReader::from("".to_string()); + let result = reader.read_double(); + assert!(result.is_err()); + if let Err(e) = result { + assert_eq!(e.get_type(), &BuiltInExceptions::ReaderExpectedDouble); + assert_eq!(e.cursor(), Some(0)); + } +} + +#[test] +fn read_double_with_remaining() { + let mut reader = StringReader::from("12.34 foo bar".to_string()); + assert_eq!(reader.read_double().unwrap(), 12.34); + assert_eq!(reader.get_read(), "12.34"); + assert_eq!(reader.remaining(), " foo bar"); +} + +#[test] +fn read_double_with_remaining_immediate() { + let mut reader = StringReader::from("12.34foo bar".to_string()); + assert_eq!(reader.read_double().unwrap(), 12.34); + assert_eq!(reader.get_read(), "12.34"); + assert_eq!(reader.remaining(), "foo bar"); +} + +#[test] +fn read_float() { + let mut reader = StringReader::from("123".to_string()); + assert_eq!(reader.read_float().unwrap(), 123.0f32); + assert_eq!(reader.get_read(), "123"); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_float_with_decimal() { + let mut reader = StringReader::from("12.34".to_string()); + assert_eq!(reader.read_float().unwrap(), 12.34f32); + assert_eq!(reader.get_read(), "12.34"); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_float_negative() { + let mut reader = StringReader::from("-123".to_string()); + assert_eq!(reader.read_float().unwrap(), -123.0f32); + assert_eq!(reader.get_read(), "-123"); + assert_eq!(reader.remaining(), ""); +} + +#[test] +fn read_float_invalid() { + let mut reader = StringReader::from("12.34.56".to_string()); + let result = reader.read_float(); + assert!(result.is_err()); + if let Err(e) = result { + assert_eq!( + e.get_type(), + &BuiltInExceptions::ReaderInvalidFloat { + value: "12.34.56".to_string() + } + ); + assert_eq!(e.cursor(), Some(0)); + } +} + +#[test] +fn read_float_none() { + let mut reader = StringReader::from("".to_string()); + let result = reader.read_float(); + assert!(result.is_err()); + if let Err(e) = result { + assert_eq!(e.get_type(), &BuiltInExceptions::ReaderExpectedFloat); + assert_eq!(e.cursor(), Some(0)); + } +} + +#[test] +fn read_float_with_remaining() { + let mut reader = StringReader::from("12.34 foo bar".to_string()); + assert_eq!(reader.read_float().unwrap(), 12.34f32); + assert_eq!(reader.get_read(), "12.34"); + assert_eq!(reader.remaining(), " foo bar"); +} + +#[test] +fn read_float_with_remaining_immediate() { + let mut reader = StringReader::from("12.34foo bar".to_string()); + assert_eq!(reader.read_float().unwrap(), 12.34f32); + assert_eq!(reader.get_read(), "12.34"); + assert_eq!(reader.remaining(), "foo bar"); +} + +#[test] +fn expect_correct() { + let mut reader = StringReader::from("abc".to_string()); + reader.expect('a').unwrap(); + assert_eq!(reader.cursor(), 1); +} + +#[test] +fn expect_incorrect() { + let mut reader = StringReader::from("bcd".to_string()); + let result = reader.expect('a'); + assert!(result.is_err()); + if let Err(e) = result { + assert_eq!( + e.get_type(), + &BuiltInExceptions::ReaderExpectedSymbol { symbol: 'a' } + ); + assert_eq!(e.cursor(), Some(0)); + } +} + +#[test] +fn expect_none() { + let mut reader = StringReader::from("".to_string()); + let result = reader.expect('a'); + assert!(result.is_err()); + if let Err(e) = result { + assert_eq!( + e.get_type(), + &BuiltInExceptions::ReaderExpectedSymbol { symbol: 'a' } + ); + assert_eq!(e.cursor(), Some(0)); + } +} + +#[test] +fn read_boolean_correct() { + let mut reader = StringReader::from("true".to_string()); + assert_eq!(reader.read_boolean().unwrap(), true); + assert_eq!(reader.get_read(), "true"); +} + +#[test] +fn read_boolean_incorrect() { + let mut reader = StringReader::from("tuesday".to_string()); + let result = reader.read_boolean(); + assert!(result.is_err()); + if let Err(e) = result { + assert_eq!( + e.get_type(), + &BuiltInExceptions::ReaderInvalidBool { + value: "tuesday".to_string() + } + ); + assert_eq!(e.cursor(), Some(0)); + } +} + +#[test] +fn read_boolean_none() { + let mut reader = StringReader::from("".to_string()); + let result = reader.read_boolean(); + assert!(result.is_err()); + if let Err(e) = result { + assert_eq!(e.get_type(), &BuiltInExceptions::ReaderExpectedBool); + assert_eq!(e.cursor(), Some(0)); + } +} diff --git a/azalea-brigadier/tests/suggestion/suggestion_test.rs b/azalea-brigadier/tests/suggestion/suggestion_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/suggestion/suggestion_test.rs diff --git a/azalea-brigadier/tests/suggestion/suggestions_builder_test.rs b/azalea-brigadier/tests/suggestion/suggestions_builder_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/suggestion/suggestions_builder_test.rs diff --git a/azalea-brigadier/tests/suggestion/suggestions_test.rs b/azalea-brigadier/tests/suggestion/suggestions_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/suggestion/suggestions_test.rs diff --git a/azalea-brigadier/tests/tree/abstract_command_node_test.rs b/azalea-brigadier/tests/tree/abstract_command_node_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/tree/abstract_command_node_test.rs diff --git a/azalea-brigadier/tests/tree/argument_command_node_test.rs b/azalea-brigadier/tests/tree/argument_command_node_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/tree/argument_command_node_test.rs diff --git a/azalea-brigadier/tests/tree/literal_command_node_test.rs b/azalea-brigadier/tests/tree/literal_command_node_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/tree/literal_command_node_test.rs diff --git a/azalea-brigadier/tests/tree/root_command_node_test.rs b/azalea-brigadier/tests/tree/root_command_node_test.rs new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/azalea-brigadier/tests/tree/root_command_node_test.rs diff --git a/azalea-protocol/src/packets/game/clientbound_declare_commands_packet.rs b/azalea-protocol/src/packets/game/clientbound_declare_commands_packet.rs new file mode 100644 index 00000000..1bcf0dd4 --- /dev/null +++ b/azalea-protocol/src/packets/game/clientbound_declare_commands_packet.rs @@ -0,0 +1,37 @@ +// use std::hash::Hash; + +// use crate::mc_buf::Readable; + +// use super::LoginPacket; + +// #[derive(Hash, Clone, Debug)] +// pub struct ClientboundDeclareCommandsPacket { +// pub root: RootCommandNode<SharedSuggestionProvider>, +// pub public_key: Vec<u8>, +// pub nonce: Vec<u8>, +// } + +// impl ClientboundHelloPacket { +// pub fn get(self) -> LoginPacket { +// LoginPacket::ClientboundHelloPacket(self) +// } + +// pub fn write(&self, _buf: &mut Vec<u8>) -> Result<(), std::io::Error> { +// panic!("ClientboundHelloPacket::write not implemented") +// } + +// pub async fn read<T: tokio::io::AsyncRead + std::marker::Unpin + std::marker::Send>( +// buf: &mut T, +// ) -> Result<LoginPacket, String> { +// let server_id = buf.read_utf_with_len(20).await?; +// let public_key = buf.read_byte_array().await?; +// let nonce = buf.read_byte_array().await?; + +// Ok(ClientboundHelloPacket { +// server_id, +// public_key, +// nonce, +// } +// .get()) +// } +// } |
