package world.bentobox.bentobox.api.commands; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.logging.Logger; import org.bukkit.World; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.PluginIdentifiableCommand; import org.bukkit.entity.Player; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.Settings; import world.bentobox.bentobox.api.addons.Addon; import world.bentobox.bentobox.api.events.command.CommandEvent; import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.managers.IslandWorldManager; import world.bentobox.bentobox.managers.IslandsManager; import world.bentobox.bentobox.managers.PlayersManager; import world.bentobox.bentobox.managers.RanksManager; import world.bentobox.bentobox.util.Util; /** * BentoBox composite command. Provides an abstract implementation of a command. * * @author tastybento * @author Poslovitch */ public abstract class CompositeCommand extends Command implements PluginIdentifiableCommand, BentoBoxCommand { private static final String COMMANDS = "commands."; private final BentoBox plugin; /** * True if the command is for the player only (not for the console) */ private boolean onlyPlayer = false; /** * True if the command is only for the console * * @since 1.24.0 */ private boolean onlyConsole = false; /** * True if command is a configurable rank */ private boolean configurableRankCommand = false; /** * Make default command rank as owner rank. * * @since 1.20.0 */ private int defaultCommandRank = RanksManager.OWNER_RANK; /** * True if command is hidden from help and tab complete * * @since 1.13.0 */ private boolean hidden = false; /** * The parameters string for this command. It is the commands followed by a * locale reference. */ private String parameters = ""; /** * The parent command to this one. If this is a top-level command it will be * empty. */ protected final CompositeCommand parent; /** * The permission required to execute this command */ private String permission = ""; /** * This is the command level. 0 is the top, 1 is the first level sub command. */ private final int subCommandLevel; /** * Map of sub commands */ private final Map subCommands; /** * Map of aliases for subcommands */ private final Map subCommandAliases; /** * The command chain from the very top, e.g., island team promote */ private String usage; /** * The prefix to be used in this command */ @Nullable private final String permissionPrefix; /** * The world that this command operates in. This is an overworld and will cover * any associated nether or end If the world value does not exist, then the * command is general across worlds */ private World world; /** * The addon creating this command, if any */ private final Addon addon; /** * The top level label */ private final String topLabel; /** * Cool down tracker */ private final Map> cooldowns = new HashMap<>(); /** * Top level command * * @param addon - addon creating the command * @param label - string for this command * @param aliases - aliases */ protected CompositeCommand(Addon addon, String label, String... aliases) { super(label, "", "", Arrays.asList(aliases)); this.addon = addon; this.topLabel = label; this.plugin = BentoBox.getInstance(); setAliases(new ArrayList<>(Arrays.asList(aliases))); parent = null; setUsage(""); subCommandLevel = 0; // Top level subCommands = new LinkedHashMap<>(); subCommandAliases = new LinkedHashMap<>(); // Register command if it is not already registered if (plugin.getCommand(label) == null) { plugin.getCommandsManager().registerCommand(this); } // Default references to description and parameters setDescription(COMMANDS + label + ".description"); setParametersHelp(COMMANDS + label + ".parameters"); permissionPrefix = (addon != null) ? addon.getPermissionPrefix() : ""; // Run setup setup(); if (getSubCommand("help").isEmpty() && !label.equals("help")) { new DefaultHelpCommand(this); } } /** * This is the top-level command constructor for commands that have no parent. * * @param label - string for this command * @param aliases - aliases for this command */ protected CompositeCommand(String label, String... aliases) { this((Addon) null, label, aliases); } /** * Sub-command constructor * * @param parent - the parent composite command * @param label - string label for this subcommand * @param aliases - aliases for this subcommand */ protected CompositeCommand(CompositeCommand parent, String label, String... aliases) { this(parent.getAddon(), parent, label, aliases); } /** * Command to register a command from an addon under a parent command (that * could be from another addon) * * @param addon - this command's addon * @param parent - parent command * @param aliases - aliases for this command */ protected CompositeCommand(Addon addon, CompositeCommand parent, String label, String... aliases) { super(label, "", "", Arrays.asList(aliases)); this.topLabel = parent.getTopLabel(); this.plugin = BentoBox.getInstance(); this.addon = addon; this.parent = parent; subCommandLevel = parent.getLevel() + 1; // Add this sub-command to the parent parent.getSubCommands().put(label.toLowerCase(java.util.Locale.ENGLISH), this); setAliases(new ArrayList<>(Arrays.asList(aliases))); subCommands = new LinkedHashMap<>(); subCommandAliases = new LinkedHashMap<>(); // Add aliases to the parent for this command for (String alias : aliases) { parent.getSubCommandAliases().put(alias.toLowerCase(java.util.Locale.ENGLISH), this); } setUsage(""); // Inherit permission prefix this.permissionPrefix = parent.getPermissionPrefix(); // Default references to description and parameters StringBuilder reference = new StringBuilder(); reference.append(label); CompositeCommand p = this; int index = 0; while (p.getParent() != null && index < 20) { reference.insert(0, p.getParent().getLabel() + "."); p = p.getParent(); index++; } setDescription(COMMANDS + reference + ".description"); setParametersHelp(COMMANDS + reference + ".parameters"); setup(); // If this command does not define its own help class, then use the default help // command if (getSubCommand("help").isEmpty() && !label.equals("help")) { new DefaultHelpCommand(this); } } /** * This method deals with the command execution. It traverses the tree of * subcommands until it finds the right object and then runs execute on it. */ @Override public boolean execute(@NonNull CommandSender sender, @NonNull String label, String[] args) { // Get the User instance for this sender User user = User.getInstance(sender); // Fire an event to see if this command should be cancelled CommandEvent event = CommandEvent.builder().setCommand(this).setLabel(label).setSender(sender).setArgs(args) .build(); if (event.isCancelled()) { return false; } // Get command CompositeCommand cmd = getCommandFromArgs(args); String cmdLabel = (cmd.subCommandLevel > 0) ? args[cmd.subCommandLevel - 1] : label; List cmdArgs = Arrays.asList(args).subList(cmd.subCommandLevel, args.length); return cmd.call(user, cmdLabel, cmdArgs); } /** * Calls this composite command. Does not traverse the tree of subcommands in * args. Event is not fired and it cannot be cancelled. * * @param user - user calling this command * @param cmdLabel - label used * @param cmdArgs - list of args * @return {@code true} if successful, {@code false} if not. * @since 1.5.3 */ public boolean call(User user, String cmdLabel, List cmdArgs) { // Check for console and permissions if (isOnlyPlayer() && !user.isPlayer()) { user.sendMessage("general.errors.use-in-game"); return false; } if (isOnlyConsole() && user.isPlayer()) { user.sendMessage("general.errors.use-in-console"); return false; } if (!this.runPermissionCheck(user)) { // Error message is displayed by permission check. return false; } // Set the user's addon context user.setAddon(addon); // Execute and trim args return canExecute(user, cmdLabel, cmdArgs) && execute(user, cmdLabel, cmdArgs); } /** * This method checks and returns if user has access to the called command. It * also recursively checks if user has access to the all parent commands. * * @param user User who permission must be checked. * @return {@code true} is user can execute given command, {@code false} * otherwise. */ private boolean runPermissionCheck(User user) { // Check perms, but only if this isn't the console if (user.isPlayer() && !user.isOp() && this.getPermission() != null && !this.getPermission().isEmpty() && !user.hasPermission(this.getPermission())) { user.sendMessage("general.errors.no-permission", TextVariables.PERMISSION, this.getPermission()); return false; } // Recursive permission check to find if user has access to the parent command. return this.getParent() == null || this.getParent().runPermissionCheck(user); } /** * Get the current composite command based on the arguments * * @param args - arguments * @return the current composite command based on the arguments */ private CompositeCommand getCommandFromArgs(String[] args) { CompositeCommand subCommand = this; // Run through any arguments for (String arg : args) { // get the subcommand corresponding to the arg if (subCommand.hasSubCommands()) { Optional sub = subCommand.getSubCommand(arg); if (sub.isEmpty()) { return subCommand; } // Step down one subCommand = sub.orElse(subCommand); // Set the label subCommand.setLabel(arg); } else { // We are at the end of the walk return subCommand; } // else continue the loop } return subCommand; } /** * Convenience method to get the island manager * * @return IslandsManager */ protected IslandsManager getIslands() { return plugin.getIslands(); } /** * Convenience method to get the island manager * * @return IslandsManager */ protected IslandsManager getIslandsManager() { return plugin.getIslandsManager(); } /** * @return this command's sub-level. Top level is 0. Every time a command * registers with a parent, their level will be set. */ protected int getLevel() { return subCommandLevel; } /** * @return Logger */ public Logger getLogger() { return plugin.getLogger(); } /** * Convenience method to obtain team members of the active island for user. Note * that the user may have more than one island in this world. * * @param world - world to check * @param user - the User * @return set of UUIDs of all team members, or empty set if there is no island */ protected Set getMembers(World world, User user) { Island island = plugin.getIslands().getIsland(world, user); if (island == null) { return Set.of(); } return island.getMemberSet(); } public String getParameters() { return parameters; } /** * @return the parent command object */ public CompositeCommand getParent() { return parent; } @Override public String getPermission() { return permission; } /** * Convenience method to get the player manager * * @return PlayersManager */ protected PlayersManager getPlayers() { return plugin.getPlayers(); } @Override public @NonNull BentoBox getPlugin() { return plugin; } /** * Get the island worlds manager * * @return island worlds manager */ public IslandWorldManager getIWM() { return plugin.getIWM(); } /** * @return Settings object */ public Settings getSettings() { return plugin.getSettings(); } /** * Returns the CompositeCommand object referring to this command label * * @param label - command label or alias * @return CompositeCommand or null if none found */ public Optional getSubCommand(String label) { label = label.toLowerCase(java.util.Locale.ENGLISH); if (subCommands.containsKey(label)) { return Optional.ofNullable(subCommands.get(label)); } // Try aliases if (subCommandAliases.containsKey(label)) { return Optional.ofNullable(subCommandAliases.get(label)); } return Optional.empty(); } /** * @return Map of sub commands for this command */ public Map getSubCommands() { return subCommands; } /** * Returns a map of sub commands for this command. As it needs more calculations * to handle the Help subcommand, it is preferable to use * {@link #getSubCommands()} when no such distinction is needed. * * @param ignoreHelp Whether the Help subcommand should not be returned in the * map or not. * @return Map of sub commands for this command * @see #hasSubCommands(boolean) */ public Map getSubCommands(boolean ignoreHelp) { if (ignoreHelp && getSubCommand("help").isPresent()) { Map result = subCommands; result.remove("help"); return result; } return getSubCommands(); } @Override public @NonNull String getUsage() { return "/" + usage; } /** * Check if this command has a specific sub command. * * @param subCommand - sub command * @return true if this command has this sub command */ protected boolean hasSubCommand(String subCommand) { return subCommands.containsKey(subCommand) || subCommandAliases.containsKey(subCommand); } /** * Check if this command has any sub commands. * * @return true if this command has subcommands */ protected boolean hasSubCommands() { return !subCommands.isEmpty(); } /** * Check if this command has any sub commands. As it needs more calculations to * handle the Help subcommand, it is preferable to use {@link #hasSubCommands()} * when no such distinction is needed. * * @param ignoreHelp Whether the Help subcommand should not be taken into * account or not. * @return true if this command has subcommands * @see #getSubCommands(boolean) */ protected boolean hasSubCommands(boolean ignoreHelp) { return !getSubCommands(ignoreHelp).isEmpty(); } /** * Convenience method to check if a user has a team. * * @param world - the world to check * @param user - the User * @return true if player is in a team * @see Consider checking the island itself {@link Island#inTeam(UUID)} */ protected boolean inTeam(World world, User user) { return plugin.getIslands().inTeam(world, user.getUniqueId()); } /** * Check if this command is only for players. * * @return true or false */ public boolean isOnlyPlayer() { return onlyPlayer; } /** * Check if this command is only for consoles. * * @return true or false */ public boolean isOnlyConsole() { return onlyConsole; } /** * Sets whether this command should only be run by players. If this is set to * {@code true}, this command will only be runnable by objects implementing * {@link Player}.
*
* The default value provided when instantiating this CompositeCommand is * {@code false}. Therefore, this method should only be used in case you want to * explicitly edit the value. * * @param onlyPlayer {@code true} if this command should only be run by players. */ public void setOnlyPlayer(boolean onlyPlayer) { this.onlyPlayer = onlyPlayer; } /** * Sets whether this command should only be run in the console. This is for * commands that dump a lot of data or are for debugging. The default value * provided when instantiating this CompositeCommand is {@code false}. * Therefore, this method should only be used in case you want to explicitly * edit the value. * * @param onlyConsole {@code true} if this command should only be run in the * console. * @since 1.24.0 */ public void setOnlyConsole(boolean onlyConsole) { this.onlyConsole = onlyConsole; } /** * Sets locale reference to this command's description. It is used to display * the help of this command. * *
*
* * A default value is provided when instantiating this CompositeCommand: * *
    *
  • {@code "commands." + getLabel() + ".description"} if this is a top-level * command;
  • *
  • {@code "commands." + getParent.getLabel() + getLabel() + ".description"} * if this is a sub-command.
    * Note that it can have up to 20 parent commands' labels being inserted before * this sub-command's label. Here are a few examples : *
      *
    • /bentobox info : {@code "commands.bentobox.info.description"};
    • *
    • /bsbadmin range set : * {@code "commands.bsbadmin.range.set.description"};
    • *
    • /mycommand sub1 sub2 sub3 [...] sub22 : * {@code "commands.sub3.[...].sub20.sub21.sub22.description"}.
    • *
    *
  • *
* * This method should therefore only be used in case you want to provide a * different value than the default one. * * @param description The locale command's description reference to set. * @return The instance of this {@link Command}. */ @Override public @NonNull Command setDescription(@NonNull String description) { super.setDescription(description); return this; } /** * Sets locale reference to this command's parameters. It is used to display the * help of this command. * *
*
* * A default value is provided when instantiating this CompositeCommand: * *
    *
  • {@code "commands." + getLabel() + ".parameters"} if this is a top-level * command;
  • *
  • {@code "commands." + getParent.getLabel() + getLabel() + ".parameters"} * if this is a sub-command.
    * Note that it can have up to 20 parent commands' labels being inserted before * this sub-command's label. Here are a few examples : *
      *
    • /bentobox info : {@code "commands.bentobox.info.parameters"};
    • *
    • /bsbadmin range set : * {@code "commands.bsbadmin.range.set.parameters"};
    • *
    • /mycommand sub1 sub2 sub3 [...] sub22 : * {@code "commands.sub3.[...].sub20.sub21.sub22.parameters"}.
    • *
    *
  • *
* * This method should therefore only be used in case you want to provide a * different value than the default one. * * @param parametersHelp The locale command's paramaters reference to set. */ public void setParametersHelp(String parametersHelp) { this.parameters = parametersHelp; } /* * (non-Javadoc) * * @see org.bukkit.command.Command#setPermission(java.lang.String) */ @Override public void setPermission(String permission) { this.permission = ((permissionPrefix != null && !permissionPrefix.isEmpty()) ? permissionPrefix : "") + permission; } /** * Inherits the permission from parent command */ public void inheritPermission() { this.permission = parent.getPermission(); } /** * This creates the full linking chain of commands */ @Override public @NonNull Command setUsage(@NonNull String usage) { // Go up the chain CompositeCommand parentCommand = getParent(); StringBuilder u = new StringBuilder().append(getLabel()).append(" ").append(usage); while (parentCommand != null) { u.insert(0, " "); u.insert(0, parentCommand.getLabel()); parentCommand = parentCommand.getParent(); } this.usage = u.toString().trim(); return this; } @Override @NonNull public List tabComplete(final @NonNull CommandSender sender, final @NonNull String alias, final String[] args) { // Get command object based on args entered so far CompositeCommand command = getCommandFromArgs(args); // Check for console and permissions if ((command.isOnlyPlayer() && !(sender instanceof Player)) || (command.isOnlyConsole() && sender instanceof Player)) { return List.of(); } if (command.getPermission() != null && !command.getPermission().isEmpty() && !sender.hasPermission(command.getPermission()) && !sender.isOp()) { return List.of(); } // Add any tab completion from the subcommand List options = new ArrayList<>( command.tabComplete(User.getInstance(sender), alias, new LinkedList<>(Arrays.asList(args))) .orElseGet(ArrayList::new)); if (command.hasSubCommands()) { options.addAll(getSubCommandLabels(sender, command)); } /* * /!\ The following check is likely a poor quality patch-up job. If any better * solution can be applied, don't hesitate to do so. */ // See https://github.com/BentoBoxWorld/BentoBox/issues/416 // "help" shouldn't appear twice, so remove it if it is already in the args. if (Arrays.asList(args).contains("help")) { options.remove("help"); } /* ------------ */ String lastArg = args.length != 0 ? args[args.length - 1] : ""; return Util.tabLimit(options, lastArg).stream().sorted().toList(); } /** * Returns a list containing all the labels of the subcommands for the provided * CompositeCommand excluding any hidden commands * * @param sender the CommandSender * @param command the CompositeCommand to get the subcommands from * @return a list of subcommands labels or an empty list. */ @NonNull private List getSubCommandLabels(@NonNull CommandSender sender, @NonNull CompositeCommand command) { List result = new ArrayList<>(); for (CompositeCommand cc : command.getSubCommands().values()) { // Player or not if (sender instanceof Player) { if (!cc.isHidden() && !cc.isOnlyConsole() && (cc.getPermission().isEmpty() || sender.hasPermission(cc.getPermission()))) { result.add(cc.getLabel()); } } else if (!cc.isOnlyPlayer()) { result.add(cc.getLabel()); } } return result; } /** * Show help * * @param command - command that this help is for * @param user - the User * @return result of help command or false if no help defined */ protected boolean showHelp(CompositeCommand command, User user) { return command.getSubCommand("help") .map(helpCommand -> helpCommand.execute(user, helpCommand.getLabel(), new ArrayList<>())).orElse(false); } /** * @return the subCommandAliases */ public Map getSubCommandAliases() { return subCommandAliases; } /** * If the permission prefix has been set, will return the prefix plus a trailing * dot. * * @return the permissionPrefix */ @Nullable public String getPermissionPrefix() { return permissionPrefix; } /** * The the world that this command applies to. * * @return the world */ public World getWorld() { // Search up the tree until the world at the top is found return parent != null ? parent.getWorld() : world; } /** * @param world the world to set */ public void setWorld(World world) { this.world = world; } /** * Get the parental addon * * @return the addon */ @SuppressWarnings("unchecked") public T getAddon() { return (T) addon; } /** * @return top level label, e.g., island */ public String getTopLabel() { return topLabel; } /** * Set a cool down - can be set by other commands on this one * * @param uniqueId - the unique ID that is having the cooldown * @param targetUUID - the target (if any) * @param timeInSeconds - time in seconds to cool down * @since 1.5.0 */ public void setCooldown(String uniqueId, String targetUUID, int timeInSeconds) { cooldowns.computeIfAbsent(uniqueId, k -> new HashMap<>()).put(targetUUID, System.currentTimeMillis() + timeInSeconds * 1000L); } /** * Set a cool down - can be set by other commands on this one * * @param uniqueId - the UUID that is having the cooldown * @param targetUUID - the target UUID (if any) * @param timeInSeconds - time in seconds to cool down */ public void setCooldown(UUID uniqueId, UUID targetUUID, int timeInSeconds) { cooldowns.computeIfAbsent(uniqueId.toString(), k -> new HashMap<>()).put( targetUUID == null ? null : targetUUID.toString(), System.currentTimeMillis() + timeInSeconds * 1000L); } /** * Set a cool down for a user - can be set by other commands on this one * * @param uniqueId - the UUID that is having the cooldown * @param timeInSeconds - time in seconds to cool down * @since 1.5.0 */ public void setCooldown(UUID uniqueId, int timeInSeconds) { setCooldown(uniqueId, null, timeInSeconds); } /** * Check if cool down is in progress for user * * @param user - the caller of the command * @param targetUUID - the target (if any) * @return true if cool down in place, false if not */ protected boolean checkCooldown(User user, UUID targetUUID) { return checkCooldown(user, user.getUniqueId().toString(), targetUUID == null ? null : targetUUID.toString()); } /** * Check if cool down is in progress for user * * @param user - the user to check * @return true if cool down in place, false if not * @since 1.5.0 */ protected boolean checkCooldown(User user) { return checkCooldown(user, user.getUniqueId().toString(), null); } /** * Check if cool down is in progress * * @param user - the caller of the command * @param uniqueId - the id that needs to be checked * @param targetUUID - the target (if any) * @return true if cool down in place, false if not * @since 1.5.0 */ protected boolean checkCooldown(User user, String uniqueId, String targetUUID) { if (!cooldowns.containsKey(uniqueId) || user.isOp() || user.hasPermission(getPermissionPrefix() + "mod.bypasscooldowns")) { return false; } cooldowns.putIfAbsent(uniqueId, new HashMap<>()); if (cooldowns.get(uniqueId).getOrDefault(targetUUID, 0L) - System.currentTimeMillis() <= 0) { // Cool down is done cooldowns.get(uniqueId).remove(targetUUID); return false; } int timeToGo = (int) ((cooldowns.get(uniqueId).getOrDefault(targetUUID, 0L) - System.currentTimeMillis()) / 1000); user.sendMessage("general.errors.you-must-wait", TextVariables.NUMBER, String.valueOf(timeToGo)); return true; } /** * @return the configurableRankCommand */ public boolean isConfigurableRankCommand() { return configurableRankCommand; } /** * This command can be configured for use by different ranks */ public void setConfigurableRankCommand() { this.configurableRankCommand = true; } /** * Sets default command rank. * * @param rank the rank * @since 1.20.0 */ public void setDefaultCommandRank(int rank) { this.defaultCommandRank = rank; } /** * Gets default command rank. * * @return the default command rank * @since 1.20.0 */ public int getDefaultCommandRank() { return this.defaultCommandRank; } /** * Checks if a command is hidden * * @return the hidden * @since 1.13.0 */ public boolean isHidden() { return hidden; } /** * Sets a command and all its help and tab complete as hidden * * @param hidden whether command is hidden or not * @since 1.13.0 */ public void setHidden(boolean hidden) { this.hidden = hidden; } }