第 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 事件驱动机制、优先级和自定义事件。