作者声明:创作过程中使用了AI辅助
以下提及的项目全部是 Minecraft 模组或插件项目。
引言
把一套 Bukkit 命令系统搬到 Fabric 时,最劝退的不是 Brigadier 本身,而是要为了一个平台把所有命令逻辑全部重写一遍的痛苦。
由于之前有给一个其他项目写多平台的经历,因为那个项目代码少所以各平台的逻辑全部重复写了很多遍,造成更新时需要修改所有平台的代码,非常痛苦,因此这次给项目做多平台兼容没有使用这种方法,而是构建了 core 和 common 桥接接口。
但在编写 Fabric 平台逻辑的时候又遇到了 Brigadier 给每个命令建节点的问题,那不又回到了痛苦的重复工作吗?这事我当然不想干。
项目的 core 层早就定型了,权限校验、参数解析、补全逻辑全在那儿。如果按 Brigadier 原生写法给每个子命令建节点,等于把同样的业务用另一种范式再表达一次,以后改命令还得同步维护两套代码。这不是技术问题,是体力活。
因此我选择了让 Brigadier 当个收口,其他的事还是交给 core 管。
开发
用 greedyString 当万能容器
别为每个子命令建 LiteralArgumentBuilder。用一个 greedyString() 把用户输入的所有东西原样吞下来,再丢给 core 切分:
RequiredArgumentBuilder arg = RequiredArgumentBuilder
.argument("args", StringArgumentType.greedyString());
arg.executes(ctx -> {
String raw = StringArgumentType.getString(ctx, "args");
return commandExecutor.onCommand(ctx.getSource(), parseArgs(raw)) ? 1 : 0;
});
选 greedyString() 而不是普通 string(),是因为它遇到空格不会停。/<command> kick player reason with spaces 这种带空格的参数才不会被截断。
空参数兜底
上面那段代码有个盲区。只敲 /<command> 不带参数时,greedyString 节点不会被匹配,arg.executes 根本不会触发。
必须在根节点单独挂一个 executes:
literal.then(arg);
// 处理 /<command> 后面没跟参数的情况
literal.executes(ctx ->
commandExecutor.onCommand(ctx.getSource(), new String[0]) ? 1 : 0
);
防御性解析
既然要手动切字符串,就别信 split(" ")。在聊天框多敲两个空格是常事,直接 split 会产生空字符串元素,下游分分钟数组越界。
static String[] parseArgs(String input) {
if (input == null || input.trim().isEmpty()) return new String[0];
List<String> tokens = new ArrayList<>();
for (String s : input.trim().split("\\s+")) {
if (!s.isEmpty()) tokens.add(s);
}
return tokens.toArray(new String[0]);
}
\s+ 处理连续空白,trim() 干掉首尾。
命令补全
Brigadier 的 suggests 是异步的,但返回值本质就是字符串列表。我项目 core 层的 onTabComplete 返回的也是 List<String>,两者可以直接对接:
arg.suggests((ctx, builder) -> {
List<String> completions = commandExecutor.onTabComplete(
ctx.getSource(),
parseArgs(builder.getInput())
);
if (completions != null) completions.forEach(builder::suggest);
return builder.buildFuture();
});
代价是客户端没有语法高亮,因为服务端没下发完整语法树。但补全内容是对的,对于后台管理工具来说够用了。
总结
什么时候该这么干
说清楚边界比假装万能更重要。
适合的场景:有现成的跨平台 Core、从 Bukkit 迁移、命令体系已经稳定且数量较多。
不适合的场景:纯 Fabric 新项目、命令结构简单、需要客户端语法高亮和自动帮助生成。
Brigadier 原生 API 在设计上确实更优雅。但工程决策不是选最优雅的,是选最省事的。当你的约束条件决定了重写成本远高于兼容成本时,“不够优雅”就是当下最合理的方案。