diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java index 510842a2..c0a85c49 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java @@ -1,10 +1,16 @@ package com.comphenix.protocol; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.logging.Level; import java.util.logging.Logger; -import org.apache.commons.lang.builder.ToStringBuilder; -import org.apache.commons.lang.builder.ToStringStyle; +import net.minecraft.server.Packet; +import net.sf.cglib.proxy.Factory; + import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -12,6 +18,7 @@ import org.bukkit.command.CommandSender; import org.bukkit.plugin.Plugin; import com.comphenix.protocol.concurrency.AbstractIntervalTree; +import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.ConnectionSide; import com.comphenix.protocol.events.ListenerPriority; import com.comphenix.protocol.events.ListeningWhitelist; @@ -19,6 +26,8 @@ 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.google.common.collect.DiscreteDomains; import com.google.common.collect.Range; import com.google.common.collect.Ranges; @@ -48,16 +57,21 @@ class CommandPacket implements CommandExecutor { private Plugin plugin; private Logger logger; + private ErrorReporter reporter; private ProtocolManager manager; + + private ChatExtensions chatter; // Registered packet listeners private AbstractIntervalTree clientListeners = createTree(ConnectionSide.CLIENT_SIDE); private AbstractIntervalTree serverListeners = createTree(ConnectionSide.SERVER_SIDE); - public CommandPacket(Plugin plugin, Logger logger, ProtocolManager manager) { + public CommandPacket(Plugin plugin, Logger logger, ErrorReporter reporter, ProtocolManager manager) { this.plugin = plugin; this.logger = logger; + this.reporter = reporter; this.manager = manager; + this.chatter = new ChatExtensions(manager); } /** @@ -111,6 +125,33 @@ class CommandPacket implements CommandExecutor { }; } + /** + * Send a message without invoking the packet listeners. + * @param player - 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, "Cannot send chat message.", e, 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, "Cannot send chat message.", e, message, message); + } + } + /* * Description: Adds or removes a simple packet listener. Usage: / add|remove client|server|both [ID start] [ID stop] [detailed] @@ -127,37 +168,189 @@ class CommandPacket implements CommandExecutor { SubCommand subCommand = parseCommand(args, 0); ConnectionSide side = parseSide(args, 1, ConnectionSide.BOTH); - int idStart = parseInteger(args, 2, 0); - int idStop = parseInteger(args, 3, 255); + Integer lastIndex = args.length - 1; + Boolean detailed = parseBoolean(args, lastIndex); + + // See if the last element is a boolean + if (detailed == null) { + detailed = false; + } else { + lastIndex--; + } // Make sure the packet IDs are valid - if (idStart < 0 || idStart > 255) - throw new IllegalAccessError("The starting packet ID must be within 0 - 255."); - if (idStop < 0 || idStop > 255) - throw new IllegalAccessError("The stop packet ID must be within 0 - 255."); + List> ranges = getRanges(args, 2, lastIndex, Ranges.closed(0, 255)); + + if (ranges.isEmpty()) { + // Use every packet ID + ranges.add(Ranges.closed(0, 255)); + } - // Special case. If stop is not set, but start is set, use a interval size of 1. - if (args.length == 3) - idStop = idStart + 1; - - boolean detailed = parseBoolean(args, 4, false); - // Perform command - if (subCommand == SubCommand.ADD) - addPacketListeners(side, idStart, idStop, detailed); - else - removePacketListeners(side, idStart, idStop, detailed); + if (subCommand == SubCommand.ADD) { + for (Range range : ranges) { + DetailedPacketListener listener = addPacketListeners(side, range.lowerEndpoint(), range.upperEndpoint(), detailed); + sendMessageSilently(sender, ChatColor.BLUE + "Added listener " + getWhitelistInfo(listener)); + } + + } else if (subCommand == SubCommand.REMOVE) { + 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."); + } } catch (NumberFormatException e) { - sender.sendMessage(ChatColor.DARK_RED + "Cannot parse number: " + e.getMessage()); + sendMessageSilently(sender, ChatColor.RED + "Cannot parse number: " + e.getMessage()); } catch (IllegalArgumentException e) { - sender.sendMessage(ChatColor.DARK_RED + e.getMessage()); + sendMessageSilently(sender, ChatColor.RED + e.getMessage()); } + + return true; } return false; } + /** + * Parse ranges from an array of tokens. + * @param args - array of tokens. + * @param offset - beginning offset. + * @param legalRange - range of legal values. + * @return The parsed ranges. + */ + public static List> getRanges(String[] args, int offset, int lastIndex, Range legalRange) { + List tokens = tokenizeInput(args, offset, lastIndex); + List> ranges = new ArrayList>(); + + for (int i = 0; i < tokens.size(); i++) { + Range range; + String current = tokens.get(i); + String next = i + 1 < tokens.size() ? tokens.get(i + 1) : null; + + // Yoda equality is done for null-safety + if ("-".equals(current)) { + throw new IllegalArgumentException("A hyphen must appear between two numbers."); + } else if ("-".equals(next)) { + if (i + 2 >= tokens.size()) + throw new IllegalArgumentException("Cannot form a range without a upper limit."); + + // This is a proper range + range = Ranges.closed(Integer.parseInt(current), Integer.parseInt(tokens.get(i + 2))); + ranges.add(range); + + // Skip the two next tokens + i += 2; + + } else { + // Just a single number + range = Ranges.singleton(Integer.parseInt(current)); + ranges.add(range); + } + + // Validate ranges + if (!legalRange.encloses(range)) { + throw new IllegalArgumentException(range + " is not in the range " + range.toString()); + } + } + + return simplify(ranges, legalRange.upperEndpoint()); + } + + /** + * Simplify a list of ranges by assuming a maximum value. + * @param ranges - the list of ranges to simplify. + * @param maximum - the maximum value (minimum value is always 0). + * @return A simplified list of ranges. + */ + private static List> simplify(List> ranges, int maximum) { + List> result = new ArrayList>(); + boolean[] set = new boolean[maximum + 1]; + int start = -1; + + // Set every ID + for (Range range : ranges) { + for (int id : range.asSet(DiscreteDomains.integers())) { + set[id] = true; + } + } + + // Generate ranges from this set + for (int i = 0; i <= set.length; i++) { + if (i < set.length && set[i]) { + if (start < 0) { + start = i; + } + } else { + if (start > 0) { + result.add(Ranges.closed(start, i - 1)); + start = -1; + } + } + } + + return result; + } + + private static List tokenizeInput(String[] args, int offset, int lastIndex) { + List tokens = new ArrayList(); + + // Tokenize the input + for (int i = offset; i <= lastIndex; i++) { + String text = args[i]; + StringBuilder number = new StringBuilder(); + + for (int j = 0; j < text.length(); j++) { + char current = text.charAt(j); + + if (Character.isDigit(current)) { + number.append(current); + } else if (Character.isWhitespace(current)) { + // That's ok + } else if (current == '-') { + // Add the number token first + if (number.length() > 0) { + tokens.add(number.toString()); + number.setLength(0); + } + + tokens.add(Character.toString(current)); + } else { + throw new IllegalArgumentException("Illegal character '" + current + "' found."); + } + } + + // Add the number token, if it hasn't already + if (number.length() > 0) + tokens.add(number.toString()); + } + + return tokens; + } + + /** + * 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 { if (side.isForClient()) return Packets.Client.getSupported(); @@ -174,7 +367,7 @@ class CommandPacket implements CommandExecutor { try { // Only use supported packet IDs - packets = getValidPackets(side); + packets = new HashSet(getValidPackets(side)); packets.retainAll(range); } catch (FieldAccessException e) { @@ -207,17 +400,32 @@ class CommandPacket implements CommandExecutor { private void printInformation(PacketEvent event) { String verb = side.isForClient() ? "Received" : "Sent"; String shortDescription = String.format( - "%s packet %s (%s)", + "%s %s (%s) from %s", verb, + Packets.getDeclaredName(event.getPacketID()), event.getPacketID(), - Packets.getDeclaredName(event.getPacketID()) + event.getPlayer().getName() ); // Detailed will print the packet's content too if (detailed) { - logger.info(shortDescription + ":\n" + - ToStringBuilder.reflectionToString(event.getPacket().getHandle(), ToStringStyle.MULTI_LINE_STYLE) - ); + try { + Packet packet = event.getPacket().getHandle(); + Class clazz = packet.getClass(); + + // Get the first Minecraft super class + while ((!clazz.getName().startsWith("net.minecraft.server") || + Factory.class.isAssignableFrom(clazz)) && clazz != Object.class) { + clazz = clazz.getSuperclass(); + } + + logger.info(shortDescription + ":\n" + + PrettyPrinter.printObject(packet, clazz, Packet.class) + ); + + } catch (IllegalAccessException e) { + logger.log(Level.WARNING, "Unable to use reflection.", e); + } } else { logger.info(shortDescription + "."); } @@ -244,20 +452,24 @@ class CommandPacket implements CommandExecutor { } }; } - - public void addPacketListeners(ConnectionSide side, int idStart, int idStop, boolean 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 (listener != null) { getListenerTree(side).put(idStart, idStop, listener); - else + return listener; + } else { throw new IllegalArgumentException("No packets found in the range " + idStart + " - " + idStop + "."); + } } - public void removePacketListeners(ConnectionSide side, int idStart, int idStop, boolean detailed) { + public Set.Entry> removePacketListeners( + ConnectionSide side, int idStart, int idStop, boolean detailed) { + // The interval tree will automatically remove the listeners for us - getListenerTree(side).remove(idStart, idStop); + return getListenerTree(side).remove(idStart, idStop); } private AbstractIntervalTree getListenerTree(ConnectionSide side) { @@ -286,9 +498,7 @@ class CommandPacket implements CommandExecutor { String text = args[index].toLowerCase(); // Parse the side gracefully - if ("both".startsWith(text)) - return ConnectionSide.BOTH; - else if ("client".startsWith(text)) + if ("client".startsWith(text)) return ConnectionSide.CLIENT_SIDE; else if ("server".startsWith(text)) return ConnectionSide.SERVER_SIDE; @@ -299,27 +509,18 @@ class CommandPacket implements CommandExecutor { return defaultValue; } } - + // Parse a boolean - private boolean parseBoolean(String[] args, int index, boolean defaultValue) { + private Boolean parseBoolean(String[] args, int index) { if (index < args.length) { - return Boolean.parseBoolean(args[index]); + if (args[index].equalsIgnoreCase("true")) + return true; + else if (args[index].equalsIgnoreCase("false")) + return false; + else + return null; } else { - return defaultValue; + return null; } } - - // And an integer - private int parseInteger(String[] args, int index, int defaultValue) { - if (index < args.length) { - return Integer.parseInt(args[index]); - } else { - return defaultValue; - } - } - - public void cleanupAll() { - clientListeners.clear(); - serverListeners.clear(); - } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index c9da1902..eb713965 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -19,6 +19,8 @@ package com.comphenix.protocol; import java.io.File; import java.io.IOException; +import java.util.logging.Handler; +import java.util.logging.LogRecord; import java.util.logging.Logger; import org.bukkit.Server; @@ -42,6 +44,7 @@ import com.comphenix.protocol.reflect.compiler.BackgroundCompiler; public class ProtocolLibrary extends JavaPlugin { private static final long MILLI_PER_SECOND = 1000; + private static final String PERMISSION_INFO = "protocol.info"; // There should only be one protocol manager, so we'll make it static private static PacketFilterManager protocolManager; @@ -70,17 +73,21 @@ public class ProtocolLibrary extends JavaPlugin { // Updater private Updater updater; + // Logger + private Logger logger; + // Commands private CommandProtocol commandProtocol; private CommandPacket commandPacket; @Override public void onLoad() { + // Load configuration + logger = getLoggerSafely(); + // Add global parameters DetailedErrorReporter reporter = new DetailedErrorReporter(); - - // Load configuration - updater = new Updater(this, "protocollib", getFile(), "protocol.info"); + updater = new Updater(this, logger, "protocollib", getFile(), "protocol.info"); try { config = new ProtocolConfig(this); @@ -99,7 +106,10 @@ public class ProtocolLibrary extends JavaPlugin { // Initialize command handlers commandProtocol = new CommandProtocol(this, updater); - commandPacket = new CommandPacket(this, getLoggerSafely(), protocolManager); + commandPacket = new CommandPacket(this, logger, reporter, protocolManager); + + // Send logging information to player listeners too + broadcastUsers(PERMISSION_INFO); } catch (Throwable e) { reporter.reportDetailed(this, "Cannot load ProtocolLib.", e, protocolManager); @@ -121,6 +131,26 @@ public class ProtocolLibrary extends JavaPlugin { config = new ProtocolConfig(this); } + private void broadcastUsers(final String permission) { + // Broadcast information to every user too + logger.addHandler(new Handler() { + @Override + public void publish(LogRecord record) { + commandPacket.broadcastMessageSilently(record.getMessage(), permission); + } + + @Override + public void flush() { + // Not needed. + } + + @Override + public void close() throws SecurityException { + // Do nothing. + } + }); + } + @Override public void onEnable() { try { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractIntervalTree.java b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractIntervalTree.java index de627887..e2748cbc 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractIntervalTree.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractIntervalTree.java @@ -6,8 +6,6 @@ import java.util.NavigableMap; import java.util.Set; import java.util.TreeMap; -import org.apache.commons.lang.NotImplementedException; - import com.google.common.collect.Range; import com.google.common.collect.Ranges; @@ -35,11 +33,18 @@ public abstract class AbstractIntervalTree, TValue */ public class Entry implements Map.Entry, TValue> { private final Range key; - private final TValue value; - - public Entry(Range key, TValue value) { + private EndPoint left; + private EndPoint right; + + Entry(Range key, EndPoint left, EndPoint right) { + if (left == null) + throw new IllegalAccessError("left cannot be NUll"); + if (right == null) + throw new IllegalAccessError("right cannot be NUll"); + this.key = key; - this.value = value; + this.left = left; + this.right = right; } @Override @@ -49,12 +54,17 @@ public abstract class AbstractIntervalTree, TValue @Override public TValue getValue() { - return value; + return left.value; } @Override public TValue setValue(TValue value) { - throw new NotImplementedException(); + TValue old = left.value; + + // Set both end points + left.value = value; + right.value = value; + return old; } } @@ -83,8 +93,8 @@ public abstract class AbstractIntervalTree, TValue * @param lowerBound - lowest value to remove. * @param upperBound - highest value to remove. */ - public void remove(TKey lowerBound, TKey upperBound) { - remove(lowerBound, upperBound, false); + public Set remove(TKey lowerBound, TKey upperBound) { + return remove(lowerBound, upperBound, false); } /** @@ -93,13 +103,13 @@ public abstract class AbstractIntervalTree, TValue * @param upperBound - highest value to remove. * @param preserveOutside - whether or not to preserve the intervals that are partially outside. */ - public void remove(TKey lowerBound, TKey upperBound, boolean preserveDifference) { + public Set remove(TKey lowerBound, TKey upperBound, boolean preserveDifference) { checkBounds(lowerBound, upperBound); NavigableMap range = bounds.subMap(lowerBound, true, upperBound, true); boolean emptyRange = range.isEmpty(); - TKey first = emptyRange ? range.firstKey() : null; - TKey last = emptyRange ? range.lastKey() : null; + TKey first = !emptyRange ? range.firstKey() : null; + TKey last = !emptyRange ? range.lastKey() : null; Set resized = new HashSet(); Set removed = new HashSet(); @@ -136,6 +146,7 @@ public abstract class AbstractIntervalTree, TValue // Remove the range as well range.clear(); + return removed; } // Helper @@ -154,7 +165,8 @@ public abstract class AbstractIntervalTree, TValue if (endPoint != null) { endPoint.state = State.BOTH; } else { - endPoint = bounds.put(key, new EndPoint(state, value)); + endPoint = new EndPoint(state, value); + bounds.put(key, endPoint); } return endPoint; } @@ -184,11 +196,11 @@ public abstract class AbstractIntervalTree, TValue private Entry putUnsafe(TKey lowerBound, TKey upperBound, TValue value) { // OK. Add the end points now if (value != null) { - addEndPoint(lowerBound, value, State.OPEN); - addEndPoint(upperBound, value, State.CLOSE); + EndPoint left = addEndPoint(lowerBound, value, State.OPEN); + EndPoint right = addEndPoint(upperBound, value, State.CLOSE); Range range = Ranges.closed(lowerBound, upperBound); - return new Entry(range, value); + return new Entry(range, left, right); } else { return null; } @@ -248,11 +260,12 @@ public abstract class AbstractIntervalTree, TValue for (Map.Entry entry : bounds.entrySet()) { switch (entry.getValue().state) { case BOTH: - destination.add(new Entry(Ranges.singleton(entry.getKey()), entry.getValue().value)); + EndPoint point = entry.getValue(); + destination.add(new Entry(Ranges.singleton(entry.getKey()), point, point)); break; case CLOSE: Range range = Ranges.closed(last.getKey(), entry.getKey()); - destination.add(new Entry(range, entry.getValue().value)); + destination.add(new Entry(range, last.getValue(), entry.getValue())); break; case OPEN: // We don't know the full range yet @@ -271,7 +284,7 @@ public abstract class AbstractIntervalTree, TValue public void putAll(AbstractIntervalTree other) { // Naively copy every range. for (Entry entry : other.entrySet()) { - put(entry.key.lowerEndpoint(), entry.key.upperEndpoint(), entry.value); + put(entry.key.lowerEndpoint(), entry.key.upperEndpoint(), entry.getValue()); } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/ListeningWhitelist.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/ListeningWhitelist.java index 04dd6af9..79546e56 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/ListeningWhitelist.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/ListeningWhitelist.java @@ -137,6 +137,20 @@ public class ListeningWhitelist { return false; } + /** + * Determine if the given whitelist is empty or not. + * @param whitelist - the whitelist to test. + * @return TRUE if the whitelist is empty, FALSE otherwise. + */ + public static boolean isEmpty(ListeningWhitelist whitelist) { + if (whitelist == EMPTY_WHITELIST) + return true; + else if (whitelist == null) + return true; + else + return whitelist.getWhitelist().isEmpty(); + } + @Override public boolean equals(final Object obj){ if(obj instanceof ListeningWhitelist){ @@ -157,5 +171,4 @@ public class ListeningWhitelist { .add("priority", priority) .add("packets", whitelist).toString(); } - } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Updater.java b/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Updater.java index 14eee6d5..7c2e18fc 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Updater.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Updater.java @@ -212,43 +212,14 @@ public class Updater * @param permission * Permission needed to read the output of the update process. */ - public Updater(Plugin plugin, String slug, File file, String permission) + public Updater(Plugin plugin, Logger logger, String slug, File file, String permission) { this.plugin = plugin; this.file = file; this.slug = slug; - - // Prevent issues with older versions of Bukkit - try { - logger = plugin.getLogger(); - logger.getLevel(); - } catch (Throwable e) { - logger = Logger.getLogger("Minecraft"); - } - - broadcastUsers(plugin.getServer(), permission); + this.logger = logger; } - private void broadcastUsers(final Server server, final String permission) { - // Broadcast information to every user too - logger.addHandler(new Handler() { - @Override - public void publish(LogRecord record) { - server.broadcast(record.getMessage(), permission); - } - - @Override - public void flush() { - // Not needed. - } - - @Override - public void close() throws SecurityException { - // Do nothing. - } - }); - } - /** * Update the plugin. * diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/PrettyPrinter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/PrettyPrinter.java new file mode 100644 index 00000000..f440cc3b --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/PrettyPrinter.java @@ -0,0 +1,172 @@ +package com.comphenix.protocol.reflect; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.HashSet; +import java.util.Set; + +import com.google.common.primitives.Primitives; + +/** + * Used to print the content of an arbitrary class. + * + * @author Kristian + */ +public class PrettyPrinter { + + /** + * How far we will recurse. + */ + public final static int RECURSE_DEPTH = 3; + + /** + * Print the content of an object. + * @param object - the object to serialize. + * @param stop - superclass that will stop the process. + * @return String representation of the class. + * @throws IllegalAccessException + */ + public static String printObject(Object object, Class start, Class stop) throws IllegalAccessException { + return printObject(object, start, stop, RECURSE_DEPTH); + } + + /** + * Print the content of an object. + * @param object - the object to serialize. + * @param stop - superclass that will stop the process. + * @param depth - how far in the hierachy until we stop. + * @return String representation of the class. + * @throws IllegalAccessException + */ + public static String printObject(Object object, Class start, Class stop, int hierachyDepth) throws IllegalAccessException { + StringBuilder output = new StringBuilder(); + Set previous = new HashSet(); + + // Start and stop + output.append("{ "); + printObject(output, object, start, stop, previous, hierachyDepth); + output.append(" }"); + + return output.toString(); + } + + @SuppressWarnings("rawtypes") + private static void printIterables(StringBuilder output, Iterable iterable, Class current, Class stop, + Set previous, int hierachyIndex) throws IllegalAccessException { + + boolean first = true; + output.append("("); + + for (Object value : iterable) { + if (first) + first = false; + else + output.append(", "); + + // Handle exceptions + if (value != null) + printValue(output, value, value.getClass(), stop, previous, hierachyIndex - 1); + else + output.append("NULL"); + } + + output.append(")"); + } + + private static void printArray(StringBuilder output, Object array, Class current, Class stop, + Set previous, int hierachyIndex) throws IllegalAccessException { + + Class component = current.getComponentType(); + boolean first = true; + + if (!component.isArray()) + output.append(component.getName()); + output.append("["); + + for (int i = 0; i < Array.getLength(array); i++) { + if (first) + first = false; + else + output.append(", "); + + // Handle exceptions + try { + printValue(output, Array.get(array, i), component, stop, previous, hierachyIndex - 1); + } catch (ArrayIndexOutOfBoundsException e) { + e.printStackTrace(); + break; + } catch (IllegalArgumentException e) { + e.printStackTrace(); + break; + } + } + + output.append("]"); + } + + // Internal recursion method + private static void printObject(StringBuilder output, Object object, Class current, Class stop, + Set previous, int hierachyIndex) throws IllegalAccessException { + // Trickery + boolean first = true; + + // See if we're supposed to skip this class + if (current == Object.class || (stop != null && current.equals(stop))) { + return; + } + + // Don't iterate twice + previous.add(object); + + // Hard coded limit + if (hierachyIndex < 0) { + output.append("..."); + return; + } + + for (Field field : current.getDeclaredFields()) { + int mod = field.getModifiers(); + + // Skip a good number of the fields + if (!Modifier.isTransient(mod) && !Modifier.isStatic(mod)) { + Class type = field.getType(); + Object value = FieldUtils.readField(field, object, true); + + if (first) + first = false; + else + output.append(", "); + + output.append(field.getName()); + output.append(" = "); + printValue(output, value, type, stop, previous, hierachyIndex - 1); + } + } + + // Recurse + printObject(output, object, current.getSuperclass(), stop, previous, hierachyIndex); + } + + @SuppressWarnings("rawtypes") + private static void printValue(StringBuilder output, Object value, Class type, + Class stop, Set previous, int hierachyIndex) throws IllegalAccessException { + // Just print primitive types + if (value == null) { + output.append("NULL"); + } else if (type.isPrimitive() || Primitives.isWrapperType(type) || type == String.class || hierachyIndex <= 0) { + output.append(value); + } else if (type.isArray()) { + printArray(output, value, type, stop, previous, hierachyIndex); + } else if (Iterable.class.isAssignableFrom(type)) { + printIterables(output, (Iterable) value, type, stop, previous, hierachyIndex); + } 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); + output.append(" }"); + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/ChatExtensions.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/ChatExtensions.java new file mode 100644 index 00000000..fdf6bc21 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/ChatExtensions.java @@ -0,0 +1,85 @@ +package com.comphenix.protocol.utility; + +import java.lang.reflect.InvocationTargetException; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import com.comphenix.protocol.Packets; +import com.comphenix.protocol.ProtocolManager; +import com.comphenix.protocol.injector.PacketConstructor; +import com.comphenix.protocol.reflect.FieldAccessException; + +/** + * Utility methods for sending chat messages. + * + * @author Kristian + */ +public class ChatExtensions { + + // Used to sent chat messages + private PacketConstructor chatConstructor; + private ProtocolManager manager; + + public ChatExtensions(ProtocolManager manager) { + this.manager = manager; + } + + /** + * Send a message without invoking the packet listeners. + * @param player - the player to send it to. + * @param message - the message to send. + * @return TRUE if the message was sent successfully, FALSE otherwise. + * @throws InvocationTargetException If we were unable to send the message. + */ + public void sendMessageSilently(CommandSender receiver, String message) throws InvocationTargetException { + if (receiver == null) + throw new IllegalArgumentException("receiver cannot be NULL."); + if (message == null) + throw new IllegalArgumentException("message cannot be NULL."); + + // Handle the player case by manually sending packets + if (receiver instanceof Player) { + sendMessageSilently((Player) receiver, message); + } else { + receiver.sendMessage(message); + } + } + + /** + * Send a message without invoking the packet listeners. + * @param player - the player to send it to. + * @param message - the message to send. + * @return TRUE if the message was sent successfully, FALSE otherwise. + * @throws InvocationTargetException If we were unable to send the message. + */ + private void sendMessageSilently(Player player, String message) throws InvocationTargetException { + if (chatConstructor == null) + chatConstructor = manager.createPacketConstructor(Packets.Server.CHAT, message); + + try { + manager.sendServerPacket(player, chatConstructor.createPacket(message), false); + } catch (FieldAccessException e) { + throw new InvocationTargetException(e); + } + } + + /** + * 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. + * @throws InvocationTargetException If we were unable to send the message. + */ + public void broadcastMessageSilently(String message, String permission) throws InvocationTargetException { + if (message == null) + throw new IllegalArgumentException("message cannot be NULL."); + + // Send this message to every online player + for (Player player : Bukkit.getServer().getOnlinePlayers()) { + if (permission == null || player.hasPermission(permission)) { + sendMessageSilently(player, message); + } + } + } +} diff --git a/ProtocolLib/src/main/resources/plugin.yml b/ProtocolLib/src/main/resources/plugin.yml index 8fd825ea..6088ff69 100644 --- a/ProtocolLib/src/main/resources/plugin.yml +++ b/ProtocolLib/src/main/resources/plugin.yml @@ -15,7 +15,7 @@ commands: permission-message: You don't have packet: description: Add or remove a simple packet listener. - usage: / add|remove client|server|both [ID start] [ID stop] [detailed] + usage: / add|remove client|server [ID start]-[ID stop] [detailed] permission: experiencemod.admin permission-message: You don't have