Custom parameter types

This page will explain how you can create and resolve custom parameter types

One of the core features of Lamp is the ability to use custom parameter types for commands. This allows us to use types with specific meanings, restrict values to certain options, or provide customized tab completions.

We will illustrate this with a simple Quests plugin.

Creating a Quest type

Let's create a Quest class that contains all relevant data for a single Quest. Because this is slightly irrelevant to our end goal, we will use a relatively simple implementation.

public record Quest(
        String id,
        String description
) {}

We will create a class that handles, stores, and retrieves all Quest objects. Let's call it QuestManager. It will contain basic functionality for creating, updating, querying and deleting quests

Creating a QuestManager

public final class QuestManager {

    private final Map<String, Quest> quests = new HashMap<>();

    public boolean questExists(@NotNull String name) {
        return quests.containsKey(name);
    }

    public void add(@NotNull Quest quest) {
        if (questExists(quest.name()))
            throw new IllegalArgumentException("Quest with name '" + quest.name() + "' already exists!");
        quests.put(quest.name(), quest);
    }

    public Quest remove(@NotNull Quest quest) {
        return quests.remove(quest.name());
    }

    public void clearAllQuests() {
        quests.clear();
    }
    
    public Quest quest(@NotNull String name) {
        return quests.get(name);
    }
    
    public Map<String, Quest> quests() {
        return quests;
    }
}

We will create a single instance of this QuestManager in our main class.

public final class QuestsPlugin extends JavaPlugin {

+    private final QuestManager questManager = new QuestManager();

}

Now, let's tell Lamp how to resolve a Quest parameter.

To create custom parameter types, we must implement the ParameterType interface. This interface describes how parameters are resolved and what suggestions they receive by default.

Creating a QuestParameterType

public final class QuestParameterType implements ParameterType<BukkitCommandActor, Quest> {

    @Override
    public Quest parse(@NotNull MutableStringStream input, @NotNull ExecutionContext<BukkitCommandActor> context) {
        /* Resolve a Quest here */
    }
}

You may have noticed that ParameterType contains generics. In fact, it requires that you define two generics when you use it:

  • A: A subclass of CommandActor that the ParameterType can work with. If we are creating a general ParameterType that works with any platform, we can have this as the CommandActor interface. If we, however, need a ParameterType that only works with Bukkit, for example, this would be BukkitCommandActor or any of its subclasses. In other words, what is the most general CommandActor implementation we can work with?

  • T: The type of object we need to resolve. In this case, it is a Quest type. This is used by #parse(...) to dictate what types we are expected to return.

We would like to resolve our objects from our QuestManager. For this, let's create a constructor that receives a QuestManager:

private final QuestManager questManager;

public QuestParameterType(QuestManager questManager) {
    this.questManager = questManager;
}

Let's create a simple implementation of our parse function:

@Override
public Quest parse(@NotNull MutableStringStream input, @NotNull ExecutionContext<BukkitCommandActor> context) {
    String name = input.readString();
    Quest quest = questManager.quest(name);
    if (quest == null)
        throw new CommandErrorException("No such quest: " + name);
    return quest;
}

Let's break down what we are doing here:

  • input.readString(): This consumes a string from a MutableStringStream. You can think of MutableStringStream as a String that is tracked by a cursor that moves along that string. When we read something from it, we move the cursor forward and receive the value that the cursor passed over. readString() will consume an entire string token. This means that if the user gives a value enclosed by double-quotations, for example, "Hello world", this will return Hello world. Otherwise, this will consume the next string until it finds a space. A ParameterType is free to consume as much of a MutableStringStream as it needs. The only requirement is that if it consumes a token, it must consume it entirely. It cannot consume part of a string, for example.

  • throw new CommandErrorException("No such quest: " + name): This will signal that the command execution failed, and tell the user that they supplied an invalid quest.

Adding tab completion to our Quest type

A nicety in ParameterType is that it allows us to define custom suggestions for our quest type. These can be supplied using the defaultSuggestions method:

@Override public @NotNull SuggestionProvider<BukkitCommandActor> defaultSuggestions() {
    return (context) -> List.copyOf(questManager.quests().keySet());
}

Registering our QuestParameterType to Lamp

Let's create our Lamp instance:

@Override public void onEnable() {
    var lamp = BukkitLamp.builder(this)
        .parameterTypes(builder -> {
            builder.addParameterType(Quest.class, new QuestParameterType(questManager));
        })
        .build();
}

That's it! We can now use Quest in our commands to our heart's delight.

💡 You may have noticed that the builder provides addXXX and addXXXLast. Why the two variants?

