diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java index 72eac4c4..c9051ebd 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java @@ -1,544 +1,560 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program 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 2 of - * the License, or (at your option) any later version. - * - * This program 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 this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol; - -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.logging.Level; -import java.util.logging.Logger; - -import net.sf.cglib.proxy.Factory; - -import org.bukkit.ChatColor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; - -import com.comphenix.protocol.concurrency.AbstractIntervalTree; -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.error.Report; -import com.comphenix.protocol.error.ReportType; -import com.comphenix.protocol.events.ConnectionSide; -import com.comphenix.protocol.events.ListenerPriority; -import com.comphenix.protocol.events.ListeningWhitelist; -import com.comphenix.protocol.events.PacketEvent; -import com.comphenix.protocol.events.PacketListener; -import com.comphenix.protocol.injector.GamePhase; -import com.comphenix.protocol.reflect.FieldAccessException; -import com.comphenix.protocol.reflect.PrettyPrinter; -import com.comphenix.protocol.utility.ChatExtensions; -import com.comphenix.protocol.utility.MinecraftReflection; -import com.google.common.collect.DiscreteDomains; -import com.google.common.collect.Range; -import com.google.common.collect.Ranges; -import com.google.common.collect.Sets; - -/** - * Handles the "packet" debug command. - * - * @author Kristian - */ -class CommandPacket extends CommandBase { - public static final ReportType REPORT_CANNOT_SEND_MESSAGE = new ReportType("Cannot send chat message."); - - private interface DetailedPacketListener extends PacketListener { - /** - * Determine whether or not the given packet listener is detailed or not. - * @return TRUE if it is detailed, FALSE otherwise. - */ - public boolean isDetailed(); - } - - private enum SubCommand { - ADD, REMOVE, NAMES, PAGE; - } - - /** - * Name of this command. - */ - public static final String NAME = "packet"; - - /** - * Number of lines per page. - */ - public static final int PAGE_LINE_COUNT = 9; - - private Plugin plugin; - private Logger logger; - private ProtocolManager manager; - - private ChatExtensions chatter; - - // Paged message - private Map> pagedMessage = new WeakHashMap>(); - - // Registered packet listeners - private AbstractIntervalTree clientListeners = createTree(ConnectionSide.CLIENT_SIDE); - private AbstractIntervalTree serverListeners = createTree(ConnectionSide.SERVER_SIDE); - - // Filter packet events - private CommandFilter filter; - - public CommandPacket(ErrorReporter reporter, Plugin plugin, Logger logger, CommandFilter filter, ProtocolManager manager) { - super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 1); - this.plugin = plugin; - this.logger = logger; - this.manager = manager; - this.filter = filter; - this.chatter = new ChatExtensions(manager); - } - - /** - * Construct a packet listener interval tree. - * @return Construct the tree. - */ - private AbstractIntervalTree createTree(final ConnectionSide side) { - return new AbstractIntervalTree() { - @Override - protected Integer decrementKey(Integer key) { - return key != null ? key - 1 : null; - } - - @Override - protected Integer incrementKey(Integer key) { - return key != null ? key + 1 : null; - } - - @Override - protected void onEntryAdded(Entry added) { - // Ensure that the starting ID and the ending ID is correct - // This is necessary because the interval tree may change the range. - if (added != null) { - Range key = added.getKey(); - DetailedPacketListener listener = added.getValue(); - DetailedPacketListener corrected = createPacketListener( - side, key.lowerEndpoint(), key.upperEndpoint(), listener.isDetailed()); - - added.setValue(corrected); - - if (corrected != null) { - manager.addPacketListener(corrected); - } else { - // Never mind - remove(key.lowerEndpoint(), key.upperEndpoint()); - } - } - } - - @Override - protected void onEntryRemoved(Entry removed) { - // Remove the listener - if (removed != null) { - DetailedPacketListener listener = removed.getValue(); - - if (listener != null) { - manager.removePacketListener(listener); - } - } - } - }; - } - - /** - * Send a message without invoking the packet listeners. - * @param receiver - the player to send it to. - * @param message - the message to send. - * @return TRUE if the message was sent successfully, FALSE otherwise. - */ - public void sendMessageSilently(CommandSender receiver, String message) { - try { - chatter.sendMessageSilently(receiver, message); - } catch (InvocationTargetException e) { - reporter.reportDetailed(this, - Report.newBuilder(REPORT_CANNOT_SEND_MESSAGE).error(e).callerParam(receiver, message) - ); - } - } - - /** - * Broadcast a message without invoking any packet listeners. - * @param message - message to send. - * @param permission - permission required to receieve the message. NULL to target everyone. - */ - public void broadcastMessageSilently(String message, String permission) { - try { - chatter.broadcastMessageSilently(message, permission); - } catch (InvocationTargetException e) { - reporter.reportDetailed(this, - Report.newBuilder(REPORT_CANNOT_SEND_MESSAGE).error(e).callerParam(message, permission) - ); - } - } - - private void printPage(CommandSender sender, int pageIndex) { - List paged = pagedMessage.get(sender); - - // Make sure the player has any pages - if (paged != null) { - int lastPage = ((paged.size() - 1) / PAGE_LINE_COUNT) + 1; - - for (int i = PAGE_LINE_COUNT * (pageIndex - 1); i < PAGE_LINE_COUNT * pageIndex; i++) { - if (i < paged.size()) { - sendMessageSilently(sender, " " + paged.get(i)); - } - } - - // More data? - if (pageIndex < lastPage) { - sendMessageSilently(sender, "Send /packet page " + (pageIndex + 1) + " for the next page."); - } - - } else { - sendMessageSilently(sender, ChatColor.RED + "No pages found."); - } - } - - /* - * Description: Adds or removes a simple packet listener. - Usage: / add|remove client|server|both [ID start] [ID stop] [detailed] - */ - @Override - protected boolean handleCommand(CommandSender sender, String[] args) { - try { - SubCommand subCommand = parseCommand(args, 0); - - // Commands with different parameters - if (subCommand == SubCommand.PAGE) { - int page = Integer.parseInt(args[1]); - - if (page > 0) - printPage(sender, page); - else - sendMessageSilently(sender, ChatColor.RED + "Page index must be greater than zero."); - return true; - } - - ConnectionSide side = parseSide(args, 1, ConnectionSide.BOTH); - - Integer lastIndex = args.length - 1; - Boolean detailed = parseBoolean(args, "detailed", lastIndex); - - // See if the last element is a boolean - if (detailed == null) { - detailed = false; - } else { - lastIndex--; - } - - // Make sure the packet IDs are valid - List> ranges = RangeParser.getRanges(args, 2, lastIndex, Ranges.closed(0, 255)); - - if (ranges.isEmpty()) { - // Use every packet ID - ranges.add(Ranges.closed(0, 255)); - } - - // Perform commands - if (subCommand == SubCommand.ADD) { - // The add command is dangerous - don't default on the connection side - if (args.length == 1) { - sender.sendMessage(ChatColor.RED + "Please specify a connectionn side."); - return false; - } - - executeAddCommand(sender, side, detailed, ranges); - } else if (subCommand == SubCommand.REMOVE) { - executeRemoveCommand(sender, side, detailed, ranges); - } else if (subCommand == SubCommand.NAMES) { - executeNamesCommand(sender, side, ranges); - } - - } catch (NumberFormatException e) { - sendMessageSilently(sender, ChatColor.RED + "Cannot parse number: " + e.getMessage()); - } catch (IllegalArgumentException e) { - sendMessageSilently(sender, ChatColor.RED + e.getMessage()); - } - - return true; - } - - private void executeAddCommand(CommandSender sender, ConnectionSide side, Boolean detailed, List> ranges) { - for (Range range : ranges) { - DetailedPacketListener listener = addPacketListeners(side, range.lowerEndpoint(), range.upperEndpoint(), detailed); - sendMessageSilently(sender, ChatColor.BLUE + "Added listener " + getWhitelistInfo(listener)); - } - } - - private void executeRemoveCommand(CommandSender sender, ConnectionSide side, Boolean detailed, List> ranges) { - int count = 0; - - // Remove each packet listener - for (Range range : ranges) { - count += removePacketListeners(side, range.lowerEndpoint(), range.upperEndpoint(), detailed).size(); - } - - sendMessageSilently(sender, ChatColor.BLUE + "Fully removed " + count + " listeners."); - } - - private void executeNamesCommand(CommandSender sender, ConnectionSide side, List> ranges) { - Set named = getNamedPackets(side); - List messages = new ArrayList(); - - // Print the equivalent name of every given ID - for (Range range : ranges) { - for (int id : range.asSet(DiscreteDomains.integers())) { - if (named.contains(id)) { - messages.add(ChatColor.WHITE + "" + id + ": " + ChatColor.BLUE + Packets.getDeclaredName(id)); - } - } - } - - if (sender instanceof Player && messages.size() > 0 && messages.size() > PAGE_LINE_COUNT) { - // Divide the messages into chuncks - pagedMessage.put(sender, messages); - printPage(sender, 1); - - } else { - // Just print the damn thing - for (String message : messages) { - sendMessageSilently(sender, message); - } - } - } - - /** - * Retrieve whitelist information about a given listener. - * @param listener - the given listener. - * @return Whitelist information. - */ - private String getWhitelistInfo(PacketListener listener) { - boolean sendingEmpty = ListeningWhitelist.isEmpty(listener.getSendingWhitelist()); - boolean receivingEmpty = ListeningWhitelist.isEmpty(listener.getReceivingWhitelist()); - - if (!sendingEmpty && !receivingEmpty) - return String.format("Sending: %s, Receiving: %s", listener.getSendingWhitelist(), listener.getReceivingWhitelist()); - else if (!sendingEmpty) - return listener.getSendingWhitelist().toString(); - else if (!receivingEmpty) - return listener.getReceivingWhitelist().toString(); - else - return "[None]"; - } - - private Set getValidPackets(ConnectionSide side) throws FieldAccessException { - HashSet supported = Sets.newHashSet(); - - if (side.isForClient()) - supported.addAll(Packets.Client.getSupported()); - else if (side.isForServer()) - supported.addAll(Packets.Server.getSupported()); - - return supported; - } - - private Set getNamedPackets(ConnectionSide side) { - - Set valids = null; - Set result = Sets.newHashSet(); - - try { - valids = getValidPackets(side); - } catch (FieldAccessException e) { - valids = Ranges.closed(0, 255).asSet(DiscreteDomains.integers()); - } - - // Check connection side - if (side.isForClient()) - result.addAll(Packets.Client.getRegistry().values()); - if (side.isForServer()) - result.addAll(Packets.Server.getRegistry().values()); - - // Remove invalid packets - result.retainAll(valids); - return result; - } - - public DetailedPacketListener createPacketListener(final ConnectionSide side, int idStart, int idStop, final boolean detailed) { - Set range = Ranges.closed(idStart, idStop).asSet(DiscreteDomains.integers()); - Set packets; - - try { - // Only use supported packet IDs - packets = new HashSet(getValidPackets(side)); - packets.retainAll(range); - - } catch (FieldAccessException e) { - // Don't filter anything then - packets = range; - } - - // Ignore empty sets - if (packets.isEmpty()) - return null; - - // Create the listener we will be using - final ListeningWhitelist whitelist = new ListeningWhitelist(ListenerPriority.MONITOR, packets, GamePhase.BOTH); - - return new DetailedPacketListener() { - @Override - public void onPacketSending(PacketEvent event) { - if (side.isForServer() && filter.filterEvent(event)) { - printInformation(event); - } - } - - @Override - public void onPacketReceiving(PacketEvent event) { - if (side.isForClient() && filter.filterEvent(event)) { - printInformation(event); - } - } - - private void printInformation(PacketEvent event) { - String format = side.isForClient() ? - "Received %s (%s) from %s" : - "Sent %s (%s) to %s"; - String shortDescription = String.format(format, - Packets.getDeclaredName(event.getPacketID()), - event.getPacketID(), - event.getPlayer().getName() - ); - - // Detailed will print the packet's content too - if (detailed) { - try { - Object packet = event.getPacket().getHandle(); - Class clazz = packet.getClass(); - - // Get the first Minecraft super class - while ((!MinecraftReflection.isMinecraftClass(clazz) || - Factory.class.isAssignableFrom(clazz)) && clazz != Object.class) { - clazz = clazz.getSuperclass(); - } - - logger.info(shortDescription + ":\n" + - PrettyPrinter.printObject(packet, clazz, MinecraftReflection.getPacketClass()) - ); - - } catch (IllegalAccessException e) { - logger.log(Level.WARNING, "Unable to use reflection.", e); - } - } else { - logger.info(shortDescription + "."); - } - } - - @Override - public ListeningWhitelist getSendingWhitelist() { - return side.isForServer() ? whitelist : ListeningWhitelist.EMPTY_WHITELIST; - } - - @Override - public ListeningWhitelist getReceivingWhitelist() { - return side.isForClient() ? whitelist : ListeningWhitelist.EMPTY_WHITELIST; - } - - @Override - public Plugin getPlugin() { - return plugin; - } - - @Override - public boolean isDetailed() { - return detailed; - } - }; - } - - public DetailedPacketListener addPacketListeners(ConnectionSide side, int idStart, int idStop, boolean detailed) { - DetailedPacketListener listener = createPacketListener(side, idStart, idStop, detailed); - - // The trees will manage the listeners for us - if (listener != null) { - if (side.isForClient()) - clientListeners.put(idStart, idStop, listener); - if (side.isForServer()) - serverListeners.put(idStart, idStop, listener); - return listener; - } else { - throw new IllegalArgumentException("No packets found in the range " + idStart + " - " + idStop + "."); - } - } - - public Set.Entry> removePacketListeners( - ConnectionSide side, int idStart, int idStop, boolean detailed) { - - HashSet.Entry> result = Sets.newHashSet(); - - // The interval tree will automatically remove the listeners for us - if (side.isForClient()) - result.addAll(clientListeners.remove(idStart, idStop, true)); - if (side.isForServer()) - result.addAll(serverListeners.remove(idStart, idStop, true)); - return result; - } - - private SubCommand parseCommand(String[] args, int index) { - String text = args[index].toLowerCase(); - - // Parse this too - if ("add".startsWith(text)) - return SubCommand.ADD; - else if ("remove".startsWith(text)) - return SubCommand.REMOVE; - else if ("names".startsWith(text)) - return SubCommand.NAMES; - else if ("page".startsWith(text)) - return SubCommand.PAGE; - else - throw new IllegalArgumentException(text + " is not a valid sub command. Must be add or remove."); - } - - private ConnectionSide parseSide(String[] args, int index, ConnectionSide defaultValue) { - if (index < args.length) { - String text = args[index].toLowerCase(); - - // Parse the side gracefully - if ("client".startsWith(text)) - return ConnectionSide.CLIENT_SIDE; - else if ("server".startsWith(text)) - return ConnectionSide.SERVER_SIDE; - else - throw new IllegalArgumentException(text + " is not a connection side."); - - } else { - return defaultValue; - } - } - - // Parse a boolean - private Boolean parseBoolean(String[] args, String parameterName, int index) { - if (index < args.length) { - if (args[index].equalsIgnoreCase("true")) - return true; - else if (args[index].equalsIgnoreCase(parameterName)) - return true; - else if (args[index].equalsIgnoreCase("false")) - return false; - else - return null; - } else { - return null; - } - } -} +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program 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 2 of + * the License, or (at your option) any later version. + * + * This program 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 this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import net.sf.cglib.proxy.Factory; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.concurrency.AbstractIntervalTree; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.events.ConnectionSide; +import com.comphenix.protocol.events.ListenerPriority; +import com.comphenix.protocol.events.ListeningWhitelist; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.events.PacketListener; +import com.comphenix.protocol.injector.GamePhase; +import com.comphenix.protocol.reflect.EquivalentConverter; +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.PrettyPrinter; +import com.comphenix.protocol.reflect.PrettyPrinter.ObjectPrinter; +import com.comphenix.protocol.utility.ChatExtensions; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.wrappers.BukkitConverters; +import com.google.common.collect.DiscreteDomains; +import com.google.common.collect.Range; +import com.google.common.collect.Ranges; +import com.google.common.collect.Sets; + +/** + * Handles the "packet" debug command. + * + * @author Kristian + */ +class CommandPacket extends CommandBase { + public static final ReportType REPORT_CANNOT_SEND_MESSAGE = new ReportType("Cannot send chat message."); + + private interface DetailedPacketListener extends PacketListener { + /** + * Determine whether or not the given packet listener is detailed or not. + * @return TRUE if it is detailed, FALSE otherwise. + */ + public boolean isDetailed(); + } + + private enum SubCommand { + ADD, REMOVE, NAMES, PAGE; + } + + /** + * Name of this command. + */ + public static final String NAME = "packet"; + + /** + * Number of lines per page. + */ + public static final int PAGE_LINE_COUNT = 9; + + private Plugin plugin; + private Logger logger; + private ProtocolManager manager; + + private ChatExtensions chatter; + + // Paged message + private Map> pagedMessage = new WeakHashMap>(); + + // Registered packet listeners + private AbstractIntervalTree clientListeners = createTree(ConnectionSide.CLIENT_SIDE); + private AbstractIntervalTree serverListeners = createTree(ConnectionSide.SERVER_SIDE); + + // Filter packet events + private CommandFilter filter; + + public CommandPacket(ErrorReporter reporter, Plugin plugin, Logger logger, CommandFilter filter, ProtocolManager manager) { + super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 1); + this.plugin = plugin; + this.logger = logger; + this.manager = manager; + this.filter = filter; + this.chatter = new ChatExtensions(manager); + } + + /** + * Construct a packet listener interval tree. + * @return Construct the tree. + */ + private AbstractIntervalTree createTree(final ConnectionSide side) { + return new AbstractIntervalTree() { + @Override + protected Integer decrementKey(Integer key) { + return key != null ? key - 1 : null; + } + + @Override + protected Integer incrementKey(Integer key) { + return key != null ? key + 1 : null; + } + + @Override + protected void onEntryAdded(Entry added) { + // Ensure that the starting ID and the ending ID is correct + // This is necessary because the interval tree may change the range. + if (added != null) { + Range key = added.getKey(); + DetailedPacketListener listener = added.getValue(); + DetailedPacketListener corrected = createPacketListener( + side, key.lowerEndpoint(), key.upperEndpoint(), listener.isDetailed()); + + added.setValue(corrected); + + if (corrected != null) { + manager.addPacketListener(corrected); + } else { + // Never mind + remove(key.lowerEndpoint(), key.upperEndpoint()); + } + } + } + + @Override + protected void onEntryRemoved(Entry removed) { + // Remove the listener + if (removed != null) { + DetailedPacketListener listener = removed.getValue(); + + if (listener != null) { + manager.removePacketListener(listener); + } + } + } + }; + } + + /** + * Send a message without invoking the packet listeners. + * @param receiver - the player to send it to. + * @param message - the message to send. + * @return TRUE if the message was sent successfully, FALSE otherwise. + */ + public void sendMessageSilently(CommandSender receiver, String message) { + try { + chatter.sendMessageSilently(receiver, message); + } catch (InvocationTargetException e) { + reporter.reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_SEND_MESSAGE).error(e).callerParam(receiver, message) + ); + } + } + + /** + * Broadcast a message without invoking any packet listeners. + * @param message - message to send. + * @param permission - permission required to receieve the message. NULL to target everyone. + */ + public void broadcastMessageSilently(String message, String permission) { + try { + chatter.broadcastMessageSilently(message, permission); + } catch (InvocationTargetException e) { + reporter.reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_SEND_MESSAGE).error(e).callerParam(message, permission) + ); + } + } + + private void printPage(CommandSender sender, int pageIndex) { + List paged = pagedMessage.get(sender); + + // Make sure the player has any pages + if (paged != null) { + int lastPage = ((paged.size() - 1) / PAGE_LINE_COUNT) + 1; + + for (int i = PAGE_LINE_COUNT * (pageIndex - 1); i < PAGE_LINE_COUNT * pageIndex; i++) { + if (i < paged.size()) { + sendMessageSilently(sender, " " + paged.get(i)); + } + } + + // More data? + if (pageIndex < lastPage) { + sendMessageSilently(sender, "Send /packet page " + (pageIndex + 1) + " for the next page."); + } + + } else { + sendMessageSilently(sender, ChatColor.RED + "No pages found."); + } + } + + /* + * Description: Adds or removes a simple packet listener. + Usage: / add|remove client|server|both [ID start] [ID stop] [detailed] + */ + @Override + protected boolean handleCommand(CommandSender sender, String[] args) { + try { + SubCommand subCommand = parseCommand(args, 0); + + // Commands with different parameters + if (subCommand == SubCommand.PAGE) { + int page = Integer.parseInt(args[1]); + + if (page > 0) + printPage(sender, page); + else + sendMessageSilently(sender, ChatColor.RED + "Page index must be greater than zero."); + return true; + } + + ConnectionSide side = parseSide(args, 1, ConnectionSide.BOTH); + + Integer lastIndex = args.length - 1; + Boolean detailed = parseBoolean(args, "detailed", lastIndex); + + // See if the last element is a boolean + if (detailed == null) { + detailed = false; + } else { + lastIndex--; + } + + // Make sure the packet IDs are valid + List> ranges = RangeParser.getRanges(args, 2, lastIndex, Ranges.closed(0, 255)); + + if (ranges.isEmpty()) { + // Use every packet ID + ranges.add(Ranges.closed(0, 255)); + } + + // Perform commands + if (subCommand == SubCommand.ADD) { + // The add command is dangerous - don't default on the connection side + if (args.length == 1) { + sender.sendMessage(ChatColor.RED + "Please specify a connectionn side."); + return false; + } + + executeAddCommand(sender, side, detailed, ranges); + } else if (subCommand == SubCommand.REMOVE) { + executeRemoveCommand(sender, side, detailed, ranges); + } else if (subCommand == SubCommand.NAMES) { + executeNamesCommand(sender, side, ranges); + } + + } catch (NumberFormatException e) { + sendMessageSilently(sender, ChatColor.RED + "Cannot parse number: " + e.getMessage()); + } catch (IllegalArgumentException e) { + sendMessageSilently(sender, ChatColor.RED + e.getMessage()); + } + + return true; + } + + private void executeAddCommand(CommandSender sender, ConnectionSide side, Boolean detailed, List> ranges) { + for (Range range : ranges) { + DetailedPacketListener listener = addPacketListeners(side, range.lowerEndpoint(), range.upperEndpoint(), detailed); + sendMessageSilently(sender, ChatColor.BLUE + "Added listener " + getWhitelistInfo(listener)); + } + } + + private void executeRemoveCommand(CommandSender sender, ConnectionSide side, Boolean detailed, List> ranges) { + int count = 0; + + // Remove each packet listener + for (Range range : ranges) { + count += removePacketListeners(side, range.lowerEndpoint(), range.upperEndpoint(), detailed).size(); + } + + sendMessageSilently(sender, ChatColor.BLUE + "Fully removed " + count + " listeners."); + } + + private void executeNamesCommand(CommandSender sender, ConnectionSide side, List> ranges) { + Set named = getNamedPackets(side); + List messages = new ArrayList(); + + // Print the equivalent name of every given ID + for (Range range : ranges) { + for (int id : range.asSet(DiscreteDomains.integers())) { + if (named.contains(id)) { + messages.add(ChatColor.WHITE + "" + id + ": " + ChatColor.BLUE + Packets.getDeclaredName(id)); + } + } + } + + if (sender instanceof Player && messages.size() > 0 && messages.size() > PAGE_LINE_COUNT) { + // Divide the messages into chuncks + pagedMessage.put(sender, messages); + printPage(sender, 1); + + } else { + // Just print the damn thing + for (String message : messages) { + sendMessageSilently(sender, message); + } + } + } + + /** + * Retrieve whitelist information about a given listener. + * @param listener - the given listener. + * @return Whitelist information. + */ + private String getWhitelistInfo(PacketListener listener) { + boolean sendingEmpty = ListeningWhitelist.isEmpty(listener.getSendingWhitelist()); + boolean receivingEmpty = ListeningWhitelist.isEmpty(listener.getReceivingWhitelist()); + + if (!sendingEmpty && !receivingEmpty) + return String.format("Sending: %s, Receiving: %s", listener.getSendingWhitelist(), listener.getReceivingWhitelist()); + else if (!sendingEmpty) + return listener.getSendingWhitelist().toString(); + else if (!receivingEmpty) + return listener.getReceivingWhitelist().toString(); + else + return "[None]"; + } + + private Set getValidPackets(ConnectionSide side) throws FieldAccessException { + HashSet supported = Sets.newHashSet(); + + if (side.isForClient()) + supported.addAll(Packets.Client.getSupported()); + else if (side.isForServer()) + supported.addAll(Packets.Server.getSupported()); + + return supported; + } + + private Set getNamedPackets(ConnectionSide side) { + + Set valids = null; + Set result = Sets.newHashSet(); + + try { + valids = getValidPackets(side); + } catch (FieldAccessException e) { + valids = Ranges.closed(0, 255).asSet(DiscreteDomains.integers()); + } + + // Check connection side + if (side.isForClient()) + result.addAll(Packets.Client.getRegistry().values()); + if (side.isForServer()) + result.addAll(Packets.Server.getRegistry().values()); + + // Remove invalid packets + result.retainAll(valids); + return result; + } + + public DetailedPacketListener createPacketListener(final ConnectionSide side, int idStart, int idStop, final boolean detailed) { + Set range = Ranges.closed(idStart, idStop).asSet(DiscreteDomains.integers()); + Set packets; + + try { + // Only use supported packet IDs + packets = new HashSet(getValidPackets(side)); + packets.retainAll(range); + + } catch (FieldAccessException e) { + // Don't filter anything then + packets = range; + } + + // Ignore empty sets + if (packets.isEmpty()) + return null; + + // Create the listener we will be using + final ListeningWhitelist whitelist = new ListeningWhitelist(ListenerPriority.MONITOR, packets, GamePhase.BOTH); + + return new DetailedPacketListener() { + @Override + public void onPacketSending(PacketEvent event) { + if (side.isForServer() && filter.filterEvent(event)) { + printInformation(event); + } + } + + @Override + public void onPacketReceiving(PacketEvent event) { + if (side.isForClient() && filter.filterEvent(event)) { + printInformation(event); + } + } + + private void printInformation(PacketEvent event) { + String format = side.isForClient() ? + "Received %s (%s) from %s" : + "Sent %s (%s) to %s"; + String shortDescription = String.format(format, + Packets.getDeclaredName(event.getPacketID()), + event.getPacketID(), + event.getPlayer().getName() + ); + + // Detailed will print the packet's content too + if (detailed) { + try { + Object packet = event.getPacket().getHandle(); + Class clazz = packet.getClass(); + + // Get the first Minecraft super class + while ((!MinecraftReflection.isMinecraftClass(clazz) || + Factory.class.isAssignableFrom(clazz)) && clazz != Object.class) { + clazz = clazz.getSuperclass(); + } + + logger.info(shortDescription + ":\n" + + PrettyPrinter.printObject(packet, clazz, MinecraftReflection.getPacketClass(), PrettyPrinter.RECURSE_DEPTH, new ObjectPrinter() { + @Override + public boolean print(StringBuilder output, Object value) { + if (value != null) { + EquivalentConverter converter = BukkitConverters.getGenericConverters().get(value.getClass()); + + if (converter != null) { + output.append(converter.getSpecific(value)); + return true; + } + } + return false; + } + }) + ); + + } catch (IllegalAccessException e) { + logger.log(Level.WARNING, "Unable to use reflection.", e); + } + } else { + logger.info(shortDescription + "."); + } + } + + @Override + public ListeningWhitelist getSendingWhitelist() { + return side.isForServer() ? whitelist : ListeningWhitelist.EMPTY_WHITELIST; + } + + @Override + public ListeningWhitelist getReceivingWhitelist() { + return side.isForClient() ? whitelist : ListeningWhitelist.EMPTY_WHITELIST; + } + + @Override + public Plugin getPlugin() { + return plugin; + } + + @Override + public boolean isDetailed() { + return detailed; + } + }; + } + + public DetailedPacketListener addPacketListeners(ConnectionSide side, int idStart, int idStop, boolean detailed) { + DetailedPacketListener listener = createPacketListener(side, idStart, idStop, detailed); + + // The trees will manage the listeners for us + if (listener != null) { + if (side.isForClient()) + clientListeners.put(idStart, idStop, listener); + if (side.isForServer()) + serverListeners.put(idStart, idStop, listener); + return listener; + } else { + throw new IllegalArgumentException("No packets found in the range " + idStart + " - " + idStop + "."); + } + } + + public Set.Entry> removePacketListeners( + ConnectionSide side, int idStart, int idStop, boolean detailed) { + + HashSet.Entry> result = Sets.newHashSet(); + + // The interval tree will automatically remove the listeners for us + if (side.isForClient()) + result.addAll(clientListeners.remove(idStart, idStop, true)); + if (side.isForServer()) + result.addAll(serverListeners.remove(idStart, idStop, true)); + return result; + } + + private SubCommand parseCommand(String[] args, int index) { + String text = args[index].toLowerCase(); + + // Parse this too + if ("add".startsWith(text)) + return SubCommand.ADD; + else if ("remove".startsWith(text)) + return SubCommand.REMOVE; + else if ("names".startsWith(text)) + return SubCommand.NAMES; + else if ("page".startsWith(text)) + return SubCommand.PAGE; + else + throw new IllegalArgumentException(text + " is not a valid sub command. Must be add or remove."); + } + + private ConnectionSide parseSide(String[] args, int index, ConnectionSide defaultValue) { + if (index < args.length) { + String text = args[index].toLowerCase(); + + // Parse the side gracefully + if ("client".startsWith(text)) + return ConnectionSide.CLIENT_SIDE; + else if ("server".startsWith(text)) + return ConnectionSide.SERVER_SIDE; + else + throw new IllegalArgumentException(text + " is not a connection side."); + + } else { + return defaultValue; + } + } + + // Parse a boolean + private Boolean parseBoolean(String[] args, String parameterName, int index) { + if (index < args.length) { + if (args[index].equalsIgnoreCase("true")) + return true; + else if (args[index].equalsIgnoreCase(parameterName)) + return true; + else if (args[index].equalsIgnoreCase("false")) + return false; + else + return null; + } else { + return null; + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/PrettyPrinter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/PrettyPrinter.java index d8df1d37..19ccea42 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/PrettyPrinter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/PrettyPrinter.java @@ -33,12 +33,34 @@ import com.google.common.primitives.Primitives; * @author Kristian */ public class PrettyPrinter { + /** + * Represents a generic object printer. + * @author Kristian + */ + public interface ObjectPrinter { + public static final ObjectPrinter DEFAULT = new ObjectPrinter() { + @Override + public boolean print(StringBuilder output, Object value) { + return false; + } + }; + + /** + * Print the content of the given object. + *

+ * Return FALSE in order for let the default printer take over. + * @param output - where to print the output. + * @param value - the value to print, may be NULL. + * @return TRUE if we processed the value and added to the output, FALSE otherwise. + */ + public boolean print(StringBuilder output, Object value); + } /** * How far we will recurse. */ public final static int RECURSE_DEPTH = 3; - + /** * Print the content of an object. * @param object - the object to serialize. @@ -74,6 +96,19 @@ public class PrettyPrinter { * @throws IllegalAccessException */ public static String printObject(Object object, Class start, Class stop, int hierachyDepth) throws IllegalAccessException { + return printObject(object, start, stop, hierachyDepth, ObjectPrinter.DEFAULT); + } + + /** + * Print the content of an object. + * @param object - the object to serialize. + * @param stop - superclass that will stop the process. + * @param hierachyDepth - maximum recursion level. + * @param transformer - a generic object printer. + * @return String representation of the class. + * @throws IllegalAccessException + */ + public static String printObject(Object object, Class start, Class stop, int hierachyDepth, ObjectPrinter printer) throws IllegalAccessException { if (object == null) throw new IllegalArgumentException("object cannot be NULL."); @@ -82,7 +117,7 @@ public class PrettyPrinter { // Start and stop output.append("{ "); - printObject(output, object, start, stop, previous, hierachyDepth, true); + printObject(output, object, start, stop, previous, hierachyDepth, true, printer); output.append(" }"); return output.toString(); @@ -90,7 +125,7 @@ public class PrettyPrinter { @SuppressWarnings("rawtypes") private static void printIterables(StringBuilder output, Iterable iterable, Class current, Class stop, - Set previous, int hierachyIndex) throws IllegalAccessException { + Set previous, int hierachyIndex, ObjectPrinter printer) throws IllegalAccessException { boolean first = true; output.append("("); @@ -102,7 +137,7 @@ public class PrettyPrinter { output.append(", "); // Print value - printValue(output, value, stop, previous, hierachyIndex - 1); + printValue(output, value, stop, previous, hierachyIndex - 1, printer); } output.append(")"); @@ -119,7 +154,7 @@ public class PrettyPrinter { * @throws IllegalAccessException If any reflection went wrong. */ private static void printMap(StringBuilder output, Map map, Class current, Class stop, - Set previous, int hierachyIndex) throws IllegalAccessException { + Set previous, int hierachyIndex, ObjectPrinter printer) throws IllegalAccessException { boolean first = true; output.append("["); @@ -130,16 +165,16 @@ public class PrettyPrinter { else output.append(", "); - printValue(output, entry.getKey(), stop, previous, hierachyIndex - 1); + printValue(output, entry.getKey(), stop, previous, hierachyIndex - 1, printer); output.append(": "); - printValue(output, entry.getValue(), stop, previous, hierachyIndex - 1); + printValue(output, entry.getValue(), stop, previous, hierachyIndex - 1, printer); } output.append("]"); } private static void printArray(StringBuilder output, Object array, Class current, Class stop, - Set previous, int hierachyIndex) throws IllegalAccessException { + Set previous, int hierachyIndex, ObjectPrinter printer) throws IllegalAccessException { Class component = current.getComponentType(); boolean first = true; @@ -156,7 +191,7 @@ public class PrettyPrinter { // Handle exceptions try { - printValue(output, Array.get(array, i), component, stop, previous, hierachyIndex - 1); + printValue(output, Array.get(array, i), component, stop, previous, hierachyIndex - 1, printer); } catch (ArrayIndexOutOfBoundsException e) { e.printStackTrace(); break; @@ -171,7 +206,8 @@ public class PrettyPrinter { // Internal recursion method private static void printObject(StringBuilder output, Object object, Class current, Class stop, - Set previous, int hierachyIndex, boolean first) throws IllegalAccessException { + Set previous, int hierachyIndex, boolean first, + ObjectPrinter printer) throws IllegalAccessException { // See if we're supposed to skip this class if (current == Object.class || (stop != null && current.equals(stop))) { @@ -203,42 +239,46 @@ public class PrettyPrinter { output.append(field.getName()); output.append(" = "); - printValue(output, value, type, stop, previous, hierachyIndex - 1); + printValue(output, value, type, stop, previous, hierachyIndex - 1, printer); } } // Recurse - printObject(output, object, current.getSuperclass(), stop, previous, hierachyIndex, first); + printObject(output, object, current.getSuperclass(), stop, previous, hierachyIndex, first, printer); } private static void printValue(StringBuilder output, Object value, Class stop, - Set previous, int hierachyIndex) throws IllegalAccessException { + Set previous, int hierachyIndex, ObjectPrinter printer) throws IllegalAccessException { // Handle the NULL case - printValue(output, value, value != null ? value.getClass() : null, stop, previous, hierachyIndex); + printValue(output, value, value != null ? value.getClass() : null, stop, previous, hierachyIndex, printer); } @SuppressWarnings({"rawtypes", "unchecked"}) private static void printValue(StringBuilder output, Object value, Class type, - Class stop, Set previous, int hierachyIndex) throws IllegalAccessException { + Class stop, Set previous, int hierachyIndex, + ObjectPrinter printer) throws IllegalAccessException { + // Just print primitive types - if (value == null) { + if (printer.print(output, value)) { + return; + } else if (value == null) { output.append("NULL"); } else if (type.isPrimitive() || Primitives.isWrapperType(type)) { output.append(value); } else if (type == String.class || hierachyIndex <= 0) { output.append("\"" + value + "\""); } else if (type.isArray()) { - printArray(output, value, type, stop, previous, hierachyIndex); + printArray(output, value, type, stop, previous, hierachyIndex, printer); } else if (Iterable.class.isAssignableFrom(type)) { - printIterables(output, (Iterable) value, type, stop, previous, hierachyIndex); + printIterables(output, (Iterable) value, type, stop, previous, hierachyIndex, printer); } else if (Map.class.isAssignableFrom(type)) { - printMap(output, (Map) value, type, stop, previous, hierachyIndex); + printMap(output, (Map) value, type, stop, previous, hierachyIndex, printer); } else if (ClassLoader.class.isAssignableFrom(type) || previous.contains(value)) { // Don't print previous objects output.append("\"" + value + "\""); } else { output.append("{ "); - printObject(output, value, value.getClass(), stop, previous, hierachyIndex, true); + printObject(output, value, value.getClass(), stop, previous, hierachyIndex, true, printer); output.append(" }"); } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index 40fe3d58..d6bef053 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -45,6 +45,8 @@ import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract; import com.comphenix.protocol.reflect.fuzzy.FuzzyMatchers; import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract; import com.comphenix.protocol.wrappers.WrappedDataWatcher; +import com.comphenix.protocol.wrappers.nbt.NbtFactory; +import com.comphenix.protocol.wrappers.nbt.NbtType; import com.google.common.base.Joiner; /** @@ -868,6 +870,21 @@ public class MinecraftReflection { return setMinecraftClass("NBTBase", nbtBase); } } + + /** + * Retrieve the NBT Compound class. + * @return The NBT Compond class. + */ + public static Class getNBTCompoundClass() { + try { + return getMinecraftClass("NBTTagCompound"); + } catch (RuntimeException e) { + return setMinecraftClass( + "NBTTagCompound", + NbtFactory.ofWrapper(NbtType.TAG_COMPOUND, "Test").getHandle().getClass() + ); + } + } /** * Retrieve the EntityTracker (NMS) class. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java index 5d3be1fa..a29a03eb 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java @@ -22,6 +22,7 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; import org.bukkit.World; import org.bukkit.WorldType; @@ -35,8 +36,10 @@ import com.comphenix.protocol.reflect.FieldAccessException; import com.comphenix.protocol.reflect.instances.DefaultInstances; import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.wrappers.nbt.NbtBase; +import com.comphenix.protocol.wrappers.nbt.NbtCompound; import com.comphenix.protocol.wrappers.nbt.NbtFactory; import com.google.common.base.Objects; +import com.google.common.collect.ImmutableMap; /** * Contains several useful equivalent converters for normal Bukkit types. @@ -47,6 +50,10 @@ public class BukkitConverters { // Check whether or not certain classes exists private static boolean hasWorldType = false; + // The static maps + private static Map, EquivalentConverter> specificConverters; + private static Map, EquivalentConverter> genericConverters; + // Used to access the world type private static Method worldTypeName; private static Method worldTypeGetType; @@ -416,4 +423,44 @@ public class BukkitConverters { } }; } + + /** + * Retrieve every converter that is associated with a specific class. + * @return Every converter with a unique specific class. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public static Map, EquivalentConverter> getSpecificConverters() { + if (specificConverters == null) { + // Generics doesn't work, as usual + specificConverters = ImmutableMap., EquivalentConverter>builder(). + put(WrappedDataWatcher.class, (EquivalentConverter) getDataWatcherConverter()). + put(ItemStack.class, (EquivalentConverter) getItemStackConverter()). + put(NbtBase.class, (EquivalentConverter) getNbtConverter()). + put(NbtCompound.class, (EquivalentConverter) getNbtConverter()). + put(WrappedWatchableObject.class, (EquivalentConverter) getWatchableObjectConverter()). + put(WorldType.class, (EquivalentConverter) getWorldTypeConverter()). + build(); + } + return specificConverters; + } + + /** + * Retrieve every converter that is associated with a generic class. + * @return Every converter with a unique generic class. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public static Map, EquivalentConverter> getGenericConverters() { + if (genericConverters == null) { + // Generics doesn't work, as usual + genericConverters = ImmutableMap., EquivalentConverter>builder(). + put(MinecraftReflection.getDataWatcherClass(), (EquivalentConverter) getDataWatcherConverter()). + put(MinecraftReflection.getItemStackClass(), (EquivalentConverter) getItemStackConverter()). + put(MinecraftReflection.getNBTBaseClass(), (EquivalentConverter) getNbtConverter()). + put(MinecraftReflection.getNBTCompoundClass(), (EquivalentConverter) getNbtConverter()). + put(MinecraftReflection.getWatchableObjectClass(), (EquivalentConverter) getWatchableObjectConverter()). + put(MinecraftReflection.getWorldTypeClass(), (EquivalentConverter) getWorldTypeConverter()). + build(); + } + return genericConverters; + } }