Safely typed argument suggestions
So far, we've covered how to replace suggestions using the replaceSuggestions()
method. The issue with using strings for suggestion listings is that they are prone to errors - it is possible to suggest something which is not actually a valid argument, which makes that suggestion unusable. As a result, some arguments include the replaceSafeSuggestions()
, which provides type-safety checks for argument suggestions, as well as automatic "Bukkit-to-suggestion" conversion.
The whole point of the safe argument suggestions method is that parameters entered in this method are guaranteed to work.
The use of the safe replace suggestions function is the same as replaceSuggestions()
from the previous section, except instead of returning a String[]
, you now return a T[]
, where T
is the class corresponding to the argument. This is described in more detail in the table below.
Argument replaceSafeSuggestions(SafeSuggestions<T> suggestions);
Argument includeSafeSuggestions(SafeSuggestions<T> suggestions);
The SafeSuggestions
interface
Similar to the ArgumentSuggestions
interface, safe suggestions use the SafeSuggestions
interface which is a functional interface that takes in a mapping function from an Object to a String and returns some ArgumentSuggestions
which represent the argument's suggestions. Again, this is typically implemented for anyone that wants to use a more powerful suggestion system.
As with ArgumentSuggestions
, the CommandAPI provides some methods to generate safe suggestions:
SafeSuggestions<T> suggest(T... suggestions);
SafeSuggestions<T> suggest(Function<SuggestionInfo, T[]> suggestions);
SafeSuggestions<T> suggestAsync(Function<SuggestionInfo, CompletableFuture<T[]>> suggestions);
SafeSuggestions<T> tooltips(Tooltip<T>... suggestions);
SafeSuggestions<T> tooltips(Function<SuggestionInfo, Tooltip<T>[]> suggestions);
SafeSuggestions<T> tooltipsAsync(Function<SuggestionInfo, CompletableFuture<Tooltip<T>[]>> suggestions);
Supported arguments
Not all arguments support safe suggestions. This is mostly due to implementation constraints or inadequate support by the Bukkit API.
The list of supported arguments are displayed in the following table. The parameter T
(shown in the method signatures above) are also provided for each argument. This parameter is the same as the cast argument described in Argument Casting, except for a few exceptions which are outlined in bold.
Argument | Class (T) |
---|---|
AdvancementArgument | org.bukkit.advancement.Advancement |
AxisArgument | java.util.EnumSet<org.bukkit.Axis> |
BiomeArgument | org.bukkit.block.Biome |
BooleanArgument | Boolean |
ChatColorArgument | org.bukkit.ChatColor |
DoubleArgument | Double |
EnchantmentArgument | org.bukkit.enchantments.Enchantment |
EntityTypeArgument | org.bukkit.entity.EntityType |
FloatArgument | Float |
FloatRangeArgument | dev.jorel.commandapi.wrappers.FloatRange |
FunctionArgument | org.bukkit.NamespacedKey |
GreedyStringArgument | String |
IntegerArgument | Integer |
IntegerRangeArgument | dev.jorel.commandapi.wrappers.IntegerRange |
ItemStackArgument | org.bukkit.inventory.ItemStack |
Location2DArgument | dev.jorel.commandapi.wrappers.Location2D |
LocationArgument | org.bukkit.Location |
LongArgument | Long |
LootTableArgument | org.bukkit.loot.LootTable |
MathOperationArgument | dev.jorel.commandapi.wrappers.MathOperation |
NBTCompoundArgument | de.tr7zw.nbtapi.NBTContainer |
ObjectiveArgument | org.bukkit.scoreboard.Objective |
OfflinePlayerArgument | org.bukkit.OfflinePlayer |
ParticleArgument | org.bukkit.Particle |
PlayerArgument | org.bukkit.entity.Player |
PotionEffectArgument | org.bukkit.potion.PotionEffectType |
RecipeArgument | org.bukkit.inventory.Recipe |
RotationArgument | dev.jorel.commandapi.wrappers.Rotation |
ScoreboardSlotArgument | dev.jorel.commandapi.wrappers.ScoreboardSlot |
SoundArgument | org.bukkit.Sound |
TeamArgument | org.bukkit.scoreboard.Team |
TimeArgument | dev.jorel.commandapi.wrappers.Time |
WorldArgument | org.bukkit.World |
Safe time arguments
While most of the arguments are fairly straight forward, I'd like to bring your attention to the TimeArgument
's safe suggestions function. This uses dev.jorel.commandapi.wrappers.Time
as the class for T
to ensure type-safety. The Time
class has three static methods:
Time ticks(int ticks);
Time days(int days);
Time seconds(int seconds);
These create representations of ticks (e.g. 40t
), days (e.g. 2d
) and seconds (e.g. 60s
) respectively.
Safe function arguments
Although all safe arguments are indeed "type-safe", the function argument uses a NamespacedKey
which cannot be checked fully at compile time. As a result, this is argument should be used with caution - providing a NamespacedKey
suggestion that does not exist when the server is running will cause that command to fail if that suggestion is used.
Safe scoreboard slot arguments
Scoreboard slots now include two new static methods so they can be used with safe arguments:
ScoreboardSlot of(DisplaySlot slot);
ScoreboardSlot ofTeamColor(ChatColor color);
This allows you to create ScoreboardSlot
instances which can be used with the safe replace suggestions method.
Examples
While this should be fairly straight forward, here's a few examples of how this can be used in practice:
Example - Safe recipe arguments
Say we have a plugin that registers custom items which can be crafted. In this example, we use an "emerald sword" with a custom crafting recipe. Now say that we want to have a command that gives the player the item from our declared recipes, which will have the following syntax:
/giverecipe <recipe>
To do this, we first register our custom items:
// Create our itemstack
ItemStack emeraldSword = new ItemStack(Material.DIAMOND_SWORD);
ItemMeta meta = emeraldSword.getItemMeta();
meta.setDisplayName("Emerald Sword");
meta.setUnbreakable(true);
emeraldSword.setItemMeta(meta);
// Create and register our recipe
ShapedRecipe emeraldSwordRecipe = new ShapedRecipe(new NamespacedKey(this, "emerald_sword"), emeraldSword);
emeraldSwordRecipe.shape(
"AEA",
"AEA",
"ABA"
);
emeraldSwordRecipe.setIngredient('A', Material.AIR);
emeraldSwordRecipe.setIngredient('E', Material.EMERALD);
emeraldSwordRecipe.setIngredient('B', Material.BLAZE_ROD);
getServer().addRecipe(emeraldSwordRecipe);
// Omitted, more itemstacks and recipes
// Create our itemstack
val emeraldSword = ItemStack(Material.DIAMOND_SWORD)
val meta = emeraldSword.itemMeta
meta?.setDisplayName("Emerald Sword")
meta?.isUnbreakable = true
emeraldSword.itemMeta = meta
// Create and register our recipe
val emeraldSwordRecipe = ShapedRecipe(NamespacedKey(this, "emerald_sword"), emeraldSword)
emeraldSwordRecipe.shape(
"AEA",
"AEA",
"ABA"
)
emeraldSwordRecipe.setIngredient('A', Material.AIR)
emeraldSwordRecipe.setIngredient('E', Material.EMERALD)
emeraldSwordRecipe.setIngredient('B', Material.BLAZE_ROD)
server.addRecipe(emeraldSwordRecipe)
// Omitted, more itemstacks and recipes
Once we've done that, we can now include them in our command registration. To do this, we use replaceSafeSuggestions(recipes)
and then register our command as normal:
// Safely override with the recipe we've defined
List<Argument<?>> arguments = new ArrayList<>();
arguments.add(new RecipeArgument("recipe").replaceSafeSuggestions(SafeSuggestions.suggest(info ->
new Recipe[] { emeraldSwordRecipe, /* Other recipes here */ }
)));
// Register our command
new CommandAPICommand("giverecipe")
.withArguments(arguments)
.executesPlayer((player, args) -> {
Recipe recipe = (Recipe) args.get("recipe");
player.getInventory().addItem(recipe.getResult());
})
.register();
// Safely override with the recipe we've defined
val arguments = listOf<Argument<*>>(
RecipeArgument("recipe").replaceSafeSuggestions(SafeSuggestions.suggest {
arrayOf(emeraldSwordRecipe, /* Other recipes here */)
})
)
// Register our command
CommandAPICommand("giverecipe")
.withArguments(arguments)
.executesPlayer(PlayerCommandExecutor { player, args ->
val recipe = args["recipe"] as Recipe
player.inventory.addItem(recipe.result)
})
.register()
Example - Safe /spawnmob suggestions
Say we have a command to spawn mobs:
/spawnmob <mob>
Now say that we don't want non-op players to spawn bosses. To do this, we'll create a List<EntityType>
which is the list of all mobs that non-ops are allowed to spawn:
EntityType[] forbiddenMobs = new EntityType[] {EntityType.ENDER_DRAGON, EntityType.WITHER};
List<EntityType> allowedMobs = new ArrayList<>(Arrays.asList(EntityType.values()));
allowedMobs.removeAll(Arrays.asList(forbiddenMobs)); // Now contains everything except enderdragon and wither
val forbiddenMobs = listOf<EntityType>(EntityType.ENDER_DRAGON, EntityType.WITHER)
val allowedMobs = EntityType.values().toMutableList()
allowedMobs.removeAll(forbiddenMobs) // Now contains everything except enderdragon and wither
We then use our safe arguments to return an EntityType[]
as the list of values that are suggested to the player. In this example, we use the sender()
method to determine if the sender has permissions to view the suggestions:
List<Argument<?>> safeArguments = new ArrayList<>();
safeArguments.add(new EntityTypeArgument("mob").replaceSafeSuggestions(SafeSuggestions.suggest(
info -> {
if (info.sender().isOp()) {
// All entity types
return EntityType.values();
} else {
// Only allowedMobs
return allowedMobs.toArray(new EntityType[0]);
}
})
));
val safeArguments = listOf<Argument<*>>(
EntityTypeArgument("mob").replaceSafeSuggestions(SafeSuggestions.suggest {
info ->
if (info.sender().isOp) {
// All entity types
EntityType.values()
} else {
// Only allowedMobs
allowedMobs.toTypedArray()
}
}
)
)
Now we register our command as normal:
new CommandAPICommand("spawnmob")
.withArguments(safeArguments)
.executesPlayer((player, args) -> {
EntityType entityType = (EntityType) args.get("mob");
player.getWorld().spawnEntity(player.getLocation(), entityType);
})
.register();
CommandAPICommand("spawnmob")
.withArguments(safeArguments)
.executesPlayer(PlayerCommandExecutor { player, args ->
val entityType = args["mob"] as EntityType
player.world.spawnEntity(player.location, entityType)
})
.register()
Example - Removing a potion effect from a player
Say we wanted to remove a potion effect from a player. To do this, we'll use the following command syntax:
/removeeffect <player> <potioneffect>
Now, we don't want to remove a potion effect that already exists on a player, so instead we'll use the safe arguments to find a list of potion effects on the target player and then only suggest those potion effects. To do this, we'll use the previousArguments()
method, as it allows us to access the previously defined <player>
argument.
List<Argument<?>> safeArgs = new ArrayList<>();
safeArgs.add(new EntitySelectorArgument.OnePlayer("target"));
safeArgs.add(new PotionEffectArgument("potioneffect").replaceSafeSuggestions(SafeSuggestions.suggest(
info -> {
Player target = (Player) info.previousArgs().get(0);
// Convert PotionEffect[] into PotionEffectType[]
return target.getActivePotionEffects().stream()
.map(PotionEffect::getType)
.toArray(PotionEffectType[]::new);
})
));
val safeArgs = mutableListOf<Argument<*>>()
safeArgs.add(EntitySelectorArgument.OnePlayer("target"))
safeArgs.add(PotionEffectArgument("potioneffect").replaceSafeSuggestions(SafeSuggestions.suggest {
info ->
val target = info.previousArgs()["target"] as Player
// Convert PotionEffect[] into PotionEffectType[]
target.activePotionEffects.map{ it.type }.toTypedArray()
})
)
And then we can register our command as normal:
new CommandAPICommand("removeeffect")
.withArguments(safeArgs)
.executesPlayer((player, args) -> {
Player target = (Player) args.get("target");
PotionEffectType potionEffect = (PotionEffectType) args.get("potioneffect");
target.removePotionEffect(potionEffect);
})
.register();
CommandAPICommand("removeeffect")
.withArguments(safeArgs)
.executesPlayer(PlayerCommandExecutor { _, args ->
val target = args["target"] as Player
val potionEffect = args["potioneffect"] as PotionEffectType
target.removePotionEffect(potionEffect)
})
.register()