User Tools

Site Tools


tutorial:command_argument_types

Command Argument Types

Brigadier has support for custom argument types and this article goes into showing how to create an argument type.

Warning: Custom arguments require client mod installation to be registered correctly! If you are making a server plugin, consider using existing argument type and a custom suggestions provider instead.

Parsing

For this example we will create a UuidArgumentType.

First create a class which extends ArgumentType. Note that ArgumentType is a generic, which will define what type the ArgumentType will return.

public class UuidArgumentType implements ArgumentType<UUID> {

ArgumentType requires you to implement the parse method, the type it returns will match with the Generic type.

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

This method is where all of your parsing will occur. Either this method will return the object based on the arguments provided in the command line or throw a CommandSyntaxException and parsing will fail.

Next you will store the current position of the cursor, this is so you can substring out only the specific argument. This will always be at the beginning of where your argument appears on the command line.

  1. int argBeginning = reader.getCursor(); // The starting position of the cursor is at the beginning of the argument.
  2. if (!reader.canRead()) {
  3. reader.skip();
  4. }

Now we grab the entire argument. Depending on your argument type, you may have a different criteria or be similar to some arguments where detecting a { on the command line will require it to be closed.

  1. while (reader.canRead() && (Character.isLetterOrDigit(reader.peek()) || reader.peek() == '-')) { // "peek" provides the character at the current cursor position.
  2. reader.skip(); // Tells the StringReader to move it's cursor to the next position.
  3. }

Then we will ask the StringReader what the current position of the cursor is an substring our argument out of the command line.

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

Now finally we check if our argument is correct and parse the specific argument to our liking, and throwing an exception if the parsing fails.

  1. public static final DynamicCommandExceptionType INVALID_UUID = new DynamicCommandExceptionType(o -> Text.literal("Invalid uuid: " + o));
  2.  
  3. @Override
  4. public UUID parse(StringReader reader) throws CommandSyntaxException {
  5. // ...
  6. try {
  7. UUID uuid = UUID.fromString(uuidString); // Now our actual logic.
  8. return uuid; // And we return our type, in this case the parser will consider this argument to have parsed properly and then move on.
  9. } catch (InvalidArgumentException ex) {
  10. // UUIDs can throw an exception when made by a string, so we catch the exception and repackage it into a CommandSyntaxException type.
  11. // Create with context tells Brigadier to supply some context to tell the user where the command failed at.
  12. // Though normal create method could be used.
  13. reader.setCursor(argBeginning);
  14. throw INVALID_UUID.createWithContext(reader, ex.getMessage());
  15. }
  16. // ...
  17. }

Defining Argument examples

Sometimes argument should have examples. It is usually stored in an immutable collection as a static final field. Examples are used to detect ambiguities.

    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 has support to show examples for what the argument should look like,
        // this should contain a Collection of only the argument this type will return.
        // This is mainly used to detect ambiguity, which means an argument of this type may be parsed as another type.
        return EXAMPLES;
    }

Register the argument type

The ArgumentType is done, however your client will refuse the parse the argument and throw an error. This is because the server will tell the client what argument type the command node is. And the client will not parse any argument types which it does not know how to parse. To fix this we need to register an ArgumentSerializer within your ModInitializer. For simple argument types (which has a constructor takes no parameter or takes one CommandRegistryAccess), you can simply use ConstantArgumentSerializer.of. For more complex argument types, you may need to create your own ArgumentSerializer.

