强曰为道

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

第 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 的优势

特性传统 CommandExecutorBrigadier
参数类型检查手动内置(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(),结合封装方法简化代码
BrigadierPaper 原生支持,提供类型安全和自动补全
Adventure APIPaper 内置,支持富文本、点击事件、悬浮提示

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