From 75d1bc5a885134c0ec6851d9da86012c6b38c411 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Tue, 14 Jul 2026 14:08:57 +0200 Subject: [PATCH] feat(command): add dialog command --- pumpkin-protocol/src/java/client/dialog.rs | 21 +++- pumpkin/src/command/argument_types/dialog.rs | 57 +++++++++ pumpkin/src/command/argument_types/mod.rs | 1 + pumpkin/src/command/commands/dialog.rs | 110 ++++++++++++++++++ pumpkin/src/command/commands/mod.rs | 2 + .../loader/wasm/wasm_host/wit/v0_1/player.rs | 8 +- 6 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 pumpkin/src/command/argument_types/dialog.rs create mode 100644 pumpkin/src/command/commands/dialog.rs diff --git a/pumpkin-protocol/src/java/client/dialog.rs b/pumpkin-protocol/src/java/client/dialog.rs index 31379b727..3166a95d1 100644 --- a/pumpkin-protocol/src/java/client/dialog.rs +++ b/pumpkin-protocol/src/java/client/dialog.rs @@ -2,7 +2,26 @@ use pumpkin_util::text::TextComponent; use serde::Serialize; #[derive(Serialize)] -pub struct DialogNBT<'a>(pub &'a Dialog); +pub struct DialogNBT<'a>(pub DialogNBTSource<'a>); + +impl<'a> DialogNBT<'a> { + #[must_use] + pub const fn from_dialog(dialog: &'a Dialog) -> Self { + Self(DialogNBTSource::Struct(dialog)) + } + + #[must_use] + pub const fn from_nbt(compound: &'a pumpkin_nbt::compound::NbtCompound) -> Self { + Self(DialogNBTSource::Nbt(compound)) + } +} + +#[derive(Serialize)] +#[serde(untagged)] +pub enum DialogNBTSource<'a> { + Struct(&'a Dialog), + Nbt(&'a pumpkin_nbt::compound::NbtCompound), +} #[derive(Serialize)] pub struct Dialog { diff --git a/pumpkin/src/command/argument_types/dialog.rs b/pumpkin/src/command/argument_types/dialog.rs new file mode 100644 index 000000000..3822ab610 --- /dev/null +++ b/pumpkin/src/command/argument_types/dialog.rs @@ -0,0 +1,57 @@ +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_util::identifier::Identifier; + +use crate::command::{ + argument_types::{ + FromStringReader, + argument_type::{ArgumentType, JavaClientArgumentType}, + nbt::NbtCompoundArgumentType, + }, + context::command_context::CommandContext, + errors::command_syntax_error::CommandSyntaxError, + string_reader::StringReader, +}; + +#[derive(Clone, Debug, PartialEq)] +pub enum DialogArg { + Id(Identifier), + Nbt(NbtCompound), +} + +pub struct DialogArgumentType; + +impl ArgumentType for DialogArgumentType { + type Item = DialogArg; + + fn parse(&self, reader: &mut StringReader) -> Result { + if reader.peek() == Some('{') { + let compound = NbtCompoundArgumentType.parse(reader)?; + Ok(DialogArg::Nbt(compound)) + } else { + let id = Identifier::from_reader(reader)?; + Ok(DialogArg::Id(id)) + } + } + + fn client_side_parser(&'_ self) -> JavaClientArgumentType { + JavaClientArgumentType::Dialog + } + + fn examples(&self) -> Vec { + examples!( + "minecraft:server_links", + "custom:my_dialog", + "{type:\"minecraft:notice\",title:\"Hello\"}" + ) + } +} + +impl DialogArgumentType { + /// Returns the parsed [`DialogArg`] from the name of the argument. + pub fn get<'a>( + context: &'a CommandContext, + name: &'_ str, + ) -> Result<&'a DialogArg, CommandSyntaxError> { + context.get_argument(name) + } +} diff --git a/pumpkin/src/command/argument_types/mod.rs b/pumpkin/src/command/argument_types/mod.rs index 18e486631..d066d3549 100644 --- a/pumpkin/src/command/argument_types/mod.rs +++ b/pumpkin/src/command/argument_types/mod.rs @@ -221,6 +221,7 @@ pub mod attribute; pub mod block; pub mod coordinates; pub mod core; +pub mod dialog; pub mod entity; pub mod entity_anchor; pub mod entity_selector; diff --git a/pumpkin/src/command/commands/dialog.rs b/pumpkin/src/command/commands/dialog.rs new file mode 100644 index 000000000..b145139e8 --- /dev/null +++ b/pumpkin/src/command/commands/dialog.rs @@ -0,0 +1,110 @@ +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::dialog::{DialogArg, DialogArgumentType}; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::error_types::LiteralCommandErrorType; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; +use pumpkin_protocol::IdOr; +use pumpkin_protocol::java::client::dialog::DialogNBT; +use pumpkin_protocol::java::client::play::{CPlayClearDialog, CPlayShowDialog}; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; + +const DESCRIPTION: &str = "Manages player dialog screens."; +const PERMISSION: &str = "minecraft:command.dialog"; + +const ARG_TARGETS: &str = "targets"; +const ARG_DIALOG: &str = "dialog"; + +struct DialogClearExecutor; + +impl CommandExecutor for DialogClearExecutor { + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { + Box::pin(async move { + let targets = EntityArgumentType::get_players(context, ARG_TARGETS).await?; + + let count = targets.len(); + let packet = CPlayClearDialog::new(); + for player in &targets { + player.client.send_packet_now(&packet).await; + } + + let msg = if count == 1 { + TextComponent::text(format!( + "Cleared dialog for {}", + targets[0].gameprofile.name + )) + } else { + TextComponent::text(format!("Cleared dialogs for {count} players")) + }; + context.source.send_feedback(msg, true).await; + + Ok(count as i32) + }) + } +} + +static REGISTRY_ERROR: LiteralCommandErrorType = LiteralCommandErrorType::new( + "Registry-defined dialogs are not yet supported. Please specify the dialog inline using SNBT.", +); + +struct DialogShowExecutor; + +impl CommandExecutor for DialogShowExecutor { + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { + Box::pin(async move { + let targets = EntityArgumentType::get_players(context, ARG_TARGETS).await?; + let dialog_arg = DialogArgumentType::get(context, ARG_DIALOG)?; + + match dialog_arg { + DialogArg::Nbt(compound) => { + let count = targets.len(); + let dialog_nbt = DialogNBT::from_nbt(compound); + let packet = CPlayShowDialog::new(IdOr::Value(dialog_nbt)); + + for player in &targets { + player.client.send_packet_now(&packet).await; + } + + let msg = if count == 1 { + TextComponent::text(format!( + "Showed dialog to {}", + targets[0].gameprofile.name + )) + } else { + TextComponent::text(format!("Showed dialog to {count} players")) + }; + context.source.send_feedback(msg, true).await; + + Ok(count as i32) + } + DialogArg::Id(_id) => Err(REGISTRY_ERROR.create_without_context()), + } + }) + } +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &mut PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("dialog", DESCRIPTION) + .requires(PERMISSION) + .then(literal("clear").then( + argument(ARG_TARGETS, EntityArgumentType::Players).executes(DialogClearExecutor), + )) + .then( + literal("show").then( + argument(ARG_TARGETS, EntityArgumentType::Players).then( + argument(ARG_DIALOG, DialogArgumentType).executes(DialogShowExecutor), + ), + ), + ), + ); +} diff --git a/pumpkin/src/command/commands/mod.rs b/pumpkin/src/command/commands/mod.rs index ec8093486..286f5395d 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -18,6 +18,7 @@ mod damage; mod data; pub mod defaultgamemode; mod deop; +mod dialog; mod difficulty; mod effect; mod enchant; @@ -165,6 +166,7 @@ pub async fn default_dispatcher( banlist::register(&mut dispatcher, registry); difficulty::register(&mut dispatcher, registry); + dialog::register(&mut dispatcher, registry); help::register(&mut dispatcher, registry); kill::register(&mut dispatcher, registry); op::register(&mut dispatcher, registry); diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs index e3deef818..4840f9859 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs @@ -1763,7 +1763,9 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState { client .send_packet_now( &pumpkin_protocol::java::client::config::CConfigShowDialog::new( - pumpkin_protocol::IdOr::Value(DialogNBT(&protocol_dialog)), + pumpkin_protocol::IdOr::Value(DialogNBT::from_dialog( + &protocol_dialog, + )), ), ) .await; @@ -1772,7 +1774,9 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState { client .send_packet_now( &pumpkin_protocol::java::client::play::CPlayShowDialog::new( - pumpkin_protocol::IdOr::Value(DialogNBT(&protocol_dialog)), + pumpkin_protocol::IdOr::Value(DialogNBT::from_dialog( + &protocol_dialog, + )), ), ) .await;