When you use the addXXXLast variant, you are essentially giving your ParameterType less priority over others. When two ParameterTypes, one registered with addXXX while the other with addXXXLast conflict, the addXXX one will be used.

Using addXXXLast is very useful if you want to leave area for later overriding. Lamp uses it under the hood to provide all default ParameterTypes, which means you can override any of the default ones easily.

Creating our QuestCommands class

Let's create a simple QuestCommands class:

public class QuestCommands {}

And register it:

lamp.register(new QuestCommands());

We need to access this QuestManager from our command class. How are we going to do this?

We have multiple solutions:

  • Pass it to the constructor: It is the traditional Java way of doing things. It involves no magic and no overhead. It, however, creates a tightly-coupled class that

  • Create a Lamp dependency: This is the way Lamp encourages. It is a simple form of dependency injection that allows us to create loosely coupled code. In simpler terms, it says "I want a QuestManager. I don't care where this QuestManager comes from. I just want it"

We will go with the second way. It will keep our code clean and easy for future refactoring. Dependency injection also comes with many benefits, and this is not the place to discuss them. You can read up on the topic for more details.

To create a dependency, we must register it in our Lamp instance as follows:

var lamp = BukkitLamp.builder(this)
    // ...
    .dependency(QuestManager.class, questManager)
    // ...
    .build();

Now, to use it in our QuestsCommand class:

public class QuestCommands {

    @Dependency
    private QuestManager questManager;

}

That's it! We can now create our Quest commands easily. And we have a QuestManager at our disposal for all Quest-related operations.

@Command("quest")
@CommandPermission("quests.command")
public class QuestCommands {

    @Dependency 
    private QuestManager questManager;

    @Subcommand("create")
    public void createQuest(BukkitCommandActor actor, String name, String description) {
        /*...*/
    }

    @Subcommand("delete")
    public void deleteQuest(BukkitCommandActor actor, Quest quest) {
        /*...*/
    }
    
    @Subcommand("start")
    public void startQuest(Player actor, Quest quest) {
        /*...*/
    }

    @Subcommand("clear")
    public void clearQuests(BukkitCommandActor actor) {
        /*...*/
    }
}

Using a ParameterType Factory

ParameterType.Factory allows you to dynamically create ParameterType instances based on the type of parameter and annotations. This is particularly useful for complex parameter parsing scenarios. Below is an example demonstrating how to create a custom factory for handling enum types.

Example: Enum Parameter Type Factory

This example shows how to implement a ParameterType.Factory that handles enum types. The factory converts a string input into an enum constant and provides suggestions based on enum names.

public enum EnumParameterTypeFactory implements ParameterType.Factory<CommandActor> {
    INSTANCE;

    @Override
    @SuppressWarnings({"rawtypes", "unchecked"})
    public <T> ParameterType<CommandActor, T> create(@NotNull Type parameterType, @NotNull AnnotationList annotations, @NotNull Lamp<CommandActor> lamp) {
        Class<?> rawType = getRawType(parameterType);
        if (!rawType.isEnum())
            return null;
        Enum<?>[] enumConstants = (Enum<?>[]) rawType.getEnumConstants();
        Map<String, Enum<?>> byKeys = new HashMap<>();
        List<String> suggestions = new ArrayList<>();
        for (Enum<?> enumConstant : enumConstants) {
            String name = enumConstant.name().toLowerCase();
            byKeys.put(name, enumConstant);
            suggestions.add(name);
        }
        return new EnumParameterType(byKeys, suggestions);
    }

    private record EnumParameterType<E extends Enum<E>>(
            Map<String, E> byKeys,
            List<String> suggestions
    ) implements ParameterType<CommandActor, E> {

        @Override
        public E parse(@NotNull MutableStringStream input, @NotNull ExecutionContext<CommandActor> context) {
            String key = input.readUnquotedString();
            E value = byKeys.get(key.toLowerCase());
            if (value != null)
                return value;
            throw new EnumNotFoundException(key);
        }

        @Override
        public @NotNull SuggestionProvider<CommandActor> defaultSuggestions() {
            return SuggestionProvider.of(suggestions);
        }

        @Override
        public @NotNull PrioritySpec parsePriority() {
            return PrioritySpec.highest();
        }
    }
}

This example demonstrates how to create a ParameterType.Factory that can parse enums and provide suggestions based on the enum values.

Well done! In this tutorial, we have learned the following:

  • How to create a custom parameter type

  • How to provide default suggestions for a parameter type

  • How to create and use dependencies

In the next tutorial, we will go through suggestions and auto-completion

Last updated