Brigadier API#
Brigadier 是一个由 Mojang 为 Minecraft: Java Edition 开发的开源指令解析和调度工具,你可以在 GitHub ↗ 上找到这个项目的源代码。
Fabric, Forge, Paper 等模组加载器或服务器核心提供了对 Brigadier API 的支持,使开发者能够轻松地创建和管理自定义指令,包括注册新的指令、添加参数、实现自动补全等功能。
本篇文章将以 Fabric 为例,介绍如何使用 Brigadier API 来创建和管理 Minecraft 指令。
Commands#
Command 是一个函数式接口,它接受一个泛型参数 CommandContext<S> ,并返回一个表示指令执行结果的整数值,并且可能会抛出 CommandSyntaxException 异常。我们通常使用 Lambda 表达式或者方法引用作为 Command 的实现。
Command<ServerCommandSource> command = context -> {
// 指令的具体实现逻辑
// 当指令成功执行的时候,一般会返回 1
return 0;
};java注册指令#
在了解了 Command 接口之后,我们还需要了解注册指令的方式。在 Fabric 中,我们使用 ModInitializer#onInitialize 方法来执行注册指令的逻辑,我们可以将其写在模组主类中,但是我更建议你将其写在一个单独的类中。
public class ModCommand implements ModInitializer {
// ...
@Override
public void onInitialize() {
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
dispatcher.register(
Commands.literal("testcommand").executes(command)
);
// 可以在这里注册其它的指令
});
}
}java这里 executes() 传入的参数 command 是我们上一节创建的 Command 对象哦。通常,我们直接传入一个 Lambda 表达式或者一个方法引用。
我们重新实现一下 command。
Command<ServerCommandSource> command = context -> {
// 指令的具体实现逻辑
context.getSource().sendSuccess(() -> Component.literal("Called /testcommand"), false);
return 1;
};java然后不要忘记在资源文件下的 fabric.mod.json 中配置模组的入口点。
"entrypoints": {
// ...
"main": [
"org.f14a.fabricdemo.Fabricdemo",
"org.f14a.fabricdemo.command.ModCommand"
]
}, json现在运行游戏,当 /testcommand 被调用时,command 将会向执行指令的玩家(或服务器控制台)发送一条消息。请注意,sendSuccess() 的第一个参数是一个 Supplier<Component>, 需要提供一个 Lambda 表达式,第二参数的含义是是否向所有 op 发送指令的执行结果,应该根据指令的具体情况来决定。
现在让我们把指令实现写的稍微复杂一点。
public class ModCommand implements ModInitializer {
private static int executeDedicatedServer(CommandContext<CommandSourceStack> context) {
context.getSource().sendSuccess(() -> Component.literal("You are running a dedicated server. "), false);
return 1;
}
private static int executeClientServer(CommandContext<CommandSourceStack> context) {
context.getSource().sendSuccess(() -> Component.literal("You are running a client integrated server. "), false);
return 1;
}
// ...
@Override
public void onInitialize() {
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
dispatcher.register(
// Differentiate between dedicated server and client integrated server.
if(environment.includeDedicated) {
dispatcher.register(Commands.literal("whoami").executes(ModCommand::executeDedicatedServer));
}
else {
dispatcher.register(Commands.literal("whoami").executes(ModCommand::executeClientServer));
}
)
});
}
}java这是 Fabric 文档 ↗ 中对参数 environment 的演示代码(我稍微改过了一下),includeDedicated 和 includeIntegrated 分别在游戏运行在专用服务器(也就是单独启动的游戏服务端)和集成在客户端中的服务端时值为 true ,然后就没有别的用途了。
这里我们使用方法引用传入 executes()。
在实际使用中,我们有时会设计一些只有 op 才能使用的指令,这类指令对游戏的影响通常比较大。我们可以使用 requires() 进行权限检测。
public class ModCommand implements ModInitializer {
// ...
@Override
public void onInitialize() {
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
// Display the words if the user has specific permission.
dispatcher.register(Commands.literal("amianop").requires(source -> source.hasPermission(1)).executes(context -> {
context.getSource().sendSuccess(() -> Component.literal("You are an OP"), false);
return 1;
}));
});
}
}javarequires() 接受一个 Predicate<S>,当且仅当 Predicate<S> 在某种情况下返回 true 时,指令才能被执行,并且会出现在自动补全的列表中。在这里我们使用 hasPermission() 来判断执行者是否有对应的权限。
当然,你可以为这个 Predicate<S> 设计独特的逻辑,比如小游戏中未加入任何队伍的玩家才能使用加入队伍的指令。
另外注意到我们这次为 executes() 传入的是一个 Lambda 表达式,具体是直接传入 Command<ServerCommandSource> 的变量,还是 Lambda 表达式,还是方法引用,应该根据你的自身喜好和具体实现来决定,比如如果指令实现逻辑复杂的话,使用 Lambda 表达式就不太合适了。
为指令添加子指令#
为指令添加子指令,我们只需使用 then()。
public class ModCommand implements ModInitializer {
// ...
@Override
public void onInitialize() {
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
// All time say the words.
dispatcher.register(Commands.literal("testcommand").executes(context -> {
// sendSuccess(Supplier<Component> message, boolean broadcastToOps)
context.getSource().sendSuccess(() -> Component.literal("Called /testcommand"), false);
// Throw an exception when something goes wrong.
//throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().create();
// Return 1 when everything is ok.
return 1;
}).then(Commands.literal("subcommand").executes(context -> {
context.getSource().sendSuccess(() -> Component.literal("Called /testcommand subcommand"), false);
return 1;
}))
);
});
}
}java如果觉得括号太多了,看起来很复杂,可以将代码复制到编辑器里面。
以下为简化版本。
public class ModCommand implements ModInitializer {
// ...
@Override
public void onInitialize() {
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
// All time say the words.
dispatcher.register(
Commands.literal("testcommand").executes(/*something*/)
.then(
Commands.literal("subcommand").executes(/*something*/)
)
);
});
}
}java发现了吗,then() 的参数实际上是一个新的 Commands.literal() ,而这个新的对象,同样可以使用 then() 继续套娃,得到一个树状的指令结构。
读者在此一定要分清楚, then() 是哪个对象调用的,就意味着传入的参数表示的指令是这个对象的子节点。即,区分
A.then(B).then(C).then(D); // A -> B
// -> C
// -> D
A.then(B.then(C).then(D)); // A -> B -> C
// -> D
A.then(B).then(C.then(D)); // A -> B
// -> C -> D
A.then(B.then(C.then(D))); // A -> B -> C -> D 链式结构java之间的区别。
在设计比较复杂的指令结构时,最好经常换行缩进,或者写清楚注释。否则代码会难以阅读。另外,如果你觉得这种方式比较丑陋,后面我会补充一种比较美观(个人认为)的写法。
为指令添加参数#
在了解如何构造树状结构的指令后,我们还可以进一步让指令功能更完善,比如,就像原版的大部分指令一样,我们可以为指令添加参数。
public class ModCommand implements ModInitializer {
// ...
@Override
public void onInitialize() {
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
// Command with arguments
dispatcher.register(Commands.literal("command_with_args")
.then(Commands.argument("value", IntegerArgumentType.integer(0)).executes(context -> {
int value = IntegerArgumentType.getInteger(context, "value");
context.getSource().sendSuccess(() -> Component.literal("You call the command with argument: %d".formatted(value)), false);
return 1;
}))
);
});
}
}java我们不再使用 literal() 来创建节点,而是使用 argument() 来创建一个节点,它接受两个参数,第一个参数是参数的名称(如果没有自动补全,它会显示在文字框上面),第二个参数是参数的类型,一个 ArgumentType<T> 的对象。integer() 的唯一参数指定了参数的最小值, integer() 也有无参数和接受两个参数的重载版本,分别表示无范围限制和指定范围。
下面是一个更复杂的例子,它为这个指令添加了多个参数。
public class ModCommand implements ModInitializer {
// ...
@Override
public void onInitialize() {
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
// Command with arguments
dispatcher.register(Commands.literal("command_with_args")
.then(Commands.argument("value", IntegerArgumentType.integer(0)).executes(context -> {
int value = IntegerArgumentType.getInteger(context, "value");
context.getSource().sendSuccess(() -> Component.literal("You call the command with argument: %d".formatted(value)), false);
return 1;
}).then(Commands.argument("player", StringArgumentType.string()).executes(context -> {
int value = IntegerArgumentType.getInteger(context, "value");
String player = StringArgumentType.getString(context, "player");
context.getSource().sendSuccess(() -> Component.literal("%s call the command with argument: %d".formatted(player, value)), false);
return 1;
})))
);
});
}
}java值得注意的是,我们不必为 execute() 传入只有一个参数的方法引用,上述代码中两次调用 executes() 的 Lambda 表达式代码重复了很多,我们可以这么写:
public class ModCommand implements ModInitializer {
// Actually it cannot be used to Commands#executes because of incompatible method signature.
private static int executeWithParameters(CommandContext<CommandSourceStack> context, String player, int value) {
context.getSource().sendSuccess(() -> Component.literal("%s call the command with argument: %d".formatted(player, value)), false);
return 1;
}
// ...
@Override
public void onInitialize() {
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
// Equals to
dispatcher.register(Commands.literal("command_with_args_")
.then(Commands.argument("value", IntegerArgumentType.integer(0)).executes(context ->
ModCommand.executeWithParameters(context,
context.getSource().getDisplayName().getString(),
IntegerArgumentType.getInteger(context, "value"))
).then(Commands.argument("player", StringArgumentType.string()).executes(context ->
ModCommand.executeWithParameters(context,
StringArgumentType.getString(context, "player"),
IntegerArgumentType.getInteger(context, "value"))
)))
);
});
}
}java这样写的好处是,我们将指令的实现逻辑集中在一个方法中,避免了代码重复,提高了代码的可维护性。
自定义参数类型#
下面我将演示如何创建一个自定义的参数类型。
所有的参数类型都需要实现 ArgumentType<T> 接口,所以我们第一步一定是创建一个类并实现这个接口。
这个接口有四个方法,但是我们只需要实现 parse() 方法就可以了。parse() 方法接受一个 StringReader 对象作为参数,并返回一个类型为 T 的对象(这里的 T 是 BlockPos)。我们需要从 StringReader 中读取输入的字符串,并将其解析为我们想要的类型。
我们尝试创建一个参数类型,它包含一个方块的位置,即三个整数。
public class BlockPosArgumentType implements ArgumentType<BlockPos> {
@Override
public BlockPos parse(StringReader stringReader) throws CommandSyntaxException {
try {
int x = stringReader.readInt();
stringReader.expect(' ');
int y = stringReader.readInt();
stringReader.expect(' ');
int z = stringReader.readInt();
return new BlockPos(x, y, z);
}
catch (Exception e) {
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherParseException().create("Failed to parse BlockPos argument %s.");
}
}
}java在我们的 parse() 方法中,我们尝试从 StringReader 中读取三个整数,并将它们封装在一个 BlockPos 对象中返回。并且,我们尝试使用 try-catch 来保证健壮性。无论什么情况我们都需要用一个 try-catch 来处理可能出现的异常。
通过 ArgumentTypeRegistry#registerArgumentType 注册这个参数类型后就可以使用了,在这里我们选择在注册所有的指令之前注册参数类型。
public class ModCommand implements ModInitializer {
private static int executeBlockPosArgument(CommandContext<CommandSourceStack> context) {
BlockPos blockPos = context.getArgument("blockpos", BlockPos.class);
String blockName = context.getSource().getLevel().getBlockState(blockPos).getBlock().getName().getString();
context.getSource().sendSuccess(() -> Component.literal("You provided %s at %s".formatted(blockName, blockPos.toShortString())), false);
return 1;
}
// ...
@Override
public void onInitialize() {
// Register custom argument type
ArgumentTypeRegistry.registerArgumentType(
ResourceLocation.fromNamespaceAndPath(Fabricdemo.MOD_ID, "block_pos"),
BlockPosArgumentType.class,
SingletonArgumentInfo.contextFree(BlockPosArgumentType::new)
);
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
// Get a BlockPos argument
dispatcher.register(Commands.literal("whatisthis")
.then(Commands.argument("blockpos", new BlockPosArgumentType())
// suggest()方法用于提供自动补全,下一节会详细介绍
.suggests(new BlockPosSuggestionProvider())
.executes(ModCommand::executeBlockPosArgument))
);
});
}
}java构造我们自定义的参数类型 BlockPosArgumentType 的实例后传入 argument() 方法中,就可以使用这个参数类型了。
上述代码还使用了一个叫做 suggests() 的方法,这个方法接受一个 SuggestionProvider<S> 用于为参数实现自动补全,下一节我会介绍如何实现这个接口。
为参数实现自动补全#
光实现了参数类型还不够,我们还需要为参数实现自动补全功能。Brigadier 提供了 SuggestionProvider<S> 接口来实现这个功能。
public class BlockPosSuggestionProvider implements SuggestionProvider<CommandSourceStack> {
@Override
public CompletableFuture<Suggestions> getSuggestions(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
if (source.getPlayer() != null){
builder.suggest("%d %d %d".formatted(
source.getPlayer().blockPosition().getX(),
source.getPlayer().blockPosition().getY(),
source.getPlayer().blockPosition().getZ()
));
}
return builder.buildFuture();
}
}javaS 一般是一个 CommandSourceStack 对象,我们从第一个参数中获得 context (他的类型CommandContext<CommandSourceStack>是不是有点眼熟?),以及一个 SuggestionsBuilder 对象。我们使用 SuggestionsBuilder#suggest() 来添加建议,这个方法可以多次调用,即为自动补全添加多条建议。最后调用 buildFuture() 方法来返回一个 CompletableFuture<Suggestions> 对象。
对于注册的指令,我们只需要在添加参数时调用 suggests() 方法,并传入我们实现的 SuggestionProvider 对象即可。
这是最常见的实现方式。但其实 ArgumentType<BlockPos> 也有提供自动补全的方法 listSuggestions(),我们重写其方法按理来说也能实现自动补全的功能,然而这个方法的签名是
default <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
return Suggestions.empty();
}java可见它并没有指定 S 的类型,所以我们有时无法访问 CommandSourceStack 方法,可见这种方法会受到很多限制。
事实上,传入这个方法的 S 是一个客户端类,我们可以通过反射来访问。
@Override
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
if (context.getSource() instanceof CommandSourceStack source){
if (source.getPlayer() != null){
builder.suggest("%d %d %d".formatted(
source.getPlayer().blockPosition().getX(),
source.getPlayer().blockPosition().getY(),
source.getPlayer().blockPosition().getZ()
));
}
}
else {
try {
Class<?> cls = Class.forName("net.minecraft.client.multiplayer.ClientSuggestionProvider");
if (cls.isInstance(context.getSource())) {
Object playerObj = cls.getMethod("getPlayer").invoke(context.getSource());
if (playerObj instanceof Player player) {
builder.suggest("%d %d %d".formatted(
player.blockPosition().getX(),
player.blockPosition().getY(),
player.blockPosition().getZ()
));
}
}
} catch (ClassNotFoundException | InvocationTargetException | IllegalAccessException |
NoSuchMethodException ignored) {
;
}
}
return builder.buildFuture();
}java这样做我们可以把自动补全的逻辑实现在自定义参数类型中,从而免去了调用 suggests() 方法。
在客户端注册指令#
需要指出的是,上述代码的所有类都是加载在服务端的,一般情况下注册指令都是由服务端完成的。但是 Fabric 也为客户端模组提供了在客户端注册指令的方式,通过在将指令实现在客户端上,可以避免当服务端没有安装模组时,客户端模组无法使用这个指令的问题。
与之前类似,为了实现在客户端注册指令,我们要在模组的客户端入口点中注册指令。
public class ClientModCommand implements ClientModInitializer {
// ...
@Override
public void onInitialize() {
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> {
dispatcher.register(
ClientCommandManager.literal("clienttestcommand").executes( context -> {
context.getSource().sendFeedback(Component.literal("Called /clienttestcommand"));
return 1;
})
);
});
}
}java需要注意的是,指令注册的方式与之前有相对细微的区别,请仔细阅读代码,以区别先前的实现。
我们调用的事件是 ClientCommandRegistrationCallback.EVENT,并且传入的 Lambda 表达式的参数中没有 environment,因为客户端模组只能运行在集成服务器环境中。此外,我们使用 ClientCommandManager 来创建指令节点。最后,向执行者发送消息时,我们使用 sendFeedback() 方法,它接受一个 Component 对象而不是先前的 Supplier<Component>,而且也不会广播给其它 op。
不要忘记在 fabric.mod.json 中注册客户端入口点。
"entrypoints": {
// ...
"client": [
"org.f14a.fabricdemo.client.FabricdemoClient",
"org.f14a.fabricdemo.command.ClientModCommand"
]
}, json更美观的注册指令写法#
当指令结构比较复杂时,使用 then() 方法嵌套的写法会显得非常臃肿,不易阅读。为了让代码更美观,我们可以先创建各个节点的变量,然后再使用 then() 方法构建树状结构,最后将根节点注册到指令调度器中。
在这里我将演示如何使用这种方式来注册一个比较复杂的指令结构。
public class ModCommand implements ModInitializer {
private static int executePlain(CommandContext<CommandSourceStack> context) {
context.getSource().sendSuccess(() -> Component.literal("Executed command. "), false);
return 1;
}
@Override
public void onInitialize() {
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
// Easy-to-read ver
LiteralArgumentBuilder<CommandSourceStack> atree = Commands.literal("atree");
LiteralArgumentBuilder<CommandSourceStack> atree_sub1 = Commands.literal("sub1");
LiteralArgumentBuilder<CommandSourceStack> atree_sub2 = Commands.literal("sub2");
LiteralArgumentBuilder<CommandSourceStack> atree_sub1_leaf1 = Commands.literal("leaf1");
LiteralArgumentBuilder<CommandSourceStack> atree_sub1_leaf2 = Commands.literal("leaf2");
LiteralArgumentBuilder<CommandSourceStack> atree_sub2_leaf1 = Commands.literal("leaf1");
LiteralArgumentBuilder<CommandSourceStack> atree_sub2_node = Commands.literal("node");
LiteralArgumentBuilder<CommandSourceStack> atree_sub2_node_leaf1 = Commands.literal("leaf1");
LiteralArgumentBuilder<CommandSourceStack> atree_sub2_node_leaf2 = Commands.literal("leaf2");
List<LiteralArgumentBuilder<CommandSourceStack>> commands = List.of(
atree,
atree_sub1,
atree_sub2,
atree_sub1_leaf1,
atree_sub1_leaf2,
atree_sub2_leaf1,
atree_sub2_node,
atree_sub2_node_leaf1,
atree_sub2_node_leaf2
);
// Realize commands.
commands.forEach(cmd -> cmd.executes(ModCommand::executePlain));
// Build the tree structure.
atree.then(
atree_sub1.then(
atree_sub1_leaf1
).then(
atree_sub1_leaf2
)
).then(
atree_sub2.then(
atree_sub1_leaf1
).then(
atree_sub2_node.then(
atree_sub2_node_leaf1
).then(
atree_sub2_node_leaf2
)
)
);
// Finally register the root command.
dispatcher.register(atree);
});
}
}java如果你觉得这种写法更美观的话,可以尝试使用它来注册指令。一般情况下,如果你合理使用缩进,使用嵌套的写法也不会太混乱。