diff --git a/azalea-brigadier/src/builder/argument_builder.rs b/azalea-brigadier/src/builder/argument_builder.rs index 643a3bd0..10191463 100755 --- a/azalea-brigadier/src/builder/argument_builder.rs +++ b/azalea-brigadier/src/builder/argument_builder.rs @@ -16,6 +16,7 @@ pub enum ArgumentBuilderType { } /// A node that hasn't yet been built. +#[derive(Clone)] pub struct ArgumentBuilder { arguments: CommandNode, @@ -134,6 +135,10 @@ impl ArgumentBuilder { self } + pub fn arguments(&self) -> &CommandNode { + &self.arguments + } + /// Manually build this node into a [`CommandNode`]. You probably don't need /// to do this yourself. pub fn build(self) -> CommandNode { diff --git a/azalea-brigadier/src/command_dispatcher.rs b/azalea-brigadier/src/command_dispatcher.rs index 672a250b..42737fba 100755 --- a/azalea-brigadier/src/command_dispatcher.rs +++ b/azalea-brigadier/src/command_dispatcher.rs @@ -8,7 +8,13 @@ use crate::{ string_reader::StringReader, tree::CommandNode, }; -use std::{cmp::Ordering, collections::HashMap, mem, rc::Rc, sync::Arc}; +use std::{ + cmp::Ordering, + collections::{HashMap, HashSet}, + mem, + rc::Rc, + sync::Arc, +}; /// The root of the command tree. You need to make this to register commands. /// @@ -297,6 +303,182 @@ impl CommandDispatcher { }) // Ok(if forked { successful_forks } else { result }) } + + pub fn get_all_usage( + &self, + node: &CommandNode, + source: Arc, + restricted: bool, + ) -> Vec { + let mut result = vec![]; + self.get_all_usage_recursive(node, source, &mut result, "", restricted); + result + } + + fn get_all_usage_recursive( + &self, + node: &CommandNode, + source: Arc, + result: &mut Vec, + prefix: &str, + restricted: bool, + ) { + if restricted && !node.can_use(source.clone()) { + return; + } + if node.command.is_some() { + result.push(prefix.to_owned()); + } + if let Some(redirect) = &node.redirect { + let redirect = if redirect.data_ptr() == self.root.data_ptr() { + "...".to_string() + } else { + format!("-> {}", redirect.read().usage_text()) + }; + if prefix.is_empty() { + result.push(format!("{} {redirect}", node.usage_text())); + } else { + result.push(format!("{prefix} {redirect}")); + } + } else { + for child in node.children.values() { + let child = child.read(); + self.get_all_usage_recursive( + &child, + Arc::clone(&source), + result, + if prefix.is_empty() { + child.usage_text() + } else { + format!("{prefix} {}", child.usage_text()) + } + .as_str(), + restricted, + ); + } + } + } + + /// Gets the possible executable commands from a specified node. + /// + /// You may use [`Self::root`] as a target to get usage data for the entire + /// command tree. + pub fn get_smart_usage( + &self, + node: &CommandNode, + source: Arc, + ) -> Vec<(Arc>>, String)> { + let mut result = Vec::new(); + + let optional = node.command.is_some(); + for child in node.children.values() { + let usage = + self.get_smart_usage_recursive(&child.read(), source.clone(), optional, false); + if let Some(usage) = usage { + result.push((child.clone(), usage)); + } + } + + result + } + + fn get_smart_usage_recursive( + &self, + node: &CommandNode, + source: Arc, + optional: bool, + deep: bool, + ) -> Option { + if !node.can_use(source.clone()) { + return None; + } + + let this = if optional { + format!("[{}]", node.usage_text()) + } else { + node.usage_text() + }; + let child_optional = node.command.is_some(); + let open = if child_optional { "[" } else { "(" }; + let close = if child_optional { "]" } else { ")" }; + + if deep { + return Some(this); + } + + if let Some(redirect) = &node.redirect { + let redirect = if redirect.data_ptr() == self.root.data_ptr() { + "...".to_string() + } else { + format!("-> {}", redirect.read().usage_text()) + }; + return Some(format!("{this} {redirect}")); + } + + let children = node + .children + .values() + .filter(|child| child.read().can_use(source.clone())) + .collect::>(); + match children.len().cmp(&1) { + Ordering::Less => {} + Ordering::Equal => { + let usage = self.get_smart_usage_recursive( + &children[0].read(), + source.clone(), + child_optional, + child_optional, + ); + if let Some(usage) = usage { + return Some(format!("{this} {usage}")); + } + } + Ordering::Greater => { + let mut child_usage = HashSet::new(); + for child in &children { + let usage = self.get_smart_usage_recursive( + &child.read(), + source.clone(), + child_optional, + true, + ); + if let Some(usage) = usage { + child_usage.insert(usage); + } + } + match child_usage.len().cmp(&1) { + Ordering::Less => {} + Ordering::Equal => { + let usage = child_usage.into_iter().next().unwrap(); + let usage = if child_optional { + format!("[{}]", usage) + } else { + usage + }; + return Some(format!("{this} {usage}")); + } + Ordering::Greater => { + let mut builder = String::new(); + builder.push_str(open); + let mut count = 0; + for child in children { + if count > 0 { + builder.push('|'); + } + builder.push_str(&child.read().usage_text()); + count += 1; + } + if count > 0 { + builder.push_str(close); + return Some(format!("{this} {builder}")); + } + } + } + } + } + + Some(this) + } } impl Default for CommandDispatcher { diff --git a/azalea-brigadier/src/context/command_context.rs b/azalea-brigadier/src/context/command_context.rs index f78fe758..4d93006e 100755 --- a/azalea-brigadier/src/context/command_context.rs +++ b/azalea-brigadier/src/context/command_context.rs @@ -30,7 +30,7 @@ impl Clone for CommandContext { command: self.command.clone(), root_node: self.root_node.clone(), nodes: self.nodes.clone(), - range: self.range.clone(), + range: self.range, child: self.child.clone(), modifier: self.modifier.clone(), forks: self.forks, @@ -67,7 +67,7 @@ impl CommandContext { command: self.command.clone(), root_node: self.root_node.clone(), nodes: self.nodes.clone(), - range: self.range.clone(), + range: self.range, child: self.child.clone(), modifier: self.modifier.clone(), forks: self.forks, diff --git a/azalea-brigadier/src/context/command_context_builder.rs b/azalea-brigadier/src/context/command_context_builder.rs index 2fc8d4ac..99c40dac 100755 --- a/azalea-brigadier/src/context/command_context_builder.rs +++ b/azalea-brigadier/src/context/command_context_builder.rs @@ -34,7 +34,7 @@ impl Clone for CommandContextBuilder<'_, S> { source: self.source.clone(), command: self.command.clone(), child: self.child.clone(), - range: self.range.clone(), + range: self.range, modifier: self.modifier.clone(), forks: self.forks, } @@ -77,7 +77,7 @@ impl<'a, S> CommandContextBuilder<'a, S> { pub fn with_node(&mut self, node: Arc>>, range: StringRange) -> &Self { self.nodes.push(ParsedCommandNode { node: node.clone(), - range: range.clone(), + range, }); self.range = StringRange::encompassing(&self.range, &range); self.modifier = node.read().modifier.clone(); @@ -93,7 +93,7 @@ impl<'a, S> CommandContextBuilder<'a, S> { source: self.source.clone(), command: self.command.clone(), child: self.child.clone().map(|c| Rc::new(c.build(input))), - range: self.range.clone(), + range: self.range, forks: self.forks, modifier: self.modifier.clone(), input: input.to_string(), diff --git a/azalea-brigadier/src/context/parsed_command_node.rs b/azalea-brigadier/src/context/parsed_command_node.rs index bba5d121..2d69c72e 100755 --- a/azalea-brigadier/src/context/parsed_command_node.rs +++ b/azalea-brigadier/src/context/parsed_command_node.rs @@ -14,7 +14,7 @@ impl Clone for ParsedCommandNode { fn clone(&self) -> Self { Self { node: self.node.clone(), - range: self.range.clone(), + range: self.range, } } } diff --git a/azalea-brigadier/src/context/string_range.rs b/azalea-brigadier/src/context/string_range.rs index 8ca88624..75163405 100755 --- a/azalea-brigadier/src/context/string_range.rs +++ b/azalea-brigadier/src/context/string_range.rs @@ -1,6 +1,6 @@ use std::cmp; -#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Copy)] pub struct StringRange { start: usize, end: usize, diff --git a/azalea-brigadier/src/suggestion/mod.rs b/azalea-brigadier/src/suggestion/mod.rs index c404adc7..bc0e7608 100755 --- a/azalea-brigadier/src/suggestion/mod.rs +++ b/azalea-brigadier/src/suggestion/mod.rs @@ -8,6 +8,10 @@ use azalea_buf::McBufWritable; use azalea_chat::FormattedText; #[cfg(feature = "azalea-buf")] use std::io::Write; +use std::{ + fmt::{self, Display}, + hash::Hash, +}; pub use suggestions::Suggestions; pub use suggestions_builder::SuggestionsBuilder; @@ -16,22 +20,50 @@ pub use suggestions_builder::SuggestionsBuilder; /// The `M` generic is the type of the tooltip, so for example a `String` or /// just `()` if you don't care about it. #[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct Suggestion { - pub text: String, +pub struct Suggestion +where + M: Clone, +{ pub range: StringRange, + value: SuggestionValue, pub tooltip: Option, } -impl Suggestion { - pub fn apply(&self, input: &str) -> String { - if self.range.start() == 0 && self.range.end() == input.len() { - return input.to_string(); +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub enum SuggestionValue { + Integer(i32), + Text(String), +} + +impl Suggestion<()> { + pub fn new(range: StringRange, text: &str) -> Suggestion<()> { + Suggestion { + range, + value: SuggestionValue::Text(text.to_string()), + tooltip: None, } - let mut result = String::with_capacity(self.text.len()); + } +} + +impl Suggestion { + pub fn new_with_tooltip(range: StringRange, text: &str, tooltip: M) -> Self { + Self { + range, + value: SuggestionValue::Text(text.to_string()), + tooltip: Some(tooltip), + } + } + + pub fn apply(&self, input: &str) -> String { + let text = self.value.to_string(); + if self.range.start() == 0 && self.range.end() == input.len() { + return text; + } + let mut result = String::with_capacity(text.len()); if self.range.start() > 0 { result.push_str(&input[0..self.range.start()]); } - result.push_str(&self.text); + result.push_str(&text); if self.range.end() < input.len() { result.push_str(&input[self.range.end()..]); } @@ -39,30 +71,78 @@ impl Suggestion { result } - pub fn expand(&self, command: &str, range: &StringRange) -> Suggestion { - if range == &self.range { + pub fn expand(&self, command: &str, range: StringRange) -> Suggestion { + if range == self.range { return self.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); + result.push_str(&self.value.to_string()); if range.end() > self.range.end() { result.push_str(&command[self.range.end()..range.end()]); } Suggestion { - range: range.clone(), - text: result, + range, + value: SuggestionValue::Text(result), tooltip: self.tooltip.clone(), } } + + pub fn text(&self) -> String { + self.value.to_string() + } +} + +impl SuggestionValue { + pub fn cmp_ignore_case(&self, other: &Self) -> std::cmp::Ordering { + match (self, other) { + (SuggestionValue::Text(a), SuggestionValue::Text(b)) => { + a.to_lowercase().cmp(&b.to_lowercase()) + } + (SuggestionValue::Integer(a), SuggestionValue::Integer(b)) => a.cmp(b), + _ => { + let a = self.to_string(); + let b = other.to_string(); + a.cmp(&b) + } + } + } +} + +impl Display for SuggestionValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SuggestionValue::Text(text) => write!(f, "{text}"), + SuggestionValue::Integer(value) => write!(f, "{value}"), + } + } +} + +impl Ord for SuggestionValue { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match (self, other) { + (SuggestionValue::Text(a), SuggestionValue::Text(b)) => a.cmp(b), + (SuggestionValue::Integer(a), SuggestionValue::Integer(b)) => a.cmp(b), + _ => { + let a = self.to_string(); + let b = other.to_string(); + a.cmp(&b) + } + } + } +} +impl PartialOrd for SuggestionValue { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } } #[cfg(feature = "azalea-buf")] impl McBufWritable for Suggestion { fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> { - self.text.write_into(buf)?; + self.value.to_string().write_into(buf)?; self.tooltip.write_into(buf)?; Ok(()) } diff --git a/azalea-brigadier/src/suggestion/suggestions.rs b/azalea-brigadier/src/suggestion/suggestions.rs index 2a8b5e9e..69877786 100755 --- a/azalea-brigadier/src/suggestion/suggestions.rs +++ b/azalea-brigadier/src/suggestion/suggestions.rs @@ -1,6 +1,8 @@ use super::Suggestion; use crate::context::StringRange; #[cfg(feature = "azalea-buf")] +use crate::suggestion::SuggestionValue; +#[cfg(feature = "azalea-buf")] use azalea_buf::{ BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable, }; @@ -11,12 +13,19 @@ use std::io::{Cursor, Write}; use std::{collections::HashSet, hash::Hash}; #[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Suggestions { - pub range: StringRange, - pub suggestions: Vec>, +pub struct Suggestions +where + M: Clone + PartialEq + Hash, +{ + range: StringRange, + suggestions: Vec>, } impl Suggestions { + pub fn new(range: StringRange, suggestions: Vec>) -> Self { + Self { range, suggestions } + } + pub fn merge(command: &str, input: &[Suggestions]) -> Self { if input.is_empty() { return Suggestions::default(); @@ -45,20 +54,34 @@ impl Suggestions { let range = StringRange::new(start, end); let mut texts = HashSet::new(); for suggestion in suggestions { - texts.insert(suggestion.expand(command, &range)); + texts.insert(suggestion.expand(command, range)); } let mut sorted = texts.into_iter().collect::>(); - sorted.sort_by(|a, b| a.text.cmp(&b.text)); + + sorted.sort_by(|a, b| a.value.cmp_ignore_case(&b.value)); + Suggestions { range, suggestions: sorted, } } + + pub fn is_empty(&self) -> bool { + self.suggestions.is_empty() + } + + pub fn list(&self) -> &[Suggestion] { + &self.suggestions + } + + pub fn range(&self) -> StringRange { + self.range + } } // this can't be derived because that'd require the generic to have `Default` // too even if it's not actually necessary -impl Default for Suggestions { +impl Default for Suggestions { fn default() -> Self { Self { range: StringRange::default(), @@ -85,12 +108,12 @@ impl McBufReadable for Suggestions { let mut suggestions = Vec::::read_from(buf)? .into_iter() .map(|s| Suggestion { - text: s.text, + value: SuggestionValue::Text(s.text), tooltip: s.tooltip, range: range.clone(), }) .collect::>(); - suggestions.sort_by(|a, b| a.text.cmp(&b.text)); + suggestions.sort_by(|a, b| a.value.cmp(&b.value)); Ok(Suggestions { range, suggestions }) } diff --git a/azalea-brigadier/src/suggestion/suggestions_builder.rs b/azalea-brigadier/src/suggestion/suggestions_builder.rs index 66f17fb1..469a7b98 100755 --- a/azalea-brigadier/src/suggestion/suggestions_builder.rs +++ b/azalea-brigadier/src/suggestion/suggestions_builder.rs @@ -1,19 +1,24 @@ use std::collections::HashSet; +use std::hash::Hash; use crate::context::StringRange; -use super::{Suggestion, Suggestions}; +use super::{Suggestion, SuggestionValue, Suggestions}; -pub struct SuggestionsBuilder { +#[derive(PartialEq, Debug)] +pub struct SuggestionsBuilder +where + M: Clone + Eq + Hash, +{ input: String, input_lowercase: String, start: usize, remaining: String, remaining_lowercase: String, - result: HashSet, + result: HashSet>, } -impl SuggestionsBuilder { +impl SuggestionsBuilder<()> { pub fn new(input: &str, start: usize) -> Self { Self::new_with_lowercase(input, input.to_lowercase().as_str(), start) } @@ -28,7 +33,9 @@ impl SuggestionsBuilder { result: HashSet::new(), } } +} +impl SuggestionsBuilder { pub fn input(&self) -> &str { &self.input } @@ -37,7 +44,7 @@ impl SuggestionsBuilder { self.start } - pub fn remianing(&self) -> &str { + pub fn remaining(&self) -> &str { &self.remaining } @@ -45,7 +52,7 @@ impl SuggestionsBuilder { &self.remaining_lowercase } - pub fn build(&self) -> Suggestions { + pub fn build(&self) -> Suggestions { Suggestions::create(&self.input, &self.result) } @@ -55,38 +62,53 @@ impl SuggestionsBuilder { } self.result.insert(Suggestion { range: StringRange::between(self.start, self.input.len()), - text: text.to_string(), + value: SuggestionValue::Text(text.to_string()), tooltip: None, }); self } - pub fn suggest_with_tooltip(mut self, text: &str, tooltip: String) -> Self { + pub fn suggest_with_tooltip(mut self, text: &str, tooltip: M) -> Self { if text == self.remaining { return self; } self.result.insert(Suggestion { range: StringRange::between(self.start, self.input.len()), - text: text.to_string(), + value: SuggestionValue::Text(text.to_string()), tooltip: Some(tooltip), }); self } - // TODO: integer suggestions - // https://github.com/Mojang/brigadier/blob/master/src/main/java/com/mojang/brigadier/suggestion/SuggestionsBuilder.java#L74 + pub fn suggest_integer(mut self, value: i32) -> Self { + self.result.insert(Suggestion { + range: StringRange::between(self.start, self.input.len()), + value: SuggestionValue::Integer(value), + tooltip: None, + }); + self + } + + pub fn suggest_integer_with_tooltip(mut self, value: i32, tooltip: M) -> Self { + self.result.insert(Suggestion { + range: StringRange::between(self.start, self.input.len()), + value: SuggestionValue::Integer(value), + tooltip: Some(tooltip), + }); + self + } #[allow(clippy::should_implement_trait)] - pub fn add(mut self, other: SuggestionsBuilder) -> Self { + pub fn add(mut self, other: SuggestionsBuilder) -> Self { self.result.extend(other.result); self } - pub fn create_offset(&self, start: usize) -> Self { + pub fn create_offset(&self, start: usize) -> SuggestionsBuilder<()> { SuggestionsBuilder::new_with_lowercase(&self.input, &self.input_lowercase, start) } - pub fn restart(self) -> Self { + pub fn restart(&self) -> SuggestionsBuilder<()> { self.create_offset(self.start) } } diff --git a/azalea-brigadier/src/tree/mod.rs b/azalea-brigadier/src/tree/mod.rs index cec972dc..8ab3526e 100755 --- a/azalea-brigadier/src/tree/mod.rs +++ b/azalea-brigadier/src/tree/mod.rs @@ -10,7 +10,13 @@ use crate::{ modifier::RedirectModifier, string_reader::StringReader, }; -use std::{collections::HashMap, fmt::Debug, hash::Hash, ptr, sync::Arc}; +use std::{ + collections::{BTreeMap, HashMap}, + fmt::Debug, + hash::Hash, + ptr, + sync::Arc, +}; pub type Command = Option) -> i32 + Send + Sync>>; @@ -19,7 +25,8 @@ pub type Command = Option) -> i32 + Send + Sync pub struct CommandNode { pub value: ArgumentBuilderType, - pub children: HashMap>>>, + // this is a BTreeMap because children need to be ordered when getting command suggestions + pub children: BTreeMap>>>, pub literals: HashMap>>>, pub arguments: HashMap>>>, @@ -125,6 +132,13 @@ impl CommandNode { } } + pub fn usage_text(&self) -> String { + match &self.value { + ArgumentBuilderType::Argument(argument) => format!("<{}>", argument.name), + ArgumentBuilderType::Literal(literal) => literal.value.to_owned(), + } + } + pub fn child(&self, name: &str) -> Option>>> { self.children.get(name).cloned() } @@ -216,7 +230,7 @@ impl Default for CommandNode { Self { value: ArgumentBuilderType::Literal(Literal::default()), - children: HashMap::new(), + children: BTreeMap::new(), literals: HashMap::new(), arguments: HashMap::new(), diff --git a/azalea-brigadier/tests/arguments/mod.rs b/azalea-brigadier/tests/arguments/mod.rs new file mode 100644 index 00000000..29d656d1 --- /dev/null +++ b/azalea-brigadier/tests/arguments/mod.rs @@ -0,0 +1,6 @@ +mod bool_argument_type_test; +mod double_argument_type_test; +mod float_argument_type_test; +mod integer_argument_type_test; +mod long_argument_type_test; +mod string_argument_type_test; diff --git a/azalea-brigadier/tests/builder/argument_builder_test.rs b/azalea-brigadier/tests/builder/argument_builder_test.rs index ee44f5e6..d5f940dd 100755 --- a/azalea-brigadier/tests/builder/argument_builder_test.rs +++ b/azalea-brigadier/tests/builder/argument_builder_test.rs @@ -1,41 +1,17 @@ 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 builder; - -// @Before -// public void setUp() throws Exception { -// builder = new TestableArgumentBuilder<>(); -// } - -// @Test -// public void testArguments() throws Exception { -// final RequiredArgumentBuilder argument = argument("bar", -// integer()); - -// builder.then(argument); - -// assertThat(builder.getArguments(), hasSize(1)); -// assertThat(builder.getArguments(), hasItem((CommandNode) -// argument.build())); } +use azalea_brigadier::{builder::argument_builder::ArgumentBuilder, prelude::*}; #[test] fn test_arguments() { - let mut builder: ArgumentBuilder<()> = literal("foo"); + let builder: ArgumentBuilder<()> = literal("foo"); let argument: ArgumentBuilder<()> = argument("bar", integer()); - builder.then(argument.clone()); - assert_eq!(builder.arguments.children.len(), 1); + let builder = builder.then(argument.clone()); + assert_eq!(builder.arguments().children.len(), 1); let built_argument = Rc::new(argument.build()); assert!(builder - .arguments + .arguments() .children .values() .any(|e| *e.read() == *built_argument)); diff --git a/azalea-brigadier/tests/builder/mod.rs b/azalea-brigadier/tests/builder/mod.rs new file mode 100644 index 00000000..21944c68 --- /dev/null +++ b/azalea-brigadier/tests/builder/mod.rs @@ -0,0 +1,3 @@ +mod argument_builder_test; +mod literal_argument_builder_test; +mod required_argument_builder_test; diff --git a/azalea-brigadier/tests/command_dispatcher_usages_test.rs b/azalea-brigadier/tests/command_dispatcher_usages_test.rs index 8b137891..d7baff89 100755 --- a/azalea-brigadier/tests/command_dispatcher_usages_test.rs +++ b/azalea-brigadier/tests/command_dispatcher_usages_test.rs @@ -1 +1,143 @@ +use std::{collections::HashSet, sync::Arc}; +use azalea_brigadier::{prelude::*, tree::CommandNode}; +use parking_lot::RwLock; + +fn setup() -> CommandDispatcher<()> { + let command = |_: &CommandContext<()>| 0; + + let mut subject = CommandDispatcher::new(); + subject.register( + literal("a") + .then( + literal("1") + .then(literal("i").executes(command)) + .then(literal("ii").executes(command)), + ) + .then( + literal("2") + .then(literal("i").executes(command)) + .then(literal("ii").executes(command)), + ), + ); + subject.register(literal("b").then(literal("1").executes(command))); + subject.register(literal("c").executes(command)); + subject.register(literal("d").requires(|_| false).executes(command)); + subject.register( + literal("e").executes(command).then( + literal("1") + .executes(command) + .then(literal("i").executes(command)) + .then(literal("ii").executes(command)), + ), + ); + subject.register( + literal("f") + .then( + literal("1") + .then(literal("i").executes(command)) + .then(literal("ii").executes(command).requires(|_| false)), + ) + .then( + literal("2") + .then(literal("i").executes(command).requires(|_| false)) + .then(literal("ii").executes(command)), + ), + ); + subject.register( + literal("g") + .executes(command) + .then(literal("1").then(literal("i").executes(command))), + ); + subject.register( + literal("h") + .executes(command) + .then(literal("1").then(literal("i").executes(command))) + .then(literal("2").then(literal("i").then(literal("ii").executes(command)))) + .then(literal("3").executes(command)), + ); + subject.register( + literal("i") + .executes(command) + .then(literal("1").executes(command)) + .then(literal("2").executes(command)), + ); + subject.register(literal("j").redirect(subject.root.clone())); + subject.register(literal("k").redirect(get(&subject, "h"))); + subject +} + +fn get(subject: &CommandDispatcher<()>, command: &str) -> Arc>> { + subject + .parse(command.into(), ()) + .context + .nodes + .last() + .unwrap() + .node + .clone() +} + +#[test] +fn test_all_usage_no_commands() { + let subject = CommandDispatcher::<()>::new(); + let results = subject.get_all_usage(&subject.root.read(), Arc::new(()), true); + assert!(results.is_empty()); +} + +#[test] +fn test_smart_usage_no_commands() { + let subject = CommandDispatcher::<()>::new(); + let results = subject.get_smart_usage(&subject.root.read(), Arc::new(())); + assert!(results.is_empty()); +} + +#[test] +fn test_all_usage_root() { + let subject = setup(); + let results = subject.get_all_usage(&subject.root.read(), Arc::new(()), true); + + let actual = results.into_iter().collect::>(); + let expected = vec![ + "a 1 i", "a 1 ii", "a 2 i", "a 2 ii", "b 1", "c", "e", "e 1", "e 1 i", "e 1 ii", "f 1 i", + "f 2 ii", "g", "g 1 i", "h", "h 1 i", "h 2 i ii", "h 3", "i", "i 1", "i 2", "j ...", + "k -> h", + ] + .into_iter() + .map(|s| s.to_owned()) + .collect::>(); + assert_eq!(expected, actual); +} + +#[test] +fn test_smart_usage_root() { + let subject = setup(); + let results = subject.get_smart_usage(&subject.root.read(), Arc::new(())); + + let actual = results + .into_iter() + .map(|(k, v)| (k.read().name().to_owned(), v)) + .collect::>(); + + let expected = vec![ + (get(&subject, "a"), "a (1|2)"), + (get(&subject, "b"), "b 1"), + (get(&subject, "c"), "c"), + (get(&subject, "e"), "e [1]"), + (get(&subject, "f"), "f (1|2)"), + (get(&subject, "g"), "g [1]"), + (get(&subject, "h"), "h [1|2|3]"), + (get(&subject, "i"), "i [1|2]"), + (get(&subject, "j"), "j ..."), + (get(&subject, "k"), "k -> h"), + ]; + + println!("-"); + + let expected = expected + .into_iter() + .map(|(k, v)| (k.read().name().to_owned(), v.to_owned())) + .collect::>(); + + assert_eq!(actual, expected); +} diff --git a/azalea-brigadier/tests/context/mod.rs b/azalea-brigadier/tests/context/mod.rs new file mode 100644 index 00000000..e74dce59 --- /dev/null +++ b/azalea-brigadier/tests/context/mod.rs @@ -0,0 +1,2 @@ +mod command_context_test; +mod parsed_argument_test; diff --git a/azalea-brigadier/tests/exceptions/mod.rs b/azalea-brigadier/tests/exceptions/mod.rs new file mode 100644 index 00000000..72292c4d --- /dev/null +++ b/azalea-brigadier/tests/exceptions/mod.rs @@ -0,0 +1,2 @@ +mod dynamic_command_syntax_exception_type_test; +mod simple_command_syntax_exception_type_test; diff --git a/azalea-brigadier/tests/mod.rs b/azalea-brigadier/tests/mod.rs new file mode 100644 index 00000000..14c99a24 --- /dev/null +++ b/azalea-brigadier/tests/mod.rs @@ -0,0 +1,6 @@ +mod arguments; +mod builder; +mod context; +mod exceptions; +mod suggestion; +mod tree; diff --git a/azalea-brigadier/tests/suggestion/mod.rs b/azalea-brigadier/tests/suggestion/mod.rs new file mode 100644 index 00000000..7b96af2b --- /dev/null +++ b/azalea-brigadier/tests/suggestion/mod.rs @@ -0,0 +1,3 @@ +mod suggestion_test; +mod suggestions_builder_test; +mod suggestions_test; \ No newline at end of file diff --git a/azalea-brigadier/tests/suggestion/suggestion_test.rs b/azalea-brigadier/tests/suggestion/suggestion_test.rs index 9ba95807..e3c70c25 100755 --- a/azalea-brigadier/tests/suggestion/suggestion_test.rs +++ b/azalea-brigadier/tests/suggestion/suggestion_test.rs @@ -1,7 +1,12 @@ +use azalea_brigadier::{context::StringRange, suggestion::Suggestion}; + #[test] fn apply_insertation_start() { let suggestion = Suggestion::new(StringRange::at(0), "And so I said: "); - assert_eq!(suggestion.apply("Hello world!"), "And so I said: Hello world!"); + assert_eq!( + suggestion.apply("Hello world!"), + "And so I said: Hello world!" + ); } #[test] @@ -49,23 +54,35 @@ fn expand_unchanged() { #[test] fn expand_left() { let suggestion = Suggestion::new(StringRange::at(1), "oo"); - assert_eq!(suggestion.expand("f", StringRange::between(0, 1)), Suggestion::new(StringRange::between(0, 1), "foo")); + assert_eq!( + suggestion.expand("f", StringRange::between(0, 1)), + Suggestion::new(StringRange::between(0, 1), "foo") + ); } #[test] fn expand_right() { let suggestion = Suggestion::new(StringRange::at(0), "minecraft:"); - assert_eq!(suggestion.expand("fish", StringRange::between(0, 4)), Suggestion::new(StringRange::between(0, 4), "minecraft:fish")); + assert_eq!( + suggestion.expand("fish", StringRange::between(0, 4)), + Suggestion::new(StringRange::between(0, 4), "minecraft:fish") + ); } #[test] fn expand_both() { let suggestion = Suggestion::new(StringRange::at(11), "minecraft:"); - assert_eq!(suggestion.expand("give Steve fish_block", StringRange::between(5, 21)), Suggestion::new(StringRange::between(5, 21), "Steve minecraft:fish_block")); + assert_eq!( + suggestion.expand("give Steve fish_block", StringRange::between(5, 21)), + Suggestion::new(StringRange::between(5, 21), "Steve minecraft:fish_block") + ); } #[test] fn expand_replacement() { let suggestion = Suggestion::new(StringRange::between(6, 11), "strangers"); - assert_eq!(suggestion.expand("Hello world!", StringRange::between(0, 12)), Suggestion::new(StringRange::between(0, 12), "Hello strangers!")); -} \ No newline at end of file + assert_eq!( + suggestion.expand("Hello world!", StringRange::between(0, 12)), + Suggestion::new(StringRange::between(0, 12), "Hello strangers!") + ); +} diff --git a/azalea-brigadier/tests/suggestion/suggestions_builder_test.rs b/azalea-brigadier/tests/suggestion/suggestions_builder_test.rs index e69de29b..08ce65d4 100755 --- a/azalea-brigadier/tests/suggestion/suggestions_builder_test.rs +++ b/azalea-brigadier/tests/suggestion/suggestions_builder_test.rs @@ -0,0 +1,133 @@ +use std::collections::HashSet; + +use azalea_brigadier::{ + context::StringRange, + suggestion::{Suggestion, SuggestionsBuilder}, +}; + +#[test] +fn suggest_appends() { + let builder = SuggestionsBuilder::new("Hello w", 6); + let result = builder.suggest("orld!").build(); + assert_eq!( + result.list(), + vec![Suggestion::new(StringRange::between(6, 7), "orld!")] + ); + assert_eq!(result.range(), StringRange::between(6, 7)); + assert!(!result.is_empty()); +} + +#[test] +fn suggest_replaces() { + let builder = SuggestionsBuilder::new("Hello w", 6); + let result = builder.suggest("everybody").build(); + assert_eq!( + result.list(), + vec![Suggestion::new(StringRange::between(6, 7), "everybody")] + ); + assert_eq!(result.range(), StringRange::between(6, 7)); + assert!(!result.is_empty()); +} + +#[test] +fn suggest_noop() { + let builder = SuggestionsBuilder::new("Hello w", 6); + let result = builder.suggest("w").build(); + assert_eq!(result.list(), vec![]); + assert!(result.is_empty()); +} + +#[test] +fn suggest_multiple() { + let builder = SuggestionsBuilder::new("Hello w", 6); + let result = builder + .suggest("world!") + .suggest("everybody") + .suggest("weekend") + .build(); + assert_eq!( + result.list(), + vec![ + Suggestion::new(StringRange::between(6, 7), "everybody"), + Suggestion::new(StringRange::between(6, 7), "weekend"), + Suggestion::new(StringRange::between(6, 7), "world!"), + ] + ); + assert_eq!(result.range(), StringRange::between(6, 7)); + assert!(!result.is_empty()); +} + +#[test] +fn restart() { + let builder = SuggestionsBuilder::new("Hello w", 6); + let builder = builder.suggest("won't be included in restart"); + let other = builder.restart(); + assert_ne!(other, builder); + assert_eq!(other.input(), builder.input()); + assert_eq!(other.start(), builder.start()); + assert_eq!(other.remaining(), builder.remaining()); +} + +#[test] +fn sort_alphabetical() { + let builder = SuggestionsBuilder::new("Hello w", 6); + let result = builder + .suggest("2") + .suggest("4") + .suggest("6") + .suggest("8") + .suggest("30") + .suggest("32") + .build(); + let actual = result.list().iter().map(|s| s.text()).collect::>(); + assert_eq!(actual, vec!["2", "30", "32", "4", "6", "8"]); +} + +#[test] +fn sort_numerical() { + let builder = SuggestionsBuilder::new("Hello w", 6); + let result = builder + .suggest_integer(2) + .suggest_integer(4) + .suggest_integer(6) + .suggest_integer(8) + .suggest_integer(30) + .suggest_integer(32) + .build(); + let actual = result.list().iter().map(|s| s.text()).collect::>(); + assert_eq!(actual, vec!["2", "4", "6", "8", "30", "32"]); +} + +#[test] +fn sort_mixed() { + let builder = SuggestionsBuilder::new("Hello w", 6); + let result = builder + .suggest("11") + .suggest("22") + .suggest("33") + .suggest("a") + .suggest("b") + .suggest("c") + .suggest_integer(2) + .suggest_integer(4) + .suggest_integer(6) + .suggest_integer(8) + .suggest_integer(30) + .suggest_integer(32) + .suggest("3a") + .suggest("a3") + .build(); + let actual = result + .list() + .iter() + .map(|s| s.text()) + .collect::>(); + // mojang please + let expected = vec![ + "11", "2", "22", "33", "3a", "4", "6", "8", "30", "32", "a", "a3", "b", "c", + ] + .into_iter() + .map(|s| s.to_string()) + .collect::>(); + assert_eq!(actual, expected); +} diff --git a/azalea-brigadier/tests/suggestion/suggestions_test.rs b/azalea-brigadier/tests/suggestion/suggestions_test.rs index 28a8266d..12a2c73b 100755 --- a/azalea-brigadier/tests/suggestion/suggestions_test.rs +++ b/azalea-brigadier/tests/suggestion/suggestions_test.rs @@ -1,20 +1,58 @@ +use std::collections::HashSet; + +use azalea_brigadier::{ + context::StringRange, + suggestion::{Suggestion, Suggestions}, +}; + #[test] fn merge_empty() { - let merged = Suggestions::merge("foo b", vec![]); + let merged = Suggestions::<()>::merge("foo b", &[]); assert!(merged.is_empty()); } #[test] fn merge_single() { - let suggestions = Suggestions::new(StringRange::at(5), vec![Suggestion::new(StringRange::at(5), "ar")]); - let merged = Suggestions::merge("foo b", vec![suggestions]); + let suggestions = Suggestions::new( + StringRange::at(5), + vec![Suggestion::new(StringRange::at(5), "ar")], + ); + let merged = Suggestions::merge("foo b", &[suggestions.clone()]); assert_eq!(merged, suggestions); } #[test] fn merge_multiple() { - let a = Suggestions::new(StringRange::at(5), vec![Suggestion::new(StringRange::at(5), "ar"), Suggestion::new(StringRange::at(5), "az"), Suggestion::new(StringRange::at(5), "Az")]); - let b = Suggestions::new(StringRange::between(4, 5), vec![Suggestion::new(StringRange::between(4, 5), "foo"), Suggestion::new(StringRange::between(4, 5), "qux"), Suggestion::new(StringRange::between(4, 5), "apple"), Suggestion::new(StringRange::between(4, 5), "Bar")]); - let merged = Suggestions::merge("foo b", vec![a, b]); - assert_eq!(merged.get_list(), vec![Suggestion::new(StringRange::between(4, 5), "apple"), Suggestion::new(StringRange::between(4, 5), "ar"), Suggestion::new(StringRange::between(4, 5), "Az"), Suggestion::new(StringRange::between(4, 5), "bar"), Suggestion::new(StringRange::between(4, 5), "Bar"), Suggestion::new(StringRange::between(4, 5), "baz"), Suggestion::new(StringRange::between(4, 5), "bAz"), Suggestion::new(StringRange::between(4, 5), "foo"), Suggestion::new(StringRange::between(4, 5), "qux")]); -} \ No newline at end of file + let a = Suggestions::new( + StringRange::at(5), + vec![ + Suggestion::new(StringRange::at(5), "ar"), + Suggestion::new(StringRange::at(5), "az"), + Suggestion::new(StringRange::at(5), "Az"), + ], + ); + let b = Suggestions::new( + StringRange::between(4, 5), + vec![ + Suggestion::new(StringRange::between(4, 5), "foo"), + Suggestion::new(StringRange::between(4, 5), "qux"), + Suggestion::new(StringRange::between(4, 5), "apple"), + Suggestion::new(StringRange::between(4, 5), "Bar"), + ], + ); + let merged = Suggestions::merge("foo b", &[a, b]); + + let actual = merged.list().iter().cloned().collect::>(); + let expected = vec![ + Suggestion::new(StringRange::between(4, 5), "apple"), + Suggestion::new(StringRange::between(4, 5), "bar"), + Suggestion::new(StringRange::between(4, 5), "Bar"), + Suggestion::new(StringRange::between(4, 5), "baz"), + Suggestion::new(StringRange::between(4, 5), "bAz"), + Suggestion::new(StringRange::between(4, 5), "foo"), + Suggestion::new(StringRange::between(4, 5), "qux"), + ] + .into_iter() + .collect::>(); + assert_eq!(actual, expected); +} diff --git a/azalea-brigadier/tests/tree/mod.rs b/azalea-brigadier/tests/tree/mod.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/azalea-brigadier/tests/tree/mod.rs @@ -0,0 +1 @@ + diff --git a/azalea-chat/src/base_component.rs b/azalea-chat/src/base_component.rs index 43b35aef..dcc28ecc 100755 --- a/azalea-chat/src/base_component.rs +++ b/azalea-chat/src/base_component.rs @@ -1,7 +1,7 @@ use crate::{style::Style, FormattedText}; use serde::Serialize; -#[derive(Clone, Debug, PartialEq, Serialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Eq, Hash)] pub struct BaseComponent { // implements mutablecomponent #[serde(skip_serializing_if = "Vec::is_empty")] diff --git a/azalea-chat/src/component.rs b/azalea-chat/src/component.rs index e087713c..fb7e0522 100755 --- a/azalea-chat/src/component.rs +++ b/azalea-chat/src/component.rs @@ -15,7 +15,7 @@ use std::{ }; /// A chat component, basically anything you can see in chat. -#[derive(Clone, Debug, PartialEq, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)] #[serde(untagged)] pub enum FormattedText { Text(TextComponent), diff --git a/azalea-chat/src/style.rs b/azalea-chat/src/style.rs index 9c0d645e..ba4d6e72 100755 --- a/azalea-chat/src/style.rs +++ b/azalea-chat/src/style.rs @@ -6,7 +6,7 @@ use once_cell::sync::Lazy; use serde::{ser::SerializeStruct, Serialize, Serializer}; use serde_json::Value; -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug, Hash)] pub struct TextColor { pub value: u32, pub name: Option, @@ -290,7 +290,7 @@ impl TryFrom for TextColor { } } -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] pub struct Style { // These are options instead of just bools because None is different than false in this case pub color: Option, diff --git a/azalea-chat/src/text_component.rs b/azalea-chat/src/text_component.rs index 42932d0e..fefd2cb8 100755 --- a/azalea-chat/src/text_component.rs +++ b/azalea-chat/src/text_component.rs @@ -3,7 +3,7 @@ use serde::{ser::SerializeMap, Serialize, Serializer, __private::ser::FlatMapSer use std::fmt::Display; /// A component that contains text that's the same in all locales. -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] pub struct TextComponent { pub base: BaseComponent, pub text: String, diff --git a/azalea-chat/src/translatable_component.rs b/azalea-chat/src/translatable_component.rs index a1c72e35..56c6507e 100755 --- a/azalea-chat/src/translatable_component.rs +++ b/azalea-chat/src/translatable_component.rs @@ -5,7 +5,7 @@ use crate::{ }; use serde::{ser::SerializeMap, Serialize, Serializer, __private::ser::FlatMapSerializer}; -#[derive(Clone, Debug, PartialEq, Serialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Eq, Hash)] #[serde(untagged)] pub enum StringOrComponent { String(String), @@ -13,7 +13,7 @@ pub enum StringOrComponent { } /// A message whose content depends on the client's language. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct TranslatableComponent { pub base: BaseComponent, pub key: String, diff --git a/azalea-protocol/src/packets/game/clientbound_command_suggestions_packet.rs b/azalea-protocol/src/packets/game/clientbound_command_suggestions_packet.rs index 88c6f29e..d1ad1240 100755 --- a/azalea-protocol/src/packets/game/clientbound_command_suggestions_packet.rs +++ b/azalea-protocol/src/packets/game/clientbound_command_suggestions_packet.rs @@ -19,14 +19,14 @@ mod tests { #[test] fn test_suggestions() { - let suggestions = Suggestions { - range: StringRange::new(0, 5), - suggestions: vec![Suggestion { - text: "foo".to_string(), - range: StringRange::new(1, 4), - tooltip: Some(FormattedText::from("bar".to_string())), - }], - }; + let suggestions = Suggestions::new( + StringRange::new(0, 5), + vec![Suggestion::new_with_tooltip( + StringRange::new(1, 4), + "foo", + FormattedText::from("bar".to_string()), + )], + ); let mut buf = Vec::new(); suggestions.write_into(&mut buf).unwrap(); let mut cursor = Cursor::new(&buf[..]);