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`
This commit is contained in:
SomeYellowGuy
2026-05-05 14:10:56 +05:30
committed by GitHub
parent ecc0af8e7a
commit b44ba0fbce
8 changed files with 88 additions and 23 deletions

View File

@@ -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<dyn AnyArgumentType>,
suggestion_provider: Option<Arc<dyn SuggestionProvider>>,
}
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<dyn SuggestionProvider>) -> Self {
self.suggestion_provider = Some(provider);
self
}
}
impl ArgumentBuilder<LiteralDetachedNode> for LiteralArgumentBuilder {
@@ -410,6 +426,7 @@ impl ArgumentBuilder<ArgumentDetachedNode> for RequiredArgumentBuilder {
self.common.target,
self.common.modifier,
self.common.forks,
self.suggestion_provider,
);
node.children = self.common.arguments;
node

View File

@@ -42,7 +42,7 @@ pub trait ArgumentType: Send + Sync {
fn list_suggestions(
&self,
_context: &CommandContext,
_suggestions_builder: &mut SuggestionsBuilder,
_suggestions_builder: SuggestionsBuilder,
) -> Pin<Box<dyn Future<Output = Suggestions> + 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<Box<dyn Future<Output = Suggestions> + Send>>;
/// Returns the Java client-side parser used for this argument type.
@@ -154,7 +154,7 @@ impl<U: ArgumentType<Item = T>, T: Send + Sync + 'static> AnyArgumentType for U
fn list_suggestions(
&self,
context: &CommandContext,
suggestions_builder: &mut SuggestionsBuilder,
suggestions_builder: SuggestionsBuilder,
) -> Pin<Box<dyn Future<Output = Suggestions> + Send>> {
self.list_suggestions(context, suggestions_builder)
}

View File

@@ -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,
},

View File

@@ -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<Redirection>,
modifier: RedirectModifier,
forks: bool,
suggestion_provider: Option<Arc<dyn SuggestionProvider>>,
) -> 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),
}
}
}

View File

@@ -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<Box<dyn Future<Output = Suggestions> + Send>> =
let future: Option<Pin<Box<dyn Future<Output = Suggestions> + 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)
}

View File

@@ -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<dyn AnyArgumentType>,
pub suggestion_provider: Option<Arc<dyn SuggestionProvider>>,
}
impl ArgumentNodeMetadata {
pub fn new(
name: impl Into<Cow<'static, str>>,
argument_type: Arc<dyn AnyArgumentType>,
suggestion_provider: Option<Arc<dyn SuggestionProvider>>,
) -> Self {
Self {
name: name.into(),
argument_type,
suggestion_provider,
}
}
}

View File

@@ -1,3 +1,4 @@
pub mod provider;
pub mod suggestions;
use pumpkin_util::text::TextComponent;

View File

@@ -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<Box<dyn Future<Output = Suggestions> + 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>;
}