强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

PaperMC 插件开发完全指南 / 第 4 章:命令处理

第 4 章:命令处理

掌握命令注册、执行、Tab 补全、参数解析和权限检查的完整流程。


4.1 命令处理基础

Bukkit 的命令系统基于 CommandExecutor 接口。所有命令最终都通过这个接口的 onCommand 方法处理。

最简单的命令处理器

package com.example.myplugin.commands;

import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;

public class HealCommand implements CommandExecutor {

    @Override
    public boolean onCommand(@NotNull CommandSender sender,
                             @NotNull Command command,
                             @NotNull String label,
                             @NotNull String[] args) {
        if (!(sender instanceof Player player)) {
            sender.sendMessage("§c此命令只能由玩家执行!");
            return true;
        }

        // 治疗玩家
        player.setHealth(player.getMaxHealth());
        player.setFoodLevel(20);
        player.setSaturation(20f);
        player.sendMessage("§a你已被治愈!");

        return true; // 返回 true 表示命令已处理
    }
}

注册命令

// 在 onEnable() 中
getCommand("heal").setExecutor(new HealCommand());

注意: onCommand 返回 true 表示命令被正确处理,返回 false 会向玩家显示 usage 信息(来自 plugin.yml)。


4.2 子命令模式

大多数复杂插件使用"主命令 + 子命令"的结构。

命令路由器实现

package com.example.myplugin.commands;

import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;

import java.util.HashMap;
import java.util.Map;

public class MainCommand implements CommandExecutor {

    private final Map<String, SubCommand> subCommands = new HashMap<>();

    public MainCommand() {
        // 注册子命令
        subCommands.put("reload", new ReloadSubCommand());
        subCommands.put("help", new HelpSubCommand());
        subCommands.put("info", new InfoSubCommand());
    }

    @Override
    public boolean onCommand(@NotNull CommandSender sender,
                             @NotNull Command command,
                             @NotNull String label,
                             @NotNull String[] args) {
        if (args.length == 0) {
            // 无参数,显示帮助
            showHelp(sender);
            return true;
        }

        String subName = args[0].toLowerCase();
        SubCommand sub = subCommands.get(subName);

        if (sub == null) {
            sender.sendMessage("§c未知子命令: " + subName);
            showHelp(sender);
            return true;
        }

        // 权限检查
        if (!sender.hasPermission(sub.getPermission())) {
            sender.sendMessage("§c你没有权限使用此子命令!");
            return true;
        }

        // 传递参数(去掉第一个子命令名称)
        String[] subArgs = new String[args.length - 1];
        System.arraycopy(args, 1, subArgs, 0, subArgs.length);

        sub.execute(sender, subArgs);
        return true;
    }

    private void showHelp(CommandSender sender) {
        sender.sendMessage("§6===== MyPlugin 帮助 =====");
        for (var entry : subCommands.entrySet()) {
            SubCommand sub = entry.getValue();
            if (sender.hasPermission(sub.getPermission())) {
                sender.sendMessage("§e/" + "myplugin " + entry.getKey()
                    + " §7- " + sub.getDescription());
            }
        }
    }

    public Map<String, SubCommand> getSubCommands() {
        return subCommands;
    }
}

子命令接口

package com.example.myplugin.commands;

import org.bukkit.command.CommandSender;

public interface SubCommand {
    void execute(CommandSender sender, String[] args);
    String getDescription();
    String getPermission();
    String getUsage();
}

子命令实现示例

package com.example.myplugin.commands;

import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;

public class HealSubCommand implements SubCommand {

    @Override
    public void execute(CommandSender sender, String[] args) {
        if (args.length == 0) {
            // 治疗自己
            if (!(sender instanceof Player player)) {
                sender.sendMessage("§c请指定玩家名!");
                return;
            }
            healPlayer(player);
            player.sendMessage("§a你已被治愈!");
            return;
        }

        // 治疗指定玩家
        Player target = Bukkit.getPlayer(args[0]);
        if (target == null) {
            sender.sendMessage("§c玩家 " + args[0] + " 不在线!");
            return;
        }
        healPlayer(target);
        sender.sendMessage("§a已治愈 " + target.getName() + "!");
        target.sendMessage("§a你已被 " + sender.getName() + " 治愈!");
    }

    private void healPlayer(Player player) {
        player.setHealth(player.getMaxHealth());
        player.setFoodLevel(20);
        player.setSaturation(20f);
    }

    @Override
    public String getDescription() { return "治疗玩家"; }

    @Override
    public String getPermission() { return "myplugin.heal"; }

    @Override
    public String getUsage() { return "/myplugin heal [玩家]"; }
}

4.3 Tab 补全

