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.

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
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
// 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()