diff --git a/azalea-brigadier/src/arguments/argument_type.rs b/azalea-brigadier/src/arguments/argument_type.rs index f44233e18..d7bfa7d6f 100755 --- a/azalea-brigadier/src/arguments/argument_type.rs +++ b/azalea-brigadier/src/arguments/argument_type.rs @@ -1,7 +1,19 @@ use std::{any::Any, sync::Arc}; -use crate::{exceptions::CommandSyntaxException, string_reader::StringReader}; +use crate::{ + exceptions::CommandSyntaxException, + string_reader::StringReader, + suggestion::{Suggestions, SuggestionsBuilder}, +}; pub trait ArgumentType { fn parse(&self, reader: &mut StringReader) -> Result, CommandSyntaxException>; + + fn list_suggestions(&self, _builder: SuggestionsBuilder) -> Suggestions { + Suggestions::default() + } + + fn examples(&self) -> Vec { + vec![] + } } diff --git a/azalea-brigadier/src/arguments/bool_argument_type.rs b/azalea-brigadier/src/arguments/bool_argument_type.rs index 2e348c7bb..a73a9da52 100644 --- a/azalea-brigadier/src/arguments/bool_argument_type.rs +++ b/azalea-brigadier/src/arguments/bool_argument_type.rs @@ -1,7 +1,10 @@ use std::{any::Any, sync::Arc}; use crate::{ - context::CommandContext, exceptions::CommandSyntaxException, string_reader::StringReader, + context::CommandContext, + exceptions::CommandSyntaxException, + string_reader::StringReader, + suggestion::{Suggestions, SuggestionsBuilder}, }; use super::ArgumentType; @@ -13,6 +16,20 @@ impl ArgumentType for Boolean { fn parse(&self, reader: &mut StringReader) -> Result, CommandSyntaxException> { Ok(Arc::new(reader.read_boolean()?)) } + + fn list_suggestions(&self, mut builder: SuggestionsBuilder) -> Suggestions { + if "true".starts_with(builder.remaining_lowercase()) { + builder = builder.suggest("true"); + } + if "false".starts_with(builder.remaining_lowercase()) { + builder = builder.suggest("false"); + } + builder.build() + } + + fn examples(&self) -> Vec { + vec!["true".to_string(), "false".to_string()] + } } pub fn bool() -> impl ArgumentType { diff --git a/azalea-brigadier/src/arguments/double_argument_type.rs b/azalea-brigadier/src/arguments/double_argument_type.rs index 9502a6807..ea99f1cfc 100644 --- a/azalea-brigadier/src/arguments/double_argument_type.rs +++ b/azalea-brigadier/src/arguments/double_argument_type.rs @@ -40,6 +40,13 @@ impl ArgumentType for Double { } Ok(Arc::new(result)) } + + fn examples(&self) -> Vec { + vec!["0", "1.2", ".5", "-1", "-.5", "-1234.56"] + .into_iter() + .map(|s| s.to_string()) + .collect() + } } pub fn double() -> impl ArgumentType { diff --git a/azalea-brigadier/src/arguments/float_argument_type.rs b/azalea-brigadier/src/arguments/float_argument_type.rs index a2831a089..2333499a8 100644 --- a/azalea-brigadier/src/arguments/float_argument_type.rs +++ b/azalea-brigadier/src/arguments/float_argument_type.rs @@ -40,6 +40,13 @@ impl ArgumentType for Float { } Ok(Arc::new(result)) } + + fn examples(&self) -> Vec { + vec!["0", "1.2", ".5", "-1", "-.5", "-1234.56"] + .into_iter() + .map(|s| s.to_string()) + .collect() + } } pub fn float() -> impl ArgumentType { diff --git a/azalea-brigadier/src/arguments/integer_argument_type.rs b/azalea-brigadier/src/arguments/integer_argument_type.rs index a31a6e703..cc4755ee6 100644 --- a/azalea-brigadier/src/arguments/integer_argument_type.rs +++ b/azalea-brigadier/src/arguments/integer_argument_type.rs @@ -40,6 +40,13 @@ impl ArgumentType for Integer { } Ok(Arc::new(result)) } + + fn examples(&self) -> Vec { + vec!["0", "123", "-123"] + .into_iter() + .map(|s| s.to_string()) + .collect() + } } pub fn integer() -> impl ArgumentType { diff --git a/azalea-brigadier/src/arguments/long_argument_type.rs b/azalea-brigadier/src/arguments/long_argument_type.rs index d557881a3..4e36abee4 100644 --- a/azalea-brigadier/src/arguments/long_argument_type.rs +++ b/azalea-brigadier/src/arguments/long_argument_type.rs @@ -40,6 +40,13 @@ impl ArgumentType for Long { } Ok(Arc::new(result)) } + + fn examples(&self) -> Vec { + vec!["0", "123", "-123"] + .into_iter() + .map(|s| s.to_string()) + .collect() + } } pub fn long() -> impl ArgumentType { diff --git a/azalea-brigadier/src/arguments/string_argument_type.rs b/azalea-brigadier/src/arguments/string_argument_type.rs index 9fd70d13b..d38fbc79b 100644 --- a/azalea-brigadier/src/arguments/string_argument_type.rs +++ b/azalea-brigadier/src/arguments/string_argument_type.rs @@ -29,6 +29,17 @@ impl ArgumentType for StringArgument { }; Ok(Arc::new(result)) } + + fn examples(&self) -> Vec { + match self { + StringArgument::SingleWord => vec!["word", "words_with_underscores"], + StringArgument::QuotablePhrase => vec!["\"quoted phrase\"", "word", "\"\""], + StringArgument::GreedyPhrase => vec!["word", "words with spaces", "\"and symbols\""], + } + .into_iter() + .map(|s| s.to_string()) + .collect() + } } /// Match up until the next space. diff --git a/azalea-brigadier/src/builder/argument_builder.rs b/azalea-brigadier/src/builder/argument_builder.rs index 643a3bd07..9d3e8cad5 100755 --- a/azalea-brigadier/src/builder/argument_builder.rs +++ b/azalea-brigadier/src/builder/argument_builder.rs @@ -16,11 +16,12 @@ pub enum ArgumentBuilderType { } /// A node that hasn't yet been built. +#[derive(Clone)] pub struct ArgumentBuilder { arguments: CommandNode, command: Command, - requirement: Arc) -> bool + Send + Sync>, + requirement: Arc bool + Send + Sync>, target: Option>>>, forks: bool, @@ -95,13 +96,13 @@ impl ArgumentBuilder { /// # let mut subject = CommandDispatcher::::new(); /// # subject.register( /// literal("foo") - /// .requires(|s: Arc| s.opped) + /// .requires(|s: &CommandSource| s.opped) /// // ... /// # .executes(|ctx: &CommandContext| 42) /// # ); pub fn requires(mut self, requirement: F) -> Self where - F: Fn(Arc) -> bool + Send + Sync + 'static, + F: Fn(&S) -> bool + Send + Sync + 'static, { self.requirement = Arc::new(requirement); self @@ -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/builder/required_argument_builder.rs b/azalea-brigadier/src/builder/required_argument_builder.rs index 60fa713ff..1c79f6195 100755 --- a/azalea-brigadier/src/builder/required_argument_builder.rs +++ b/azalea-brigadier/src/builder/required_argument_builder.rs @@ -1,6 +1,9 @@ use super::argument_builder::{ArgumentBuilder, ArgumentBuilderType}; use crate::{ - arguments::ArgumentType, exceptions::CommandSyntaxException, string_reader::StringReader, + arguments::ArgumentType, + exceptions::CommandSyntaxException, + string_reader::StringReader, + suggestion::{Suggestions, SuggestionsBuilder}, }; use std::{any::Any, fmt::Debug, sync::Arc}; @@ -22,6 +25,17 @@ impl Argument { pub fn parse(&self, reader: &mut StringReader) -> Result, CommandSyntaxException> { self.parser.parse(reader) } + + pub fn list_suggestions(&self, builder: SuggestionsBuilder) -> Suggestions { + // TODO: custom suggestions + // https://github.com/Mojang/brigadier/blob/master/src/main/java/com/mojang/brigadier/tree/ArgumentCommandNode.java#L71 + + self.parser.list_suggestions(builder) + } + + pub fn examples(&self) -> Vec { + self.parser.examples() + } } impl From for ArgumentBuilderType { diff --git a/azalea-brigadier/src/command_dispatcher.rs b/azalea-brigadier/src/command_dispatcher.rs index 672a250b6..3b5987fca 100755 --- a/azalea-brigadier/src/command_dispatcher.rs +++ b/azalea-brigadier/src/command_dispatcher.rs @@ -6,9 +6,16 @@ use crate::{ exceptions::{BuiltInExceptions, CommandSyntaxException}, parse_results::ParseResults, string_reader::StringReader, + suggestion::{Suggestions, SuggestionsBuilder}, 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. /// @@ -63,7 +70,7 @@ impl CommandDispatcher { let cursor = original_reader.cursor(); for child in node.read().get_relevant_nodes(&mut original_reader.clone()) { - if !child.read().can_use(source.clone()) { + if !child.read().can_use(&source) { continue; } let mut context = context_so_far.clone(); @@ -297,6 +304,212 @@ impl CommandDispatcher { }) // Ok(if forked { successful_forks } else { result }) } + + pub fn get_all_usage( + &self, + node: &CommandNode, + source: &S, + 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: &S, + result: &mut Vec, + prefix: &str, + restricted: bool, + ) { + if restricted && !node.can_use(source) { + 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, + 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: &S, + ) -> 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, optional, false); + if let Some(usage) = usage { + result.push((child.clone(), usage)); + } + } + + result + } + + fn get_smart_usage_recursive( + &self, + node: &CommandNode, + source: &S, + optional: bool, + deep: bool, + ) -> Option { + if !node.can_use(source) { + 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)) + .collect::>(); + match children.len().cmp(&1) { + Ordering::Less => {} + Ordering::Equal => { + let usage = self.get_smart_usage_recursive( + &children[0].read(), + source, + 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, 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) + } + + pub fn get_completion_suggestions(parse: ParseResults) -> Suggestions { + let cursor = parse.reader.total_length(); + Self::get_completion_suggestions_with_cursor(parse, cursor) + } + + pub fn get_completion_suggestions_with_cursor( + parse: ParseResults, + cursor: usize, + ) -> Suggestions { + let context = parse.context; + + let node_before_cursor = context.find_suggestion_context(cursor); + let parent = node_before_cursor.parent; + let start = usize::min(node_before_cursor.start_pos, cursor); + + let full_input = parse.reader.string(); + let truncated_input = full_input[..cursor].to_string(); + let truncated_input_lowercase = truncated_input.to_lowercase(); + + let mut all_suggestions = Vec::new(); + for node in parent.read().children.values() { + let suggestions = node.read().list_suggestions( + context.build(&truncated_input), + SuggestionsBuilder::new_with_lowercase( + &truncated_input, + &truncated_input_lowercase, + start, + ), + ); + all_suggestions.push(suggestions); + } + + Suggestions::merge(full_input, &all_suggestions) + } } impl Default for CommandDispatcher { diff --git a/azalea-brigadier/src/context/command_context.rs b/azalea-brigadier/src/context/command_context.rs index f78fe7585..4d93006e2 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 2fc8d4ac3..752958251 100755 --- a/azalea-brigadier/src/context/command_context_builder.rs +++ b/azalea-brigadier/src/context/command_context_builder.rs @@ -2,7 +2,7 @@ use parking_lot::RwLock; use super::{ command_context::CommandContext, parsed_command_node::ParsedCommandNode, - string_range::StringRange, ParsedArgument, + string_range::StringRange, suggestion_context::SuggestionContext, ParsedArgument, }; use crate::{ command_dispatcher::CommandDispatcher, @@ -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,12 +93,49 @@ 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(), } } + + pub fn find_suggestion_context(&self, cursor: usize) -> SuggestionContext { + if self.range.start() > cursor { + panic!("Can't find node before cursor"); + } + + if self.range.end() < cursor { + if let Some(child) = &self.child { + child.find_suggestion_context(cursor) + } else if let Some(last) = self.nodes.last() { + SuggestionContext { + parent: Arc::clone(&last.node), + start_pos: last.range.end() + 1, + } + } else { + SuggestionContext { + parent: Arc::clone(&self.root), + start_pos: self.range.start(), + } + } + } else { + let mut prev = &self.root; + for node in &self.nodes { + if node.range.start() <= cursor && cursor <= node.range.end() { + return SuggestionContext { + parent: Arc::clone(prev), + start_pos: node.range.start(), + }; + } + prev = &node.node; + } + SuggestionContext { + parent: Arc::clone(prev), + start_pos: self.range.start(), + } + } + } } impl Debug for CommandContextBuilder<'_, S> { diff --git a/azalea-brigadier/src/context/mod.rs b/azalea-brigadier/src/context/mod.rs index d535602a9..28e1a12e5 100755 --- a/azalea-brigadier/src/context/mod.rs +++ b/azalea-brigadier/src/context/mod.rs @@ -3,6 +3,7 @@ mod command_context_builder; mod parsed_argument; mod parsed_command_node; mod string_range; +pub mod suggestion_context; pub use command_context::CommandContext; pub use command_context_builder::CommandContextBuilder; diff --git a/azalea-brigadier/src/context/parsed_command_node.rs b/azalea-brigadier/src/context/parsed_command_node.rs index bba5d1218..2d69c72e9 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 8ca886248..751634056 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/context/suggestion_context.rs b/azalea-brigadier/src/context/suggestion_context.rs new file mode 100644 index 000000000..58a73fdb0 --- /dev/null +++ b/azalea-brigadier/src/context/suggestion_context.rs @@ -0,0 +1,11 @@ +use std::sync::Arc; + +use parking_lot::RwLock; + +use crate::tree::CommandNode; + +#[derive(Debug)] +pub struct SuggestionContext { + pub parent: Arc>>, + pub start_pos: usize, +} diff --git a/azalea-brigadier/src/suggestion/mod.rs b/azalea-brigadier/src/suggestion/mod.rs index c404adc71..fbebfe8a0 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,45 @@ 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 { pub range: StringRange, - pub tooltip: Option, + value: SuggestionValue, + pub tooltip: Option, } -impl Suggestion { +#[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, + } + } + + pub fn new_with_tooltip(range: StringRange, text: &str, tooltip: String) -> 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 input.to_string(); + return text; } - let mut result = String::with_capacity(self.text.len()); + 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,31 +66,82 @@ 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 { +impl McBufWritable for Suggestion { fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> { - self.text.write_into(buf)?; - self.tooltip.write_into(buf)?; + self.value.to_string().write_into(buf)?; + self.tooltip + .clone() + .map(FormattedText::from) + .write_into(buf)?; Ok(()) } } diff --git a/azalea-brigadier/src/suggestion/suggestions.rs b/azalea-brigadier/src/suggestion/suggestions.rs index 2a8b5e9e6..487e42336 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, }; @@ -10,14 +12,18 @@ use azalea_chat::FormattedText; 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>, +#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] +pub struct Suggestions { + range: StringRange, + suggestions: Vec, } -impl Suggestions { - pub fn merge(command: &str, input: &[Suggestions]) -> Self { +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(); } else if input.len() == 1 { @@ -32,7 +38,7 @@ impl Suggestions { Suggestions::create(command, &texts) } - pub fn create(command: &str, suggestions: &HashSet>) -> Self { + pub fn create(command: &str, suggestions: &HashSet) -> Self { if suggestions.is_empty() { return Suggestions::default(); }; @@ -45,30 +51,33 @@ 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, } } -} -// 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 { - fn default() -> Self { - Self { - range: StringRange::default(), - suggestions: Vec::new(), - } + pub fn is_empty(&self) -> bool { + self.suggestions.is_empty() + } + + pub fn list(&self) -> &[Suggestion] { + &self.suggestions + } + + pub fn range(&self) -> StringRange { + self.range } } #[cfg(feature = "azalea-buf")] -impl McBufReadable for Suggestions { +impl McBufReadable for Suggestions { fn read_from(buf: &mut Cursor<&[u8]>) -> Result { #[derive(McBuf)] struct StandaloneSuggestion { @@ -85,19 +94,19 @@ impl McBufReadable for Suggestions { let mut suggestions = Vec::::read_from(buf)? .into_iter() .map(|s| Suggestion { - text: s.text, - tooltip: s.tooltip, - range: range.clone(), + value: SuggestionValue::Text(s.text), + tooltip: s.tooltip.map(|t| t.to_string()), + range, }) .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 }) } } #[cfg(feature = "azalea-buf")] -impl McBufWritable for Suggestions { +impl McBufWritable for Suggestions { fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> { (self.range.start() as u32).var_write_into(buf)?; (self.range.length() as u32).var_write_into(buf)?; diff --git a/azalea-brigadier/src/suggestion/suggestions_builder.rs b/azalea-brigadier/src/suggestion/suggestions_builder.rs index 66f17fb1b..4e6296dd1 100755 --- a/azalea-brigadier/src/suggestion/suggestions_builder.rs +++ b/azalea-brigadier/src/suggestion/suggestions_builder.rs @@ -2,8 +2,9 @@ use std::collections::HashSet; use crate::context::StringRange; -use super::{Suggestion, Suggestions}; +use super::{Suggestion, SuggestionValue, Suggestions}; +#[derive(PartialEq, Debug)] pub struct SuggestionsBuilder { input: String, input_lowercase: String, @@ -28,7 +29,9 @@ impl SuggestionsBuilder { result: HashSet::new(), } } +} +impl SuggestionsBuilder { pub fn input(&self) -> &str { &self.input } @@ -37,7 +40,7 @@ impl SuggestionsBuilder { self.start } - pub fn remianing(&self) -> &str { + pub fn remaining(&self) -> &str { &self.remaining } @@ -55,7 +58,7 @@ 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 @@ -67,14 +70,29 @@ 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: 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: String) -> 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 { @@ -82,11 +100,11 @@ impl SuggestionsBuilder { 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 cec972dc5..a2b1f38a2 100755 --- a/azalea-brigadier/src/tree/mod.rs +++ b/azalea-brigadier/src/tree/mod.rs @@ -9,8 +9,15 @@ use crate::{ exceptions::{BuiltInExceptions, CommandSyntaxException}, modifier::RedirectModifier, string_reader::StringReader, + suggestion::{Suggestions, SuggestionsBuilder}, +}; +use std::{ + collections::{BTreeMap, HashMap}, + fmt::Debug, + hash::Hash, + ptr, + sync::Arc, }; -use std::{collections::HashMap, fmt::Debug, hash::Hash, ptr, sync::Arc}; pub type Command = Option) -> i32 + Send + Sync>>; @@ -19,12 +26,13 @@ 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>>>, pub command: Command, - pub requirement: Arc) -> bool + Send + Sync>, + pub requirement: Arc bool + Send + Sync>, pub redirect: Option>>>, pub forks: bool, pub modifier: Option>>, @@ -90,7 +98,7 @@ impl CommandNode { } } - pub fn can_use(&self, source: Arc) -> bool { + pub fn can_use(&self, source: &S) -> bool { (self.requirement)(source) } @@ -125,6 +133,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() } @@ -195,6 +210,29 @@ impl CommandNode { } None } + + pub fn list_suggestions( + &self, + // context is here because that's how it is in mojang's brigadier, but we haven't + // implemented custom suggestions yet so this is unused rn + _context: CommandContext, + builder: SuggestionsBuilder, + ) -> Suggestions { + match &self.value { + ArgumentBuilderType::Literal(literal) => { + if literal + .value + .to_lowercase() + .starts_with(builder.remaining_lowercase()) + { + builder.suggest(&literal.value).build() + } else { + Suggestions::default() + } + } + ArgumentBuilderType::Argument(argument) => argument.list_suggestions(builder), + } + } } impl Debug for CommandNode { @@ -216,7 +254,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/bool_argument_type_test.rs b/azalea-brigadier/tests/arguments/bool_argument_type_test.rs index e69de29bb..8b1378917 100755 --- a/azalea-brigadier/tests/arguments/bool_argument_type_test.rs +++ b/azalea-brigadier/tests/arguments/bool_argument_type_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/arguments/double_argument_type_test.rs b/azalea-brigadier/tests/arguments/double_argument_type_test.rs index e69de29bb..8b1378917 100755 --- a/azalea-brigadier/tests/arguments/double_argument_type_test.rs +++ b/azalea-brigadier/tests/arguments/double_argument_type_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/arguments/float_argument_type_test.rs b/azalea-brigadier/tests/arguments/float_argument_type_test.rs index e69de29bb..8b1378917 100755 --- a/azalea-brigadier/tests/arguments/float_argument_type_test.rs +++ b/azalea-brigadier/tests/arguments/float_argument_type_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/arguments/integer_argument_type_test.rs b/azalea-brigadier/tests/arguments/integer_argument_type_test.rs index e69de29bb..8b1378917 100755 --- a/azalea-brigadier/tests/arguments/integer_argument_type_test.rs +++ b/azalea-brigadier/tests/arguments/integer_argument_type_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/arguments/long_argument_type_test.rs b/azalea-brigadier/tests/arguments/long_argument_type_test.rs index e69de29bb..8b1378917 100755 --- a/azalea-brigadier/tests/arguments/long_argument_type_test.rs +++ b/azalea-brigadier/tests/arguments/long_argument_type_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/arguments/mod.rs b/azalea-brigadier/tests/arguments/mod.rs new file mode 100644 index 000000000..29d656d11 --- /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/arguments/string_argument_type_test.rs b/azalea-brigadier/tests/arguments/string_argument_type_test.rs index e69de29bb..8b1378917 100755 --- a/azalea-brigadier/tests/arguments/string_argument_type_test.rs +++ b/azalea-brigadier/tests/arguments/string_argument_type_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/builder/argument_builder_test.rs b/azalea-brigadier/tests/builder/argument_builder_test.rs index ee44f5e66..d5f940dd1 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/literal_argument_builder_test.rs b/azalea-brigadier/tests/builder/literal_argument_builder_test.rs index e69de29bb..8b1378917 100755 --- a/azalea-brigadier/tests/builder/literal_argument_builder_test.rs +++ b/azalea-brigadier/tests/builder/literal_argument_builder_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/builder/mod.rs b/azalea-brigadier/tests/builder/mod.rs new file mode 100644 index 000000000..21944c680 --- /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/builder/required_argument_builder_test.rs b/azalea-brigadier/tests/builder/required_argument_builder_test.rs index e69de29bb..8b1378917 100755 --- a/azalea-brigadier/tests/builder/required_argument_builder_test.rs +++ b/azalea-brigadier/tests/builder/required_argument_builder_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/command_dispatcher_usages_test.rs b/azalea-brigadier/tests/command_dispatcher_usages_test.rs index 8b1378917..a80a2a97c 100755 --- a/azalea-brigadier/tests/command_dispatcher_usages_test.rs +++ b/azalea-brigadier/tests/command_dispatcher_usages_test.rs @@ -1 +1,194 @@ +use std::{collections::HashSet, sync::Arc}; +use azalea_brigadier::{prelude::*, string_reader::StringReader, 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(), &(), 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(), &()); + assert!(results.is_empty()); +} + +#[test] +fn test_all_usage_root() { + let subject = setup(); + let results = subject.get_all_usage(&subject.root.read(), &(), 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(), &()); + + 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); +} + +#[test] +fn test_smart_usage_h() { + let subject = setup(); + let results = subject.get_smart_usage(&get(&subject, "h").read(), &()); + + let actual = results + .into_iter() + .map(|(k, v)| (k.read().name().to_owned(), v)) + .collect::>(); + + let expected = vec![ + (get(&subject, "h 1"), "[1] i"), + (get(&subject, "h 2"), "[2] i ii"), + (get(&subject, "h 3"), "[3]"), + ]; + + let expected = expected + .into_iter() + .map(|(k, v)| (k.read().name().to_owned(), v.to_owned())) + .collect::>(); + + assert_eq!(actual, expected); +} + +#[test] +fn test_smart_usage_offset_h() { + let subject = setup(); + let mut offset_h = StringReader::from("/|/|/h"); + offset_h.cursor = 5; + + let results = subject.get_smart_usage(&get(&subject, "h").read(), &()); + + let actual = results + .into_iter() + .map(|(k, v)| (k.read().name().to_owned(), v)) + .collect::>(); + + let expected = vec![ + (get(&subject, "h 1"), "[1] i"), + (get(&subject, "h 2"), "[2] i ii"), + (get(&subject, "h 3"), "[3]"), + ]; + + 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/command_suggestions_test.rs b/azalea-brigadier/tests/command_suggestions_test.rs index 8b1378917..a907dd6e4 100755 --- a/azalea-brigadier/tests/command_suggestions_test.rs +++ b/azalea-brigadier/tests/command_suggestions_test.rs @@ -1 +1,446 @@ +use azalea_brigadier::{ + context::StringRange, prelude::*, string_reader::StringReader, suggestion::Suggestion, +}; +fn test_suggestions( + subject: &CommandDispatcher<()>, + contents: &str, + cursor: usize, + range: StringRange, + suggestions: Vec<&str>, +) { + let result = CommandDispatcher::get_completion_suggestions_with_cursor( + subject.parse(contents.into(), ()), + cursor, + ); + assert_eq!(result.range(), range); + + let mut expected = Vec::new(); + for suggestion in suggestions { + expected.push(Suggestion::new(range, suggestion)); + } + + assert_eq!(result.list(), expected); +} + +fn input_with_offset(input: &str, offset: usize) -> StringReader { + let mut result = StringReader::from(input); + result.cursor = offset; + result +} + +#[test] +fn get_completion_suggestions_root_commands() { + let mut subject = CommandDispatcher::<()>::new(); + subject.register(literal("foo")); + subject.register(literal("bar")); + subject.register(literal("baz")); + + let result = CommandDispatcher::get_completion_suggestions(subject.parse("".into(), ())); + + assert_eq!(result.range(), StringRange::at(0)); + assert_eq!( + result.list(), + vec![ + Suggestion::new(StringRange::at(0), "bar"), + Suggestion::new(StringRange::at(0), "baz"), + Suggestion::new(StringRange::at(0), "foo") + ] + ); +} + +#[test] +fn get_completion_suggestions_root_commands_with_input_offset() { + let mut subject = CommandDispatcher::<()>::new(); + subject.register(literal("foo")); + subject.register(literal("bar")); + subject.register(literal("baz")); + + let result = CommandDispatcher::get_completion_suggestions( + subject.parse(input_with_offset("OOO", 3), ()), + ); + + assert_eq!(result.range(), StringRange::at(3)); + assert_eq!( + result.list(), + vec![ + Suggestion::new(StringRange::at(3), "bar"), + Suggestion::new(StringRange::at(3), "baz"), + Suggestion::new(StringRange::at(3), "foo") + ] + ); +} + +#[test] +fn get_completion_suggestions_root_commands_partial() { + let mut subject = CommandDispatcher::<()>::new(); + subject.register(literal("foo")); + subject.register(literal("bar")); + subject.register(literal("baz")); + + let result = CommandDispatcher::get_completion_suggestions(subject.parse("b".into(), ())); + + assert_eq!(result.range(), StringRange::between(0, 1)); + assert_eq!( + result.list(), + vec![ + Suggestion::new(StringRange::between(0, 1), "bar"), + Suggestion::new(StringRange::between(0, 1), "baz") + ] + ); +} + +#[test] +fn get_completion_suggestions_root_commands_partial_with_input_offset() { + let mut subject = CommandDispatcher::<()>::new(); + subject.register(literal("foo")); + subject.register(literal("bar")); + subject.register(literal("baz")); + + let result = CommandDispatcher::get_completion_suggestions( + subject.parse(input_with_offset("Zb", 1), ()), + ); + + assert_eq!(result.range(), StringRange::between(1, 2)); + assert_eq!( + result.list(), + vec![ + Suggestion::new(StringRange::between(1, 2), "bar"), + Suggestion::new(StringRange::between(1, 2), "baz") + ] + ); +} + +#[test] +fn get_completion_suggestions_sub_commands() { + let mut subject = CommandDispatcher::<()>::new(); + subject.register( + literal("parent") + .then(literal("foo")) + .then(literal("bar")) + .then(literal("baz")), + ); + + let result = CommandDispatcher::get_completion_suggestions(subject.parse("parent ".into(), ())); + + assert_eq!(result.range(), StringRange::at(7)); + assert_eq!( + result.list(), + vec![ + Suggestion::new(StringRange::at(7), "bar"), + Suggestion::new(StringRange::at(7), "baz"), + Suggestion::new(StringRange::at(7), "foo") + ] + ); +} + +#[test] +fn get_completion_suggestions_moving_cursor_sub_commands() { + let mut subject = CommandDispatcher::<()>::new(); + subject.register( + literal("parent_one") + .then(literal("faz")) + .then(literal("fbz")) + .then(literal("gaz")), + ); + + subject.register(literal("parent_two")); + + test_suggestions( + &subject, + "parent_one faz ", + 0, + StringRange::at(0), + vec!["parent_one", "parent_two"], + ); + test_suggestions( + &subject, + "parent_one faz ", + 1, + StringRange::between(0, 1), + vec!["parent_one", "parent_two"], + ); + test_suggestions( + &subject, + "parent_one faz ", + 7, + StringRange::between(0, 7), + vec!["parent_one", "parent_two"], + ); + test_suggestions( + &subject, + "parent_one faz ", + 8, + StringRange::between(0, 8), + vec!["parent_one"], + ); + test_suggestions(&subject, "parent_one faz ", 10, StringRange::at(0), vec![]); + test_suggestions( + &subject, + "parent_one faz ", + 11, + StringRange::at(11), + vec!["faz", "fbz", "gaz"], + ); + test_suggestions( + &subject, + "parent_one faz ", + 12, + StringRange::between(11, 12), + vec!["faz", "fbz"], + ); + test_suggestions( + &subject, + "parent_one faz ", + 13, + StringRange::between(11, 13), + vec!["faz"], + ); + test_suggestions(&subject, "parent_one faz ", 14, StringRange::at(0), vec![]); + test_suggestions(&subject, "parent_one faz ", 15, StringRange::at(0), vec![]); +} + +#[test] +fn get_completion_suggestions_sub_commands_partial() { + let mut subject = CommandDispatcher::<()>::new(); + subject.register( + literal("parent") + .then(literal("foo")) + .then(literal("bar")) + .then(literal("baz")), + ); + + let parse = subject.parse("parent b".into(), ()); + + let result = CommandDispatcher::get_completion_suggestions(parse); + + assert_eq!(result.range(), StringRange::between(7, 8)); + assert_eq!( + result.list(), + vec![ + Suggestion::new(StringRange::between(7, 8), "bar"), + Suggestion::new(StringRange::between(7, 8), "baz") + ] + ); +} + +#[test] +fn get_completion_suggestions_sub_commands_partial_with_input_offset() { + let mut subject = CommandDispatcher::<()>::new(); + subject.register( + literal("parent") + .then(literal("foo")) + .then(literal("bar")) + .then(literal("baz")), + ); + + let parse = subject.parse(input_with_offset("junk parent b", 5), ()); + + let result = CommandDispatcher::get_completion_suggestions(parse); + + assert_eq!(result.range(), StringRange::between(12, 13)); + assert_eq!( + result.list(), + vec![ + Suggestion::new(StringRange::between(12, 13), "bar"), + Suggestion::new(StringRange::between(12, 13), "baz") + ] + ); +} + +#[test] +fn get_completion_suggestions_redirect() { + let mut subject = CommandDispatcher::<()>::new(); + let actual = subject.register(literal("actual").then(literal("sub"))); + subject.register(literal("redirect").redirect(actual)); + + let parse = subject.parse("redirect ".into(), ()); + + let result = CommandDispatcher::get_completion_suggestions(parse); + + assert_eq!(result.range(), StringRange::at(9)); + assert_eq!( + result.list(), + vec![Suggestion::new(StringRange::at(9), "sub")] + ); +} + +#[test] +fn get_completion_suggestions_redirect_partial() { + let mut subject = CommandDispatcher::<()>::new(); + let actual = subject.register(literal("actual").then(literal("sub"))); + subject.register(literal("redirect").redirect(actual)); + + let parse = subject.parse("redirect s".into(), ()); + + let result = CommandDispatcher::get_completion_suggestions(parse); + + assert_eq!(result.range(), StringRange::between(9, 10)); + assert_eq!( + result.list(), + vec![Suggestion::new(StringRange::between(9, 10), "sub")] + ); +} + +#[test] +fn get_completion_suggestions_moving_cursor_redirect() { + let mut subject = CommandDispatcher::<()>::new(); + let actual_one = subject.register( + literal("actual_one") + .then(literal("faz")) + .then(literal("fbz")) + .then(literal("gaz")), + ); + + subject.register(literal("actual_two")); + + subject.register(literal("redirect_one").redirect(actual_one.clone())); + subject.register(literal("redirect_two").redirect(actual_one)); + + test_suggestions( + &subject, + "redirect_one faz ", + 0, + StringRange::at(0), + vec!["actual_one", "actual_two", "redirect_one", "redirect_two"], + ); + test_suggestions( + &subject, + "redirect_one faz ", + 9, + StringRange::between(0, 9), + vec!["redirect_one", "redirect_two"], + ); + test_suggestions( + &subject, + "redirect_one faz ", + 10, + StringRange::between(0, 10), + vec!["redirect_one"], + ); + test_suggestions( + &subject, + "redirect_one faz ", + 12, + StringRange::at(0), + vec![], + ); + test_suggestions( + &subject, + "redirect_one faz ", + 13, + StringRange::at(13), + vec!["faz", "fbz", "gaz"], + ); + test_suggestions( + &subject, + "redirect_one faz ", + 14, + StringRange::between(13, 14), + vec!["faz", "fbz"], + ); + test_suggestions( + &subject, + "redirect_one faz ", + 15, + StringRange::between(13, 15), + vec!["faz"], + ); + test_suggestions( + &subject, + "redirect_one faz ", + 16, + StringRange::at(0), + vec![], + ); + test_suggestions( + &subject, + "redirect_one faz ", + 17, + StringRange::at(0), + vec![], + ); +} + +#[test] +fn get_completion_suggestions_redirect_partial_with_input_offset() { + let mut subject = CommandDispatcher::<()>::new(); + let actual = subject.register(literal("actual").then(literal("sub"))); + subject.register(literal("redirect").redirect(actual)); + + let parse = subject.parse(input_with_offset("/redirect s", 1), ()); + + let result = CommandDispatcher::get_completion_suggestions(parse); + + assert_eq!(result.range(), StringRange::between(10, 11)); + assert_eq!( + result.list(), + vec![Suggestion::new(StringRange::between(10, 11), "sub")] + ); +} + +#[test] +fn get_completion_suggestions_redirect_lots() { + let mut subject = CommandDispatcher::<()>::new(); + let loop_ = subject.register(literal("redirect")); + subject.register( + literal("redirect").then(literal("loop").then(argument("loop", integer()).redirect(loop_))), + ); + + let result = CommandDispatcher::get_completion_suggestions( + subject.parse("redirect loop 1 loop 02 loop 003 ".into(), ()), + ); + + assert_eq!(result.range(), StringRange::at(33)); + assert_eq!( + result.list(), + vec![Suggestion::new(StringRange::at(33), "loop")] + ); +} + +#[test] +fn get_completion_suggestions_execute_simulation() { + let mut subject = CommandDispatcher::<()>::new(); + let execute = subject.register(literal("execute")); + subject.register( + literal("execute") + .then(literal("as").then(argument("name", word()).redirect(execute.clone()))) + .then(literal("store").then(argument("name", word()).redirect(execute))) + .then(literal("run").executes(|_| 0)), + ); + + let parse = subject.parse("execute as Dinnerbone as".into(), ()); + + let result = CommandDispatcher::get_completion_suggestions(parse); + + assert!(result.is_empty()); +} + +#[test] +fn get_completion_suggestions_execute_simulation_partial() { + let mut subject = CommandDispatcher::<()>::new(); + let execute = subject.register(literal("execute")); + subject.register( + literal("execute") + .then( + literal("as") + .then(literal("bar").redirect(execute.clone())) + .then(literal("baz").redirect(execute.clone())), + ) + .then(literal("store").then(argument("name", word()).redirect(execute))) + .then(literal("run").executes(|_| 0)), + ); + + let parse = subject.parse("execute as bar as ".into(), ()); + + let result = CommandDispatcher::get_completion_suggestions(parse); + + assert_eq!(result.range(), StringRange::at(18)); + assert_eq!( + result.list(), + vec![ + Suggestion::new(StringRange::at(18), "bar"), + Suggestion::new(StringRange::at(18), "baz") + ] + ); +} diff --git a/azalea-brigadier/tests/context/command_context_test.rs b/azalea-brigadier/tests/context/command_context_test.rs index e69de29bb..8b1378917 100755 --- a/azalea-brigadier/tests/context/command_context_test.rs +++ b/azalea-brigadier/tests/context/command_context_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/context/mod.rs b/azalea-brigadier/tests/context/mod.rs new file mode 100644 index 000000000..e74dce590 --- /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/context/parsed_argument_test.rs b/azalea-brigadier/tests/context/parsed_argument_test.rs index e69de29bb..8b1378917 100755 --- a/azalea-brigadier/tests/context/parsed_argument_test.rs +++ b/azalea-brigadier/tests/context/parsed_argument_test.rs @@ -0,0 +1 @@ + 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 index e69de29bb..8b1378917 100755 --- a/azalea-brigadier/tests/exceptions/dynamic_command_syntax_exception_type_test.rs +++ b/azalea-brigadier/tests/exceptions/dynamic_command_syntax_exception_type_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/exceptions/mod.rs b/azalea-brigadier/tests/exceptions/mod.rs new file mode 100644 index 000000000..72292c4d9 --- /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/exceptions/simple_command_syntax_exception_type_test.rs b/azalea-brigadier/tests/exceptions/simple_command_syntax_exception_type_test.rs index e69de29bb..8b1378917 100755 --- a/azalea-brigadier/tests/exceptions/simple_command_syntax_exception_type_test.rs +++ b/azalea-brigadier/tests/exceptions/simple_command_syntax_exception_type_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/mod.rs b/azalea-brigadier/tests/mod.rs new file mode 100644 index 000000000..14c99a247 --- /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 000000000..0ac50d529 --- /dev/null +++ b/azalea-brigadier/tests/suggestion/mod.rs @@ -0,0 +1,3 @@ +mod suggestion_test; +mod suggestions_builder_test; +mod suggestions_test; diff --git a/azalea-brigadier/tests/suggestion/suggestion_test.rs b/azalea-brigadier/tests/suggestion/suggestion_test.rs index 9ba95807e..e3c70c25d 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 e69de29bb..08ce65d4f 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 28a8266da..987dfb71a 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 000000000..8b1378917 --- /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 43b35aef6..dcc28ecc7 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 e087713ca..fb7e0522f 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 9c0d645ee..ba4d6e724 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 42932d0e0..fefd2cb8e 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 a1c72e351..56c6507e5 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-client/src/attack.rs b/azalea-client/src/attack.rs index 644af5de6..56f3b1d0c 100644 --- a/azalea-client/src/attack.rs +++ b/azalea-client/src/attack.rs @@ -15,7 +15,7 @@ use derive_more::{Deref, DerefMut}; use crate::{ interact::SwingArmEvent, local_player::{LocalGameMode, SendPacketEvent}, - movement::walk_listener, + movement::MoveEventsSet, respawn::perform_respawn, Client, }; @@ -28,7 +28,7 @@ impl Plugin for AttackPlugin { Update, handle_attack_event .before(update_bounding_box) - .before(walk_listener) + .before(MoveEventsSet) .after(perform_respawn), ) .add_systems( @@ -106,7 +106,7 @@ pub fn handle_attack_event( ticks_since_last_attack.0 = 0; - physics.delta = physics.delta.multiply(0.6, 1.0, 0.6); + physics.velocity = physics.velocity.multiply(0.6, 1.0, 0.6); **sprinting = false; } } diff --git a/azalea-client/src/interact.rs b/azalea-client/src/interact.rs index b7b47ec66..32b68925c 100644 --- a/azalea-client/src/interact.rs +++ b/azalea-client/src/interact.rs @@ -37,6 +37,7 @@ use crate::{ local_player::{ handle_send_packet_event, LocalGameMode, PermissionLevel, PlayerAbilities, SendPacketEvent, }, + movement::MoveEventsSet, respawn::perform_respawn, Client, }; @@ -62,7 +63,7 @@ impl Plugin for InteractPlugin { .chain(), update_modifiers_for_held_item .after(InventorySet) - .after(crate::movement::walk_listener), + .after(MoveEventsSet), ), ); } diff --git a/azalea-client/src/mining.rs b/azalea-client/src/mining.rs index 806a7b91d..e1193f734 100644 --- a/azalea-client/src/mining.rs +++ b/azalea-client/src/mining.rs @@ -18,6 +18,7 @@ use crate::{ }, inventory::{InventoryComponent, InventorySet}, local_player::{LocalGameMode, PermissionLevel, PlayerAbilities, SendPacketEvent}, + movement::MoveEventsSet, Client, }; @@ -43,6 +44,7 @@ impl Plugin for MinePlugin { .chain() .in_set(MiningSet) .after(InventorySet) + .after(MoveEventsSet) .before(azalea_entity::update_bounding_box) .after(azalea_entity::update_fluid_on_eyes) .after(crate::interact::update_hit_result_component) diff --git a/azalea-client/src/movement.rs b/azalea-client/src/movement.rs index 286281159..945f0c0cb 100644 --- a/azalea-client/src/movement.rs +++ b/azalea-client/src/movement.rs @@ -1,5 +1,6 @@ use crate::client::Client; use crate::local_player::SendPacketEvent; +use azalea_core::position::Vec3; use azalea_entity::{metadata::Sprinting, Attributes, Jumping}; use azalea_entity::{InLoadedChunk, LastSentPosition, LookDirection, Physics, Position}; use azalea_physics::{ai_step, PhysicsSet}; @@ -13,6 +14,7 @@ use azalea_protocol::packets::game::{ use azalea_world::{MinecraftEntityId, MoveEntityError}; use bevy_app::{App, FixedUpdate, Plugin, Update}; use bevy_ecs::prelude::{Event, EventWriter}; +use bevy_ecs::schedule::SystemSet; use bevy_ecs::{ component::Component, entity::Entity, event::EventReader, query::With, schedule::IntoSystemConfigs, system::Query, @@ -44,7 +46,13 @@ impl Plugin for PlayerMovePlugin { fn build(&self, app: &mut App) { app.add_event::() .add_event::() - .add_systems(Update, (sprint_listener, walk_listener).chain()) + .add_event::() + .add_systems( + Update, + (handle_sprint, handle_walk, handle_knockback) + .chain() + .in_set(MoveEventsSet), + ) .add_systems( FixedUpdate, ( @@ -60,6 +68,9 @@ impl Plugin for PlayerMovePlugin { } } +#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] +pub struct MoveEventsSet; + impl Client { /// Set whether we're jumping. This acts as if you held space in /// vanilla. If you want to jump once, use the `jump` function. @@ -391,10 +402,9 @@ pub struct StartWalkEvent { pub direction: WalkDirection, } -/// Start walking in the given direction. To sprint, use -/// [`Client::sprint`]. To stop walking, call walk with -/// `WalkDirection::None`. -pub fn walk_listener( +/// The system that makes the player start walking when they receive a +/// [`StartWalkEvent`]. +pub fn handle_walk( mut events: EventReader, mut query: Query<(&mut PhysicsState, &mut Sprinting, &mut Attributes)>, ) { @@ -415,8 +425,9 @@ pub struct StartSprintEvent { pub entity: Entity, pub direction: SprintDirection, } -/// Start sprinting in the given direction. -pub fn sprint_listener( +/// The system that makes the player start sprinting when they receive a +/// [`StartSprintEvent`]. +pub fn handle_sprint( mut query: Query<&mut PhysicsState>, mut events: EventReader, ) { @@ -459,6 +470,36 @@ fn has_enough_impulse_to_start_sprinting(physics_state: &PhysicsState) -> bool { // } } +/// An event sent by the server that sets or adds to our velocity. Usually +/// `KnockbackKind::Set` is used for normal knockback and `KnockbackKind::Add` +/// is used for explosions, but some servers (notably Hypixel) use explosions +/// for knockback. +#[derive(Event)] +pub struct KnockbackEvent { + pub entity: Entity, + pub knockback: KnockbackType, +} + +pub enum KnockbackType { + Set(Vec3), + Add(Vec3), +} + +pub fn handle_knockback(mut query: Query<&mut Physics>, mut events: EventReader) { + for event in events.iter() { + if let Ok(mut physics) = query.get_mut(event.entity) { + match event.knockback { + KnockbackType::Set(velocity) => { + physics.velocity = velocity; + } + KnockbackType::Add(velocity) => { + physics.velocity += velocity; + } + } + } + } +} + #[derive(Clone, Copy, Debug, Default)] pub enum WalkDirection { #[default] diff --git a/azalea-client/src/packet_handling/game.rs b/azalea-client/src/packet_handling/game.rs index e5e211ee1..3b30f52e1 100644 --- a/azalea-client/src/packet_handling/game.rs +++ b/azalea-client/src/packet_handling/game.rs @@ -44,6 +44,7 @@ use crate::{ GameProfileComponent, Hunger, InstanceHolder, LocalGameMode, PlayerAbilities, SendPacketEvent, TabList, }, + movement::{KnockbackEvent, KnockbackType}, raw_connection::RawConnection, ClientInformation, PlayerInfo, ReceivedRegistries, }; @@ -422,7 +423,7 @@ pub fn process_packet_events(ecs: &mut World) { continue; }; - let delta_movement = physics.delta; + let delta_movement = physics.velocity; let is_x_relative = p.relative_arguments.x; let is_y_relative = p.relative_arguments.y; @@ -459,7 +460,7 @@ pub fn process_packet_events(ecs: &mut World) { y_rot += direction.y_rot; } - physics.delta = Vec3 { + physics.velocity = Vec3 { x: delta_x, y: delta_y, z: delta_z, @@ -797,15 +798,21 @@ pub fn process_packet_events(ecs: &mut World) { continue; }; + // this is to make sure the same entity velocity update doesn't get sent + // multiple times when in swarms commands.entity(entity).add(RelativeEntityUpdate { partial_world: instance_holder.partial_instance.clone(), - update: Box::new(move |entity| { - let mut physics = entity.get_mut::().unwrap(); - physics.delta = Vec3 { - x: p.xa as f64 / 8000., - y: p.ya as f64 / 8000., - z: p.za as f64 / 8000., - }; + update: Box::new(move |entity_mut| { + entity_mut.world_scope(|world| { + world.send_event(KnockbackEvent { + entity, + knockback: KnockbackType::Set(Vec3 { + x: p.xa as f64 / 8000., + y: p.ya as f64 / 8000., + z: p.za as f64 / 8000., + }), + }) + }); }), }); @@ -1186,15 +1193,18 @@ pub fn process_packet_events(ecs: &mut World) { ClientboundGamePacket::DeleteChat(_) => {} ClientboundGamePacket::Explode(p) => { trace!("Got explode packet {p:?}"); - let mut system_state: SystemState> = SystemState::new(ecs); - let mut query = system_state.get_mut(ecs); - let mut physics = query.get_mut(player_entity).unwrap(); + let mut system_state: SystemState> = + SystemState::new(ecs); + let mut knockback_events = system_state.get_mut(ecs); - physics.delta += Vec3 { - x: p.knockback_x as f64, - y: p.knockback_y as f64, - z: p.knockback_z as f64, - }; + knockback_events.send(KnockbackEvent { + entity: player_entity, + knockback: KnockbackType::Set(Vec3 { + x: p.knockback_x as f64, + y: p.knockback_y as f64, + z: p.knockback_z as f64, + }), + }); system_state.apply(ecs); } diff --git a/azalea-core/src/lib.rs b/azalea-core/src/lib.rs index 2595471d6..3b29f6a22 100755 --- a/azalea-core/src/lib.rs +++ b/azalea-core/src/lib.rs @@ -1,6 +1,4 @@ #![doc = include_str!("../README.md")] -#![feature(int_roundings)] -#![feature(const_for)] #![feature(lazy_cell)] #![allow(incomplete_features)] #![feature(generic_const_exprs)] diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs index cc99b6847..196a70d5c 100755 --- a/azalea-core/src/position.rs +++ b/azalea-core/src/position.rs @@ -352,8 +352,8 @@ impl From<&BlockPos> for ChunkPos { #[inline] fn from(pos: &BlockPos) -> Self { ChunkPos { - x: pos.x.div_floor(16), - z: pos.z.div_floor(16), + x: pos.x >> 4, + z: pos.z >> 4, } } } @@ -361,8 +361,8 @@ impl From for ChunkPos { #[inline] fn from(pos: BlockPos) -> Self { ChunkPos { - x: pos.x.div_floor(16), - z: pos.z.div_floor(16), + x: pos.x >> 4, + z: pos.z >> 4, } } } @@ -398,9 +398,9 @@ impl From<&BlockPos> for ChunkBlockPos { #[inline] fn from(pos: &BlockPos) -> Self { ChunkBlockPos { - x: pos.x.rem_euclid(16) as u8, + x: (pos.x & 0xF) as u8, y: pos.y, - z: pos.z.rem_euclid(16) as u8, + z: (pos.z & 0xF) as u8, } } } @@ -408,9 +408,9 @@ impl From for ChunkBlockPos { #[inline] fn from(pos: BlockPos) -> Self { ChunkBlockPos { - x: pos.x.rem_euclid(16) as u8, + x: (pos.x & 0xF) as u8, y: pos.y, - z: pos.z.rem_euclid(16) as u8, + z: (pos.z & 0xF) as u8, } } } @@ -431,7 +431,7 @@ impl From<&ChunkBlockPos> for ChunkSectionBlockPos { fn from(pos: &ChunkBlockPos) -> Self { ChunkSectionBlockPos { x: pos.x, - y: pos.y.rem_euclid(16) as u8, + y: (pos.y & 0xF) as u8, z: pos.z, } } diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs index 9b6739455..bf3dfc823 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -35,7 +35,7 @@ pub fn move_relative( acceleration: &Vec3, ) { let input_vector = input_vector(direction, speed, acceleration); - physics.delta += input_vector; + physics.velocity += input_vector; } pub fn input_vector(direction: &LookDirection, speed: f32, acceleration: &Vec3) -> Vec3 { @@ -208,7 +208,8 @@ pub struct LookDirection { /// bounding box. #[derive(Debug, Component, Clone)] pub struct Physics { - pub delta: Vec3, + /// How fast the entity is moving. + pub velocity: Vec3, /// X acceleration. pub xxa: f32, @@ -232,7 +233,7 @@ pub struct Physics { impl Physics { pub fn new(dimensions: EntityDimensions, pos: &Vec3) -> Self { Self { - delta: Vec3::default(), + velocity: Vec3::default(), xxa: 0., yya: 0., diff --git a/azalea-physics/src/collision/mod.rs b/azalea-physics/src/collision/mod.rs index 1a8f44414..2c739b24c 100644 --- a/azalea-physics/src/collision/mod.rs +++ b/azalea-physics/src/collision/mod.rs @@ -201,8 +201,8 @@ pub fn move_colliding( // if self.isRemoved() { return; } if horizontal_collision { - let delta_movement = &physics.delta; - physics.delta = Vec3 { + let delta_movement = &physics.velocity; + physics.velocity = Vec3 { x: if x_collision { 0. } else { delta_movement.x }, y: delta_movement.y, z: if z_collision { 0. } else { delta_movement.z }, @@ -213,7 +213,7 @@ pub fn move_colliding( // blockBelow.updateEntityAfterFallOn(this.level, this); // the default implementation of updateEntityAfterFallOn sets the y movement to // 0 - physics.delta.y = 0.; + physics.velocity.y = 0.; } if on_ground { diff --git a/azalea-physics/src/lib.rs b/azalea-physics/src/lib.rs index 8c88b97a5..615cc3ad9 100644 --- a/azalea-physics/src/lib.rs +++ b/azalea-physics/src/lib.rs @@ -113,9 +113,9 @@ fn travel( // if should_discard_friction(self) { if false { - physics.delta = movement; + physics.velocity = movement; } else { - physics.delta = Vec3 { + physics.velocity = Vec3 { x: movement.x * inertia as f64, y: movement.y * 0.9800000190734863f64, z: movement.z * inertia as f64, @@ -145,14 +145,14 @@ pub fn ai_step( // vanilla does movement interpolation here, doesn't really matter much for a // bot though - if physics.delta.x.abs() < 0.003 { - physics.delta.x = 0.; + if physics.velocity.x.abs() < 0.003 { + physics.velocity.x = 0.; } - if physics.delta.y.abs() < 0.003 { - physics.delta.y = 0.; + if physics.velocity.y.abs() < 0.003 { + physics.velocity.y = 0.; } - if physics.delta.z.abs() < 0.003 { - physics.delta.z = 0.; + if physics.velocity.z.abs() < 0.003 { + physics.velocity.z = 0.; } if let Some(jumping) = jumping { @@ -194,8 +194,8 @@ pub fn jump_from_ground( let world = world_lock.read(); let jump_power: f64 = jump_power(&world, position) as f64 + jump_boost_power(); - let old_delta_movement = physics.delta; - physics.delta = Vec3 { + let old_delta_movement = physics.velocity; + physics.velocity = Vec3 { x: old_delta_movement.x, y: jump_power, z: old_delta_movement.z, @@ -203,7 +203,7 @@ pub fn jump_from_ground( if **sprinting { // sprint jumping gives some extra velocity let y_rot = look_direction.y_rot * 0.017453292; - physics.delta += Vec3 { + physics.velocity += Vec3 { x: (-math::sin(y_rot) * 0.2) as f64, y: 0., z: (math::cos(y_rot) * 0.2) as f64, @@ -245,7 +245,7 @@ fn handle_relative_friction_and_calculate_movement( // entity.delta = entity.handle_on_climbable(entity.delta); move_colliding( &MoverType::Own, - &physics.delta.clone(), + &physics.velocity.clone(), world, position, physics, @@ -259,7 +259,7 @@ fn handle_relative_friction_and_calculate_movement( // Vec3(var3.x, 0.2D, var3.z); } // TODO: powdered snow - physics.delta + physics.velocity } // private float getFrictionInfluencedSpeed(float friction) { @@ -400,7 +400,7 @@ mod tests { // delta is applied before gravity, so the first tick only sets the delta assert_eq!(entity_pos.y, 70.); let entity_physics = app.world.get::(entity).unwrap(); - assert!(entity_physics.delta.y < 0.); + assert!(entity_physics.velocity.y < 0.); } app.world.run_schedule(FixedUpdate); app.update(); @@ -463,7 +463,7 @@ mod tests { // delta will change, but it won't move until next tick assert_eq!(entity_pos.y, 70.); let entity_physics = app.world.get::(entity).unwrap(); - assert!(entity_physics.delta.y < 0.); + assert!(entity_physics.velocity.y < 0.); } app.world.run_schedule(FixedUpdate); app.update(); 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 88c6f29e2..13887331a 100755 --- a/azalea-protocol/src/packets/game/clientbound_command_suggestions_packet.rs +++ b/azalea-protocol/src/packets/game/clientbound_command_suggestions_packet.rs @@ -1,13 +1,12 @@ use azalea_brigadier::suggestion::Suggestions; use azalea_buf::McBuf; -use azalea_chat::FormattedText; use azalea_protocol_macros::ClientboundGamePacket; #[derive(Clone, Debug, McBuf, ClientboundGamePacket)] pub struct ClientboundCommandSuggestionsPacket { #[var] pub id: u32, - pub suggestions: Suggestions, + pub suggestions: Suggestions, } #[cfg(test)] @@ -19,14 +18,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", + "bar".to_string(), + )], + ); let mut buf = Vec::new(); suggestions.write_into(&mut buf).unwrap(); let mut cursor = Cursor::new(&buf[..]); diff --git a/azalea/src/auto_tool.rs b/azalea/src/auto_tool.rs index d7501f044..55ec69248 100644 --- a/azalea/src/auto_tool.rs +++ b/azalea/src/auto_tool.rs @@ -35,7 +35,7 @@ pub fn best_tool_in_hotbar_for_block(block: BlockState, menu: &Menu) -> BestTool menu, &Physics { on_ground: true, - delta: Default::default(), + velocity: Default::default(), xxa: Default::default(), yya: Default::default(), zza: Default::default(), diff --git a/azalea/src/bot.rs b/azalea/src/bot.rs index 768ae7675..509d0e2d7 100644 --- a/azalea/src/bot.rs +++ b/azalea/src/bot.rs @@ -21,6 +21,7 @@ use azalea_physics::PhysicsSet; use bevy_app::{FixedUpdate, Update}; use bevy_ecs::prelude::Event; use bevy_ecs::schedule::IntoSystemConfigs; +use futures_lite::Future; use log::trace; use std::f64::consts::PI; @@ -82,7 +83,7 @@ pub trait BotClientExt { /// that's necessary you'll have to do that yourself with [`look_at`]. /// /// [`look_at`]: crate::prelude::BotClientExt::look_at - async fn mine(&mut self, position: BlockPos); + fn mine(&mut self, position: BlockPos) -> impl Future + Send; } impl BotClientExt for azalea_client::Client { diff --git a/azalea/src/container.rs b/azalea/src/container.rs index 34f867154..221b80b96 100644 --- a/azalea/src/container.rs +++ b/azalea/src/container.rs @@ -10,6 +10,7 @@ use azalea_inventory::{operations::ClickOperation, ItemSlot, Menu}; use azalea_protocol::packets::game::ClientboundGamePacket; use bevy_app::{App, Plugin, Update}; use bevy_ecs::{component::Component, prelude::EventReader, system::Commands}; +use futures_lite::Future; use std::fmt::Debug; use crate::bot::BotClientExt; @@ -22,7 +23,10 @@ impl Plugin for ContainerPlugin { } pub trait ContainerClientExt { - async fn open_container(&mut self, pos: BlockPos) -> Option; + fn open_container( + &mut self, + pos: BlockPos, + ) -> impl Future> + Send; fn open_inventory(&mut self) -> Option; } diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index c788434b9..bfa38fdad 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -1,7 +1,6 @@ #![doc = include_str!("../README.md")] #![feature(async_closure)] #![allow(incomplete_features)] -#![feature(async_fn_in_trait)] #![feature(type_changing_struct_update)] #![feature(lazy_cell)] #![feature(let_chains)] diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index 73ce2967d..e82b01790 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -25,7 +25,7 @@ use crate::pathfinder::moves::PathfinderCtx; use crate::pathfinder::world::CachedWorld; use azalea_client::chat::SendChatEvent; use azalea_client::inventory::{InventoryComponent, InventorySet}; -use azalea_client::movement::walk_listener; +use azalea_client::movement::MoveEventsSet; use azalea_client::{StartSprintEvent, StartWalkEvent}; use azalea_core::position::{BlockPos, Vec3}; use azalea_entity::metadata::Player; @@ -85,7 +85,7 @@ impl Plugin for PathfinderPlugin { handle_stop_pathfinding_event, ) .chain() - .before(walk_listener) + .before(MoveEventsSet) .before(InventorySet), ); } @@ -462,8 +462,8 @@ fn check_node_reached( && BlockPos::from(position) == movement.target // adding the delta like this isn't a perfect solution but it helps to make // sure we don't keep going if our delta is high - && (x_difference_from_center + physics.delta.x).abs() < 0.2 - && (z_difference_from_center + physics.delta.z).abs() < 0.2 + && (x_difference_from_center + physics.velocity.x).abs() < 0.2 + && (z_difference_from_center + physics.velocity.z).abs() < 0.2 } else { true }; diff --git a/azalea/src/pathfinder/moves/basic.rs b/azalea/src/pathfinder/moves/basic.rs index 4780798c1..957e24c60 100644 --- a/azalea/src/pathfinder/moves/basic.rs +++ b/azalea/src/pathfinder/moves/basic.rs @@ -97,7 +97,7 @@ fn execute_ascend_move(mut ctx: ExecuteCtx) { let side_distance = z_axis as f64 * (target_center.x - position.x).abs() + x_axis as f64 * (target_center.z - position.z).abs(); - let lateral_motion = x_axis as f64 * physics.delta.z + z_axis as f64 * physics.delta.x; + let lateral_motion = x_axis as f64 * physics.velocity.z + z_axis as f64 * physics.velocity.x; if lateral_motion > 0.1 { return; }