From b44ba0fbcebca7844bc00b47d98cfb22f8352b41 Mon Sep 17 00:00:00 2001 From: SomeYellowGuy <30015613+SomeYellowGuy@users.noreply.github.com> Date: Tue, 5 May 2026 14:10:56 +0530 Subject: [PATCH] feat(command): add suggestion providers and their support in argument nodes and fix `list_suggestions` method (#2004) * added suggestion providers and their support in argument nodes * renamed argument suggesting methods * fixed custom suggestion provider to make them work * fixed formatting * make `SuggestionProvider` use a lifetime * added comment * fixed lifetime issue * used suggestion results of suggestion providers * switched to using `SuggestionsBuilder` instead of `&mut SuggestionsBuilder` for `list_suggestions` --- pumpkin/src/command/argument_builder.rs | 17 ++++++++++ .../command/argument_types/argument_type.rs | 6 ++-- pumpkin/src/command/client_suggestions.rs | 22 +++++++++---- pumpkin/src/command/node/detached.rs | 4 ++- pumpkin/src/command/node/dispatcher.rs | 33 ++++++++++++------- pumpkin/src/command/node/mod.rs | 4 +++ pumpkin/src/command/suggestion/mod.rs | 1 + pumpkin/src/command/suggestion/provider.rs | 24 ++++++++++++++ 8 files changed, 88 insertions(+), 23 deletions(-) create mode 100644 pumpkin/src/command/suggestion/provider.rs diff --git a/pumpkin/src/command/argument_builder.rs b/pumpkin/src/command/argument_builder.rs index aeb2ab3a9..936aa5917 100644 --- a/pumpkin/src/command/argument_builder.rs +++ b/pumpkin/src/command/argument_builder.rs @@ -6,6 +6,7 @@ use crate::command::node::detached::{ use crate::command::node::{ Command, CommandExecutor, RedirectModifier, Redirection, Requirement, Requirements, }; +use crate::command::suggestion::provider::SuggestionProvider; use rustc_hash::FxHashMap; use std::borrow::Cow; use std::sync::Arc; @@ -119,6 +120,7 @@ pub struct RequiredArgumentBuilder { common: CommonArgumentBuilder, name: Cow<'static, str>, argument_type: Arc, + suggestion_provider: Option>, } mod private { @@ -356,8 +358,22 @@ impl RequiredArgumentBuilder { common: CommonArgumentBuilder::new(), name: name.into(), argument_type: Arc::new(arg_type), + suggestion_provider: None, } } + + /// Sets the [`SuggestionProvider`] of this builder for the `ArgumentDetachedNode`. + #[must_use] + pub fn suggests(self, provider: impl SuggestionProvider + 'static) -> Self { + self.suggests_arc(Arc::new(provider)) + } + + /// Sets the [`SuggestionProvider`] of this builder for the `ArgumentDetachedNode`. + #[must_use] + pub fn suggests_arc(mut self, provider: Arc) -> Self { + self.suggestion_provider = Some(provider); + self + } } impl ArgumentBuilder for LiteralArgumentBuilder { @@ -410,6 +426,7 @@ impl ArgumentBuilder for RequiredArgumentBuilder { self.common.target, self.common.modifier, self.common.forks, + self.suggestion_provider, ); node.children = self.common.arguments; node diff --git a/pumpkin/src/command/argument_types/argument_type.rs b/pumpkin/src/command/argument_types/argument_type.rs index fc8baa622..21808e029 100644 --- a/pumpkin/src/command/argument_types/argument_type.rs +++ b/pumpkin/src/command/argument_types/argument_type.rs @@ -42,7 +42,7 @@ pub trait ArgumentType: Send + Sync { fn list_suggestions( &self, _context: &CommandContext, - _suggestions_builder: &mut SuggestionsBuilder, + _suggestions_builder: SuggestionsBuilder, ) -> Pin + Send>> { Box::pin(async move { Suggestions::empty() }) } @@ -102,7 +102,7 @@ pub trait AnyArgumentType: Sealed + Send + Sync { fn list_suggestions( &self, context: &CommandContext, - suggestions_builder: &mut SuggestionsBuilder, + suggestions_builder: SuggestionsBuilder, ) -> Pin + Send>>; /// Returns the Java client-side parser used for this argument type. @@ -154,7 +154,7 @@ impl, T: Send + Sync + 'static> AnyArgumentType for U fn list_suggestions( &self, context: &CommandContext, - suggestions_builder: &mut SuggestionsBuilder, + suggestions_builder: SuggestionsBuilder, ) -> Pin + Send>> { self.list_suggestions(context, suggestions_builder) } diff --git a/pumpkin/src/command/client_suggestions.rs b/pumpkin/src/command/client_suggestions.rs index f69d784da..3c0aaf307 100644 --- a/pumpkin/src/command/client_suggestions.rs +++ b/pumpkin/src/command/client_suggestions.rs @@ -1,9 +1,3 @@ -use pumpkin_protocol::{ - codec::var_int::VarInt, - java::client::play::{CCommands, ProtoNode, ProtoNodeType}, -}; -use std::sync::Arc; - use super::tree::{Node, NodeType}; use crate::server::Server; use crate::{ @@ -14,6 +8,12 @@ use crate::{ }, entity::player::Player, }; +use pumpkin_protocol::java::client::play::SuggestionProviders; +use pumpkin_protocol::{ + codec::var_int::VarInt, + java::client::play::{CCommands, ProtoNode, ProtoNodeType}, +}; +use std::sync::Arc; #[expect(clippy::too_many_lines)] pub async fn send_c_commands_packet( @@ -151,7 +151,15 @@ pub async fn send_c_commands_packet( name: &argument_attached_node.meta.name, is_executable: argument_attached_node.owned.command.is_some(), parser: arg_type.client_side_parser(), - override_suggestion_type: arg_type.override_suggestion_providers(), + override_suggestion_type: if argument_attached_node + .meta + .suggestion_provider + .is_some() + { + Some(SuggestionProviders::AskServer) + } else { + arg_type.override_suggestion_providers() + }, redirect_target, restricted: !satisfies_requirements, }, diff --git a/pumpkin/src/command/node/detached.rs b/pumpkin/src/command/node/detached.rs index c0a5c77e1..e331d62bb 100644 --- a/pumpkin/src/command/node/detached.rs +++ b/pumpkin/src/command/node/detached.rs @@ -3,6 +3,7 @@ use crate::command::node::{ ArgumentNodeMetadata, Command, CommandNodeMetadata, LiteralNodeMetadata, NodeMetadata, OwnedNodeData, RedirectModifier, Redirection, Requirements, }; +use crate::command::suggestion::provider::SuggestionProvider; use rustc_hash::FxHashMap; use std::borrow::Cow; use std::num::NonZero; @@ -150,6 +151,7 @@ impl ArgumentDetachedNode { redirect: Option, modifier: RedirectModifier, forks: bool, + suggestion_provider: Option>, ) -> Self { Self { owned: OwnedNodeData { @@ -161,7 +163,7 @@ impl ArgumentDetachedNode { }, children: FxHashMap::default(), redirect, - meta: ArgumentNodeMetadata::new(name, argument_type), + meta: ArgumentNodeMetadata::new(name, argument_type, suggestion_provider), } } } diff --git a/pumpkin/src/command/node/dispatcher.rs b/pumpkin/src/command/node/dispatcher.rs index 8c704c0bf..0503b5311 100644 --- a/pumpkin/src/command/node/dispatcher.rs +++ b/pumpkin/src/command/node/dispatcher.rs @@ -490,14 +490,15 @@ impl CommandDispatcher { let mut futures = Vec::with_capacity(capacity); let context = context.build(truncated_input); + let mut provided_suggestions = Vec::new(); for child in children { - let mut builder = SuggestionsBuilder::new(truncated_input, start); + let builder = SuggestionsBuilder::new(truncated_input, start); - let future: Pin + Send>> = + let future: Option + Send>>> = match self.tree.classify_id(child) { - NodeIdClassification::Root => Box::pin(async { Suggestions::empty() }), - NodeIdClassification::Literal(literal_node_id) => Box::pin(async move { + NodeIdClassification::Root => Some(Box::pin(async { Suggestions::empty() })), + NodeIdClassification::Literal(literal_node_id) => Some(Box::pin(async move { let node = &self.tree[literal_node_id]; if node .meta @@ -508,8 +509,8 @@ impl CommandDispatcher { } else { Suggestions::empty() } - }), - NodeIdClassification::Command(command_node_id) => Box::pin(async move { + })), + NodeIdClassification::Command(command_node_id) => Some(Box::pin(async move { let node = &self.tree[command_node_id]; if node .meta @@ -520,19 +521,27 @@ impl CommandDispatcher { } else { Suggestions::empty() } - }), + })), NodeIdClassification::Argument(argument_node_id) => { let node = &self.tree[argument_node_id]; - node.meta - .argument_type - .list_suggestions(&context, &mut builder) + if let Some(provider) = &node.meta.suggestion_provider { + // For custom suggestions sent by the server, we simply + // wait instead of adding the future to join. + provided_suggestions.push(provider.suggest(&context, builder).await); + None + } else { + Some(node.meta.argument_type.list_suggestions(&context, builder)) + } } }; - futures.push(future); + if let Some(future) = future { + futures.push(future); + } } - let suggestions = future::join_all(futures).await; + let mut suggestions = future::join_all(futures).await; + suggestions.append(&mut provided_suggestions); Suggestions::merge(full_input, suggestions) } diff --git a/pumpkin/src/command/node/mod.rs b/pumpkin/src/command/node/mod.rs index 401b4af75..bd43d092d 100644 --- a/pumpkin/src/command/node/mod.rs +++ b/pumpkin/src/command/node/mod.rs @@ -9,6 +9,7 @@ use crate::command::context::command_source::CommandSource; use crate::command::errors::command_syntax_error::CommandSyntaxError; use crate::command::node::attached::NodeId; use crate::command::node::detached::GlobalNodeId; +use crate::command::suggestion::provider::SuggestionProvider; use std::borrow::Cow; use std::pin::Pin; use std::sync::Arc; @@ -218,16 +219,19 @@ impl CommandNodeMetadata { pub struct ArgumentNodeMetadata { pub name: Cow<'static, str>, pub argument_type: Arc, + pub suggestion_provider: Option>, } impl ArgumentNodeMetadata { pub fn new( name: impl Into>, argument_type: Arc, + suggestion_provider: Option>, ) -> Self { Self { name: name.into(), argument_type, + suggestion_provider, } } } diff --git a/pumpkin/src/command/suggestion/mod.rs b/pumpkin/src/command/suggestion/mod.rs index dcea002fd..c4c46a387 100644 --- a/pumpkin/src/command/suggestion/mod.rs +++ b/pumpkin/src/command/suggestion/mod.rs @@ -1,3 +1,4 @@ +pub mod provider; pub mod suggestions; use pumpkin_util::text::TextComponent; diff --git a/pumpkin/src/command/suggestion/provider.rs b/pumpkin/src/command/suggestion/provider.rs new file mode 100644 index 000000000..97fe47415 --- /dev/null +++ b/pumpkin/src/command/suggestion/provider.rs @@ -0,0 +1,24 @@ +use crate::command::context::command_context::CommandContext; +use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder}; +use std::pin::Pin; + +/// The [`Suggestions`] future given by a [`SuggestionProvider`]. +pub type SuggestionProviderResult<'a> = Pin + Send + 'a>>; + +/// A trait allowing an object to provide suggestions using a +/// [`CommandContext`] and [`SuggestionsBuilder`]. +pub trait SuggestionProvider: Send + Sync { + /// Uses a [`CommandContext`] and [`SuggestionsBuilder`] to suggest. + /// + /// # Arguments + /// - `context`: The context to use for building the suggestions. + /// - `builder`: The builder to consume for the suggestions. + /// + /// # Returns + /// The [`Suggestions`] representing the suggested items. + fn suggest<'a>( + &'a self, + context: &'a CommandContext, + builder: SuggestionsBuilder, + ) -> SuggestionProviderResult<'a>; +}