icon | description |
---|---|
vial |
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.
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.
{% tabs %} {% tab title="Java" %}
public record Quest(
String id,
String description
) {}
{% endtab %}
{% tab title="Kotlin" %}
data class Quest(
val id: String,
val description: String
)
{% endtab %} {% endtabs %}
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
{% tabs %} {% tab title="Java" %}
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;
}
}
{% endtab %}
{% tab title="Kotlin" %}
class QuestManager {
val quests = mutableMapOf<String, Quest>()
fun questExists(name: String): Boolean {
return quests.containsKey(name)
}
fun add(quest: Quest) {
require(!questExists(quest.name)) {
"Quest with name '" + quest.name + "' already exists!"
}
quests[quest.name] = quest
}
fun remove(quest: Quest): Quest? {
return quests.remove(quest.name)
}
fun clearAllQuests() {
quests.clear()
}
fun quest(name: String): Quest? {
return quests[name]
}
}
{% endtab %} {% endtabs %}
We will create a single instance of this QuestManager in our main class.
{% tabs %} {% tab title="Java" %}
public final class QuestsPlugin extends JavaPlugin {
+ private final QuestManager questManager = new QuestManager();
}
{% endtab %}
{% tab title="Kotlin" %}
class QuestsPlugin : JavaPlugin() {
+ private val questManager = QuestManager()
}
{% endtab %} {% endtabs %}
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.
{% tabs %} {% tab title="Java" %}
public final class QuestParameterType implements ParameterType<BukkitCommandActor, Quest> {
@Override
public Quest parse(@NotNull MutableStringStream input, @NotNull ExecutionContext<BukkitCommandActor> context) {
/* Resolve a Quest here */
}
}
{% endtab %}
{% tab title="Kotlin" %}
class QuestParameterType : ParameterType<BukkitCommandActor, Quest> {
override fun parse(input: MutableStringStream, context: ExecutionContext<BukkitCommandActor>): Quest {
/* Resolve a Quest here */
}
}
{% endtab %} {% endtabs %}
You may have noticed that ParameterType
contains generics. In fact, it requires that you define two generics when you use it:
A
: A subclass ofCommandActor
that the ParameterType can work with. If we are creating a general ParameterType that works with any platform, we can have this as theCommandActor
interface. If we, however, need a ParameterType that only works with Bukkit, for example, this would beBukkitCommandActor
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 aQuest
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:
{% tabs %} {% tab title="Java" %}
private final QuestManager questManager;
public QuestParameterType(QuestManager questManager) {
this.questManager = questManager;
}
{% endtab %}
{% tab title="Kotlin" %}
class QuestParameterType(private val questManager: QuestManager) /* ... */
{% endtab %} {% endtabs %}
Let's create a simple implementation of our parse
function:
{% tabs %} {% tab title="Java" %}
@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;
}
{% endtab %}
{% tab title="Kotlin" %}
override fun parse(input: MutableStringStream, context: ExecutionContext<BukkitCommandActor>): Quest {
val name = input.readString()
val quest = questManager.quest(name)
?: throw CommandErrorException("No such quest: $name")
return quest
}
{% endtab %} {% endtabs %}
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 returnHello world
. Otherwise, this will consume the next string until it finds a space.
AParameterType
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.
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:
{% tabs %} {% tab title="Java" %}
@Override public @NotNull SuggestionProvider<BukkitCommandActor> defaultSuggestions() {
return (context) -> List.copyOf(questManager.quests().keySet());
}
{% endtab %}
{% tab title="Kotlin" %}
override fun defaultSuggestions(): SuggestionProvider<BukkitCommandActor> {
return SuggestionProvider { _ -> questManager.quests.keys }
}
{% endtab %} {% endtabs %}
Let's create our Lamp instance:
{% tabs %} {% tab title="Java" %}
@Override public void onEnable() {
var lamp = BukkitLamp.builder(this)
.parameterTypes(builder -> {
builder.addParameterType(Quest.class, new QuestParameterType(questManager));
})
.build();
}
{% endtab %}
{% tab title="Kotlin" %}
override fun onEnable() {
val lamp = BukkitLamp.builder(this)
.parameterTypes {
it.addParameterType(Quest::class.java, QuestParameterType(questManager))
}
.build()
}
{% endtab %} {% endtabs %}
That's it! We can now use Quest
in our commands to our heart's delight.
💡 You may have noticed that the
builder
providesaddXXX
andaddXXXLast
. 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 withaddXXX
while the other withaddXXXLast
conflict, theaddXXX
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.
Let's create a simple QuestCommands class:
{% tabs %} {% tab title="Java" %}
public class QuestCommands {}
And register it:
lamp.register(new QuestCommands());
{% endtab %}
{% tab title="Kotlin" %}
class QuestCommands
And register it:
lamp.register(QuestCommands());
{% endtab %} {% endtabs %}
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:
{% tabs %} {% tab title="Java" %}
var lamp = BukkitLamp.builder(this)
// ...
.dependency(QuestManager.class, questManager)
// ...
.build();
{% endtab %}
{% tab title="Kotlin" %}
val lamp = BukkitLamp.builder(this)
// ...
.dependency(QuestManager::class.java, questManager)
// ...
.build()
{% endtab %} {% endtabs %}
Now, to use it in our QuestsCommand
class:
{% tabs %} {% tab title="Java" %}
public class QuestCommands {
@Dependency
private QuestManager questManager;
}
{% endtab %}
{% tab title="Kotlin" %}
class QuestCommands {
@Dependency
private lateinit var questManager: QuestManager
}
{% endtab %} {% endtabs %}
That's it! We can now create our Quest commands easily. And we have a QuestManager
at our disposal for all Quest-related operations.
{% tabs %} {% tab title="Java" %}
@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) {
/*...*/
}
}
{% endtab %}
{% tab title="Kotlin" %}
@Command("quest")
@CommandPermission("quests.command")
class QuestCommands {
@Dependency
private lateinit var questManager: QuestManager
@Subcommand("create")
fun createQuest(actor: BukkitCommandActor, name: String, description: String) {
/*...*/
}
@Subcommand("delete")
fun deleteQuest(actor: BukkitCommandActor, quest: Quest) {
/*...*/
}
@Subcommand("start")
fun startQuest(actor: Player, quest: Quest) {
/*...*/
}
@Subcommand("clear")
fun clearQuests(actor: BukkitCommandActor) {
/*...*/
}
}
{% endtab %} {% endtabs %}
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.
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.
{% tabs %} {% tab title="Java" %}
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();
}
}
}
{% endtab %}
{% tab title="Kotlin" %}
object EnumParameterTypeFactory : ParameterType.Factory<CommandActor> {
@Suppress("UNCHECKED_CAST")
override fun <T> create(parameterType: Type, annotations: AnnotationList, lamp: Lamp<CommandActor>): ParameterType<CommandActor, T>? {
val rawType = getRawType(parameterType)
if (!rawType.isEnum) return null
val enumConstants = rawType.enumConstants as Array<Enum<*>>
val byKeys = mutableMapOf<String, Enum<*>>()
val suggestions = mutableListOf<String>()
for (enumConstant in enumConstants) {
val name = enumConstant.name.lowercase()
byKeys[name] = enumConstant
suggestions.add(name)
}
return EnumParameterType(byKeys, suggestions) as ParameterType<CommandActor, T>
}
private data class EnumParameterType<E : Enum<E>>(
val byKeys: Map<String, E>,
val suggestions: List<String>
) : ParameterType<CommandActor, E> {
override fun parse(input: MutableStringStream, context: ExecutionContext<CommandActor>): E {
val key = input.readUnquotedString()
return byKeys[key.lowercase()] ?: throw EnumNotFoundException(key)
}
override fun defaultSuggestions(): SuggestionProvider<CommandActor> {
return SuggestionProvider.of(suggestions)
}
override fun parsePriority(): PrioritySpec {
return PrioritySpec.highest()
}
}
}
{% endtab %} {% endtabs %}
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