Java tutorial
/* * This file is part of Skript. * * Skript is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Skript is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Skript. If not, see <http://www.gnu.org/licenses/>. * * * Copyright 2011-2014 Peter Gttinger * */ package ch.njol.skript.command; import java.io.File; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.logging.Filter; import java.util.logging.LogRecord; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.Validate; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.ConsoleCommandSender; import org.bukkit.command.SimpleCommandMap; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.event.player.PlayerChatEvent; import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.server.ServerCommandEvent; import org.bukkit.help.HelpMap; import org.bukkit.help.HelpTopic; import org.bukkit.plugin.SimplePluginManager; import org.eclipse.jdt.annotation.Nullable; import ch.njol.skript.ScriptLoader; import ch.njol.skript.Skript; import ch.njol.skript.SkriptConfig; import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.classes.Parser; import ch.njol.skript.config.SectionNode; import ch.njol.skript.config.validate.SectionValidator; import ch.njol.skript.lang.Effect; import ch.njol.skript.lang.ParseContext; import ch.njol.skript.lang.SkriptParser; import ch.njol.skript.localization.ArgsMessage; import ch.njol.skript.localization.Language; import ch.njol.skript.localization.Message; import ch.njol.skript.log.BukkitLoggerFilter; import ch.njol.skript.log.RetainingLogHandler; import ch.njol.skript.log.SkriptLogger; import ch.njol.skript.registrations.Classes; import ch.njol.skript.util.Utils; import ch.njol.util.Callback; import ch.njol.util.NonNullPair; import ch.njol.util.StringUtils; /** * @author Peter Gttinger */ @SuppressWarnings("deprecation") public abstract class Commands { public final static ArgsMessage m_too_many_arguments = new ArgsMessage("commands.too many arguments"); public final static Message m_correct_usage = new Message("commands.correct usage"); public final static Message m_internal_error = new Message("commands.internal error"); private final static Map<String, ScriptCommand> commands = new HashMap<String, ScriptCommand>(); @Nullable private static SimpleCommandMap commandMap = null; @Nullable private static Map<String, Command> cmKnownCommands; @Nullable private static Set<String> cmAliases; static { try { if (Bukkit.getPluginManager() instanceof SimplePluginManager) { final Field commandMapField = SimplePluginManager.class.getDeclaredField("commandMap"); commandMapField.setAccessible(true); commandMap = (SimpleCommandMap) commandMapField.get(Bukkit.getPluginManager()); final Field knownCommandsField = SimpleCommandMap.class.getDeclaredField("knownCommands"); knownCommandsField.setAccessible(true); cmKnownCommands = (Map<String, Command>) knownCommandsField.get(commandMap); try { final Field aliasesField = SimpleCommandMap.class.getDeclaredField("aliases"); aliasesField.setAccessible(true); cmAliases = (Set<String>) aliasesField.get(commandMap); } catch (final NoSuchFieldException e) { } } } catch (final SecurityException e) { Skript.error("Please disable the security manager"); commandMap = null; } catch (final Exception e) { Skript.outdatedError(e); commandMap = null; } } private final static SectionValidator commandStructure = new SectionValidator().addEntry("usage", true) .addEntry("description", true).addEntry("permission", true).addEntry("permission message", true) .addEntry("aliases", true).addEntry("executable by", true).addSection("trigger", false); @Nullable public static List<Argument<?>> currentArguments = null; @SuppressWarnings("null") private final static Pattern escape = Pattern.compile("[" + Pattern.quote("(|)<>%\\") + "]"); @SuppressWarnings("null") private final static Pattern unescape = Pattern.compile("\\\\[" + Pattern.quote("(|)<>%\\") + "]"); private final static String escape(final String s) { return "" + escape.matcher(s).replaceAll("\\\\$0"); } private final static String unescape(final String s) { return "" + unescape.matcher(s).replaceAll("$0"); } private final static Listener commandListener = new Listener() { @SuppressWarnings("null") @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onPlayerCommand(final PlayerCommandPreprocessEvent e) { if (handleCommand(e.getPlayer(), e.getMessage().substring(1))) e.setCancelled(true); } @SuppressWarnings("null") @EventHandler(priority = EventPriority.HIGHEST) public void onServerCommand(final ServerCommandEvent e) { if (e.getCommand() == null || e.getCommand().isEmpty()) return; if (SkriptConfig.enableEffectCommands.value() && e.getCommand().startsWith(SkriptConfig.effectCommandToken.value())) { if (handleEffectCommand(e.getSender(), e.getCommand())) { e.setCommand(""); suppressUnknownCommandMessage = true; } return; } if (handleCommand(e.getSender(), e.getCommand())) { e.setCommand(""); suppressUnknownCommandMessage = true; } } }; static boolean suppressUnknownCommandMessage = false; static { BukkitLoggerFilter.addFilter(new Filter() { @Override public boolean isLoggable(final @Nullable LogRecord record) { if (record == null) return false; if (suppressUnknownCommandMessage && record.getMessage() != null && record.getMessage().toLowerCase().startsWith("unknown command")) { suppressUnknownCommandMessage = false; return false; } return true; } }); } @Nullable private final static Listener pre1_3chatListener = Skript.isRunningMinecraft(1, 3) ? null : new Listener() { @SuppressWarnings("null") @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) public void onPlayerChat(final PlayerChatEvent e) { if (!SkriptConfig.enableEffectCommands.value() || !e.getMessage().startsWith(SkriptConfig.effectCommandToken.value())) return; if (handleEffectCommand(e.getPlayer(), e.getMessage())) e.setCancelled(true); } }; @Nullable private final static Listener post1_3chatListener = !Skript.isRunningMinecraft(1, 3) ? null : new Listener() { @SuppressWarnings("null") @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) public void onPlayerChat(final AsyncPlayerChatEvent e) { if (!SkriptConfig.enableEffectCommands.value() || !e.getMessage().startsWith(SkriptConfig.effectCommandToken.value())) return; if (!e.isAsynchronous()) { if (handleEffectCommand(e.getPlayer(), e.getMessage())) e.setCancelled(true); } else { final Future<Boolean> f = Bukkit.getScheduler().callSyncMethod(Skript.getInstance(), new Callable<Boolean>() { @Override public Boolean call() throws Exception { return handleEffectCommand(e.getPlayer(), e.getMessage()); } }); try { while (true) { try { if (f.get()) e.setCancelled(true); break; } catch (final InterruptedException e1) { } } } catch (final ExecutionException e1) { Skript.exception(e1); } } } }; /** * @param sender * @param command full command string without the slash * @return whether to cancel the event */ final static boolean handleCommand(final CommandSender sender, final String command) { final String[] cmd = command.split("\\s+", 2); cmd[0] = cmd[0].toLowerCase(); if (cmd[0].endsWith("?")) { final ScriptCommand c = commands.get(cmd[0].substring(0, cmd[0].length() - 1)); if (c != null) { c.sendHelp(sender); return true; } } final ScriptCommand c = commands.get(cmd[0]); if (c != null) { // if (cmd.length == 2 && cmd[1].equals("?")) { // c.sendHelp(sender); // return true; // } if (SkriptConfig.logPlayerCommands.value() && !(sender instanceof ConsoleCommandSender)) SkriptLogger.LOGGER.info(sender.getName() + ": /" + command); c.execute(sender, "" + cmd[0], cmd.length == 1 ? "" : "" + cmd[1]); return true; } return false; } @SuppressWarnings("unchecked") final static boolean handleEffectCommand(final CommandSender sender, String command) { if (!(sender instanceof ConsoleCommandSender || sender.hasPermission("skript.effectcommands") || SkriptConfig.allowOpsToUseEffectCommands.value() && sender.isOp())) return false; final boolean wasLocal = Language.setUseLocal(false); try { command = "" + command.substring(SkriptConfig.effectCommandToken.value().length()).trim(); final RetainingLogHandler log = SkriptLogger.startRetainingLog(); try { ScriptLoader.setCurrentEvent("effect command", EffectCommandEvent.class); final Effect e = Effect.parse(command, null); ScriptLoader.deleteCurrentEvent(); if (e != null) { log.clear(); // ignore warnings and stuff log.printLog(); sender.sendMessage(ChatColor.GRAY + "executing '" + ChatColor.stripColor(command) + "'"); if (SkriptConfig.logPlayerCommands.value() && !(sender instanceof ConsoleCommandSender)) Skript.info(sender.getName() + " issued effect command: " + command); e.run(new EffectCommandEvent(sender, command)); } else { if (sender == Bukkit.getConsoleSender()) // log as SEVERE instead of INFO like printErrors below SkriptLogger.LOGGER.severe("Error in: " + ChatColor.stripColor(command)); else sender.sendMessage( ChatColor.RED + "Error in: " + ChatColor.GRAY + ChatColor.stripColor(command)); log.printErrors(sender, "(No specific information is available)"); } } finally { log.stop(); } return true; } catch (final Exception e) { Skript.exception(e, "Unexpected error while executing effect command '" + command + "' by '" + sender.getName() + "'"); sender.sendMessage(ChatColor.RED + "An internal error occurred while executing this effect. Please refer to the server log for details."); return true; } finally { Language.setUseLocal(wasLocal); } } @SuppressWarnings("null") private final static Pattern commandPattern = Pattern.compile("(?i)^command /?(\\S+)(\\s+(.+))?$"), argumentPattern = Pattern .compile("<\\s*(?:(.+?)\\s*:\\s*)?(.+?)\\s*(?:=\\s*(" + SkriptParser.wildcard + "))?\\s*>"); @Nullable public final static ScriptCommand loadCommand(final SectionNode node) { final String key = node.getKey(); if (key == null) return null; final String s = ScriptLoader.replaceOptions(key); int level = 0; for (int i = 0; i < s.length(); i++) { if (s.charAt(i) == '[') { level++; } else if (s.charAt(i) == ']') { if (level == 0) { Skript.error("Invalid placement of [optional brackets]"); return null; } level--; } } if (level > 0) { Skript.error("Invalid amount of [optional brackets]"); return null; } Matcher m = commandPattern.matcher(s); final boolean a = m.matches(); assert a; final String command = "" + m.group(1).toLowerCase(); final ScriptCommand existingCommand = commands.get(command); if (existingCommand != null && existingCommand.getLabel().equals(command)) { final File f = existingCommand.getScript(); Skript.error("A command with the name /" + command + " is already defined" + (f == null ? "" : " in " + f.getName())); return null; } final String arguments = m.group(3) == null ? "" : m.group(3); final StringBuilder pattern = new StringBuilder(); List<Argument<?>> currentArguments = new ArrayList<Argument<?>>(); m = argumentPattern.matcher(arguments); int lastEnd = 0; int optionals = 0; for (int i = 0; m.find(); i++) { pattern.append(escape("" + arguments.substring(lastEnd, m.start()))); optionals += StringUtils.count(arguments, '[', lastEnd, m.start()); optionals -= StringUtils.count(arguments, ']', lastEnd, m.start()); lastEnd = m.end(); ClassInfo<?> c; c = Classes.getClassInfoFromUserInput("" + m.group(2)); final NonNullPair<String, Boolean> p = Utils.getEnglishPlural("" + m.group(2)); if (c == null) c = Classes.getClassInfoFromUserInput(p.getFirst()); if (c == null) { Skript.error("Unknown type '" + m.group(2) + "'"); return null; } final Parser<?> parser = c.getParser(); if (parser == null || !parser.canParse(ParseContext.COMMAND)) { Skript.error("Can't use " + c + " as argument of a command"); return null; } final Argument<?> arg = Argument.newInstance(m.group(1), c, m.group(3), i, !p.getSecond(), optionals > 0); if (arg == null) return null; currentArguments.add(arg); if (arg.isOptional() && optionals == 0) { pattern.append('['); optionals++; } pattern.append("%" + (arg.isOptional() ? "-" : "") + Utils.toEnglishPlural(c.getCodeName(), p.getSecond()) + "%"); } pattern.append(escape("" + arguments.substring(lastEnd))); optionals += StringUtils.count(arguments, '[', lastEnd); optionals -= StringUtils.count(arguments, ']', lastEnd); for (int i = 0; i < optionals; i++) pattern.append(']'); String desc = "/" + command + " "; final boolean wasLocal = Language.setUseLocal(true); // use localised class names in description try { desc += StringUtils.replaceAll(pattern, "(?<!\\\\)%-?(.+?)%", new Callback<String, Matcher>() { @Override public String run(final @Nullable Matcher m) { assert m != null; final NonNullPair<String, Boolean> p = Utils.getEnglishPlural("" + m.group(1)); final String s = p.getFirst(); return "<" + Classes.getClassInfo(s).getName().toString(p.getSecond()) + ">"; } }); } finally { Language.setUseLocal(wasLocal); } desc = unescape(desc); desc = "" + desc.trim(); node.convertToEntries(0); commandStructure.validate(node); if (!(node.get("trigger") instanceof SectionNode)) return null; final String usage = ScriptLoader.replaceOptions(node.get("usage", desc)); final String description = ScriptLoader.replaceOptions(node.get("description", "")); ArrayList<String> aliases = new ArrayList<String>( Arrays.asList(ScriptLoader.replaceOptions(node.get("aliases", "")).split("\\s*,\\s*/?"))); if (aliases.get(0).startsWith("/")) aliases.set(0, aliases.get(0).substring(1)); else if (aliases.get(0).isEmpty()) aliases = new ArrayList<String>(0); final String permission = ScriptLoader.replaceOptions(node.get("permission", "")); final String permissionMessage = ScriptLoader.replaceOptions(node.get("permission message", "")); final SectionNode trigger = (SectionNode) node.get("trigger"); if (trigger == null) return null; final String[] by = ScriptLoader.replaceOptions(node.get("executable by", "console,players")) .split("\\s*,\\s*|\\s+(and|or)\\s+"); int executableBy = 0; for (final String b : by) { if (b.equalsIgnoreCase("console") || b.equalsIgnoreCase("the console")) { executableBy |= ScriptCommand.CONSOLE; } else if (b.equalsIgnoreCase("players") || b.equalsIgnoreCase("player")) { executableBy |= ScriptCommand.PLAYERS; } else { Skript.warning( "'executable by' should be either be 'players', 'console', or both, but found '" + b + "'"); } } if (!permissionMessage.isEmpty() && permission.isEmpty()) { Skript.warning("command /" + command + " has a permission message set, but not a permission"); } if (Skript.debug() || node.debug()) Skript.debug("command " + desc + ":"); final File config = node.getConfig().getFile(); if (config == null) { assert false; return null; } Commands.currentArguments = currentArguments; final ScriptCommand c; try { c = new ScriptCommand(config, command, "" + pattern.toString(), currentArguments, description, usage, aliases, permission, permissionMessage, executableBy, ScriptLoader.loadItems(trigger)); } finally { Commands.currentArguments = null; } registerCommand(c); if (Skript.logVeryHigh() && !Skript.debug()) Skript.info("registered command " + desc); currentArguments = null; return c; } // public static boolean skriptCommandExists(final String command) { // final ScriptCommand c = commands.get(command); // return c != null && c.getName().equals(command); // } public static void registerCommand(final ScriptCommand command) { if (commandMap != null) { assert cmKnownCommands != null;// && cmAliases != null; command.register(commandMap, cmKnownCommands, cmAliases); } commands.put(command.getLabel(), command); for (final String alias : command.getActiveAliases()) { commands.put(alias.toLowerCase(), command); } command.registerHelp(); } public static int unregisterCommands(final File script) { int numCommands = 0; final Iterator<ScriptCommand> commandsIter = commands.values().iterator(); while (commandsIter.hasNext()) { final ScriptCommand c = commandsIter.next(); if (script.equals(c.getScript())) { numCommands++; c.unregisterHelp(); if (commandMap != null) { assert cmKnownCommands != null;// && cmAliases != null; c.unregister(commandMap, cmKnownCommands, cmAliases); } commandsIter.remove(); } } return numCommands; } private static boolean registeredListeners = false; public final static void registerListeners() { if (!registeredListeners) { Bukkit.getPluginManager().registerEvents(commandListener, Skript.getInstance()); Bukkit.getPluginManager().registerEvents( post1_3chatListener != null ? post1_3chatListener : pre1_3chatListener, Skript.getInstance()); registeredListeners = true; } } public final static void clearCommands() { final SimpleCommandMap commandMap = Commands.commandMap; if (commandMap != null) { final Map<String, Command> cmKnownCommands = Commands.cmKnownCommands; final Set<String> cmAliases = Commands.cmAliases; assert cmKnownCommands != null;// && cmAliases != null; for (final ScriptCommand c : commands.values()) c.unregister(commandMap, cmKnownCommands, cmAliases); } for (final ScriptCommand c : commands.values()) { c.unregisterHelp(); } commands.clear(); } /** * copied from CraftBukkit (org.bukkit.craftbukkit.help.CommandAliasHelpTopic) */ public final static class CommandAliasHelpTopic extends HelpTopic { private final String aliasFor; private final HelpMap helpMap; public CommandAliasHelpTopic(final String alias, final String aliasFor, final HelpMap helpMap) { this.aliasFor = aliasFor.startsWith("/") ? aliasFor : "/" + aliasFor; this.helpMap = helpMap; name = alias.startsWith("/") ? alias : "/" + alias; Validate.isTrue(!name.equals(this.aliasFor), "Command " + name + " cannot be alias for itself"); shortText = ChatColor.YELLOW + "Alias for " + ChatColor.WHITE + this.aliasFor; } @Override public String getFullText(final @Nullable CommandSender forWho) { final StringBuilder sb = new StringBuilder(shortText); final HelpTopic aliasForTopic = helpMap.getHelpTopic(aliasFor); if (aliasForTopic != null) { sb.append("\n"); sb.append(aliasForTopic.getFullText(forWho)); } return "" + sb.toString(); } @Override public boolean canSee(final @Nullable CommandSender commandSender) { if (amendedPermission == null) { final HelpTopic aliasForTopic = helpMap.getHelpTopic(aliasFor); if (aliasForTopic != null) { return aliasForTopic.canSee(commandSender); } else { return false; } } else { return commandSender != null && commandSender.hasPermission(amendedPermission); } } } }