Tab 补全让玩家按 Tab 键时自动补全命令参数,提升用户体验。

TabCompleter 接口

package com.example.myplugin.commands;

import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class MainTabCompleter implements TabCompleter {

    private final MainCommand mainCommand;

    public MainTabCompleter(MainCommand mainCommand) {
        this.mainCommand = mainCommand;
    }

    @Override
    public @Nullable List<String> onTabComplete(
            @NotNull CommandSender sender,
            @NotNull Command command,
            @NotNull String label,
            @NotNull String[] args) {

        List<String> completions = new ArrayList<>();

        if (args.length == 1) {
            // 第一个参数:补全子命令名
            String partial = args[0].toLowerCase();
            completions = mainCommand.getSubCommands().entrySet().stream()
                .filter(e -> sender.hasPermission(e.getValue().getPermission()))
                .map(e -> e.getKey())
                .filter(name -> name.startsWith(partial))
                .collect(Collectors.toList());
        } else if (args.length == 2) {
            String subCmd = args[0].toLowerCase();
            String partial = args[1].toLowerCase();

            switch (subCmd) {
                case "heal":
                case "tp":
                    // 补全在线玩家名
                    completions = Bukkit.getOnlinePlayers().stream()
                        .map(Player::getName)
                        .filter(name -> name.toLowerCase().startsWith(partial))
                        .collect(Collectors.toList());
                    break;
                case "warp":
                    // 补全地标名(假设有 WarpManager)
                    completions = WarpManager.getInstance()
                        .getWarpNames().stream()
                        .filter(name -> name.toLowerCase().startsWith(partial))
                        .collect(Collectors.toList());
                    break;
            }
        }

        return completions;
    }
}

注册 Tab 补全

// 在 onEnable() 中
MainCommand mainCmd = new MainCommand();
MainTabCompleter tabCompleter = new MainTabCompleter(mainCmd);

var cmd = getCommand("myplugin");
cmd.setExecutor(mainCmd);
cmd.setTabCompleter(tabCompleter);

使用 Paper 的 AsyncTabCompleteEvent

Paper 提供了异步 Tab 补全事件,适合需要查数据库等耗时操作的场景:

import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent;

import java.util.ArrayList;

public class AsyncTabHandler implements Listener {

    @EventHandler
    public void onAsyncTabComplete(AsyncTabCompleteEvent event) {
        if (!event.getBuffer().startsWith("/warp ")) return;

        String arg = event.getLastToken().toLowerCase();

        // 异步获取地标列表(可能来自数据库)
        List<AsyncTabCompleteEvent.Completion> completions = new ArrayList<>();
        for (String warpName : getWarpsFromDatabase()) {
            if (warpName.toLowerCase().startsWith(arg)) {
                completions.add(
                    AsyncTabCompleteEvent.Completion.completion(warpName)
                );
            }
        }

        event.setCompletions(completions);
        event.setHandled(true);
    }
}

4.4 参数解析

实际开发中,命令参数的解析是常见的重复性工作。以下介绍几种实用的解析模式。

基础参数解析工具

public final class ArgsParser {

    private ArgsParser() {}

    /**
     * 解析整数参数
     */
    public static OptionalInt parseInt(String arg) {
        try {
            return OptionalInt.of(Integer.parseInt(arg));
        } catch (NumberFormatException e) {
            return OptionalInt.empty();
        }
    }

    /**
     * 解析双精度浮点数
     */
    public static OptionalDouble parseDouble(String arg) {
        try {
            return OptionalDouble.of(Double.parseDouble(arg));
        } catch (NumberFormatException e) {
            return OptionalDouble.empty();
        }
    }

    /**
     * 解析在线玩家
     */
    public static Optional<Player> parsePlayer(String name) {
        return Optional.ofNullable(Bukkit.getPlayer(name));
    }

    /**
     * 解析布尔值
     */
    public static Optional<Boolean> parseBool(String arg) {
        return switch (arg.toLowerCase()) {
            case "true", "on", "yes", "1" -> Optional.of(true);
            case "false", "off", "no", "0" -> Optional.of(false);
            default -> Optional.empty();
        };
    }
}

使用示例

@Override
public void execute(CommandSender sender, String[] args) {
    if (args.length < 2) {
        sender.sendMessage("§c用法: /myplugin give <玩家> <数量>");
        return;
    }

    Player target = ArgsParser.parsePlayer(args[0]).orElse(null);
    if (target == null) {
        sender.sendMessage("§c玩家 " + args[0] + " 不在线!");
        return;
    }

    OptionalInt amount = ArgsParser.parseInt(args[1]);
    if (amount.isEmpty() || amount.getAsInt() <= 0) {
        sender.sendMessage("§c数量必须是正整数!");
        return;
    }

    // 执行逻辑...
}

