User Tools

Site Tools


tutorial:command_argument_types

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
tutorial:command_argument_types [2023/02/20 06:10] solidblocktutorial:command_argument_types [2024/04/15 07:21] (current) – [Custom argument serializer] solidblock
Line 1: Line 1:
-======= Command Argument Types =======+====== Command Argument Types ======
  
-Brigadier has support for custom argument types and this section goes into showing how to create a simple argument type. +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.+**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. For this example we will create a UuidArgumentType.
  
-First create a class which extends ''ArgumentType''. Note that ''ArgumentType'' is a generic, so the generic will define what type the ''ArgumentType'' will return+First create a class which extends ''ArgumentType''. Note that ''ArgumentType'' is a generic, which will define what type the ''ArgumentType'' will return.
  
 <code java> <code java>
Line 24: Line 25:
  
 <code java [enable_line_numbers="true"]> <code java [enable_line_numbers="true"]>
-int argBeginning = reader.getCursor(); // The starting position of the cursor is at the beginning of the argument. +    int argBeginning = reader.getCursor(); // The starting position of the cursor is at the beginning of the argument. 
-if (!reader.canRead()) { +    if (!reader.canRead()) { 
-    reader.skip(); +        reader.skip(); 
-}+    }
 </code> </code>
  
-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. For a UUID we will just figure out what cursor position the argument ends at.+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.
  
 <code java [enable_line_numbers="true"]> <code java [enable_line_numbers="true"]>
-while (reader.canRead() && reader.peek() != ' ') { // "peek" provides the character at the current cursor position. +    while (reader.canRead() && (Character.isLetterOrDigit(reader.peek()) || reader.peek() == '-')) { // "peek" provides the character at the current cursor position. 
-    reader.skip(); // Tells the StringReader to move it's cursor to the next position. +        reader.skip(); // Tells the StringReader to move it's cursor to the next position. 
-}+    }
 </code> </code>
  
 Then we will ask the ''StringReader'' what the current position of the cursor is an substring our argument out of the command line. Then we will ask the ''StringReader'' what the current position of the cursor is an substring our argument out of the command line.
  
-<code java [enable_line_numbers="true"]>String uuidString = reader.getString().substring(argBeginning, reader.getCursor());</code>+<code java [enable_line_numbers="true"]> 
 +    String uuidString = reader.getString().substring(argBeginning, reader.getCursor()); 
 +</code>
  
 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. 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.
  
 <code java [enable_line_numbers="true"]> <code java [enable_line_numbers="true"]>
-try { +    public static final DynamicCommandExceptionType INVALID_UUID = new DynamicCommandExceptionType(o -> Text.literal("Invalid uuid: " + o)); 
-    UUID uuid = UUID.fromString(uuidString); // Now our actual logic. + 
-    return uuid; // And we return our type, in this case the parser will consider this argument to have parsed properly and then move on. +    @Override 
-} catch (Exception ex) { +    public UUID parse(StringReader reader) throws CommandSyntaxException { 
-    // UUIDs can throw an exception when made by a string, so we catch the exception and repackage it into a CommandSyntaxException type. +        // ... 
-    // Create with context tells Brigadier to supply some context to tell the user where the command failed at. +        try { 
-    // Though normal create method could be used. +            UUID uuid = UUID.fromString(uuidString); // Now our actual logic. 
-    throw new SimpleCommandExceptionType(Text.literal(ex.getMessage())).createWithContext(reader); +            return uuid; // And we return our type, in this case the parser will consider this argument to have parsed properly and then move on. 
-}+        } catch (InvalidArgumentException ex) { 
 +            // UUIDs can throw an exception when made by a string, so we catch the exception and repackage it into a CommandSyntaxException type. 
 +            // Create with context tells Brigadier to supply some context to tell the user where the command failed at. 
 +            // Though normal create method could be used. 
 +            reader.setCursor(argBeginning); 
 +            throw INVALID_UUID.createWithContext(reader, ex.getMessage()); 
 +        } 
 +        // ... 
 +    }
 </code> </code>
  
-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''.  +===== Defining Argument examples ===== 
-Within your ''ModInitializer''. For more complex argument types, you may need to create your own ''ArgumentSerializer''.+Sometimes argument should have examples. It is usually stored in an immutable collection as a static final field. Examples are used to detect ambiguities. 
 +<code java> 
 +    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; 
 +    } 
 +</code> 
 + 
 +===== 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''.
  
 <code java [enable_line_numbers="true"]> <code java [enable_line_numbers="true"]>
-ArgumentTypeRegistry.registerArgumentType(new Identifier("tutorial", "uuid"), UuidArgumentType.class, ConstantArgumentSerializer.of(UuidArgumentType::uuid)); +ArgumentTypeRegistry.registerArgumentType( 
 +  new Identifier("tutorial", "uuid"), 
 +  UuidArgumentType.class, ConstantArgumentSerializer.of(UuidArgumentType::uuid)); 
 // The argument should be what will create the ArgumentType. // The argument should be what will create the ArgumentType.
 </code> </code>
  
