/* * This file is part of NucleusFramework for Bukkit, licensed under the MIT License (MIT). * * Copyright (c) JCThePants (www.jcwhatever.com) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.jcwhatever.nucleus.managed.commands.utils; import com.jcwhatever.nucleus.Nucleus; import com.jcwhatever.nucleus.internal.NucLang; import com.jcwhatever.nucleus.managed.commands.CommandInfo; import com.jcwhatever.nucleus.managed.commands.ICommand; import com.jcwhatever.nucleus.managed.commands.ICommandDispatcher; import com.jcwhatever.nucleus.managed.commands.IRegisteredCommand; import com.jcwhatever.nucleus.managed.commands.arguments.ICommandArguments; import com.jcwhatever.nucleus.managed.commands.arguments.ILocationHandler; import com.jcwhatever.nucleus.managed.commands.exceptions.CommandException; import com.jcwhatever.nucleus.managed.commands.exceptions.InvalidArgumentException; import com.jcwhatever.nucleus.managed.commands.exceptions.InvalidCommandSenderException; import com.jcwhatever.nucleus.managed.commands.mixins.IInitializableCommand; import com.jcwhatever.nucleus.managed.language.Localizable; import com.jcwhatever.nucleus.managed.messaging.ChatPaginator; import com.jcwhatever.nucleus.managed.messaging.IMessenger; import com.jcwhatever.nucleus.mixins.INamed; import com.jcwhatever.nucleus.mixins.IPlayerReference; import com.jcwhatever.nucleus.mixins.IPluginOwned; import com.jcwhatever.nucleus.providers.regionselect.IRegionSelection; import com.jcwhatever.nucleus.providers.regionselect.RegionSelection; import com.jcwhatever.nucleus.regions.SimpleRegionSelection; import com.jcwhatever.nucleus.storage.settings.ISettingsManager; import com.jcwhatever.nucleus.storage.settings.PropertyDefinition; import com.jcwhatever.nucleus.storage.settings.PropertyValueType; import com.jcwhatever.nucleus.utils.CollectionUtils; import com.jcwhatever.nucleus.utils.PreCon; import com.jcwhatever.nucleus.utils.player.PlayerUtils; import com.jcwhatever.nucleus.utils.text.TextColor; import com.jcwhatever.nucleus.utils.text.TextUtils; import com.jcwhatever.nucleus.utils.text.components.IChatMessage; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import javax.annotation.Nullable; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Deque; import java.util.List; import java.util.UUID; /** * An abstract implementation of a command that adds common protected * utility methods for use in the command implementation. * * <p>The command implementation must have an {@link CommandInfo} annotation.</p> */ public abstract class AbstractCommand implements IInitializableCommand, IPluginOwned { @Localizable private static final String _SAME_WORLD_REGION_SELECT = "You need to be in the same world as the region selection."; @Localizable private static final String _INVALID_REGION = "Invalid region. Both points must be in the same world."; @Localizable private static final String _SET_SELECTION_FAILED = "Failed to set region selection."; @Localizable private static final String _NO_REGION_SELECTED = "No cuboid region selection found. Please select a region first."; @Localizable private static final String _UNRECOGNIZED_PROPERTY = "The property '{0: property name}' is not a recognized setting."; @Localizable private static final String _CLEAR_PROPERTY_FAILED = "Failed to clear value on property '{0: property name}'."; @Localizable private static final String _CLEAR_PROPERTY_SUCCESS = "'{0: property name}' value cleared."; @Localizable private static final String _INVALID_PROPERTY_VALUE = "'{0: property value}' is not a valid value for the property called '{1: property name}'."; @Localizable private static final String _SET_PROPERTY_FAILED = "Failed to set property '{0: property name}'."; @Localizable private static final String _SET_PROPERTY_SUCCESS = "'{0: property value}' value changed to {1: property name}."; @Localizable private static final String _PROPERTY_DESCRIPTION = "Description for '{0: property value}':\n{GRAY}{1: description}"; private static final CollectionUtils.ISearchTextGetter<Object> GENERIC_TEXT_GETTER = new CollectionUtils.ISearchTextGetter<Object>() { @Override public String getText(Object obj) { if (obj == null) return null; if (obj instanceof INamed) return ((INamed) obj).getName(); return obj.toString(); } }; private static final CollectionUtils.ISearchTextGetter<Player> PLAYER_OBJECT_NAME_GETTER = new CollectionUtils.ISearchTextGetter<Player>() { @Override public String getText(Player player) { return player.getName(); } }; private static final CollectionUtils.ISearchTextGetter<Object> PLAYER_NAME_GETTER = new CollectionUtils.ISearchTextGetter<Object>() { @Override public String getText(Object obj) { if (obj instanceof Player) return ((Player) obj).getName(); if (obj instanceof UUID) return PlayerUtils.getPlayerName((UUID) obj); if (obj instanceof String) return (String)obj; if (obj instanceof IPlayerReference) return ((IPlayerReference) obj).getPlayer().getName(); return GENERIC_TEXT_GETTER.getText(obj); } }; private static final CollectionUtils.ISearchTextGetter<Object> WORLD_NAME_GETTER = new CollectionUtils.ISearchTextGetter<Object>() { @Override public String getText(Object obj) { if (obj instanceof World) return ((World) obj).getName(); if (obj instanceof UUID) { World world = Bukkit.getWorld((UUID) obj); return world == null ? null : world.getName(); } if (obj instanceof String) return (String)obj; return GENERIC_TEXT_GETTER.getText(obj); } }; private Deque<Class<? extends ICommand>> _registerQueue; private IRegisteredCommand _command; private IMessenger _msg; @Override public void init(IRegisteredCommand registeredCommand) { PreCon.notNull(registeredCommand); _command = registeredCommand; _msg = Nucleus.getMessengerFactory().get(registeredCommand.getPlugin()); // register sub commands that were registered before the command was initialized if (_registerQueue != null) { while (!_registerQueue.isEmpty()) { Class<? extends ICommand> commandClass = _registerQueue.removeFirst(); _command.registerCommand(commandClass); } _registerQueue = null; } } @Override public Plugin getPlugin() { return _command.getPlugin(); } /** * Get the commands dispatcher. */ public ICommandDispatcher getDispatcher() { return _command.getDispatcher(); } /** * Get the registered command. */ public IRegisteredCommand getRegistered() { return _command; } /** * Send a debug message to the console. * * @param msg The message to send. * @param params The message format parameters. */ protected void debug(CharSequence msg, Object... params) { _msg.debug(msg, params); } /** * Tell the {@link org.bukkit.command.CommandSender} a generic message. */ protected void tell(CommandSender sender, CharSequence msg, Object... params) { _msg.tell(sender, TextUtils.format(msg, params)); } /** * Tell the {@link org.bukkit.command.CommandSender} the command * executed the request successfully. */ protected void tellSuccess(CommandSender sender, CharSequence msg, Object... params) { _msg.tell(sender, TextColor.GREEN + msg.toString(), params); } /** * Tell the {@link org.bukkit.command.CommandSender} the command failed to * perform the requested task. */ protected void tellError(CommandSender sender, CharSequence msg, Object... params) { _msg.tell(sender, TextColor.RED + msg.toString(), params); } /** * Get the command to type to get help for the current command. */ protected IChatMessage getInlineHelpCommand() { return getInlineHelpCommand(getRegistered()); } /** * Get the command to type to get help for the specified sub command. * * @param subCommandName The name of the sub-command. */ protected IChatMessage getInlineHelpCommand(String subCommandName) { PreCon.notNullOrEmpty(subCommandName); IRegisteredCommand command = getCommand(subCommandName); if (command == null) { throw new IllegalArgumentException("Command not found: " + subCommandName); } return getInlineHelpCommand(command); } /** * Get the command to type to get help for the specified command. * * @param command The command. */ protected IChatMessage getInlineHelpCommand(IRegisteredCommand command) { PreCon.notNull(command); ICommandUsageGenerator generator = Nucleus.getCommandManager() .getUsageGenerator(ICommandUsageGenerator.INLINE_HELP); return generator.generate(command); } /** * Get the command to type for the current command. */ protected IChatMessage getInlineCommand() { return getInlineCommand(getRegistered()); } /** * Get the command to type for the specified sub command. * * @param subCommandName The name of the sub-command. */ protected IChatMessage getInlineCommand(String subCommandName) { PreCon.notNullOrEmpty(subCommandName); IRegisteredCommand command = getCommand(subCommandName); if (command == null) { throw new IllegalArgumentException("Command not found: " + subCommandName); } return getInlineCommand(command); } /** * Get the command to type for the specified command. * * @param command The command. */ protected IChatMessage getInlineCommand(IRegisteredCommand command) { PreCon.notNull(command); ICommandUsageGenerator generator = Nucleus.getCommandManager() .getUsageGenerator(ICommandUsageGenerator.INLINE_COMMAND); return generator.generate(command); } /** * Set the specified players region selection. * * <p>Handles error message if any.</p> * * @param player The player * @param p1 The first location of the selection. * @param p2 The second location of the selection. * * @throws CommandException if failed to set region selection. */ protected void setRegionSelection(Player player, Location p1, Location p2) throws CommandException { PreCon.notNull(player); PreCon.notNull(p1); PreCon.notNull(p2); if (!player.getWorld().equals(p1.getWorld())) throw new CommandException(NucLang.get(_SAME_WORLD_REGION_SELECT)); if (!p1.getWorld().equals(p2.getWorld())) throw new CommandException(NucLang.get(_INVALID_REGION)); boolean isSuccess = Nucleus.getProviders() .getRegionSelection().setSelection(player, new SimpleRegionSelection(p1, p2)); if (!isSuccess) throw new CommandException(NucLang.get(_SET_SELECTION_FAILED)); } /** * Get the specified players current region selection. * * <p>Handles error message if any.</p> * * @param player The player * * @return {@link IRegionSelection} object that defines the selection. * * @throws CommandException if the player does not have a region selected. */ protected IRegionSelection getRegionSelection(Player player) throws CommandException { PreCon.notNull(player); IRegionSelection selection = RegionSelection.get(player); // Check for region selection if (selection == null) throw new CommandException(NucLang.get(_NO_REGION_SELECTED)); return selection; } /** * Create a new {@link ChatPaginator}. * * @param cmdArgs The command arguments. * @param itemsPerPage The number of items per page. * @param title The paginator title. * @param titleArgs The paginator title format arguments. */ protected ChatPaginator createPagin(ICommandArguments cmdArgs, int itemsPerPage, CharSequence title, Object... titleArgs) { PaginatorCommands paginCommands = new PaginatorCommands(cmdArgs); return new ChatPaginator(getPlugin(), itemsPerPage, paginCommands, title, titleArgs); } /** * Clear a setting from a settings manager back to it's default value. * * <p>Handles error and success messages.</p> * * @param sender The command sender * @param settings The settings manager that contains and defines possible settings. * @param args The command arguments provided by the command sender. * @param propertyArgName The name of the command argument parameter that contains the * property name of the setting. * * @throws InvalidArgumentException if the setting cannot be cleared due to an incorrect argument * or other problem. */ protected void clearSetting(CommandSender sender, final ISettingsManager settings, ICommandArguments args, String propertyArgName) throws CommandException { final String settingName = args.getString(propertyArgName); PropertyDefinition defs = settings.getDefinitions().get(settingName); if (defs == null) throw new CommandException(NucLang.get(_UNRECOGNIZED_PROPERTY, settingName)); if (!settings.set(settingName, null)) throw new CommandException(NucLang.get(_CLEAR_PROPERTY_FAILED, settingName)); tellSuccess(sender, NucLang.get(_CLEAR_PROPERTY_SUCCESS, settingName)); } /** * Set a setting into a settings manager using user command input. * * <p>Handles error and success messages to the user.</p> * * @param sender The command sender * @param settings The settings manager that contains and defines possible settings. * @param args The command arguments provided by the command sender. * @param propertyArgName The name of the command argument parameter that contains the property * name of the setting. * @param valueArgName The name of the command argument parameter that contains the value * of the property. * * @throws InvalidArgumentException If the value provided by the command sender is not valid. * @throws InvalidCommandSenderException If the command sender cannot set the value due to sender type. * @throws CommandException for all other problems. */ protected void setSetting(CommandSender sender, final ISettingsManager settings, ICommandArguments args, String propertyArgName, String valueArgName) throws CommandException { setSetting(sender, settings, args, propertyArgName, valueArgName, null); } /** * Set a setting into a settings manager using user command input. * * <p>Handles error and success messages to the user.</p> * * @param sender The command sender * @param settings The settings manager that contains and defines possible settings. * @param args The command arguments provided by the command sender. * @param propertyArgName The name of the command argument parameter that contains the property name * of the setting. * @param valueArgName The name of the command argument parameter that contains the value of the * property. * @param onSuccess A runnable to run if the setting is successfully set. * * @throws InvalidArgumentException if the value provided by the command sender is not valid. * @throws InvalidCommandSenderException if the command sender cannot set the value due to sender type. * @throws CommandException for all other problems. */ protected void setSetting(CommandSender sender, final ISettingsManager settings, ICommandArguments args, String propertyArgName, String valueArgName, @Nullable final Runnable onSuccess) throws CommandException { PreCon.notNull(sender); PreCon.notNull(settings); PreCon.notNull(args); PreCon.notNullOrEmpty(propertyArgName); PreCon.notNullOrEmpty(valueArgName); final String settingName = args.getString(propertyArgName); Object value; // get settings definitions final PropertyDefinition propertyDefinition = settings.getDefinitions().get(settingName); if (propertyDefinition == null) throw new CommandException(NucLang.get(_UNRECOGNIZED_PROPERTY, settingName)); PropertyValueType valueType = propertyDefinition.getValueType(); switch (valueType.getType()) { case LOCATION: // get location value to use args.getLocation(sender, valueArgName, new ILocationHandler() { @Override public void onLocationRetrieved (Player player, Location location) { if (settings.set(settingName, location)) { IChatMessage successMessage = NucLang.get(_SET_PROPERTY_SUCCESS, settingName, TextUtils.formatLocation(location, true)); tellSuccess(player, successMessage); if (onSuccess != null) onSuccess.run(); } else { tellError(player, NucLang.get(_SET_PROPERTY_FAILED, settingName)); tell(player, NucLang.get(_PROPERTY_DESCRIPTION, propertyDefinition.getName(), propertyDefinition.getDescription())); } } }); return; // finish case ITEM_STACK_ARRAY: value = args.getItemStack(sender, valueArgName); break; default: value = args.getString(valueArgName); break; } // make sure the result is valid if (!settings.set(settingName, value)) { throw new CommandException( NucLang.get(_INVALID_PROPERTY_VALUE, value, settingName).toString() + '\n' + NucLang.get(_PROPERTY_DESCRIPTION, propertyDefinition.getName(), propertyDefinition.getDescription()) ); } tellSuccess(sender, NucLang.get(_SET_PROPERTY_SUCCESS, settingName, args.getString(valueArgName))); if (onSuccess != null) onSuccess.run(); } /** * Register a sub command. * * @param subCommandClass The sub command class. * * @return True if the sub command was registered. */ protected boolean registerCommand(Class<? extends ICommand> subCommandClass) { PreCon.notNull(subCommandClass); if (_command == null) { if (_registerQueue == null) _registerQueue = new ArrayDeque<>(10); _registerQueue.addLast(subCommandClass); return true; } return _command.registerCommand(subCommandClass); } /** * Get a sub command by name. * * @param commandName The name of the command. * * @return The command or null if not found. */ @Nullable protected IRegisteredCommand getCommand(String commandName) { PreCon.notNull(commandName); if (_command == null) return null; return _command.getCommand(commandName); } /** * Fill a "matches" collection with names of online players that are possible matches * for the last argument. * * <p>Use with tab completion for player name arguments.</p> * * @param arguments The current arguments. * @param matches The collection of tab completion matches. */ protected void tabCompletePlayerName(String[] arguments, Collection<String> matches) { PreCon.notNull(arguments); PreCon.notNull(matches); if (arguments.length == 0) return; String tabArg = arguments[arguments.length - 1]; searchOnlinePlayers(tabArg, matches); } /** * Fill a "matches" collection with names of players that are possible matches * for the last argument. * * <p>Use with tab completion for player name arguments.</p> * * @param arguments The current arguments. * @param candidates A collection of candidates to search. * @param matches The collection of tab completion matches. */ protected void tabCompletePlayerName(String[] arguments, Collection candidates, Collection<String> matches) { PreCon.notNull(arguments); PreCon.notNull(candidates); PreCon.notNull(matches); if (arguments.length == 0) return; String tabArg = arguments[arguments.length - 1]; searchPlayers(tabArg, candidates, matches); } /** * Fill a "matches" collection with names of worlds that are possible matches * for the last argument. * * <p>Use with tab completion for world name arguments.</p> * * @param arguments The current arguments. * @param matches The collection of tab completion matches. */ protected void tabCompleteWorldName(String[] arguments, Collection<String> matches) { PreCon.notNull(arguments); PreCon.notNull(matches); if (arguments.length == 0) return; String tabArg = arguments[arguments.length - 1]; searchWorlds(tabArg, Bukkit.getWorlds(), matches); } /** * Fill a "matches" collection with names of worlds that are possible matches * for the last argument. * * <p>Use with tab completion for world name arguments.</p> * * @param arguments The current arguments. * @param candidates A collection of candidates to search. * @param matches The collection of tab completion matches. */ protected void tabCompleteWorldName(String[] arguments, Collection candidates, Collection<String> matches) { PreCon.notNull(arguments); PreCon.notNull(candidates); PreCon.notNull(matches); if (arguments.length == 0) return; String tabArg = arguments[arguments.length - 1]; searchWorlds(tabArg, candidates, matches); } /** * Fill a "matches" collection with names of worlds that are possible matches * for the last argument. * * <p>Use with tab completion for enum arguments.</p> * * @param arguments The current arguments. * @param enumType The enum type. * @param matches The collection of tab completion matches. */ protected void tabCompleteEnum(String[] arguments, Class<? extends Enum> enumType, Collection<String> matches) { PreCon.notNull(arguments); PreCon.notNull(enumType); PreCon.notNull(matches); if (arguments.length == 0) return; String tabArg = arguments[arguments.length - 1]; searchEnum(tabArg, enumType, matches); } /** * Fill a "matches" collection with names of worlds that are possible matches * for the last argument. * * <p>Use with tab completion for generic arguments.</p> * * @param arguments The current arguments. * @param candidates A collection of candidates to search. * @param matches The collection of tab completion matches. */ protected void tabCompleteSearch(String[] arguments, Collection candidates, Collection<String> matches) { tabCompleteSearch(arguments, candidates, matches, GENERIC_TEXT_GETTER); } /** * Fill a "matches" collection with names of worlds that are possible matches * for the last argument. * * <p>Use with tab completion for generic arguments.</p> * * @param arguments The current arguments. * @param candidates A collection of candidates to search. * @param matches The collection of tab completion matches. * @param textGetter A text getter to convert candidate objects into searchable text. */ protected void tabCompleteSearch(String[] arguments, Collection candidates, Collection<String> matches, CollectionUtils.ISearchTextGetter textGetter) { PreCon.notNull(arguments); PreCon.notNull(candidates); PreCon.notNull(matches); PreCon.notNull(textGetter); if (arguments.length == 0) return; String tabArg = arguments[arguments.length - 1]; search(tabArg, candidates, matches, textGetter); } /** * Fill an output collection with names of online players that are possible matches * for the specified search term. * * @param searchTerm The search term. * @param output The output collection of name matches. * * @return The output collection. */ protected Collection<String> searchOnlinePlayers(String searchTerm, Collection<String> output) { PreCon.notNull(searchTerm); PreCon.notNull(output); Collection<Player> players = new ArrayList<>(Bukkit.getOnlinePlayers()); List<Player> matchingPlayers = CollectionUtils.textSearch(players, searchTerm, PLAYER_OBJECT_NAME_GETTER); if (output instanceof ArrayList) ((ArrayList) output).ensureCapacity(matchingPlayers.size()); for (Player player : matchingPlayers) { output.add(player.getName()); } return output; } /** * Fill an output collection with names of players that are possible matches * for the specified search term. * * @param searchTerm The search term. * @param candidates The candidates to search. * @param output The output collection of name matches. * * @return The output collection. */ protected Collection<String> searchPlayers(String searchTerm, Collection candidates, Collection<String> output) { return search(searchTerm, candidates, output, PLAYER_NAME_GETTER); } /** * Fill an output collection with names of worlds that are possible matches * for the specified search term. * * @param searchTerm The search term. * @param candidates The candidates to search. * @param output The output collection of name matches. * * @return The output collection. */ protected Collection<String> searchWorlds(String searchTerm, Collection candidates, Collection<String> output) { return search(searchTerm, candidates, output, WORLD_NAME_GETTER); } /** * Fill an output collection with names of enum constants that are possible matches * for the specified search term. * * @param searchTerm The search term. * @param enumType The enum type. * @param output The output collection of name matches. * * @return The output collection. */ protected Collection<String> searchEnum(String searchTerm, Class<? extends Enum> enumType, Collection<String> output) { Collection<String> candidates = new ArrayList<>(enumType.getEnumConstants().length); for (Enum e : enumType.getEnumConstants()) { candidates.add(e.name().toLowerCase()); } return search(searchTerm, candidates, output, GENERIC_TEXT_GETTER); } /** * Generic candidate search. * * <p>Use candidates toString method to retrieve searchable text.</p> * * @param searchTerm The search term. * @param candidates The search candidates. * @param output The output result collection. * * @return The output result collection. */ protected Collection<String> search(String searchTerm, Collection candidates, Collection<String> output) { return search(searchTerm, candidates, output, GENERIC_TEXT_GETTER); } /** * Generic candidate search. * * @param searchTerm The search term. * @param candidates The search candidates. * @param output The output result collection. * @param textGetter The text getter for converting objects to strings. * * @return The output result collection. */ protected Collection<String> search(String searchTerm, Collection candidates, Collection<String> output, CollectionUtils.ISearchTextGetter textGetter) { PreCon.notNull(searchTerm); PreCon.notNull(candidates); PreCon.notNull(output); @SuppressWarnings("unchecked") List<Object> matching = CollectionUtils.textSearch(candidates, searchTerm, textGetter); if (output instanceof ArrayList) ((ArrayList) output).ensureCapacity(matching.size()); for (Object match : matching) { @SuppressWarnings("unchecked") String text = textGetter.getText(match); output.add(text); } return output; } }