4.5 权限检查模式

声明式权限检查

创建注解让权限检查更优雅:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequirePermission {
    String value();
    String message() default "§c你没有权限执行此操作!";
}

带上下文的权限检查

public class PermissionHelper {

    /**
     * 检查权限并发送消息
     * @return 是否有权限
     */
    public static boolean check(CommandSender sender,
                                String permission,
                                String errorMessage) {
        if (!sender.hasPermission(permission)) {
            sender.sendMessage(errorMessage);
            return false;
        }
        return true;
    }

    /**
     * 检查是否为玩家
     */
    public static boolean checkIsPlayer(CommandSender sender) {
        if (!(sender instanceof Player)) {
            sender.sendMessage("§c此命令只能由玩家执行!");
            return false;
        }
        return true;
    }

    /**
     * 检查参数数量
     */
    public static boolean checkArgs(CommandSender sender,
                                    String[] args,
                                    int min,
                                    String usage) {
        if (args.length < min) {
            sender.sendMessage("§c用法: " + usage);
            return false;
        }
        return true;
    }
}

使用示例

@Override
public void execute(CommandSender sender, String[] args) {
    if (!PermissionHelper.checkIsPlayer(sender)) return;
    if (!PermissionHelper.check(sender, "myplugin.heal", "§c无权限!")) return;
    if (!PermissionHelper.checkArgs(sender, args, 0, "/heal [玩家]")) return;

    Player player = (Player) sender;
    // 执行逻辑...
}

4.6 使用 Adventure API 发送消息

Paper 内置了 Adventure API,支持更丰富的消息格式:

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;

public class AdventureCommand implements CommandExecutor {

    @Override
    public boolean onCommand(@NotNull CommandSender sender,
                             @NotNull Command command,
                             @NotNull String label,
                             @NotNull String[] args) {

        // 简单文本
        sender.sendMessage(Component.text("Hello, World!", NamedTextColor.GREEN));

        // 组合文本
        Component message = Component.text()
            .content("[")
            .color(NamedTextColor.GRAY)
            .append(Component.text("点击传送到大厅", NamedTextColor.GOLD))
            .append(Component.text("]", NamedTextColor.GRAY))
            .clickEvent(ClickEvent.runCommand("/warp lobby"))
            .hoverEvent(HoverEvent.showText(
                Component.text("点击传送到大厅", NamedTextColor.YELLOW)
            ))
            .build();

        sender.sendMessage(message);

        return true;
    }
}

颜色与格式速查

NamedTextColor 颜色 TextDecoration 格式
BLACK 黑色 BOLD 粗体
DARK_BLUE 深蓝 ITALIC 斜体
GREEN 绿色 UNDERLINED 下划线
RED 红色 STRIKETHROUGH 删除线
GOLD 金色 OBFUSCATED 混淆
YELLOW 黄色 NONE 无格式
WHITE 白色

4.7 Brigadier 命令系统

Paper 支持使用 Brigadier(Mojang 的命令框架)来注册命令,支持更复杂的参数解析和自动补全。

使用 Brigadier 注册命令

import com.mojang.brigadier.Command;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.tree.LiteralCommandNode;
import io.papermc.paper.command.brigadier.Commands;
import io.papermc.paper.command.brigadier.argument.ArgumentTypes;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import org.bukkit.entity.Player;

public class BrigadierExample {

    public static void register(MyPlugin plugin) {
        plugin.getLifecycleManager().registerEventHandler(
            LifecycleEvents.COMMANDS, event -> {
                var commands = event.registrar();

                LiteralCommandNode<io.papermc.paper.command.brigadier.CommandSourceStack>
                    root = Commands.literal("myplugin")
                    .then(Commands.literal("heal")
                        .requires(source -> source.getSender()
                            .hasPermission("myplugin.heal"))
                        .executes(ctx -> {
                            if (ctx.getSource().getSender() instanceof Player p) {
                                p.setHealth(p.getMaxHealth());
                                p.sendMessage(Component.text("已治愈!", NamedTextColor.GREEN));
                            }
                            return Command.SINGLE_SUCCESS;
                        })
                        .then(Commands.argument("target", ArgumentTypes.player())
                            .executes(ctx -> {
                                // 获取目标玩家
                                // 执行治疗
                                return Command.SINGLE_SUCCESS;
                            })
                        )
                    )
                    .then(Commands.literal("reload")
                        .requires(source -> source.getSender()
                            .hasPermission("myplugin.admin"))
                        .executes(ctx -> {
                            // 重载配置
                            ctx.getSource().getSender()
                                .sendMessage(Component.text("配置已重载!", NamedTextColor.GREEN));
                            return Command.SINGLE_SUCCESS;
                        })
                    )
                    .build();

                commands.register(root, "MyPlugin 主命令");
            }
        );
    }
}

