Table of Contents

命令参数类型

Brigadier 支持自定义的参数类型,本页会展示如何创建一个简单的参数类型。

注意:自定义的参数类型要求客户端也正确注册,如果是服务器的插件,请考虑使用已存在的参数类型和一个自定义的建议提供器。

解析

本例中,我们将会创建一个 UuidArgumentType

首先创建一个类并继承 ArgumentType。注意 ArgumentType 是泛型,因此泛型会定义这个 ArgumentType 会返回什么类型。

public class UuidArgumentType implements ArgumentType<UUID> {

参数类型需要实现 parse 方法,该方法返回的类型要符合泛型的类型。

    @Override
    public UUID parse(StringReader reader) throws CommandSyntaxException {

所以的解析都发生在此方法中。此方法会根据命令行中提供的参数返回一个对象,或者抛出一个 CommandSyntaxException 即解析失败。

接下来你会存储当前指针(cursor)的位置,这样你可以截取子字符串,截出特定的参数。指针总是出现在参数在命令行中出现的开始的位置。

  1. int argBeginning = reader.getCursor(); // 指针开始的位置是参数开始的位置。
  2. if (!reader.canRead()) {
  3. reader.skip();
  4. }

现在抓取整个参数。你可能会有不同的标准,或像一些参数一样,检测到命令行中有 { 时会要求其能够闭合,具体取决于你的参数类型。

  1. while (reader.canRead() && reader.peek() != ' ') { // “peek”提供了当前指针位置的字符。
  2. reader.skip(); // 告诉 StringReader,将其指针移动到下一个位置。
  3. }

接下来我们会从这个 StringReader 中,获取命令行中的指针位置的子字符串。

  1. String uuidString = reader.getString().substring(argBeginning, reader.getCursor());

最终,我们检查我们的参数是否正确,并解析我们的参数,如果解析失败则抛出异常。

  1. public static final DynamicCommandExceptionType INVALID_UUID = new DynamicCommandExceptionType(o -> Text.literal("无效的uuid:" + o));
  2.  
  3. @Override
  4. public UUID parse(StringReader reader) throws CommandSyntaxException {
  5. // ...
  6. try {
  7. UUID uuid = UUID.fromString(uuidString); // 我们的正常逻辑。
  8. return uuid; // 我们返回我们的类型,在这个例子中,解析器会认为类型已经正确解析,并继续。
  9. } catch (InvalidArgumentException ex) {
  10. // UUID 在由字符串生成时,可能会抛出异常,因此我们捕获这个异常,并将其包装成 CommandSyntaxException 类型。
  11. // 创建时会带有环境,告诉 Brigadier,根据这个环境来告诉玩家,命令在哪里解析失败了。
  12. // 尽管也可以使用正常的 create 方法。
  13. reader.setCursor(argBeginning);
  14. throw INVALID_UUID.createWithContext(reader, ex.getMessage());
  15. }
  16. // ...
  17. }

定义参数示例

有时候参数需要有一些示例,通常存储在作为静态常量字段的不可变集合中。示例是用于检测二义性的。

    private static final Collection<String> EXAMPLES = List.of(
        "765e5d33-c991-454f-8775-b6a7a394c097", // i509VCB: Username The_1_gamers
        "069a79f4-44e9-4726-a5be-fca90e38aaf5", // Notch
        "61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"  // Dinnerbone
    );
 
    @Override
    public Collection<String> getExamples() {
        // Brigadier 支持显示示例,表示这个命令应该像是什么样子,这应该包含一个集合,
        // 集合的内容是此类型能够返回的参数。
        // 这主要用于检测二义性,即一种参数可能会被作为另一种参数解析。
        return EXAMPLES;
    }

注册参数类型

ArgumentType 完成了,你的客户端却会拒绝解析参数并抛出错误。这是因为服务器会告诉客户端这是什么参数类型。而客户端不会解析它不知道的参数类型。要解决这个问题,我们需要在 ModInitializer 中注册一个 ArgumentSerializer。对于更加复杂的参数类型,你可能还需要创建自己的 ArgumentSerializer

  1. ArgumentTypeRegistry.registerArgumentType(
  2. new Identifier("tutorial", "uuid"),
  3. UuidArgumentType.class, ConstantArgumentSerializer.of(UuidArgumentType::uuid));
  4. // 参数会创建 ArgumentType。

完整参数类型示例

UuidArgumentType.java
  1. import com.mojang.brigadier.StringReader;
  2. import com.mojang.brigadier.arguments.ArgumentType;
  3. import com.mojang.brigadier.context.CommandContext;
  4. import com.mojang.brigadier.exceptions.CommandSyntaxException;
  5. import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
  6. import net.minecraft.text.Text;
  7.  
  8. import java.util.ArrayList;
  9. import java.util.Collection;
  10. import java.util.UUID;
  11.  
  12. /**
  13.  * 代表一个返回 UUID 的参数类型。
  14.  */
  15. public class UuidArgumentType implements ArgumentType<UUID> {
  16. public static UuidArgumentType uuid() {
  17. return new UuidArgumentType();
  18. }
  19.  
  20. public static <S> UUID getUuid(String name, CommandContext<S> context) {
  21. // 注意你应该假设 CommandContext 中包含的 CommandSource 是一个泛型类型。
  22. // 如果你需要访问 ServerCommandSource,确保你在强转之前验证了命令源。
  23. return context.getArgument(name, UUID.class);
  24. }
  25.  
  26. private static final Collection<String> EXAMPLES = List.of(
  27. "765e5d33-c991-454f-8775-b6a7a394c097", // i509VCB: 用户名 The_1_gamers
  28. "069a79f4-44e9-4726-a5be-fca90e38aaf5", // Notch
  29. "61699b2e-d327-4a01-9f1e-0ea8c3f06bc6" // Dinnerbone
  30. );
  31.  
  32. @Override
  33. public UUID parse(StringReader reader) throws CommandSyntaxException {
  34. int argBeginning = reader.getCursor(); // 指针的开始位置,是参数的开始位置。
  35. if (!reader.canRead()) {
  36. reader.skip();
  37. }
  38.  
  39. // 现在我们检查参数的内容,直到命令行的末尾(即 ''canRead'' 返回了 false)
  40. // 否则我们检查到空格的位置,即表示下一个参数要开始了
  41. while (reader.canRead() && reader.peek() != ' ') { // “peek”提供了当前指针位置的字符。
  42. reader.skip(); // 告诉 StringReader,将其指针移动到下一个位置。
  43. }
  44.  
  45. // 现在我们使用当前的指针位置,取出我们需要看到的特定部分子字符串,在下一个参数开始的位置结束。
  46. String uuidString = reader.getString().substring(argBeginning, reader.getCursor());
  47. try {
  48. UUID uuid = UUID.fromString(uuidString); // 我们的正常逻辑。
  49. return uuid;
  50. // 我们返回我们的类型,在这个例子中,解析器会认为类型已经正确解析,并继续。
  51. } catch (Exception ex) {
  52. // UUID 在由字符串生成时,可能会抛出异常,因此我们捕获这个异常,并将其包装成 CommandSyntaxException 类型。
  53. // 创建时会带有环境,告诉 Brigadier,根据这个环境来告诉玩家,命令在哪里解析失败了。
  54. // 尽管应该使用正常的 create 方法。
  55. throw new SimpleCommandExceptionType(Text.literal(ex.getMessage())).createWithContext(reader);
  56. }
  57. }
  58.  
  59. @Override
  60. public Collection<String> getExamples() {
  61. // Brigadier 支持显示示例,表示这个命令应该像是什么样子,这应该包含一个集合,
  62. // 集合的内容是此类型能够返回的参数。
  63. // 这主要用于检测二义性,即一种参数可能会被作为另一种参数解析。
  64. return EXAMPLES;
  65. }
  66. }

指定建议

很多参数类型都支持建议。不像在注册过程中定义的自定义建议,当没有自定义建议时,始终会应用这些建议。建议是在客户端中计算的。例如,BlockPosArgumentType 可能会建议客户端的准星目标的位置,EntityArgumentType 可能会建议准星目标实体的准确 UUID。

关于如何提供建议,请参见命令建议。在这个例子中,我们会建议服务器中的所有玩家的 UUID,以及当前客户端的准星目标实体。

  @Override
  public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
    final String remaining = builder.getRemaining();
    if (context.getSource() instanceof FabricClientCommandSource clientCommandSource) {
      if (clientCommandSource.getClient().crosshairTarget instanceof EntityHitResult entityHitResult) {
        final UUID uuid = entityHitResult.getEntity().getUuid();
        if (CommandSource.shouldSuggest(remaining, uuid.toString())) {
          builder.suggest(uuid.toString());
        }
      }
      return CommandSource.suggestMatching(Collections2.transform(clientCommandSource.getClient().getNetworkHandler().getPlayerUuids(), UUID::toString), builder);
    }
 
    if (context.getSource() instanceof ServerCommandSource source) {
      return CommandSource.suggestMatching(Lists.transform(source.getServer().getPlayerManager().getPlayerList(), Entity::getUuidAsString), builder);
    }
 
    return builder.buildFuture();
  }

自定义参数序列化

在上面的例子中,UUID 是简单的。如果参数是复杂的,如何让它能够正确地被客户端理解呢?这就是 ArgumentSerializer 有用的地方。

ArgumentSerializer 有两个泛型参数:

在这个例子中,我们创建一个新的参数类型,包含一个布尔值和一个整数。这个例子中,需要 CommandRegistryAccess

public record ExampleArgumentType(CommandRegistryAccess commandRegistryAccess, boolean booleanValue, int intValue) implements ArgumentType<String> {
  @Override
  public String parse(StringReader reader) throws CommandSyntaxException {
    ...
  }
 
  public static class Serializer implements ArgumentSerializer<ExampleArgumentType, Serializer.Properties> {
    @Override
    public void writePacket(Properties properties, PacketByteBuf buf) {
      // 将基本的属性写到数据包中,需要确保所有的参数都以适当方式存储在数据包中。
      buf.writeBoolean(properties.booleanValue).writeInt(properties.intValue);
    }
 
    @Override
    public Properties fromPacket(PacketByteBuf buf) {
      // 从数据包中读取信息。返回的是 ''ArgumentSerializer.ArgumentTypeProperties'' 而不是 ''ArgumentType''。
      return new Properties(buf.readBoolean(), buf.readInt());
    }
 
    @Override
    public void writeJson(Properties properties, JsonObject json) {
      // 以 JSON 的形式呈现参数类型。
      json.addProperty("booleanValue", properties.booleanValue);
      json.addProperty("intValue", properties.intValue);
    }
 
    @Override
    public Properties getArgumentTypeProperties(ExampleArgumentType argumentType) {
      return new Properties(argumentType.booleanValue, argumentType.intValue);
    }
 
    public record Properties(boolean booleanValue, int intValue) implements ArgumentTypeProperties<ExampleArgumentType> {
      @Override
      public ExampleArgumentType createType(CommandRegistryAccess commandRegistryAccess) {
        // 只有在此方法中会提供 ''CommandRegistryAccess''。
        // 会创建需要 ''CommandRegistryAccess'' 的参数类型。
        return new ExampleArgumentType(commandRegistryAccess, booleanValue, intValue);
      }
 
      @Override
      public ArgumentSerializer<ExampleArgumentType, Serializer.Properties> getSerializer() {
        // 不要在这里创建新的 ''Serializer'' 对象。
        return Serializer.this;
      }
    }
  }
}

现在你可以像这样注册:

ArgumentTypeRegistry.registerArgumentType(new Identifier("tutorial", "example"), ExampleArgumentType.class, new ExampleArgumentType.Serializer());

另一种可能的定义序列化的方式

如果参数不需要 CommandRegistryAccess,那么它自己就可以继承 ArgumentSerializer.ArgumentTypeProperties

public record ExampleArgumentType(boolean booleanValue, int intValue) implements ArgumentType<String>, ArgumentSerializer.ArgumentTypeProperties<ExampleArgumentType> {
  @Override
  public String parse(StringReader reader) throws CommandSyntaxException {
    ...
  }
 
  @Override
  public ExampleArgumentType createType(CommandRegistryAccess commandRegistryAccess) {
    return this;
  }
 
  @Override
  public ArgumentSerializer<ExampleArgumentType, ?> getSerializer() {
    // 这里总是返回同一个对象,以避免无法序列化。
    return Serializer.INSTANCE;
  }
 
  public static class Serializer implements ArgumentSerializer<ExampleArgumentType, ExampleArgumentType> {
    public static final Serializer INSTANCE = new Serializer();
    private Serializer() {}
 
    @Override
    public void writePacket(ExampleArgumentType properties, PacketByteBuf buf) {
      buf.writeBoolean(properties.booleanValue).writeInt(properties.intValue);
    }
 
    @Override
    public ExampleArgumentType fromPacket(PacketByteBuf buf) {
      return new ExampleArgumentType(buf.readBoolean(), buf.readInt());
    }
 
    @Override
    public void writeJson(ExampleArgumentType properties, JsonObject json) {
      json.addProperty("booleanValue", properties.booleanValue);
      json.addProperty("intValue", properties.intValue);
    }
 
    @Override
    public ExampleArgumentType getArgumentTypeProperties(ExampleArgumentType argumentType) {
      return argumentType;
    }
  }
}

现在你可以像这样注册:

ArgumentTypeRegistry.registerArgumentType(new Identifier("tutorial", "example"), ExampleArgumentType.class, ExampleArgumentType.Serializer.INSTANCE);