package us.tastybento.bskyblock.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 java.util.stream.Collectors; import org.bukkit.Bukkit; 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.bukkit.scheduler.BukkitTask; import us.tastybento.bskyblock.BSkyBlock; import us.tastybento.bskyblock.Settings; import us.tastybento.bskyblock.api.addons.Addon; import us.tastybento.bskyblock.api.events.command.CommandEvent; import us.tastybento.bskyblock.api.localization.TextVariables; import us.tastybento.bskyblock.api.user.User; import us.tastybento.bskyblock.managers.IslandWorldManager; import us.tastybento.bskyblock.managers.IslandsManager; import us.tastybento.bskyblock.managers.PlayersManager; import us.tastybento.bskyblock.util.Util; /** * BSB composite command * @author tastybento * @author Poslovitch */ public abstract class CompositeCommand extends Command implements PluginIdentifiableCommand, BSBCommand { private final BSkyBlock plugin; /** * True if the command is for the player only (not for the console) */ private boolean onlyPlayer = 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 Map subCommands; /** * Map of aliases for subcommands */ private 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 */ private 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 Addon addon; /** * The top level label */ private String topLabel = ""; private static Map toBeConfirmed = new HashMap<>(); /** * Top level command * @param addon - addon creating the command * @param label - string for this command * @param aliases - aliases */ public CompositeCommand(Addon addon, String label, String... aliases) { super(label); this.addon = addon; this.topLabel = label; this.plugin = BSkyBlock.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); } setup(); if (!getSubCommand("help").isPresent() && !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 */ public 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 */ public CompositeCommand(CompositeCommand parent, String label, String... aliases) { super(label); this.topLabel = parent.getTopLabel(); this.plugin = BSkyBlock.getInstance(); this.parent = parent; subCommandLevel = parent.getLevel() + 1; // Add this sub-command to the parent parent.getSubCommands().put(label, 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, this); } setUsage(""); // Inherit permission prefix this.permissionPrefix = parent.getPermissionPrefix(); // Inherit world this.world = parent.getWorld(); setup(); // If this command does not define its own help class, then use the default help command if (!getSubCommand("help").isPresent() && !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(CommandSender sender, String label, String[] args) { // Get the User instance for this sender User user = User.getInstance(sender); CompositeCommand cmd = getCommandFromArgs(args); // Check for console and permissions if (cmd.onlyPlayer && !(sender instanceof Player)) { user.sendMessage("general.errors.use-in-game"); return false; } // Check perms, but only if this isn't the console if ((sender instanceof Player) && !sender.isOp() && !cmd.getPermission().isEmpty() && !sender.hasPermission(cmd.getPermission())) { user.sendMessage("general.errors.no-permission"); user.sendMessage("general.errors.you-need", TextVariables.PERMISSION, cmd.getPermission()); return false; } // 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; } // Execute and trim args return cmd.execute(user, (cmd.subCommandLevel > 0) ? args[cmd.subCommandLevel-1] : label, Arrays.asList(args).subList(cmd.subCommandLevel, args.length)); } /** * 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.isPresent()) { 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(); } /** * @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 * @param world - world to check * @param user - the User * @return set of UUIDs of all team members */ protected Set getMembers(World world, User user) { return plugin.getIslands().getMembers(world, user.getUniqueId()); } 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 BSkyBlock 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(); 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(); } /** * Convenience method to obtain the user's team leader * @param world - world to check * @param user - the User * @return UUID of player's team leader or null if user has no island */ protected UUID getTeamLeader(World world, User user) { return plugin.getIslands().getTeamLeader(world, user.getUniqueId()); } @Override public 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 */ 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; } /** * Convenience method to check if a user is a player * @param user - the User * @return true if sender is a player */ protected boolean isPlayer(User user) { return user.getPlayer() != null; } /** * Set whether this command is only for players * @param onlyPlayer - true if command only for players */ public void setOnlyPlayer(boolean onlyPlayer) { this.onlyPlayer = onlyPlayer; } /** * Sets the command parameters to be shown in help * @param parameters - string of parameters */ public void setParameters(String parameters) { this.parameters = parameters; } /* (non-Javadoc) * @see org.bukkit.command.Command#setPermission(java.lang.String) */ @Override public void setPermission(String permission) { this.permission = 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 Command setUsage(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 public List tabComplete(final CommandSender sender, final String alias, final String[] args) { List options = new ArrayList<>(); // Get command object based on args entered so far CompositeCommand cmd = getCommandFromArgs(args); // Check for console and permissions if (cmd.onlyPlayer && !(sender instanceof Player)) { return options; } if (!cmd.getPermission().isEmpty() && !sender.hasPermission(cmd.getPermission()) && !sender.isOp()) { return options; } // Add any tab completion from the subcommand options.addAll(cmd.tabComplete(User.getInstance(sender), alias, new LinkedList<>(Arrays.asList(args))).orElse(new ArrayList<>())); // Add any sub-commands automatically if (cmd.hasSubCommands()) { // Check if subcommands are visible to this sender for (CompositeCommand subCommand: cmd.getSubCommands().values()) { if (sender instanceof Player) { // Player if (subCommand.getPermission().isEmpty() || sender.hasPermission(subCommand.getPermission()) || sender.isOp()) { // Permission is okay options.add(subCommand.getLabel()); } } else { // Console if (!subCommand.onlyPlayer) { // Not a player command options.add(subCommand.getLabel()); } } } } String lastArg = args.length != 0 ? args[args.length - 1] : ""; return Util.tabLimit(options, lastArg).stream().sorted().collect(Collectors.toList()); } /** * 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 */ public String getPermissionPrefix() { return permissionPrefix; } /** * Set the permission prefix. This will be added automatically to the permission * and will apply to any sub commands too. * Do not put a dot on the end of it. * @param permissionPrefix the permissionPrefix to set */ public void setPermissionPrefix(String permissionPrefix) { this.permissionPrefix = permissionPrefix + "."; } /** * The the world that this command applies to. * @return the world */ public World getWorld() { if (world == null) { plugin.logError(getLabel() + " did not setWorld in setup!"); } return world; } /** * @param world the world to set */ public void setWorld(World world) { this.world = world; } /** * @return the addon */ public Addon getAddon() { return addon; } /** * @return top level label, e.g., island */ public String getTopLabel() { return topLabel; } /** * Tells user to confirm command by retyping * @param user - user * @param confirmed - runnable to be executed if confirmed */ public void askConfirmation(User user, Runnable confirmed) { // Check for pending confirmations if (toBeConfirmed.containsKey(user)) { if (toBeConfirmed.get(user).getTopLabel().equals(getTopLabel()) && toBeConfirmed.get(user).getLabel().equalsIgnoreCase(getLabel())) { toBeConfirmed.get(user).getTask().cancel(); Bukkit.getScheduler().runTask(getPlugin(), toBeConfirmed.get(user).getRunnable()); toBeConfirmed.remove(user); return; } else { // Player has another outstanding confirmation request that will now be cancelled user.sendMessage("general.previous-request-cancelled"); } } // Tell user that they need to confirm user.sendMessage("general.confirm", "[seconds]", String.valueOf(getSettings().getConfirmationTime())); // Set up a cancellation task BukkitTask task = Bukkit.getScheduler().runTaskLater(getPlugin(), () -> { user.sendMessage("general.request-cancelled"); toBeConfirmed.remove(user); }, getPlugin().getSettings().getConfirmationTime() * 20L); // Add to the global confirmation map toBeConfirmed.put(user, new Confirmer(getTopLabel(), getLabel(), confirmed, task)); } private class Confirmer { private final String topLabel; private final String label; private final Runnable runnable; private final BukkitTask task; /** * @param label - command label * @param runnable - runnable to run when confirmed * @param task - task ID to cancel when confirmed */ Confirmer(String topLabel, String label, Runnable runnable, BukkitTask task) { this.topLabel = topLabel; this.label = label; this.runnable = runnable; this.task = task; } /** * @return the topLabel */ public String getTopLabel() { return topLabel; } /** * @return the label */ public String getLabel() { return label; } /** * @return the runnable */ public Runnable getRunnable() { return runnable; } /** * @return the task */ public BukkitTask getTask() { return task; } } }