双葉橘

返回

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/ModCommand.java
Command<ServerCommandSource> command = context -> {
    // 指令的具体实现逻辑
    // 当指令成功执行的时候,一般会返回 1 
    return 0;
};
java

注册指令#

在了解了 Command 接口之后,我们还需要了解注册指令的方式。在 Fabric 中,我们使用 ModInitializer#onInitialize 方法来执行注册指令的逻辑,我们可以将其写在模组主类中,但是我更建议你将其写在一个单独的类中。

command/ModCommand.java
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/ModCommand.java
Command<ServerCommandSource> command = context -> {
    // 指令的具体实现逻辑
    context.getSource().sendSuccess(() -> Component.literal("Called /testcommand"), false);
    return 1;
};
java

然后不要忘记在资源文件下的 fabric.mod.json 中配置模组的入口点。

fabric.mod.json
"entrypoints": {
    // ...
    "main": [
        "org.f14a.fabricdemo.Fabricdemo",
        "org.f14a.fabricdemo.command.ModCommand"
    ]
}, 
json

现在运行游戏,当 /testcommand 被调用时,command 将会向执行指令的玩家(或服务器控制台)发送一条消息。请注意,sendSuccess() 的第一个参数是一个 Supplier<Component>, 需要提供一个 Lambda 表达式,第二参数的含义是是否向所有 op 发送指令的执行结果,应该根据指令的具体情况来决定。

现在让我们把指令实现写的稍微复杂一点。

这是 Fabric 文档 中对参数 environment 的演示代码(我稍微改过了一下),includeDedicatedincludeIntegrated 分别在游戏运行在专用服务器(也就是单独启动的游戏服务端)和集成在客户端中的服务端时值为 true ,然后就没有别的用途了。

这里我们使用方法引用传入 executes()

在实际使用中,我们有时会设计一些只有 op 才能使用的指令,这类指令对游戏的影响通常比较大。我们可以使用 requires() 进行权限检测。

command/ModCommand.java
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;
            }));
        });
    }
}
java

requires() 接受一个 Predicate<S>,当且仅当 Predicate<S> 在某种情况下返回 true 时,指令才能被执行,并且会出现在自动补全的列表中。在这里我们使用 hasPermission() 来判断执行者是否有对应的权限。

当然,你可以为这个 Predicate<S> 设计独特的逻辑,比如小游戏中未加入任何队伍的玩家才能使用加入队伍的指令。

另外注意到我们这次为 executes() 传入的是一个 Lambda 表达式,具体是直接传入 Command<ServerCommandSource> 的变量,还是 Lambda 表达式,还是方法引用,应该根据你的自身喜好和具体实现来决定,比如如果指令实现逻辑复杂的话,使用 Lambda 表达式就不太合适了。

为指令添加子指令#

为指令添加子指令,我们只需使用 then()

如果觉得括号太多了,看起来很复杂,可以将代码复制到编辑器里面。

以下为简化版本。

command/ModCommand.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

之间的区别。

在设计比较复杂的指令结构时,最好经常换行缩进,或者写清楚注释。否则代码会难以阅读。另外,如果你觉得这种方式比较丑陋,后面我会补充一种比较美观(个人认为)的写法。

为指令添加参数#

在了解如何构造树状结构的指令后,我们还可以进一步让指令功能更完善,比如,就像原版的大部分指令一样,我们可以为指令添加参数。

我们不再使用 literal() 来创建节点,而是使用 argument() 来创建一个节点,它接受两个参数,第一个参数是参数的名称(如果没有自动补全,它会显示在文字框上面),第二个参数是参数的类型,一个 ArgumentType<T> 的对象。integer() 的唯一参数指定了参数的最小值, integer() 也有无参数和接受两个参数的重载版本,分别表示无范围限制和指定范围。

下面是一个更复杂的例子,它为这个指令添加了多个参数。

值得注意的是,我们不必为 execute() 传入只有一个参数的方法引用,上述代码中两次调用 executes() 的 Lambda 表达式代码重复了很多,我们可以这么写:

这样写的好处是,我们将指令的实现逻辑集中在一个方法中,避免了代码重复,提高了代码的可维护性。

自定义参数类型#

