Skip to content

Creating Custom ArgumentTypes

Martijn edited this page Mar 7, 2022 · 22 revisions

Existing ArgumentTypes & Limitations

Before creating a custom ArgumentType, check if the type is available in either Chimera or Brigadier.

WARNING: It is recommended to use WordType.word() instead of StringArgumentType.word(), and Readers.unquoted(StringReader) instead of StringReader.readUnquoted(). See the FAQ for more information.

A custom ArgumentType can suggest completions and parse a string into a type. Due to how the client processes ArgumentTypes, injecting a custom ArgumentType that is not native to NMS and Brigadier will cause the client to disconnect. Hence, a custom type must be mapped to a native type that is recognizable by the client. However, this limits the potential of custom types.


The Type<T> Interface

Type<T> is a Chimera exclusive subinterface of ArgumentType. Custom types that implement Type<T> will be automatically mapped to the native type returned by Type.mapped().

For convenience, Type<T>s that map to commonly used native types are provided:

Type Native Type Description
PointType ArgumentVec2/ArgumnetVec3 (NMS) An unquoted 2D/3D Cartesian string
VectorType ArgumentVec2/ArgumnetVec3 (NMS) An unquoted 2/D3D Cartesian string
StringType StringArgumentType An optionally quotable string
WordType StringArgumentType A single word with no spaces

Full list of custom types   Full list of native Brigadier types


Creating a Custom ArgumentType

In this example we will create a custom type that parses a player's name to a associated rank. For brevity, we pretend that the player's name is already mapped to a rank.

We implement WordType since a player's name cannot contain spaces. A trie is used to improve look-up performance when listing suggestions later. Chimera provides the trie implementation that we use.

import com.karuslabs.commons.command.types.WordType;
import com.karuslabs.commons.util.collections.Trie;

import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;

import java.util.concurrent.CompletableFuture;


public class RankType implements WordType<Integer> {
    
    private Trie<Integer> ranks = new Trie<>();
    
    public RankType() {
        // Stub population
        ranks.put("Bob", 1); ranks.put("Bobby", 2); ranks.put("Ruth", 3); 
    }
    
    
    @Override
    public Integer parse(StringReader reader) throws CommandSyntaxException {
        // Covered later
    }
    
    @Override
    public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
        // Covered later
    }
    
}

Parsing a Given Input

First we declare a CommandSyntaxException factory. It is used to create exceptions when the given input cannot be parsed.

import com.mojang.brigadier.LiteralMessage;
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;

private static final DynamicCommandExceptionType EXCEPTION = new DynamicCommandExceptionType(name -> new LiteralMessage("Unknown name: " + name));

The RankType.parse(StringReader) implementation is trivial. The rank of the player is returned if the player's name is in ranks, otherwise the exception factory is used to throw an exception.

@Override
public Integer parse(StringReader reader) throws CommandSyntaxException {
    String name = reader.getRemaining();
    Integer rank = ranks.get(name);
    if (rank == null) {
        throw EXCEPTION.createWithContext(reader, name);
    }

    return rank;
}

Listing Suggestions

The RankType.parse(CommandContext<S>, SuggestionsBuilder) implementation is also trivial. We search ranks for names that start with the given remaining input and suggest the names that start with it.

@Override
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
    String incomplete = builder.getRemaining();
    Set<String> matches = ranks.prefixedKeys(incomplete);
    for (var match : matches) {
        builder.suggest(match);
    }

    return builder.buildFuture();
}

Putting Everything Together

After finishing this guide, RankType should look something like this:

import com.karuslabs.commons.command.types.WordType;
import com.karuslabs.commons.util.collections.Trie;
import com.mojang.brigadier.LiteralMessage;

import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import java.util.Set;

import java.util.concurrent.CompletableFuture;


public class RankType implements WordType<Integer> {
    
    private static final DynamicCommandExceptionType EXCEPTION = new DynamicCommandExceptionType(name -> new LiteralMessage("Unknown name: " + name));
    
    private Trie<Integer> ranks = new Trie<>();
    
    public RankType() {
        // Stub population
        ranks.put("Bob", 1); ranks.put("Bobby", 2); ranks.put("Ruth", 3); 
    }
    
    
    @Override
    public Integer parse(StringReader reader) throws CommandSyntaxException {
        String name = reader.getRemaining();
        Integer rank = ranks.get(name);
        if (rank == null) {
            throw EXCEPTION.createWithContext(reader, name);
        }
        
        return rank;
    }
    
    @Override
    public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
        String incomplete = builder.getRemaining();
        Set<String> matches = ranks.prefixedKeys(incomplete);
        for (var match : matches) {
            builder.suggest(match);
        }
        
        return builder.buildFuture();
    }
    
}

To use the custom type, we must apply it on an Argument

import com.karuslabs.commons.command.tree.nodes.Argument;

Argument<CommandSender, Integer> rank = Argument.of("rank", new RankType()).build();

Further Reference