From 5a0db0f50d296095567aecf52b3c5aeb43df982b Mon Sep 17 00:00:00 2001 From: Laptop59 <90901102+Laptop59@users.noreply.github.com> Date: Tue, 31 Mar 2026 00:21:05 +0530 Subject: [PATCH] chore(command): reimplemented `/difficulty` (#1924) --- pumpkin-util/src/difficulty.rs | 26 +++++ pumpkin/src/command/client_suggestions.rs | 12 ++- pumpkin/src/command/commands/difficulty.rs | 106 +++++++++++---------- pumpkin/src/command/commands/mod.rs | 12 +-- 4 files changed, 90 insertions(+), 66 deletions(-) diff --git a/pumpkin-util/src/difficulty.rs b/pumpkin-util/src/difficulty.rs index 2626e2027..df6e8085d 100644 --- a/pumpkin-util/src/difficulty.rs +++ b/pumpkin-util/src/difficulty.rs @@ -25,6 +25,32 @@ pub enum Difficulty { Hard = 3, } +impl Difficulty { + /// Gets the lowercase name of this difficulty. + /// For example, [`Difficulty::Peaceful`] will yield `"peaceful"`. + #[must_use] + pub const fn name(self) -> &'static str { + match self { + Self::Peaceful => "peaceful", + Self::Easy => "easy", + Self::Normal => "normal", + Self::Hard => "hard", + } + } + + /// Gets the translation key of this difficulty. + /// For example, [`Difficulty::Peaceful`] will yield `"options.difficulty.peaceful"`. + #[must_use] + pub const fn translation_key(self) -> &'static str { + match self { + Self::Peaceful => "options.difficulty.peaceful", + Self::Easy => "options.difficulty.easy", + Self::Normal => "options.difficulty.normal", + Self::Hard => "options.difficulty.hard", + } + } +} + impl FromStr for Difficulty { type Err = ParseDifficultyError; diff --git a/pumpkin/src/command/client_suggestions.rs b/pumpkin/src/command/client_suggestions.rs index 21c97aced..9b66a7e9c 100644 --- a/pumpkin/src/command/client_suggestions.rs +++ b/pumpkin/src/command/client_suggestions.rs @@ -98,15 +98,19 @@ pub async fn send_c_commands_packet( .map(|i| i.try_into().expect("i32 limit reached for ids")); // TODO: + // // As stated in the previous TODO, after // we can get a reference to an Arc of Server, // we can add the permission checking. // - // Luckily, for now the new dispatcher - // only has the /help commands which - // is accessible to everyone by default. - let satisfies_requirements = true; + // Right now, we incorrectly assume that + // requirements are always satisfied. Hopefully + // this can be fixed once the `/op` and `/deop` commands + // are reimplemented with the Arcs, after which we can uncomment + // the following line instead of the current one: + // // let satisfies_requirements = node.requirements().evaluate(&source).await; + let satisfies_requirements = true; match node { AttachedNode::Root(_) => { diff --git a/pumpkin/src/command/commands/difficulty.rs b/pumpkin/src/command/commands/difficulty.rs index 4e1a6c04f..302993d4e 100644 --- a/pumpkin/src/command/commands/difficulty.rs +++ b/pumpkin/src/command/commands/difficulty.rs @@ -1,37 +1,35 @@ -use crate::command::CommandResult; -use crate::command::args::difficulty::DifficultyArgumentConsumer; -use crate::command::args::{Arg, GetCloned}; -use crate::command::dispatcher::CommandError::{self, InvalidConsumption}; -use crate::command::tree::builder::argument; -use crate::command::{CommandExecutor, CommandSender, args::ConsumedArgs, tree::CommandTree}; +use crate::command::argument_builder::{ArgumentBuilder, command, literal}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; +use pumpkin_util::{Difficulty, PermissionLvl}; -const NAMES: [&str; 1] = ["difficulty"]; +const DESCRIPTION: &str = "Query or change the difficulty of the world."; +const PERMISSION: &str = "minecraft:command.difficulty"; -const DESCRIPTION: &str = "Change the difficulty of the world."; - -pub const ARG_DIFFICULTY: &str = "difficulty"; +const FAILURE_ERROR_TYPE: CommandErrorType<1> = + CommandErrorType::new("commands.difficulty.failure"); struct DifficultyQueryExecutor; impl CommandExecutor for DifficultyQueryExecutor { - fn execute<'a>( - &'a self, - sender: &'a CommandSender, - server: &'a crate::server::Server, - _args: &'a ConsumedArgs<'a>, - ) -> CommandResult<'a> { + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { Box::pin(async move { - let difficulty = server.get_difficulty(); - let difficulty_string = format!("{difficulty:?}").to_lowercase(); - let translation_key = format!("options.difficulty.{difficulty_string}"); + let difficulty = context.server().get_difficulty(); - sender - .send_message(TextComponent::translate( - "commands.difficulty.query", - [TextComponent::translate(translation_key, [])], - )) + context + .source + .send_feedback( + TextComponent::translate( + "commands.difficulty.query", + [TextComponent::translate(difficulty.translation_key(), [])], + ), + false, + ) .await; Ok(difficulty as i32) @@ -39,41 +37,34 @@ impl CommandExecutor for DifficultyQueryExecutor { } } -struct DifficultySetExecutor; +struct DifficultySetExecutor(Difficulty); impl CommandExecutor for DifficultySetExecutor { - fn execute<'a>( - &'a self, - sender: &'a CommandSender, - server: &'a crate::server::Server, - args: &'a ConsumedArgs<'a>, - ) -> CommandResult<'a> { + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { Box::pin(async move { - let Some(Arg::Difficulty(difficulty)) = args.get_cloned(&ARG_DIFFICULTY) else { - return Err(InvalidConsumption(Some(ARG_DIFFICULTY.into()))); - }; - - let difficulty_string = format!("{difficulty:?}").to_lowercase(); - let translation_key = format!("options.difficulty.{difficulty_string}"); + let difficulty = self.0; + let server = context.server(); { let level_info = server.level_info.load(); if level_info.difficulty == difficulty { - return Err(CommandError::CommandFailed(TextComponent::translate( - "commands.difficulty.failure", - [TextComponent::translate(translation_key, [])], - ))); + return Err(FAILURE_ERROR_TYPE + .create_without_context(TextComponent::text(difficulty.name()))); } } server.set_difficulty(difficulty, true).await; - sender - .send_message(TextComponent::translate( - "commands.difficulty.success", - [TextComponent::translate(translation_key, [])], - )) + context + .source + .send_feedback( + TextComponent::translate( + "commands.difficulty.success", + [TextComponent::translate(difficulty.translation_key(), [])], + ), + true, + ) .await; Ok(0) @@ -81,9 +72,22 @@ impl CommandExecutor for DifficultySetExecutor { } } -#[must_use] -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION) - .execute(DifficultyQueryExecutor) - .then(argument(ARG_DIFFICULTY, DifficultyArgumentConsumer).execute(DifficultySetExecutor)) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &mut PermissionRegistry) { + registry + .register_permission(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )) + .expect("Permission should have registered successfully"); + + dispatcher.register( + command("difficulty", DESCRIPTION) + .requires(PERMISSION) + .then(literal("peaceful").executes(DifficultySetExecutor(Difficulty::Peaceful))) + .then(literal("easy").executes(DifficultySetExecutor(Difficulty::Easy))) + .then(literal("normal").executes(DifficultySetExecutor(Difficulty::Normal))) + .then(literal("hard").executes(DifficultySetExecutor(Difficulty::Hard))) + .executes(DifficultyQueryExecutor), + ); } diff --git a/pumpkin/src/command/commands/mod.rs b/pumpkin/src/command/commands/mod.rs index 8310708ae..58027f14d 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -113,10 +113,6 @@ pub async fn default_dispatcher( dispatcher.register(say::init_command_tree(), "minecraft:command.say"); dispatcher.register(gamemode::init_command_tree(), "minecraft:command.gamemode"); dispatcher.register(gamerule::init_command_tree(), "minecraft:command.gamerule"); - dispatcher.register( - difficulty::init_command_tree(), - "minecraft:command.difficulty", - ); dispatcher.register( stopsound::init_command_tree(), "minecraft:command.stopsound", @@ -161,6 +157,7 @@ pub async fn default_dispatcher( wrapper_dispatcher }; + difficulty::register(&mut dispatcher, registry); help::register(&mut dispatcher, registry); seed::register(&mut dispatcher, registry); stop::register(&mut dispatcher, registry); @@ -382,13 +379,6 @@ fn register_level_2_permissions(registry: &mut PermissionRegistry) { PermissionDefault::Op(PermissionLvl::Two), )) .unwrap(); - registry - .register_permission(Permission::new( - "minecraft:command.difficulty", - "Sets the difficulty of the world", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap(); registry .register_permission(Permission::new( "minecraft:command.data",