Appearance
自定义命令开发
Bngeuit 采用注解式命令系统,支持主子命令结构、别名和命名空间。开发者可以通过简单的注解来注册自定义命令。
@Command 注解
@Command 注解用于标记命令类,定义命令的基本信息:
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Command {
@NotNull String name(); // 主命令名
@NotNull String[] aliases() default {}; // 别名
@NotNull String description() default "";
@NotNull String usage() default "";
}参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | String | ✅ | 主命令名(如 "test") |
aliases | String[] | ❌ | 命令别名(如 {"t", "tst"}) |
description | String | ❌ | 命令描述 |
usage | String | ❌ | 用法说明 |
@CommandBody 注解
@CommandBody 注解用于标记命令处理方法:
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CommandBody {
@NotNull String name() default ""; // 子命令名(空 = 主命令本身)
@NotNull String desc() default "";
}参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | String | ❌ | 子命令名(空字符串表示主命令本身) |
desc | String | ❌ | 子命令描述 |
方法签名
所有命令处理方法必须接受以下参数并返回 void(方法名不限):
java
void methodName(CommandSender sender, String[] args)sender- 命令发送者(用于回复消息)args- 命令参数数组
命令格式
<主命令> [子命令] [参数...]@CommandBody的name = ""→ 输入主命令时触发@CommandBody的name = "xxx"→ 输入主命令 xxx时触发
CommandSender
所有命令方法都接收 CommandSender 作为第一参数,用于回复命令来源:
| 方法 | 返回类型 | 说明 |
|---|---|---|
getSenderId() | String | 发送者标识(群 OpenID / 用户 OpenID / CONSOLE) |
sendMessage(String) | void | 向命令来源发送回复 |
实现类
| 实现 | 场景 | 说明 |
|---|---|---|
GroupCommandSender | 群聊 | getSenderId() 即群 OpenID,sendMessage() 回复到群 |
C2CCommandSender | 私聊 | getSenderId() 即用户 OpenID,sendMessage() 回复到私聊 |
ConsoleSender | 控制台 | 回复输出到 System.out,全局单例,经 Bngeuit.getConsoleSender() 获取 |
Tab 补全
@TabComplete 注解用于标记命令参数的 Tab 补全方法:
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TabComplete {
@NotNull String name() default ""; // 子命令名(空 = 主命令补全)
}方法签名
补全方法必须返回 List<String>:
java
@TabComplete(name = "subcommand")
public List<String> completeSubcommand(CommandSender sender, String[] args) {
// args 是当前已输入的参数(不包含命令名)
return List.of("option1", "option2", "option3");
}使用效果
command <Tab> → 主命令补全
command sub <Tab> → 子命令补全
command sub arg1 <Tab> → 子命令参数补全命令命名空间
每个命令归属一个插件,支持 插件名:命令 精确指定,用于解决同名命令冲突:
- 插件命令 → 命名空间 = 插件名(小写),例如
myplugin:test - 内置命令(
plugin == null)→ 保留命名空间bngeuit,例如bngeuit:stop
解析规则(大小写不敏感):
插件名:命令/bngeuit:命令精确命中 → 使用该命令- 否则回退到普通命令名(无冒号输入按普通命令匹配)
java
myplugin:test info # 精确指定某插件的 test 命令
bngeuit:stop # 内置命令
test info # 无冒号,按普通命令解析手动派发命令
框架外部可通过 Bngeuit.dispatchCommand(sender, commandLine) 以字符串形式执行任意命令(在调度器 sync 线程执行,与事件处理串行):
java
Bngeuit.dispatchCommand(Bngeuit.getConsoleSender(), "stop");
Bngeuit.dispatchCommand(Bngeuit.getConsoleSender(), "plugin reload myplugin");自动注册
插件 JAR 中的 @Command 类会被自动扫描并注册到 插件名: 命名空间,无需手动操作。
注册流程
- 框架启动时,
PluginManager扫描所有已加载插件的 JAR 文件 - 找到带有
@Command注解的类 - 扫描类中带有
@CommandBody注解的方法 - 将命令注册到命令管理器,命名空间为插件名(小写)
注意事项
- 命令类必须是公共的(
public) - 命令处理方法必须是公共的(
public) - 方法签名必须接受
(CommandSender sender, String[] args)并返回void - 避免命令名冲突,建议使用插件特有的前缀
函数式注册
除了注解式注册,还可以通过 CommandManager.registerFunctional() 编程式注册命令,适合脚本插件或动态注册场景:
java
Object token = Bngeuit.getCommandManager().registerFunctional(
"mycommand", // 命令名
List.of("mc"), // 别名
"我的命令", // 描述
(sender, args) -> { // 默认处理器
sender.sendMessage("Hello!");
},
Map.of( // 子命令处理器
"sub1", (sender, args) -> sender.sendMessage("子命令1"),
"sub2", (sender, args) -> sender.sendMessage("子命令2")
),
Map.of(), // 子命令补全(CommandHandler 形式,一般留空)
getPlugin() // 归属插件(null = 内置命令)
);
// Tab 补全需要单独注册:补全处理器必须返回候选列表
Bngeuit.getCommandManager().setFunctionalTabCompleters(token, Map.of(
"", (sender, args) -> List.of("sub1", "sub2"),
"sub1", (sender, args) -> List.of("opt1", "opt2")
));警告
registerFunctional 的第 6 个参数类型是 Map<String, CommandHandler>,而 CommandHandler.invoke 返回 void,写在该参数里的 lambda 虽然能编译,但返回的候选列表会被丢弃,Tab 补全不会生效。需要补全时,用返回的 token 调用 setFunctionalTabCompleters(token, Map<String, TabCompleteHandler>)。
最佳实践
1. 命令命名规范
java
// ✅ 好的命名
@Command(name = "myplugin", aliases = {"mp"}, description = "我的插件命令")
// ❌ 避免通用名称,容易冲突
@Command(name = "test", aliases = {"t"})2. 子命令组织
java
@Command(name = "myplugin", description = "我的插件")
public class MyPluginCommand {
@CommandBody // myplugin
public void help(CommandSender sender, String[] args) {
sender.sendMessage("使用方法: myplugin <子命令>");
}
@CommandBody(name = "reload", desc = "重载配置")
public void reload(CommandSender sender, String[] args) {
// 重载逻辑
sender.sendMessage("配置已重载");
}
@CommandBody(name = "info", desc = "查看信息")
public void info(CommandSender sender, String[] args) {
sender.sendMessage("MyPlugin v1.0.0");
}
// ── Tab 补全 ──
@TabComplete // myplugin <Tab>
public List<String> mainTab(CommandSender sender, String[] args) {
return List.of("reload", "info");
}
}3. 权限控制
java
@CommandBody(name = "admin", desc = "管理员命令")
public void admin(CommandSender sender, String[] args) {
// 检查是否为控制台
if (!(sender instanceof ConsoleSender)) {
sender.sendMessage("此命令只能在控制台使用");
return;
}
// 管理员逻辑
sender.sendMessage("管理员操作已执行");
}4. 参数验证
java
@CommandBody(name = "set", desc = "设置配置")
public void set(CommandSender sender, String[] args) {
if (args.length < 2) {
sender.sendMessage("用法: myplugin set <key> <value>");
return;
}
String key = args[0];
String value = args[1];
// 设置配置
sender.sendMessage("已设置 " + key + " = " + value);
}CommandUtil 工具类
cn.org.bukkit.bngeuit.util.CommandUtil 提供了便捷的指令匹配与参数提取工具,特别适合在事件监听器中处理非注解式命令。
常用方法
match() - 检查指令开头
java
import cn.org.bukkit.bngeuit.util.CommandUtil;
// 检查消息是否以指定指令开头
if (CommandUtil.match(content, "/help", "/帮助")) {
// 处理帮助命令
}
// match() 会自动忽略前导空白
CommandUtil.match(" /help me", "/help"); // true
CommandUtil.match("/helper", "/help"); // false(后面不是空白)extract() - 提取参数
java
// 提取指令后面的参数(指令不存在时返回 null)
String args = CommandUtil.extract(content, "/echo");
// "/echo hello world" → "hello world"
// "/echo" → ""
// "/other" → nulltryMatch() - 匹配多个指令
java
// 匹配多个指令并返回结果
CommandUtil.Result r = CommandUtil.tryMatch(content, "/help", "/echo", "/info");
if (r != null) {
switch (r.command()) {
case "/help" -> sendHelp();
case "/echo" -> sendEcho(r.args());
case "/info" -> sendInfo();
}
}contains() - 检查包含指令
java
// 检查消息中是否存在完整词匹配的指令(前后必须是空白或边界)
if (CommandUtil.contains(content, "帮助", "help")) {
// 消息中包含"帮助"或"help"(作为独立词)
}
// "我需要help吗" → false(help 前后没有空格)
// "我需要 help 吗" → true其他实用方法
java
// 按空白分割参数
String[] args = CommandUtil.splitArgs("arg1 arg2 arg3");
// 获取第一个参数
String first = CommandUtil.firstArg("arg1 arg2 arg3"); // "arg1"
// 获取第一个参数之后的剩余部分
String rest = CommandUtil.restArgs("arg1 arg2 arg3"); // "arg2 arg3"
// 检查参数是否非空
boolean has = CommandUtil.hasArgs(" "); // false使用场景
在事件监听器中处理命令
java
@SubscribeEvent
public void onGroupAt(GroupAtMessageEvent e) {
String content = e.getContent();
if (CommandUtil.match(content, "/help")) {
Bngeuit.replyGroupText(e.getGroupOpenid(), e.getId(), 1,
"可用命令: /help, /echo, /info");
return;
}
String echoText = CommandUtil.extract(content, "/echo");
if (echoText != null) {
Bngeuit.replyGroupText(e.getGroupOpenid(), e.getId(), 1, echoText);
return;
}
}多指令匹配
java
@SubscribeEvent
public void onC2CMessage(C2CMessageEvent e) {
String content = e.getContent();
CommandUtil.Result r = CommandUtil.tryMatch(content,
"/天气", "/weather", "/tq");
if (r != null) {
String city = r.args();
if (city.isEmpty()) {
Bngeuit.replyC2CText(e.getAuthor().userOpenid(), e.getId(), 1,
"请指定城市,例如: /天气 北京");
} else {
String weather = getWeather(city);
Bngeuit.replyC2CText(e.getAuthor().userOpenid(), e.getId(), 1,
city + "的天气: " + weather);
}
}
}