Brigadier 的优势

特性 传统 CommandExecutor Brigadier
参数类型检查 手动 内置(Integer, String, Player 等)
Tab 补全 手动 TabCompleter 自动生成
嵌套参数 手动解析 声明式树结构
命令建议 自动显示用法
可读性 一般 优秀

4.8 业务场景:传送命令系统

完整的 /warp 命令

public class WarpCommand implements CommandExecutor, TabCompleter {

    private final Map<String, Location> warps = new HashMap<>();

    @Override
    public boolean onCommand(CommandSender sender, Command command,
                             String label, String[] args) {
        if (!PermissionHelper.checkIsPlayer(sender)) return true;
        Player player = (Player) sender;

        if (args.length == 0) {
            // 列出所有地标
            listWarps(player);
            return true;
        }

        String subCmd = args[0].toLowerCase();

        return switch (subCmd) {
            case "create" -> handleCreate(player, args);
            case "delete" -> handleDelete(player, args);
            case "tp"     -> handleTeleport(player, args);
            case "list"   -> { listWarps(player); yield true; }
            default       -> { handleTeleport(player, args); yield true; }
        };
    }

    private boolean handleCreate(Player player, String[] args) {
        if (!PermissionHelper.check(player, "myplugin.warp.create", "§c无权限!"))
            return true;
        if (args.length < 2) {
            player.sendMessage("§c用法: /warp create <名称>");
            return true;
        }

        String name = args[1].toLowerCase();
        if (warps.containsKey(name)) {
            player.sendMessage("§c地标 '" + name + "' 已存在!");
            return true;
        }

        warps.put(name, player.getLocation().clone());
        player.sendMessage("§a地标 '" + name + "' 已创建!");
        return true;
    }

    private boolean handleTeleport(Player player, String[] args) {
        String name = args.length > 0 ? args[0].toLowerCase() : "";
        Location loc = warps.get(name);

        if (loc == null) {
            player.sendMessage("§c地标 '" + name + "' 不存在!");
            return true;
        }

        player.teleport(loc);
        player.sendMessage("§a已传送到 '" + name + "'!");
        return true;
    }

    @Override
    public List<String> onTabComplete(CommandSender sender, Command command,
                                      String label, String[] args) {
        if (args.length == 1) {
            String partial = args[0].toLowerCase();
            List<String> completions = new ArrayList<>();

            if (sender.hasPermission("myplugin.warp.create")) completions.add("create");
            if (sender.hasPermission("myplugin.warp.delete")) completions.add("delete");
            completions.add("list");
            completions.addAll(warps.keySet());

            return completions.stream()
                .filter(s -> s.startsWith(partial))
                .collect(Collectors.toList());
        }

        if (args.length == 2) {
            return warps.keySet().stream()
                .filter(s -> s.startsWith(args[1].toLowerCase()))
                .collect(Collectors.toList());
        }

        return Collections.emptyList();
    }

    private void listWarps(Player player) {
        if (warps.isEmpty()) {
            player.sendMessage("§7暂无地标。");
            return;
        }
        player.sendMessage("§6===== 可用地标 =====");
        warps.forEach((name, loc) -> {
            Component msg = Component.text(" • " + name, NamedTextColor.YELLOW)
                .clickEvent(ClickEvent.runCommand("/warp " + name))
                .hoverEvent(HoverEvent.showText(Component.text("点击传送到 " + name)));
            player.sendMessage(msg);
        });
    }
}

4.9 常见问题排查

问题 原因 解决方案
命令无响应 plugin.yml 未声明 检查 commands 字段
Tab 补全不工作 未设置 TabCompleter 调用 setTabCompleter()
参数被截断 含空格的参数未加引号 使用 String.join() 或引号包裹
权限检查无效 权限名拼写错误 对照 plugin.yml 中的声明
中文命令参数乱码 编码问题 确保 UTF-8 编码

4.10 扩展阅读


4.11 本章小结

要点 内容
命令处理 实现 CommandExecutor 接口,在 onCommand 中处理逻辑
子命令模式 使用 Map 存储子命令,统一路由分发
Tab 补全 实现 TabCompleter,返回匹配的补全列表
权限检查 使用 hasPermission(),结合封装方法简化代码
Brigadier Paper 原生支持,提供类型安全和自动补全
Adventure API Paper 内置,支持富文本、点击事件、悬浮提示

下一章: 第 5 章:事件系统 — 深入理解 Bukkit 事件驱动机制、优先级和自定义事件。