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 replaceWithSafeSuggestions(), 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 basically 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 replaceWithSafeSuggestions(Function<SuggestionInfo, S[]> 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.

ArgumentClass (T)
AdvancementArgumentorg.bukkit.advancement.Advancement
AxisArgumentjava.util.EnumSet<org.bukkit.Axis>
BiomeArgumentorg.bukkit.block.Biome
BooleanArgumentBoolean
ChatColorArgumentorg.bukkit.ChatColor
DoubleArgumentDouble
EnchantmentArgumentorg.bukkit.enchantments.Enchantment
EntityTypeArgumentorg.bukkit.entity.EntityType
EnvironmentArgumentorg.bukkit.World.Environment
FloatArgumentFloat
FloatRangeArgumentdev.jorel.commandapi.wrappers.FloatRange
FunctionArgumentorg.bukkit.NamespacedKey
GreedyStringArgumentString
IntegerArgumentInteger
IntegerRangeArgumentdev.jorel.commandapi.wrappers.IntegerRange
ItemStackArgumentorg.bukkit.inventory.ItemStack
Location2DArgumentdev.jorel.commandapi.wrappers.Location2D
LocationArgumentorg.bukkit.Location
LongArgumentLong
LootTableArgumentorg.bukkit.loot.LootTable
MathOperationArgumentdev.jorel.commandapi.wrappers.MathOperation
NBTCompoundArgumentde.tr7zw.nbtapi.NBTContainer
ObjectiveArgumentorg.bukkit.scoreboard.Objective
OfflinePlayerArgumentorg.bukkit.OfflinePlayer
ParticleArgumentorg.bukkit.Particle
PlayerArgumentorg.bukkit.entity.Player
PotionEffectArgumentorg.bukkit.potion.PotionEffectType
RecipeArgumentorg.bukkit.inventory.Recipe
RotationArgumentdev.jorel.commandapi.wrappers.Rotation
ScoreboardSlotArgumentdev.jorel.commandapi.wrappers.ScoreboardSlot
SoundArgumentorg.bukkit.Sound
TeamArgumentorg.bukkit.scoreboard.Team
TimeArgumentdev.jorel.commandapi.wrappers.Time

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

Once we've done that, we can now include them in our command registration. To do this, we use replaceWithSafeSuggestions(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").replaceWithSafeSuggestions(info -> 
    new Recipe[] { emeraldSwordRecipe, /* Other recipes here */ }
));

// Register our command
new CommandAPICommand("giverecipe")
    .withArguments(arguments)
    .executesPlayer((player, args) -> {
        Recipe recipe = (Recipe) args[0];
        player.getInventory().addItem(recipe.getResult());
    })
    .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

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> arguments = new ArrayList<>();
arguments.add(new EntityTypeArgument("mob").replaceWithSafeSuggestions(
    info -> {
        if(info.sender().isOp()) {
            // All entity types
            return EntityType.values();
        } else {
            // Only allowedMobs
            return allowedMobs.toArray(new EntityType[0]);
        }
    })
);

Now we register our command as normal:

new CommandAPICommand("spawnmob")
    .withArguments(arguments)
    .executesPlayer((player, args) -> {
        EntityType entityType = (EntityType) args[0];
        player.getWorld().spawnEntity(player.getLocation(), 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> arguments = new ArrayList<>();
arguments.add(new EntitySelectorArgument("target", EntitySelector.ONE_PLAYER));
arguments.add(new PotionEffectArgument("potioneffect").replaceWithSafeSuggestions(
    info -> {
        Player target = (Player) info.previousArgs()[0];
        
        // Convert PotionEffect[] into PotionEffectType[]
        return target.getActivePotionEffects().stream()
            .map(PotionEffect::getType)
            .toArray(PotionEffectType[]::new);
    })
);

And then we can register our command as normal:

new CommandAPICommand("removeeffect")
    .withArguments(arguments)
    .executesPlayer((player, args) -> {
    	Player target = (Player) args[0];
        PotionEffectType potionEffect = (PotionEffectType) args[1];
        target.removePotionEffect(potionEffect);
    })
    .register();