Skip to content

自定义命令开发 ​

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 "";
}

参数说明 ​

参数类型必填说明
nameString✅主命令名(如 "test")
aliasesString[]❌命令别名(如 {"t", "tst"})
descriptionString❌命令描述
usageString❌用法说明

@CommandBody 注解 ​

@CommandBody 注解用于标记命令处理方法:

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CommandBody {
    @NotNull String name() default "";  // 子命令名(空 = 主命令本身)
    @NotNull String desc() default "";
}

参数说明 ​

参数类型必填说明
nameString❌子命令名(空字符串表示主命令本身)
descString❌子命令描述

方法签名 ​

所有命令处理方法必须接受以下参数并返回 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

解析规则(大小写不敏感):

  1. 插件名:命令 / bngeuit:命令 精确命中 → 使用该命令
  2. 否则回退到普通命令名(无冒号输入按普通命令匹配)
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 类会被自动扫描并注册到 插件名: 命名空间,无需手动操作。

注册流程 ​

  1. 框架启动时,PluginManager 扫描所有已加载插件的 JAR 文件
  2. 找到带有 @Command 注解的类
  3. 扫描类中带有 @CommandBody 注解的方法
  4. 将命令注册到命令管理器,命名空间为插件名(小写)

注意事项 ​

  • 命令类必须是公共的(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" → null

tryMatch() - 匹配多个指令 ​

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);
        }
    }
}