mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat(plugin-api): support server-side command suggestions (#2344)
* feat(plugin-api): support server-side command suggestions * fix(data): use deterministic block state properties * chore: update subproject commit reference for pumpkin-plugin-wit * fix(plugin-api): avoid unwrap in suggestion handler registration * fix: point WIT submodule at accessible fork * fix: restore upstream WIT submodule URL * fix: update WIT submodule for command suggestions * Update pumpkin-plugin-wit --------- Co-authored-by: Alexander Medvedev <lilalexmed@proton.me>
This commit is contained in:
@@ -16,6 +16,9 @@ use crate::{
|
||||
pub(crate) static NEXT_COMMAND_ID: AtomicU32 = AtomicU32::new(0);
|
||||
pub(crate) static COMMAND_HANDLERS: Mutex<BTreeMap<u32, Box<dyn CommandHandler>>> =
|
||||
Mutex::new(BTreeMap::new());
|
||||
pub(crate) static COMMAND_SUGGESTION_HANDLERS: Mutex<
|
||||
BTreeMap<u32, Box<dyn CommandSuggestionHandler>>,
|
||||
> = Mutex::new(BTreeMap::new());
|
||||
|
||||
/// Handles the execution of a registered command.
|
||||
///
|
||||
@@ -37,6 +40,62 @@ pub trait CommandHandler: Send + Sync {
|
||||
) -> Result<i32, CommandError>;
|
||||
}
|
||||
|
||||
/// Handles server-side suggestions for a registered command argument.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use pumpkin_plugin_api::command::{CommandSuggestion, CommandSuggestions, SuggestionRequest};
|
||||
/// use pumpkin_plugin_api::commands::CommandSuggestionHandler;
|
||||
/// use pumpkin_plugin_api::Server;
|
||||
///
|
||||
/// struct PatternSuggestions;
|
||||
///
|
||||
/// impl CommandSuggestionHandler for PatternSuggestions {
|
||||
/// fn suggest(
|
||||
/// &self,
|
||||
/// _sender: pumpkin_plugin_api::command::CommandSender,
|
||||
/// _server: Server,
|
||||
/// request: SuggestionRequest,
|
||||
/// ) -> CommandSuggestions {
|
||||
/// let token_start = request
|
||||
/// .input
|
||||
/// .rfind([',', ' '])
|
||||
/// .map_or(request.start as usize, |index| index + 1);
|
||||
/// let block_start = request.input[token_start..]
|
||||
/// .rfind('%')
|
||||
/// .map_or(token_start, |index| token_start + index + 1);
|
||||
/// let prefix = &request.input[block_start..];
|
||||
/// let values = ["stone", "stripped_oak_log", "dirt", "diamond_block"]
|
||||
/// .into_iter()
|
||||
/// .filter(|block| block.starts_with(prefix))
|
||||
/// .map(|block| CommandSuggestion {
|
||||
/// value: block.to_string(),
|
||||
/// tooltip: None,
|
||||
/// })
|
||||
/// .collect();
|
||||
///
|
||||
/// CommandSuggestions {
|
||||
/// start: block_start as u32,
|
||||
/// length: (request.input.len() - block_start) as u32,
|
||||
/// values,
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait CommandSuggestionHandler: Send + Sync {
|
||||
/// Computes suggestions for the current command input.
|
||||
///
|
||||
/// `request.remaining` contains the replacement text currently covered by
|
||||
/// the suggestion range. Handlers may return a narrower range when only part
|
||||
/// of an argument should be replaced, for example after the last comma in a
|
||||
/// weighted block pattern.
|
||||
fn suggest(
|
||||
&self,
|
||||
sender: CommandSender,
|
||||
server: Server,
|
||||
request: SuggestionRequest,
|
||||
) -> CommandSuggestions;
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Attaches an execution handler to this command.
|
||||
///
|
||||
@@ -74,4 +133,26 @@ impl CommandNode {
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Attaches a server-side suggestion handler to this argument node.
|
||||
///
|
||||
/// The node is advertised to Java clients with `minecraft:ask_server`, and
|
||||
/// the handler is called whenever the client requests completions for this
|
||||
/// argument.
|
||||
pub fn suggest<H: CommandSuggestionHandler + Send + Sync + 'static>(self, handler: H) -> Self {
|
||||
let id = NEXT_COMMAND_ID.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
COMMAND_SUGGESTION_HANDLERS
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.insert(id, Box::new(handler));
|
||||
|
||||
self.suggest_with_handler_id(id);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub use crate::wit::pumpkin::plugin::command::{
|
||||
CommandSuggestion, CommandSuggestions, SuggestionRequest,
|
||||
};
|
||||
|
||||
@@ -67,8 +67,11 @@
|
||||
//! ```
|
||||
|
||||
use crate::{
|
||||
commands::COMMAND_HANDLERS, events::EVENT_HANDLERS, logging::WitSubscriber,
|
||||
scheduler::TASK_HANDLERS, text::TextComponent,
|
||||
commands::{COMMAND_HANDLERS, COMMAND_SUGGESTION_HANDLERS},
|
||||
events::EVENT_HANDLERS,
|
||||
logging::WitSubscriber,
|
||||
scheduler::TASK_HANDLERS,
|
||||
text::TextComponent,
|
||||
};
|
||||
|
||||
/// Plugin command registration and handling utilities.
|
||||
@@ -98,8 +101,8 @@ pub mod worldgen;
|
||||
/// Command WIT API re-exports.
|
||||
pub mod command {
|
||||
pub use crate::wit::pumpkin::plugin::command::{
|
||||
Arg, ArgumentType, Command, CommandError, CommandNode, CommandSender, ConsumedArgs,
|
||||
StringType,
|
||||
Arg, ArgumentType, Command, CommandError, CommandNode, CommandSender, CommandSuggestion,
|
||||
CommandSuggestions, ConsumedArgs, StringType, SuggestionRequest,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -249,6 +252,27 @@ impl wit::Guest for Component {
|
||||
)
|
||||
}
|
||||
|
||||
/// WIT entry point — dispatches an incoming command suggestion request to the registered handler.
|
||||
fn handle_command_suggestion(
|
||||
handler_id: u32,
|
||||
sender: command::CommandSender,
|
||||
server: Server,
|
||||
request: command::SuggestionRequest,
|
||||
) -> command::CommandSuggestions {
|
||||
let handlers = COMMAND_SUGGESTION_HANDLERS
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(handler) = handlers.get(&handler_id) {
|
||||
handler.suggest(sender, server, request)
|
||||
} else {
|
||||
command::CommandSuggestions {
|
||||
start: request.start,
|
||||
length: 0,
|
||||
values: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// WIT entry point — dispatches a scheduled task invocation to the registered handler for `handler_id`.
|
||||
fn handle_task(handler_id: u32, server: Server) {
|
||||
let mut handlers = TASK_HANDLERS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
Submodule crates/pumpkin-plugin-wit updated: ddd4b082c0...b44607b19f
@@ -257,7 +257,11 @@ fn nodes_to_proto_node_builders<'a>(
|
||||
for i in children {
|
||||
let node = &nodes[*i];
|
||||
match &node.node_type {
|
||||
NodeType::Argument { name, consumer } => {
|
||||
NodeType::Argument {
|
||||
name,
|
||||
consumer,
|
||||
suggestion_provider,
|
||||
} => {
|
||||
let (node_is_executable, node_children) =
|
||||
nodes_to_proto_node_builders(cmd_src, nodes, &node.children);
|
||||
child_nodes.push(ProtoNodeBuilder {
|
||||
@@ -267,8 +271,11 @@ fn nodes_to_proto_node_builders<'a>(
|
||||
is_executable: node_is_executable,
|
||||
redirect_target: None,
|
||||
parser: consumer.get_client_side_parser(),
|
||||
override_suggestion_type: consumer
|
||||
.get_client_side_suggestion_type_override(),
|
||||
override_suggestion_type: if suggestion_provider.is_some() {
|
||||
Some(SuggestionProviders::AskServer)
|
||||
} else {
|
||||
consumer.get_client_side_suggestion_type_override()
|
||||
},
|
||||
restricted: false,
|
||||
},
|
||||
});
|
||||
@@ -488,7 +495,7 @@ fn collect_overloads_from_nodes(
|
||||
});
|
||||
collect_overloads_from_nodes(nodes, &node.children, &mut params, overloads, ctx);
|
||||
}
|
||||
NodeType::Argument { name, consumer } => {
|
||||
NodeType::Argument { name, consumer, .. } => {
|
||||
let mut params = current_params.clone();
|
||||
params.push(CommandParameter {
|
||||
name: name.clone(),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use pumpkin_data::translation;
|
||||
use pumpkin_protocol::java::client::play::CommandSuggestion;
|
||||
use pumpkin_util::text::TextComponent;
|
||||
use pumpkin_util::text::click::ClickEvent;
|
||||
use pumpkin_util::text::color::NamedColor;
|
||||
@@ -15,8 +14,12 @@ use crate::command::dispatcher::CommandError::{
|
||||
CommandFailed, InvalidConsumption, InvalidRequirement, PermissionDenied, SyntaxError,
|
||||
};
|
||||
use crate::command::tree::{Command, CommandTree, NodeType, RawArg, RawArgs};
|
||||
use crate::command::{
|
||||
context::string_range::StringRange,
|
||||
suggestion::{Suggestion, suggestions::Suggestions},
|
||||
};
|
||||
use crate::server::Server;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CommandError {
|
||||
@@ -268,10 +271,10 @@ impl CommandDispatcher {
|
||||
src: &'a CommandSender,
|
||||
server: &'a Server,
|
||||
cmd: &'a str,
|
||||
) -> Vec<CommandSuggestion> {
|
||||
) -> Suggestions {
|
||||
let mut parts = cmd.split_whitespace();
|
||||
let Some(key) = parts.next() else {
|
||||
return Vec::new();
|
||||
return Suggestions::empty();
|
||||
};
|
||||
let mut raw_args: RawArgs<'a> = parts
|
||||
.rev()
|
||||
@@ -284,7 +287,7 @@ impl CommandDispatcher {
|
||||
.collect();
|
||||
|
||||
let Ok(tree) = self.get_tree(key) else {
|
||||
return Vec::new();
|
||||
return Suggestions::empty();
|
||||
};
|
||||
|
||||
// Gate suggestions on command permissions, mirroring `dispatch`. Many
|
||||
@@ -293,14 +296,14 @@ impl CommandDispatcher {
|
||||
// consumers would leak privileged data (e.g. banned/op/whitelisted
|
||||
// player names) to unprivileged players via tab-complete.
|
||||
let Some(permission) = self.permissions.get(key) else {
|
||||
return Vec::new();
|
||||
return Suggestions::empty();
|
||||
};
|
||||
|
||||
if !src.has_permission(server, permission.as_str()).await {
|
||||
return Vec::new();
|
||||
return Suggestions::empty();
|
||||
}
|
||||
|
||||
let mut suggestions = HashSet::new();
|
||||
let mut suggestions = Vec::new();
|
||||
|
||||
// try paths and collect the nodes that fail
|
||||
// todo: make this more fine-grained
|
||||
@@ -312,27 +315,27 @@ impl CommandDispatcher {
|
||||
debug!(
|
||||
"Error while parsing command \"{cmd}\": {s:?} was consumed, but couldn't be parsed"
|
||||
);
|
||||
return Vec::new();
|
||||
return Suggestions::empty();
|
||||
}
|
||||
Err(InvalidRequirement) => {
|
||||
debug!(
|
||||
"Error while parsing command \"{cmd}\": a requirement that was expected was not met."
|
||||
);
|
||||
return Vec::new();
|
||||
return Suggestions::empty();
|
||||
}
|
||||
Err(PermissionDenied) => {
|
||||
debug!("Permission denied for command \"{cmd}\"");
|
||||
return Vec::new();
|
||||
return Suggestions::empty();
|
||||
}
|
||||
Err(CommandFailed(_)) => {
|
||||
debug!("Command failed");
|
||||
return Vec::new();
|
||||
return Suggestions::empty();
|
||||
}
|
||||
Err(SyntaxError(_)) => {
|
||||
return Vec::new();
|
||||
return Suggestions::empty();
|
||||
}
|
||||
Ok(Some(new_suggestions)) => {
|
||||
suggestions.extend(new_suggestions);
|
||||
suggestions.push(new_suggestions);
|
||||
}
|
||||
Ok(None) => {
|
||||
debug!("Command none");
|
||||
@@ -340,9 +343,7 @@ impl CommandDispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
let mut suggestions = Vec::from_iter(suggestions);
|
||||
suggestions.sort_by(|a, b| a.suggestion.cmp(&b.suggestion));
|
||||
suggestions
|
||||
Suggestions::merge(cmd, suggestions)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
@@ -621,7 +622,7 @@ impl CommandDispatcher {
|
||||
tree: &'a CommandTree,
|
||||
raw_args: &mut RawArgs<'a>,
|
||||
input: &'a str,
|
||||
) -> Result<Option<Vec<CommandSuggestion>>, CommandError> {
|
||||
) -> Result<Option<Suggestions>, CommandError> {
|
||||
//let mut parsed_args: ConsumedArgs = HashMap::new();
|
||||
|
||||
for node in path.iter().map(|&i| &tree.nodes[i]) {
|
||||
@@ -634,15 +635,50 @@ impl CommandDispatcher {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
NodeType::Argument { consumer, name: _ } => {
|
||||
NodeType::Argument {
|
||||
consumer,
|
||||
suggestion_provider,
|
||||
name: _,
|
||||
} => {
|
||||
match consumer.consume_with_syntax(src, server, raw_args).await {
|
||||
Ok(Some(_consumed)) => {
|
||||
//parsed_args.insert(name, consumed);
|
||||
}
|
||||
Ok(None) => {
|
||||
return if raw_args.is_empty() {
|
||||
let suggestions = consumer.suggest(src, server, input).await?;
|
||||
Ok(suggestions)
|
||||
let start = input
|
||||
.char_indices()
|
||||
.rfind(|(_, c)| c.is_whitespace())
|
||||
.map_or(0, |(index, c)| index + c.len_utf8());
|
||||
if let Some(provider) = suggestion_provider {
|
||||
Ok(Some(
|
||||
provider
|
||||
.suggest(src, server, input, start, input.len())
|
||||
.await,
|
||||
))
|
||||
} else {
|
||||
let suggestions = consumer.suggest(src, server, input).await?;
|
||||
let range = StringRange::between(start, input.len());
|
||||
Ok(suggestions.map(|suggestions| {
|
||||
Suggestions::new(
|
||||
range,
|
||||
suggestions
|
||||
.into_iter()
|
||||
.map(|suggestion| match suggestion.tooltip {
|
||||
Some(tooltip) => Suggestion::with_tooltip(
|
||||
range,
|
||||
suggestion.suggestion,
|
||||
tooltip,
|
||||
),
|
||||
None => Suggestion::without_tooltip(
|
||||
range,
|
||||
suggestion.suggestion,
|
||||
),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
};
|
||||
|
||||
@@ -712,23 +712,26 @@ impl CommandDispatcher {
|
||||
/// This function currently panics if the source provided was a dummy source.
|
||||
/// This is subject to change in the future.
|
||||
pub async fn suggest(&self, input: &str, source: &CommandSource) -> Vec<CommandSuggestion> {
|
||||
self.suggest_with_range(input, source)
|
||||
.await
|
||||
.suggestions
|
||||
.into_iter()
|
||||
.map(|suggestion| CommandSuggestion {
|
||||
suggestion: suggestion.text.cached_text().clone(),
|
||||
tooltip: suggestion.tooltip,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn suggest_with_range(&self, input: &str, source: &CommandSource) -> Suggestions {
|
||||
// Never suggest arguments for a command that has been turned off.
|
||||
if self.is_disabled(Self::command_name(input)) {
|
||||
return Vec::new();
|
||||
return Suggestions::empty();
|
||||
}
|
||||
|
||||
let future1 = async move {
|
||||
let parsed = self.parse_input(input, source).await;
|
||||
let suggestions = self.get_completion_suggestions_at_end(parsed).await;
|
||||
|
||||
suggestions
|
||||
.suggestions
|
||||
.into_iter()
|
||||
.map(|suggestion| CommandSuggestion {
|
||||
suggestion: suggestion.text.cached_text().clone(),
|
||||
tooltip: suggestion.tooltip,
|
||||
})
|
||||
.collect::<Vec<CommandSuggestion>>()
|
||||
self.get_completion_suggestions_at_end(parsed).await
|
||||
};
|
||||
|
||||
let future2 = async move {
|
||||
@@ -737,9 +740,9 @@ impl CommandDispatcher {
|
||||
.await
|
||||
};
|
||||
|
||||
let (mut a, mut b) = future::join(future1, future2).await;
|
||||
a.append(&mut b);
|
||||
a
|
||||
let (a, b) = future::join(future1, future2).await;
|
||||
let suggestions = <[Suggestions; 2]>::from((a, b));
|
||||
Suggestions::merge(input, suggestions)
|
||||
}
|
||||
|
||||
/// Gets all the commands usable in this dispatcher, sorted.
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::sync::Arc;
|
||||
use super::CommandExecutor;
|
||||
use crate::command::CommandSender;
|
||||
use crate::command::args::{ArgumentConsumer, DefaultNameArgConsumer};
|
||||
use crate::command::tree::{CommandTree, Node, NodeType};
|
||||
use crate::command::tree::{CommandSuggestionProvider, CommandTree, Node, NodeType};
|
||||
|
||||
impl CommandTree {
|
||||
/// Add a child [Node] to the root of this [`CommandTree`].
|
||||
@@ -127,6 +127,18 @@ impl NonLeafNodeBuilder {
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn suggests(mut self, provider: impl CommandSuggestionProvider + 'static) -> Self {
|
||||
if let NodeType::Argument {
|
||||
suggestion_provider,
|
||||
..
|
||||
} = &mut self.node_type
|
||||
{
|
||||
*suggestion_provider = Some(Arc::new(provider));
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches a string literal.
|
||||
@@ -157,6 +169,7 @@ pub fn argument(
|
||||
node_type: NodeType::Argument {
|
||||
name: name.into(),
|
||||
consumer: Arc::new(consumer),
|
||||
suggestion_provider: None,
|
||||
},
|
||||
child_nodes: Vec::new(),
|
||||
leaf_nodes: Vec::new(),
|
||||
@@ -171,6 +184,7 @@ pub fn argument_default_name(
|
||||
node_type: NodeType::Argument {
|
||||
name: consumer.default_name().to_string(),
|
||||
consumer: Arc::new(consumer),
|
||||
suggestion_provider: None,
|
||||
},
|
||||
child_nodes: Vec::new(),
|
||||
leaf_nodes: Vec::new(),
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::{CommandExecutor, args::ArgumentConsumer};
|
||||
use crate::command::CommandSender;
|
||||
use crate::command::suggestion::suggestions::Suggestions;
|
||||
use crate::server::Server;
|
||||
use std::pin::Pin;
|
||||
use std::{borrow::Cow, collections::VecDeque, fmt::Debug, sync::Arc};
|
||||
|
||||
pub mod builder;
|
||||
@@ -26,6 +29,19 @@ impl Debug for RawArg<'_> {
|
||||
/// see [`crate::command::tree::builder::argument`]
|
||||
pub type RawArgs<'a> = Vec<RawArg<'a>>;
|
||||
|
||||
pub type CommandSuggestionResult<'a> = Pin<Box<dyn Future<Output = Suggestions> + Send + 'a>>;
|
||||
|
||||
pub trait CommandSuggestionProvider: Send + Sync {
|
||||
fn suggest<'a>(
|
||||
&'a self,
|
||||
src: &'a CommandSender,
|
||||
server: &'a Server,
|
||||
input: &'a str,
|
||||
start: usize,
|
||||
end: usize,
|
||||
) -> CommandSuggestionResult<'a>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Node {
|
||||
pub children: Vec<usize>,
|
||||
@@ -43,6 +59,7 @@ pub enum NodeType {
|
||||
Argument {
|
||||
name: String,
|
||||
consumer: Arc<dyn ArgumentConsumer + Send>,
|
||||
suggestion_provider: Option<Arc<dyn CommandSuggestionProvider>>,
|
||||
},
|
||||
Require {
|
||||
predicate: Arc<dyn Fn(&CommandSender) -> bool + Send + Sync>,
|
||||
|
||||
@@ -597,7 +597,7 @@ impl Completer for PumpkinCommandCompleter {
|
||||
candidates.push(string.clone());
|
||||
}
|
||||
}
|
||||
NodeType::Argument { name, consumer } => {
|
||||
NodeType::Argument { name, consumer, .. } => {
|
||||
let suggest_future = consumer.suggest(&src, server, typing);
|
||||
|
||||
if let Ok(Some(suggestions)) = suggest_future.await {
|
||||
|
||||
@@ -6,10 +6,16 @@ use pumpkin_util::text::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
command::{CommandExecutor, dispatcher::CommandError},
|
||||
command::{
|
||||
CommandExecutor,
|
||||
context::string_range::StringRange,
|
||||
dispatcher::CommandError,
|
||||
suggestion::{Suggestion, suggestions::Suggestions},
|
||||
tree::{CommandSuggestionProvider, CommandSuggestionResult},
|
||||
},
|
||||
plugin::loader::wasm::wasm_host::{
|
||||
DowncastResourceExt, PluginInstance, WasmPlugin,
|
||||
wit::v0_1::pumpkin::plugin::command::CommandError as CommandErrorWit,
|
||||
wit::v0_1::pumpkin::plugin::command::{CommandError as CommandErrorWit, SuggestionRequest},
|
||||
},
|
||||
server::Server,
|
||||
};
|
||||
@@ -111,3 +117,91 @@ impl CommandExecutor for WasmCommandExecutor {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WasmCommandSuggestionProvider {
|
||||
pub handler_id: u32,
|
||||
pub plugin: Arc<WasmPlugin>,
|
||||
pub server: Arc<Server>,
|
||||
}
|
||||
|
||||
impl CommandSuggestionProvider for WasmCommandSuggestionProvider {
|
||||
fn suggest<'a>(
|
||||
&'a self,
|
||||
src: &'a crate::command::CommandSender,
|
||||
_server: &'a Server,
|
||||
input: &'a str,
|
||||
start: usize,
|
||||
end: usize,
|
||||
) -> CommandSuggestionResult<'a> {
|
||||
Box::pin(async move {
|
||||
let mut store = self.plugin.store.lock().await;
|
||||
|
||||
let sender_resource = match store.data_mut().add_command_sender(src.clone()) {
|
||||
Ok(resource) => resource,
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"Failed to create command sender resource for suggestions: {error}"
|
||||
);
|
||||
return Suggestions::empty();
|
||||
}
|
||||
};
|
||||
let server_resource = match store.data_mut().add_server(self.server.clone()) {
|
||||
Ok(resource) => resource,
|
||||
Err(error) => {
|
||||
tracing::error!("Failed to create server resource for suggestions: {error}");
|
||||
return Suggestions::empty();
|
||||
}
|
||||
};
|
||||
|
||||
let request = SuggestionRequest {
|
||||
input: input.to_string(),
|
||||
cursor: input.len().try_into().unwrap_or(u32::MAX),
|
||||
start: start.try_into().unwrap_or(u32::MAX),
|
||||
remaining: input[start.min(input.len())..end.min(input.len())].to_string(),
|
||||
};
|
||||
|
||||
let response = match self.plugin.plugin_instance {
|
||||
PluginInstance::V0_1(ref plugin) => {
|
||||
plugin
|
||||
.call_handle_command_suggestion(
|
||||
&mut *store,
|
||||
self.handler_id,
|
||||
sender_resource,
|
||||
server_resource,
|
||||
&request,
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
let response = match response {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
tracing::error!("Wasm command suggestion failed: {error}");
|
||||
return Suggestions::empty();
|
||||
}
|
||||
};
|
||||
|
||||
let start = response.start as usize;
|
||||
let end = start.saturating_add(response.length as usize);
|
||||
let range = StringRange::between(start, end.min(input.len()));
|
||||
let suggestions = response
|
||||
.values
|
||||
.into_iter()
|
||||
.map(|suggestion| {
|
||||
if let Some(tooltip) = suggestion.tooltip {
|
||||
Suggestion::with_tooltip(
|
||||
range,
|
||||
suggestion.value,
|
||||
tooltip.consume(store.data_mut()).provider,
|
||||
)
|
||||
} else {
|
||||
Suggestion::without_tooltip(range, suggestion.value)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Suggestions::new(range, suggestions)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ use crate::{
|
||||
PluginHostState, ServerResource, TextComponentResource,
|
||||
},
|
||||
wit::v0_1::{
|
||||
commands::executor::WasmCommandExecutor,
|
||||
commands::executor::{WasmCommandExecutor, WasmCommandSuggestionProvider},
|
||||
pumpkin::{
|
||||
self,
|
||||
plugin::{
|
||||
@@ -647,6 +647,32 @@ impl pumpkin::plugin::command::HostCommandNode for PluginHostState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn suggest_with_handler_id(
|
||||
&mut self,
|
||||
node: Resource<CommandNode>,
|
||||
handler_id: u32,
|
||||
) -> wasmtime::Result<()> {
|
||||
let plugin = self
|
||||
.plugin
|
||||
.as_ref()
|
||||
.and_then(std::sync::Weak::upgrade)
|
||||
.ok_or_else(|| wasmtime::Error::msg("Plugin dropped"))?;
|
||||
let server = self
|
||||
.server
|
||||
.clone()
|
||||
.ok_or_else(|| wasmtime::Error::msg("Server not initialized"))?;
|
||||
|
||||
let provider = WasmCommandSuggestionProvider {
|
||||
handler_id,
|
||||
plugin,
|
||||
server,
|
||||
};
|
||||
let resource = self.get_node_mut(&node)?;
|
||||
let builder = std::mem::replace(&mut resource.provider, literal(""));
|
||||
resource.provider = builder.suggests(provider);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn require_with_handler_id(
|
||||
&mut self,
|
||||
_node: Resource<CommandNode>,
|
||||
|
||||
@@ -248,7 +248,7 @@ impl LootFunctionExt for LootFunction {
|
||||
Block::properties(Block::from_state_id(state.id), state.id)
|
||||
{
|
||||
let actual_props = props_data.to_props();
|
||||
let mut properties_to_copy = std::collections::HashMap::new();
|
||||
let mut properties_to_copy = std::collections::BTreeMap::new();
|
||||
for &prop_name in *properties {
|
||||
if let Some((_, value)) = actual_props.iter().find(|(k, _)| k == &prop_name)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user