Skip to content

Adding a Command

This guide explains how to implement and register a new command in the Pumpkin server codebase.

Commands in Pumpkin are implemented in the crates/pumpkin/src/command/commands/ directory and registered into the central CommandDispatcher during server initialization.


Anatomy of a Pumpkin Command

A typical command file contains:

  1. Metadata Constants: Command description, permission node identifier, and argument names.
  2. Permission Registration: Declaring default OP requirements in the server's PermissionRegistry.
  3. Command Executors: Structs implementing the CommandExecutor trait for each terminal node or subcommand.
  4. Registration Function: Constructing the argument tree using fluent builders (command, literal, argument).

Step-by-Step Implementation: Creating /heal

Let's implement a complete /heal command that can heal either the executing player or a set of target players:

/heal                 -> Heals the executing sender
/heal <targets>       -> Heals target player(s)
/heal <targets> <amount> -> Heals target player(s) by a specified amount

1. Create the File

Create crates/pumpkin/src/command/commands/heal.rs:

rust
use std::sync::Arc;

use crate::command::argument_builder::{ArgumentBuilder, argument, command};
use crate::command::argument_types::core::float::FloatArgumentType;
use crate::command::argument_types::entity::EntityArgumentType;
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 crate::entity::EntityBase;
use pumpkin_data::translation;
use pumpkin_util::PermissionLvl;
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
use pumpkin_util::text::TextComponent;

const DESCRIPTION: &str = "Restores health to target entities.";
const PERMISSION: &str = "pumpkin:command.heal";

const ARG_TARGETS: &str = "targets";
const ARG_AMOUNT: &str = "amount";

const ERROR_NOT_PLAYER: CommandErrorType<0> = CommandErrorType::new(
    translation::java::PERMISSIONS_REQUIRES_PLAYER,
    translation::java::PERMISSIONS_REQUIRES_PLAYER,
);

2. Implement the Executors

Command execution logic is encapsulated in structs implementing CommandExecutor:

rust
/// Heals the executing player to full health.
struct SelfHealExecutor;

impl CommandExecutor for SelfHealExecutor {
    fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
        let player = context
            .source
            .output
            .as_player()
            .ok_or_else(|| ERROR_NOT_PLAYER.create_without_context())?;

        // Restore player health
        player.living_entity.set_health(20.0);

        context.source.send_feedback(
            TextComponent::text("Fully restored your health!"),
            true, // Broadcast to operators if configured
        );

        // Return number of affected entities
        Ok(1)
    }
}

/// Heals specified target players.
struct TargetsHealExecutor {
    has_amount: bool,
}

impl CommandExecutor for TargetsHealExecutor {
    fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
        let targets = EntityArgumentType::get_players(context, ARG_TARGETS)?;

        let amount = if self.has_amount {
            FloatArgumentType::get(context, ARG_AMOUNT)?
        } else {
            20.0
        };

        let count = targets.len();
        for target in &targets {
            let current = target.living_entity.health.load();
            target.living_entity.set_health((current + amount).min(20.0));
        }

        let msg = if count == 1 {
            TextComponent::text(format!("Healed {} by {} HP", targets[0].gameprofile.name, amount))
        } else {
            TextComponent::text(format!("Healed {} players by {} HP", count, amount))
        };

        context.source.send_feedback(msg, true);

        Ok(count as i32)
    }
}

3. Build the Command Tree

Use the fluent builder functions command, literal, and argument to assemble the syntax tree and register the permission:

rust
pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) {
    // 1. Register permission node with default requirement (OP level 2)
    registry.register_permission_or_panic(Permission::new(
        PERMISSION,
        DESCRIPTION,
        PermissionDefault::Op(PermissionLvl::Two),
    ));

    // 2. Assemble command tree
    dispatcher.register(
        command("heal", DESCRIPTION)
            .requires(PERMISSION)
            .executes(SelfHealExecutor)
            .then(
                argument(ARG_TARGETS, EntityArgumentType::Players)
                    .executes(TargetsHealExecutor { has_amount: false })
                    .then(
                        argument(ARG_AMOUNT, FloatArgumentType::new(1.0, 100.0))
                            .executes(TargetsHealExecutor { has_amount: true }),
                    ),
            ),
    );
}

Command Structure Patterns

Adding Subcommand Literals

Use literal("subcommand_name") to create keyword branches:

rust
use crate::command::argument_builder::{argument, command, literal};

dispatcher.register(
    command("weather", "Set the world weather")
        .requires("minecraft:command.weather")
        .then(literal("clear").executes(ClearWeatherExecutor))
        .then(literal("rain").executes(RainWeatherExecutor))
        .then(literal("thunder").executes(ThunderWeatherExecutor))
);

Cross-Platform Translation Feedback

Pumpkin servers serve both Java and Bedrock Edition players simultaneously. Whenever possible, use vanilla translation keys with TextComponent::translate_cross so messages render localized on the client:

rust
use pumpkin_data::translation;
use pumpkin_util::text::TextComponent;

let msg = TextComponent::translate_cross(
    translation::java::COMMANDS_KILL_SUCCESS_SINGLE,
    translation::bedrock::COMMANDS_KILL_SUCCESSFUL,
    [target.get_display_name()],
);

context.source.send_feedback(msg, true);

Error Handling in Executors

Executors must return CommandExecutorResult, which resolves to Result<i32, CommandSyntaxError>. If validation fails, return an error cleanly without panicking:

rust
use crate::command::errors::error_types::CommandErrorType;

const ERROR_INVALID_VALUE: CommandErrorType<0> = CommandErrorType::new(
    "Amount must be greater than zero.",
    "Amount must be greater than zero.",
);

if amount <= 0.0 {
    return Err(ERROR_INVALID_VALUE.create_without_context());
}

Registering in default_dispatcher

Once your command is implemented, register it in crates/pumpkin/src/command/commands/mod.rs:

  1. Declare the Module:

    rust
    // In crates/pumpkin/src/command/commands/mod.rs
    mod heal;
  2. Invoke register in default_dispatcher:

    rust
    pub fn default_dispatcher(
        permission_manager: &PermissionManager,
        commands_config: &CommandsConfig,
    ) -> CommandDispatcher {
        let registry = &permission_manager.registry;
        let mut dispatcher = CommandDispatcher::new();
    
        // ... existing registrations ...
        heal::register(&mut dispatcher, registry);
    
        dispatcher
    }

Best Practices Checklist

  • rust
    let server = context.source.server();
    if server.level_info.load().game_rules.send_command_feedback {
        target.send_system_message(&feedback);
    }

Lançado sob a licença MIT.