package tc.oc.commons.bukkit.commands; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Provider; import com.google.common.base.Splitter; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.sk89q.bukkit.util.CommandsManagerRegistration; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabExecutor; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import tc.oc.commons.core.commands.CommandRegistryImpl; import tc.oc.commons.core.commands.CommandRegistry; import tc.oc.commons.core.plugin.PluginScoped; import tc.oc.minecraft.api.event.Enableable; @PluginScoped public class BukkitCommandRegistry extends CommandRegistryImpl<CommandSender> implements CommandRegistry, TabExecutor, Enableable { // The Minecraft client gets confused by spaces in tab completion results, // so we replace them with this special character, and do the inverse // replacement for all incoming command text. private static final char FAKE_SPACE_CHAR = '\u2508'; private final @Nullable CommandsManagerRegistration registrator; @Inject BukkitCommandRegistry(Plugin plugin) { // Don't register any commands if this plugin is disabled this.registrator = plugin.isActive() ? new CommandsManagerRegistration(plugin, this, this, this.commandsManager) : null; } /** * Register a class containing commands */ @Override public <T> void register(Class<T> clazz, @Nullable Provider<? extends T> provider) { if(registrator != null) { registrator.register(clazz, provider); } } @Override public void disable() { if(registrator != null) { registrator.unregisterCommands(); } } @Override public boolean onCommand(CommandSender sender, Command command, String alias, String[] args) { handleCommand(sender, command.getName(), decode(args)); return true; } @Override public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) { // Save the word being completed, before and after decoding final String encodedWord = args[args.length - 1]; args = decode(args); final String decodedWord = args[args.length - 1]; List<String> options = handleCompletion(sender, command.getName(), args); if(options != null) { // If sender is a player, encode the completion results. // A single result doesn't need to be encoded, because the // client won't try to cycle through the results. if(sender instanceof Player && options.size() > 1) { options = encode(options); } // When completing a word with an encoded space, we have to add the // part that was lost from decoding back to each completion result // e.g. the client tries to complete "the-fen", but the command method // returns results for "fen", so we have to restore "the-" for each result. if(!encodedWord.equals(decodedWord)) { final String extra = encodedWord.substring(0, encodedWord.length() - decodedWord.length()); options = Lists.transform(options, option -> extra + option); } } return options; } private static List<String> encode(List<String> decodedOptions) { List<String> escapedOptions = decodedOptions; for(int i = 0; i < decodedOptions.size(); i++) { final String decodedOption = decodedOptions.get(i); final String encodedOption = decodedOption.replace(' ', FAKE_SPACE_CHAR); if(!decodedOption.equals(encodedOption)) { if(escapedOptions == decodedOptions) { escapedOptions = new ArrayList<>(decodedOptions); } escapedOptions.set(i, encodedOption); } } return escapedOptions; } private static String[] decode(String[] encodedArgs) { List<String> decodedArgs = null; for(int i = 0; i < encodedArgs.length; i++) { final String encodedArg = encodedArgs[i]; if(encodedArg.indexOf(FAKE_SPACE_CHAR) != -1) { if(decodedArgs == null) { decodedArgs = new ArrayList<>(); for(int j = 0; j < i; j++) { decodedArgs.add(encodedArgs[j]); } } Iterables.addAll(decodedArgs, Splitter.on(FAKE_SPACE_CHAR).split(encodedArg)); } else if(decodedArgs != null) { decodedArgs.add(encodedArg); } } return decodedArgs == null ? encodedArgs : decodedArgs.toArray(new String[decodedArgs.size()]); } }