-And here is the whole code of the argument type:+===== Example of a whole argument type =====
  
 <file java UuidArgumentType.java [enable_line_numbers="true"]> <file java UuidArgumentType.java [enable_line_numbers="true"]>
Line 74: Line 106:
 import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
 import net.minecraft.text.Text; import net.minecraft.text.Text;
-import net.minecraft.util.SystemUtil; 
  
 import java.util.ArrayList; import java.util.ArrayList;
Line 84: Line 115:
  */  */
 public class UuidArgumentType implements ArgumentType<UUID> { public class UuidArgumentType implements ArgumentType<UUID> {
 +    public static final DynamicCommandExceptionType INVALID_UUID = new DynamicCommandExceptionType(o -> Text.literal("Invalid uuid: " + o));
 +    
     public static UuidArgumentType uuid() {     public static UuidArgumentType uuid() {
         return new UuidArgumentType();         return new UuidArgumentType();
Line 94: Line 127:
     }     }
  
-    private static final Collection<String> EXAMPLES = SystemUtil.consume(new ArrayList<>(), list -> { +    private static final Collection<String> EXAMPLES = List.of
-        list.add("765e5d33-c991-454f-8775-b6a7a394c097"); // i509VCB: Username The_1_gamers +        "765e5d33-c991-454f-8775-b6a7a394c097"// i509VCB: Username The_1_gamers 
-        list.add("069a79f4-44e9-4726-a5be-fca90e38aaf5"); // Notch +        "069a79f4-44e9-4726-a5be-fca90e38aaf5"// Notch 
-        list.add("61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"); // Dinnerbone +        "61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"  // Dinnerbone 
-    });+    );
  
     @Override     @Override
Line 109: Line 142:
         // Now we check the contents of the argument till either we hit the end of the         // Now we check the contents of the argument till either we hit the end of the
         // command line (when ''canRead'' becomes false)         // command line (when ''canRead'' becomes false)
-        // Otherwise we go till reach reach a space, which signifies the next argument +        // Otherwise we go till reach reach a character that cannot compose a UUID 
-        while (reader.canRead() && reader.peek() != ' ') { // peek provides the character at the current cursor position.+        while (reader.canRead() && (Character.isLetterOrDigit(reader.peek()) || reader.peek() == '-')) { // peek provides the character at the current cursor position.
             reader.skip(); // Tells the StringReader to move it's cursor to the next position.             reader.skip(); // Tells the StringReader to move it's cursor to the next position.
         }         }
Line 122: Line 155:
             // And we return our type, in this case the parser will consider this             // And we return our type, in this case the parser will consider this
             // argument to have parsed properly and then move on.             // argument to have parsed properly and then move on.
-        } catch (Exception ex) { +        } catch (InvalidArgumentException ex) { 
-            // UUIDs can throw an exception when made by a string, so we catch the exception +            // UUIDs can throw an exception when made by a string, so we catch the exception and repackage it into a CommandSyntaxException type. 
-            // and repackage it into a CommandSyntaxException type. +            // Create with context tells Brigadier to supply some context to tell the user where the command failed at.
-            // Create with context tells Brigadier to supply some context to tell the user +
-            // where the command failed at.+
             // Though normal create method could be used.             // Though normal create method could be used.
-            throw new SimpleCommandExceptionType(Text.literal(ex.getMessage())).createWithContext(reader);+            reader.setCursor(argBeginning); 
 +            throw INVALID_UUID.createWithContext(reader, ex.getMessage());
         }         }
     }     }
Line 136: Line 168:
         // Brigadier has support to show examples for what the argument should look like,         // 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 should contain a Collection of only the argument this type will return.
-        // This is mainly used to calculate ambiguous commands which share the exact same +        // This is mainly used to detect ambiguity, which means an argument of this type may be parsed as another type.
         return EXAMPLES;         return EXAMPLES;
     }     }
Line 142: Line 174:
 </file> </file>
  
 +===== 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.
 +
 +<code java>
 +  @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();
 +  }
 +</code>
 +
 +===== 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.
 +
 +<code java>
 +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;
 +      }
 +    }
 +  }
 +}
 +</code>
 +
 +And now you can register it like this:
 +<code java>
 +ArgumentTypeRegistry.registerArgumentType(new Identifier("tutorial", "example"), ExampleArgumentType.class, new ExampleArgumentType.Serializer());
 +</code>
 +
 +==== Another possible way to define serializer ====
 +
 +If the argument does not require ''CommandRegistryAccess'', it itself may extend ''ArgumentSerializer.ArgumentTypeProperties'':
 +
 +<code java>
 +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;
 +    }
 +  }
 +}
 +</code>
 +
 +And now you can register it like this:
 +<code java>
 +ArgumentTypeRegistry.registerArgumentType(new Identifier("tutorial", "example"), ExampleArgumentType.class, ExampleArgumentType.Serializer.INSTANCE);
 +</code>
tutorial/command_argument_types.1676873408.txt.gz · Last modified: 2023/02/20 06:10 by solidblock