  1. ArgumentTypeRegistry.registerArgumentType(
  2. new Identifier("tutorial", "uuid"),
  3. UuidArgumentType.class, ConstantArgumentSerializer.of(UuidArgumentType::uuid));
  4. // The argument should be what will create the ArgumentType.

Example of a whole argument type

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.  * Represents an ArgumentType that will return a UUID.
  14.  */
  15. public class UuidArgumentType implements ArgumentType<UUID> {
  16. public static final DynamicCommandExceptionType INVALID_UUID = new DynamicCommandExceptionType(o -> Text.literal("Invalid uuid: " + o));
  17.  
  18. public static UuidArgumentType uuid() {
  19. return new UuidArgumentType();
  20. }
  21.  
  22. public static <S> UUID getUuid(String name, CommandContext<S> context) {
  23. // Note that you should assume the CommandSource wrapped inside of the CommandContext will always be a generic type.
  24. // If you need to access the ServerCommandSource make sure you verify the source is a server command source before casting.
  25. return context.getArgument(name, UUID.class);
  26. }
  27.  
  28. private static final Collection<String> EXAMPLES = List.of(
  29. "765e5d33-c991-454f-8775-b6a7a394c097", // i509VCB: Username The_1_gamers
  30. "069a79f4-44e9-4726-a5be-fca90e38aaf5", // Notch
  31. "61699b2e-d327-4a01-9f1e-0ea8c3f06bc6" // Dinnerbone
  32. );
  33.  
  34. @Override
  35. public UUID parse(StringReader reader) throws CommandSyntaxException {
  36. int argBeginning = reader.getCursor(); // The starting position of the cursor is at the beginning of the argument.
  37. if (!reader.canRead()) {
  38. reader.skip();
  39. }
  40.  
  41. // Now we check the contents of the argument till either we hit the end of the
  42. // command line (when ''canRead'' becomes false)
  43. // Otherwise we go till reach reach a character that cannot compose a UUID
  44. while (reader.canRead() && (Character.isLetterOrDigit(reader.peek()) || reader.peek() == '-')) { // peek provides the character at the current cursor position.
  45. reader.skip(); // Tells the StringReader to move it's cursor to the next position.
  46. }
  47.  
  48. // Now we substring the specific part we want to see using the starting cursor
  49. // position and the ends where the next argument starts.
  50. String uuidString = reader.getString().substring(argBeginning, reader.getCursor());
  51. try {
  52. UUID uuid = UUID.fromString(uuidString); // Now our actual logic.
  53. return uuid;
  54. // And we return our type, in this case the parser will consider this
  55. // argument to have parsed properly and then move on.
  56. } catch (InvalidArgumentException ex) {
  57. // UUIDs can throw an exception when made by a string, so we catch the exception and repackage it into a CommandSyntaxException type.
  58. // Create with context tells Brigadier to supply some context to tell the user where the command failed at.
  59. // Though normal create method could be used.
  60. reader.setCursor(argBeginning);
  61. throw INVALID_UUID.createWithContext(reader, ex.getMessage());
  62. }
  63. }
  64.  
  65. @Override
  66. public Collection<String> getExamples() {
  67. // Brigadier has support to show examples for what the argument should look like,
  68. // this should contain a Collection of only the argument this type will return.
  69. // This is mainly used to detect ambiguity, which means an argument of this type may be parsed as another type.
  70. return EXAMPLES;
  71. }
  72. }

Specifying suggestions

Many argument types supports suggestions. Unlike custom suggestions defined in registration of commands, these suggestions are always applied when there is no custom suggestions. Suggestions can be calculated in client. For example, BlockPosArgumentType may suggest the position of the client-side cross-hair target, and EntityArgumentType may suggest the excat UUID of the entity that the cross-hair targets.

For how to provide suggestions, see command suggestions. In this example, we suggest UUIDs of all players in the server, as well as the cross-hair target entity in client.

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

Custom argument serializer

In the example above, the UUID is simple. If the argument is complex, how to make it correctly send to and understood by the client? This is where ArgumentSerializer come into use.

ArgumentSerializer has generic with two type parameters:

  • The ArgumentType.
  • The ArgumentSerializer.ArgumentTypeProperties<A>, where A is the first type parameter. It is usally an immutable object (can be records) containing the basic information (not including CommandRegistryAccess) of the argument type. In some cases, when the ArgumentType is immutable and does not contain CommandRegistryAccess, it itself can extend ArgumentSerializer.ArgumentTypeProperties<A>.

In this example, we create a new complicated argument type, which contains a boolean value and an integer value. CommandRegistryAccess is required in this case.

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) {
      // Writes the basic properties to a packet. You should ensure all properties
      // can be in some ways stored in the packet.
      buf.writeBoolean(properties.booleanValue).writeInt(properties.intValue);
    }
 
    @Override
    public Properties fromPacket(PacketByteBuf buf) {
      // Reads the information in the packet. It returns the ''ArgumentSerializer.ArgumentTypeProperties'' instead of ''ArgumentType''.
      return new Properties(buf.readBoolean(), buf.readInt());
    }
 
    @Override
    public void writeJson(Properties properties, JsonObject json) {
      // Present the argument type in the format of 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) {
        // Only in this method, ''CommandRegistryAccess'' is provided, 
        // and the argument type that require ''CommandRegistryAccess'' can be created.
        return new ExampleArgumentType(commandRegistryAccess, booleanValue, intValue);
      }
 
      @Override
      public ArgumentSerializer<ExampleArgumentType, Serializer.Properties> getSerializer() {
        // Do not create a new ''Serializer'' object here.
        return Serializer.this;
      }
    }
  }
}

And now you can register it like this:

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

Another possible way to define serializer

If the argument does not require CommandRegistryAccess, it itself may extend 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() {
    // always return a same object here to avoid failing to serialize.
    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;
    }
  }
}

And now you can register it like this:

ArgumentTypeRegistry.registerArgumentType(new Identifier("tutorial", "example"), ExampleArgumentType.class, ExampleArgumentType.Serializer.INSTANCE);
tutorial/command_argument_types.txt · Last modified: 2024/04/15 07:21 by solidblock