下面我将演示如何创建一个自定义的参数类型。

所有的参数类型都需要实现 ArgumentType<T> 接口,所以我们第一步一定是创建一个类并实现这个接口。

这个接口有四个方法,但是我们只需要实现 parse() 方法就可以了。parse() 方法接受一个 StringReader 对象作为参数,并返回一个类型为 T 的对象(这里的 TBlockPos)。我们需要从 StringReader 中读取输入的字符串,并将其解析为我们想要的类型。

我们尝试创建一个参数类型,它包含一个方块的位置,即三个整数。

在我们的 parse() 方法中,我们尝试从 StringReader 中读取三个整数,并将它们封装在一个 BlockPos 对象中返回。并且,我们尝试使用 try-catch 来保证健壮性。无论什么情况我们都需要用一个 try-catch 来处理可能出现的异常。

通过 ArgumentTypeRegistry#registerArgumentType 注册这个参数类型后就可以使用了,在这里我们选择在注册所有的指令之前注册参数类型。

构造我们自定义的参数类型 BlockPosArgumentType 的实例后传入 argument() 方法中,就可以使用这个参数类型了。

上述代码还使用了一个叫做 suggests() 的方法,这个方法接受一个 SuggestionProvider<S> 用于为参数实现自动补全,下一节我会介绍如何实现这个接口。

为参数实现自动补全#

光实现了参数类型还不够,我们还需要为参数实现自动补全功能。Brigadier 提供了 SuggestionProvider<S> 接口来实现这个功能。

command/BlockPosSuggestionProvider.java
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();
    }
}
java

S 一般是一个 CommandSourceStack 对象,我们从第一个参数中获得 context (他的类型CommandContext<CommandSourceStack>是不是有点眼熟?),以及一个 SuggestionsBuilder 对象。我们使用 SuggestionsBuilder#suggest() 来添加建议,这个方法可以多次调用,即为自动补全添加多条建议。最后调用 buildFuture() 方法来返回一个 CompletableFuture<Suggestions> 对象。

对于注册的指令,我们只需要在添加参数时调用 suggests() 方法,并传入我们实现的 SuggestionProvider 对象即可。

这是最常见的实现方式。但其实 ArgumentType<BlockPos> 也有提供自动补全的方法 listSuggestions(),我们重写其方法按理来说也能实现自动补全的功能,然而这个方法的签名是

com/mojang/brigadier/arguments/ArgumentType.java
default <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
    return Suggestions.empty();
}
java

可见它并没有指定 S 的类型,所以我们有时无法访问 CommandSourceStack 方法,可见这种方法会受到很多限制。

事实上,传入这个方法的 S 是一个客户端类,我们可以通过反射来访问。

这样做我们可以把自动补全的逻辑实现在自定义参数类型中,从而免去了调用 suggests() 方法。

在客户端注册指令#

需要指出的是,上述代码的所有类都是加载在服务端的,一般情况下注册指令都是由服务端完成的。但是 Fabric 也为客户端模组提供了在客户端注册指令的方式,通过在将指令实现在客户端上,可以避免当服务端没有安装模组时,客户端模组无法使用这个指令的问题。

与之前类似,为了实现在客户端注册指令,我们要在模组的客户端入口点中注册指令。

command/ClientModCommand.java
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 中注册客户端入口点。

fabric.mod.json
"entrypoints": {
    // ...
    "client": [
        "org.f14a.fabricdemo.client.FabricdemoClient",
        "org.f14a.fabricdemo.command.ClientModCommand"
    ]
}, 
json

更美观的注册指令写法#

当指令结构比较复杂时,使用 then() 方法嵌套的写法会显得非常臃肿,不易阅读。为了让代码更美观,我们可以先创建各个节点的变量,然后再使用 then() 方法构建树状结构,最后将根节点注册到指令调度器中。

在这里我将演示如何使用这种方式来注册一个比较复杂的指令结构。

如果你觉得这种写法更美观的话,可以尝试使用它来注册指令。一般情况下,如果你合理使用缩进,使用嵌套的写法也不会太混乱。

Minecraft Brigadier API详解————以Fabric为例
https://syju.org/blog/brigadier
作者 Futaba_Tachibana
发布时间 2025年12月7日
版权说明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