diff --git a/ProtocolLib/.classpath b/ProtocolLib/.classpath index 3a647a97..c5d993a4 100644 --- a/ProtocolLib/.classpath +++ b/ProtocolLib/.classpath @@ -1,6 +1,6 @@ - + @@ -13,6 +13,11 @@ + + + + + diff --git a/ProtocolLib/.settings/org.eclipse.core.resources.prefs b/ProtocolLib/.settings/org.eclipse.core.resources.prefs index 6e796e59..78478a74 100644 --- a/ProtocolLib/.settings/org.eclipse.core.resources.prefs +++ b/ProtocolLib/.settings/org.eclipse.core.resources.prefs @@ -1,4 +1,5 @@ eclipse.preferences.version=1 encoding//src/main/java=cp1252 +encoding//src/main/resources=cp1252 encoding//src/test/java=cp1252 encoding/=cp1252 diff --git a/ProtocolLib/dependency-reduced-pom.xml b/ProtocolLib/dependency-reduced-pom.xml index c0332d8a..dc9bd600 100644 --- a/ProtocolLib/dependency-reduced-pom.xml +++ b/ProtocolLib/dependency-reduced-pom.xml @@ -4,7 +4,7 @@ com.comphenix.protocol ProtocolLib ProtocolLib - 1.4.0 + 1.5.1 Provides read/write access to the Minecraft protocol. http://dev.bukkit.org/server-mods/protocollib/ @@ -38,7 +38,7 @@ clean install - src/main/java + src/main/resources **/*.java diff --git a/ProtocolLib/pom.xml b/ProtocolLib/pom.xml index b0f132f2..685bbf5f 100644 --- a/ProtocolLib/pom.xml +++ b/ProtocolLib/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.comphenix.protocol ProtocolLib - 1.4.0 + 1.5.1 jar Provides read/write access to the Minecraft protocol. @@ -36,7 +36,7 @@ src/test/java - src/main/java + src/main/resources **/*.java diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/AsynchronousManager.java b/ProtocolLib/src/main/java/com/comphenix/protocol/AsynchronousManager.java index a16f5db5..4d1d8f35 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/AsynchronousManager.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/AsynchronousManager.java @@ -18,11 +18,11 @@ package com.comphenix.protocol; import java.util.Set; -import java.util.logging.Logger; import org.bukkit.plugin.Plugin; import com.comphenix.protocol.async.AsyncListenerHandler; +import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.events.PacketListener; @@ -81,10 +81,10 @@ public interface AsynchronousManager { public abstract PacketStream getPacketStream(); /** - * Retrieve the default error logger. - * @return Default logger. + * Retrieve the default error reporter. + * @return Default reporter. */ - public abstract Logger getLogger(); + public abstract ErrorReporter getErrorReporter(); /** * Remove listeners, close threads and transmit every delayed packet. @@ -99,4 +99,22 @@ public interface AsynchronousManager { * @param packet - packet to signal. */ public abstract void signalPacketTransmission(PacketEvent packet); + + /** + * Register a synchronous listener that handles packets when they time out. + * @param listener - synchronous listener that will handle timed out packets. + */ + public abstract void registerTimeoutHandler(PacketListener listener); + + /** + * Unregisters a given timeout listener. + * @param listener - the timeout listener to unregister. + */ + public abstract void unregisterTimeoutHandler(PacketListener listener); + + /** + * Get a immutable list of every registered timeout handler. + * @return List of every registered timeout handler. + */ + public abstract Set getTimeoutHandlers(); } \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java new file mode 100644 index 00000000..dbc2e819 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java @@ -0,0 +1,132 @@ +package com.comphenix.protocol; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import com.comphenix.protocol.async.AsyncListenerHandler; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.events.ListeningWhitelist; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.injector.BukkitUnwrapper; +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.MethodUtils; +import com.comphenix.protocol.reflect.ObjectCloner; +import com.comphenix.protocol.reflect.compiler.BackgroundCompiler; +import com.comphenix.protocol.reflect.compiler.StructureCompiler; +import com.comphenix.protocol.reflect.instances.CollectionGenerator; +import com.comphenix.protocol.reflect.instances.DefaultInstances; +import com.comphenix.protocol.reflect.instances.PrimitiveGenerator; + +/** + * Used to fix ClassLoader leaks that may lead to filling up the permanent generation. + * + * @author Kristian + */ +class CleanupStaticMembers { + + private ClassLoader loader; + private ErrorReporter reporter; + + public CleanupStaticMembers(ClassLoader loader, ErrorReporter reporter) { + this.loader = loader; + this.reporter = reporter; + } + + /** + * Ensure that the previous ClassLoader is not leaking. + */ + public void resetAll() { + // This list must always be updated + Class[] publicClasses = { + AsyncListenerHandler.class, ListeningWhitelist.class, PacketContainer.class, + BukkitUnwrapper.class, DefaultInstances.class, CollectionGenerator.class, + PrimitiveGenerator.class, FuzzyReflection.class, MethodUtils.class, + BackgroundCompiler.class, StructureCompiler.class, + ObjectCloner.class, Packets.Server.class, Packets.Client.class + }; + + String[] internalClasses = { + "com.comphenix.protocol.events.SerializedOfflinePlayer", + "com.comphenix.protocol.injector.player.InjectedServerConnection", + "com.comphenix.protocol.injector.player.NetworkFieldInjector", + "com.comphenix.protocol.injector.player.NetworkObjectInjector", + "com.comphenix.protocol.injector.player.NetworkServerInjector", + "com.comphenix.protocol.injector.player.PlayerInjector", + "com.comphenix.protocol.injector.player.TemporaryPlayerFactory", + "com.comphenix.protocol.injector.EntityUtilities", + "com.comphenix.protocol.injector.MinecraftRegistry", + "com.comphenix.protocol.injector.PacketInjector", + "com.comphenix.protocol.injector.ReadPacketModifier", + "com.comphenix.protocol.injector.StructureCache", + "com.comphenix.protocol.reflect.compiler.BoxingHelper", + "com.comphenix.protocol.reflect.compiler.MethodDescriptor" + }; + + resetClasses(publicClasses); + resetClasses(getClasses(loader, internalClasses)); + } + + private void resetClasses(Class[] classes) { + // Reset each class one by one + for (Class clazz : classes) { + resetClass(clazz); + } + } + + private void resetClass(Class clazz) { + for (Field field : clazz.getFields()) { + Class type = field.getType(); + + // Only check static non-primitive fields. We also skip strings. + if (Modifier.isStatic(field.getModifiers()) && + !type.isPrimitive() && !type.equals(String.class)) { + + try { + setFinalStatic(field, null); + } catch (IllegalAccessException e) { + // Just inform the player + reporter.reportWarning(this, "Unable to reset field " + field.getName() + ": " + e.getMessage(), e); + } + } + } + } + + // HACK! HAACK! + private static void setFinalStatic(Field field, Object newValue) throws IllegalAccessException { + int modifier = field.getModifiers(); + boolean isFinal = Modifier.isFinal(modifier); + + Field modifiersField = isFinal ? FieldUtils.getField(Field.class, "modifiers", true) : null; + + // We have to remove the final field first + if (isFinal) { + FieldUtils.writeField(modifiersField, field, modifier & ~Modifier.FINAL, true); + } + + // Now we can safely modify the field + FieldUtils.writeStaticField(field, newValue, true); + + // Revert modifier + if (isFinal) { + FieldUtils.writeField(modifiersField, field, modifier, true); + } + } + + private Class[] getClasses(ClassLoader loader, String[] names) { + List> output = new ArrayList>(); + + for (String name : names) { + try { + output.add(loader.loadClass(name)); + } catch (ClassNotFoundException e) { + // Warn the user + reporter.reportWarning(this, "Unable to unload class " + name, e); + } + } + + return output.toArray(new Class[0]); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandBase.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandBase.java new file mode 100644 index 00000000..be72d60f --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandBase.java @@ -0,0 +1,51 @@ +package com.comphenix.protocol; + +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; + +/** + * Base class for all our commands. + * + * @author Kristian + */ +abstract class CommandBase implements CommandExecutor { + + public static final String PERMISSION_ADMIN = "protocol.admin"; + + private String permission; + private String name; + private int minimumArgumentCount; + + public CommandBase(String permission, String name) { + this(permission, name, 0); + } + + public CommandBase(String permission, String name, int minimumArgumentCount) { + this.name = name; + this.permission = permission; + this.minimumArgumentCount = minimumArgumentCount; + } + + @Override + public final boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + // Make sure we're dealing with the correct command + if (!command.getName().equalsIgnoreCase(name)) { + return false; + } + if (!sender.hasPermission(permission)) { + sender.sendMessage(ChatColor.RED + "You haven't got permission to run this command."); + return true; + } + + // Check argument length + if (args != null && args.length >= minimumArgumentCount) { + return handleCommand(sender, args); + } else { + return false; + } + } + + protected abstract boolean handleCommand(CommandSender sender, String[] args); +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java new file mode 100644 index 00000000..86eccd23 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java @@ -0,0 +1,512 @@ +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.minecraft.server.Packet; +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.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.google.common.collect.DiscreteDomains; +import com.google.common.collect.Range; +import com.google.common.collect.Ranges; + +/** + * Handles the "packet" debug command. + * + * @author Kristian + */ +class CommandPacket extends CommandBase { + + 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 ErrorReporter reporter; + 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); + + public CommandPacket(Plugin plugin, Logger logger, ErrorReporter reporter, ProtocolManager manager) { + super(CommandBase.PERMISSION_ADMIN, NAME, 2); + this.plugin = plugin; + this.logger = logger; + this.reporter = reporter; + this.manager = manager; + 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, "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); + } + } + + 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) { + 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 { + if (side.isForClient()) + return Packets.Client.getSupported(); + else if (side.isForServer()) + return Packets.Server.getSupported(); + else + throw new IllegalArgumentException("Illegal side: " + side); + } + + private Set getNamedPackets(ConnectionSide side) { + + Set valids = null; + Set result = null; + + try { + valids = getValidPackets(side); + } catch (FieldAccessException e) { + valids = Ranges.closed(0, 255).asSet(DiscreteDomains.integers()); + } + + // Check connection side + if (side.isForClient()) + result = Packets.Client.getRegistry().values(); + else if (side.isForServer()) + result = Packets.Server.getRegistry().values(); + else + throw new IllegalArgumentException("Illegal side: " + side); + + // 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()) { + printInformation(event); + } + } + + @Override + public void onPacketReceiving(PacketEvent event) { + if (side.isForClient()) { + printInformation(event); + } + } + + private void printInformation(PacketEvent event) { + String verb = side.isForClient() ? "Received" : "Sent"; + String shortDescription = String.format( + "%s %s (%s) from %s", + verb, + Packets.getDeclaredName(event.getPacketID()), + event.getPacketID(), + event.getPlayer().getName() + ); + + // Detailed will print the packet's content too + if (detailed) { + 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 + "."); + } + } + + @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) { + getListenerTree(side).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) { + + // The interval tree will automatically remove the listeners for us + return getListenerTree(side).remove(idStart, idStop); + } + + private AbstractIntervalTree getListenerTree(ConnectionSide side) { + if (side.isForClient()) + return clientListeners; + else if (side.isForServer()) + return serverListeners; + else + throw new IllegalArgumentException("Not a legal connection side."); + } + + 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/CommandProtocol.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandProtocol.java new file mode 100644 index 00000000..494d6f84 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandProtocol.java @@ -0,0 +1,89 @@ +package com.comphenix.protocol; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.metrics.Updater; +import com.comphenix.protocol.metrics.Updater.UpdateResult; +import com.comphenix.protocol.metrics.Updater.UpdateType; + +/** + * Handles the "protocol" administration command. + * + * @author Kristian + */ +class CommandProtocol extends CommandBase { + /** + * Name of this command. + */ + public static final String NAME = "protocol"; + + private Plugin plugin; + private Updater updater; + private ProtocolConfig config; + + public CommandProtocol(Plugin plugin, Updater updater, ProtocolConfig config) { + super(CommandBase.PERMISSION_ADMIN, NAME, 1); + this.plugin = plugin; + this.updater = updater; + this.config = config; + } + + @Override + protected boolean handleCommand(CommandSender sender, String[] args) { + String subCommand = args[0]; + + // Only return TRUE if we executed the correct command + if (subCommand.equalsIgnoreCase("config") || subCommand.equalsIgnoreCase("reload")) + reloadConfiguration(sender); + else if (subCommand.equalsIgnoreCase("check")) + checkVersion(sender); + else if (subCommand.equalsIgnoreCase("update")) + updateVersion(sender); + else + return false; + return true; + } + + public void checkVersion(final CommandSender sender) { + // Perform on an async thread + plugin.getServer().getScheduler().scheduleAsyncDelayedTask(plugin, new Runnable() { + @Override + public void run() { + UpdateResult result = updater.update(UpdateType.NO_DOWNLOAD, true); + sender.sendMessage(ChatColor.BLUE + "[ProtocolLib] " + result.toString()); + } + }); + + updateFinished(); + } + + public void updateVersion(final CommandSender sender) { + // Perform on an async thread + plugin.getServer().getScheduler().scheduleAsyncDelayedTask(plugin, new Runnable() { + @Override + public void run() { + UpdateResult result = updater.update(UpdateType.DEFAULT, true); + sender.sendMessage(ChatColor.BLUE + "[ProtocolLib] " + result.toString()); + } + }); + + updateFinished(); + } + + /** + * Prevent further automatic updates until the next delay. + */ + public void updateFinished() { + long currentTime = System.currentTimeMillis() / ProtocolLibrary.MILLI_PER_SECOND; + + config.setAutoLastTime(currentTime); + config.saveAll(); + } + + public void reloadConfiguration(CommandSender sender) { + plugin.reloadConfig(); + sender.sendMessage(ChatColor.BLUE + "Reloaded configuration!"); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java new file mode 100644 index 00000000..09e5313d --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java @@ -0,0 +1,188 @@ +package com.comphenix.protocol; + +import java.io.File; + +import org.bukkit.configuration.Configuration; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.plugin.Plugin; + +/** + * Represents the configuration of ProtocolLib. + * + * @author Kristian + */ +class ProtocolConfig { + + private static final String SECTION_GLOBAL = "global"; + private static final String SECTION_AUTOUPDATER = "auto updater"; + + private static final String METRICS_ENABLED = "metrics"; + + private static final String UPDATER_NOTIFY = "notify"; + private static final String UPDATER_DOWNLAD = "download"; + private static final String UPDATER_DELAY = "delay"; + private static final String UPDATER_LAST_TIME = "last"; + + // Defaults + private static final long DEFAULT_UPDATER_DELAY = 60; + + private Plugin plugin; + private Configuration config; + private boolean loadingSections; + + private ConfigurationSection global; + private ConfigurationSection updater; + + public ProtocolConfig(Plugin plugin) { + this(plugin, plugin.getConfig()); + } + + public ProtocolConfig(Plugin plugin, Configuration config) { + this.plugin = plugin; + reloadConfig(); + } + + /** + * Reload configuration file. + */ + public void reloadConfig() { + this.config = plugin.getConfig(); + loadSections(true); + } + + /** + * Load data sections. + * @param copyDefaults - whether or not to copy configuration defaults. + */ + private void loadSections(boolean copyDefaults) { + if (loadingSections) + return; + + if (config != null) { + global = config.getConfigurationSection(SECTION_GLOBAL); + } + if (global != null) { + updater = global.getConfigurationSection(SECTION_AUTOUPDATER); + } + + // Automatically copy defaults + if (copyDefaults && (!getFile().exists() || global == null || updater == null)) { + loadingSections = true; + + if (config != null) + config.options().copyDefaults(true); + plugin.saveDefaultConfig(); + config = plugin.getConfig(); + + loadingSections = false; + loadSections(false); + + // Inform the user + System.out.println("[ProtocolLib] Created default configuration."); + } + } + + /** + * Retrieve a reference to the configuration file. + * @return Configuration file on disk. + */ + public File getFile() { + return new File(plugin.getDataFolder(), "config.yml"); + } + + /** + * Retrieve whether or not ProtocolLib should determine if a new version has been released. + * @return TRUE if it should do this automatically, FALSE otherwise. + */ + public boolean isAutoNotify() { + return updater.getBoolean(UPDATER_NOTIFY, true); + } + + /** + * Set whether or not ProtocolLib should determine if a new version has been released. + * @param value - TRUE to do this automatically, FALSE otherwise. + */ + public void setAutoNotify(boolean value) { + updater.set(UPDATER_NOTIFY, value); + } + + /** + * Retrieve whether or not ProtocolLib should automatically download the new version. + * @return TRUE if it should, FALSE otherwise. + */ + public boolean isAutoDownload() { + return updater != null && updater.getBoolean(UPDATER_DOWNLAD, true); + } + + /** + * Set whether or not ProtocolLib should automatically download the new version. + * @param value - TRUE if it should. FALSE otherwise. + */ + public void setAutoDownload(boolean value) { + updater.set(UPDATER_DOWNLAD, value); + } + + /** + * Retrieve the amount of time to wait until checking for a new update. + * @return The amount of time to wait. + */ + public long getAutoDelay() { + // Note that the delay must be greater than 59 seconds + return Math.max(updater.getInt(UPDATER_DELAY, 0), DEFAULT_UPDATER_DELAY); + } + + /** + * Set the amount of time to wait until checking for a new update. + *

+ * This time must be greater than 59 seconds. + * @param delaySeconds - the amount of time to wait. + */ + public void setAutoDelay(long delaySeconds) { + // Silently fix the delay + if (delaySeconds < DEFAULT_UPDATER_DELAY) + delaySeconds = DEFAULT_UPDATER_DELAY; + updater.set(UPDATER_DELAY, delaySeconds); + } + + /** + * Retrieve the last time we updated, in seconds since 1970.01.01 00:00. + * @return Last update time. + */ + public long getAutoLastTime() { + return updater.getLong(UPDATER_LAST_TIME, 0); + } + + /** + * Retrieve whether or not metrics is enabled. + * @return TRUE if metrics is enabled, FALSE otherwise. + */ + public boolean isMetricsEnabled() { + return global.getBoolean(METRICS_ENABLED, true); + } + + /** + * Set whether or not metrics is enabled. + *

+ * This setting will take effect next time ProtocolLib is started. + * + * @param enabled - whether or not metrics is enabled. + */ + public void setMetricsEnabled(boolean enabled) { + global.set(METRICS_ENABLED, enabled); + } + + /** + * Set the last time we updated, in seconds since 1970.01.01 00:00. + * @param lastTimeSeconds - new last update time. + */ + public void setAutoLastTime(long lastTimeSeconds) { + updater.set(UPDATER_LAST_TIME, lastTimeSeconds); + } + + /** + * Save the current configuration file. + */ + public void saveAll() { + plugin.saveConfig(); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index d36cebe1..1a1d5db3 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -18,7 +18,8 @@ package com.comphenix.protocol; import java.io.IOException; -import java.util.logging.Level; +import java.util.logging.Handler; +import java.util.logging.LogRecord; import java.util.logging.Logger; import org.bukkit.Server; @@ -26,12 +27,12 @@ import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import com.comphenix.protocol.async.AsyncFilterManager; -import com.comphenix.protocol.events.ConnectionSide; -import com.comphenix.protocol.events.MonitorAdapter; -import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.error.DetailedErrorReporter; +import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.injector.DelayedSingleTask; import com.comphenix.protocol.injector.PacketFilterManager; import com.comphenix.protocol.metrics.Statistics; +import com.comphenix.protocol.metrics.Updater; import com.comphenix.protocol.reflect.compiler.BackgroundCompiler; /** @@ -41,11 +42,18 @@ import com.comphenix.protocol.reflect.compiler.BackgroundCompiler; */ public class ProtocolLibrary extends JavaPlugin { + /** + * The number of milliseconds per second. + */ + 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; - // Error logger - private Logger logger; + // Error reporter + private ErrorReporter reporter; // Metrics and statistisc private Statistics statistisc; @@ -62,78 +70,142 @@ public class ProtocolLibrary extends JavaPlugin { // Used to unhook players after a delay private DelayedSingleTask unhookTask; - // Used for debugging - private boolean debugListener; + // Settings/options + private ProtocolConfig config; + + // Updater + private Updater updater; + + // Logger + private Logger logger; + + // Commands + private CommandProtocol commandProtocol; + private CommandPacket commandPacket; @Override public void onLoad() { + // Load configuration logger = getLoggerSafely(); - unhookTask = new DelayedSingleTask(this); - protocolManager = new PacketFilterManager(getClassLoader(), getServer(), unhookTask, logger); + + // Add global parameters + DetailedErrorReporter reporter = new DetailedErrorReporter(); + updater = new Updater(this, logger, "protocollib", getFile(), "protocol.info"); + + try { + config = new ProtocolConfig(this); + } catch (Exception e) { + reporter.reportWarning(this, "Cannot load configuration", e); + + // Load it again + deleteConfig(); + config = new ProtocolConfig(this); + } + + try { + unhookTask = new DelayedSingleTask(this); + protocolManager = new PacketFilterManager(getClassLoader(), getServer(), unhookTask, reporter); + reporter.addGlobalParameter("manager", protocolManager); + + // Initialize command handlers + commandProtocol = new CommandProtocol(this, updater, config); + 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); + disablePlugin(); + } + } + + private void deleteConfig() { + config.getFile().delete(); } @Override - public void onEnable() { - Server server = getServer(); - PluginManager manager = server.getPluginManager(); - - // Initialize background compiler - if (backgroundCompiler == null) { - backgroundCompiler = new BackgroundCompiler(getClassLoader()); - BackgroundCompiler.setInstance(backgroundCompiler); + public void reloadConfig() { + super.reloadConfig(); + // Reload configuration + if (config != null) { + config.reloadConfig(); } - - // Notify server managers of incompatible plugins - checkForIncompatibility(manager); - - // Player login and logout events - protocolManager.registerEvents(manager, 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); + } - // Worker that ensures that async packets are eventually sent - createAsyncTask(server); - //toggleDebugListener(); + @Override + public void flush() { + // Not needed. + } + + @Override + public void close() throws SecurityException { + // Do nothing. + } + }); + } + + @Override + public void onEnable() { + try { + Server server = getServer(); + PluginManager manager = server.getPluginManager(); + + // Don't do anything else! + if (manager == null) + return; + + // Initialize background compiler + if (backgroundCompiler == null) { + backgroundCompiler = new BackgroundCompiler(getClassLoader()); + BackgroundCompiler.setInstance(backgroundCompiler); + } + + // Set up command handlers + getCommand(CommandProtocol.NAME).setExecutor(commandProtocol); + getCommand(CommandPacket.NAME).setExecutor(commandPacket); + + // Notify server managers of incompatible plugins + checkForIncompatibility(manager); + + // Player login and logout events + protocolManager.registerEvents(manager, this); + + // Worker that ensures that async packets are eventually sent + // It also performs the update check. + createAsyncTask(server); + + } catch (Throwable e) { + reporter.reportDetailed(this, "Cannot enable ProtocolLib.", e); + disablePlugin(); + return; + } // Try to enable statistics try { - statistisc = new Statistics(this); + if (config.isMetricsEnabled()) { + statistisc = new Statistics(this); + } } catch (IOException e) { - logger.log(Level.SEVERE, "Unable to enable metrics.", e); + reporter.reportDetailed(this, "Unable to enable metrics.", e, statistisc); } catch (Throwable e) { - logger.log(Level.SEVERE, "Metrics cannot be enabled. Incompatible Bukkit version.", e); + reporter.reportDetailed(this, "Metrics cannot be enabled. Incompatible Bukkit version.", e, statistisc); } } - + /** - * Toggle a listener that prints every sent and received packet. + * Disable the current plugin. */ - void toggleDebugListener() { - - if (debugListener) { - protocolManager.removePacketListeners(this); - } else { - // DEBUG DEBUG - protocolManager.addPacketListener(new MonitorAdapter(this, ConnectionSide.BOTH, logger) { - @Override - public void onPacketReceiving(PacketEvent event) { - Object handle = event.getPacket().getHandle(); - - System.out.println(String.format( - "RECEIVING %s@%s from %s.", - handle.getClass().getSimpleName(), handle.hashCode(), event.getPlayer().getName() - )); - }; - @Override - public void onPacketSending(PacketEvent event) { - Object handle = event.getPacket().getHandle(); - - System.out.println(String.format( - "SENDING %s@%s from %s.", - handle.getClass().getSimpleName(), handle.hashCode(), event.getPlayer().getName() - )); - } - }); - } - debugListener = !debugListener; + private void disablePlugin() { + getServer().getPluginManager().disablePlugin(this); } private void createAsyncTask(Server server) { @@ -149,16 +221,35 @@ public class ProtocolLibrary extends JavaPlugin { // We KNOW we're on the main thread at the moment manager.sendProcessedPackets(tickCounter++, true); + + // Check for updates too + checkUpdates(); } }, ASYNC_PACKET_DELAY, ASYNC_PACKET_DELAY); } catch (Throwable e) { if (asyncPacketTask == -1) { - logger.log(Level.SEVERE, "Unable to create packet timeout task.", e); + reporter.reportDetailed(this, "Unable to create packet timeout task.", e); } } } + private void checkUpdates() { + // Ignore milliseconds - it's pointless + long currentTime = System.currentTimeMillis() / MILLI_PER_SECOND; + + // Should we update? + if (currentTime > config.getAutoLastTime() + config.getAutoDelay()) { + // Initiate the update as if it came from the console + if (config.isAutoDownload()) + commandProtocol.updateVersion(getServer().getConsoleSender()); + else if (config.isAutoNotify()) + commandProtocol.checkVersion(getServer().getConsoleSender()); + else + commandProtocol.updateFinished(); + } + } + private void checkForIncompatibility(PluginManager manager) { // Plugin authors: Notify me to remove these String[] incompatiblePlugins = {}; @@ -166,7 +257,7 @@ public class ProtocolLibrary extends JavaPlugin { for (String plugin : incompatiblePlugins) { if (manager.getPlugin(plugin) != null) { // Check for versions, ect. - logger.severe("Detected incompatible plugin: " + plugin); + reporter.reportWarning(this, "Detected incompatible plugin: " + plugin); } } } @@ -190,6 +281,24 @@ public class ProtocolLibrary extends JavaPlugin { protocolManager.close(); protocolManager = null; statistisc = null; + + // Leaky ClassLoader begone! + CleanupStaticMembers cleanup = new CleanupStaticMembers(getClassLoader(), reporter); + cleanup.resetAll(); + } + + // Get the Bukkit logger first, before we try to create our own + private Logger getLoggerSafely() { + Logger log = null; + + try { + log = getLogger(); + } catch (Throwable e) { } + + // Use the default logger instead + if (log == null) + log = Logger.getLogger("Minecraft"); + return log; } /** @@ -203,26 +312,11 @@ public class ProtocolLibrary extends JavaPlugin { /** * Retrieve the metrics instance used to measure users of this library. *

- * Note that this method may return NULL when the server is reloading or shutting down. + * Note that this method may return NULL when the server is reloading or shutting down. It is also + * NULL if metrics has been disabled. * @return Metrics instance container. */ public Statistics getStatistics() { return statistisc; } - - // Get the Bukkit logger first, before we try to create our own - private Logger getLoggerSafely() { - - Logger log = null; - - try { - log = getLogger(); - } catch (Throwable e) { - // We'll handle it - } - - if (log == null) - log = Logger.getLogger("Minecraft"); - return log; - } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/RangeParser.java b/ProtocolLib/src/main/java/com/comphenix/protocol/RangeParser.java new file mode 100644 index 00000000..d396be43 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/RangeParser.java @@ -0,0 +1,142 @@ +package com.comphenix.protocol; + +import java.util.ArrayList; +import java.util.List; + +import com.google.common.collect.DiscreteDomains; +import com.google.common.collect.Range; +import com.google.common.collect.Ranges; + +/** + * Used to parse ranges in CommandPacket. + * + * @author Kristian + */ +class RangeParser { + /** + * Parse a range from a given text. + * @param text - the text. + * @param legalRange - range of legal values. + * @return The parsed ranges. + */ + public static List> getRanges(String text, Range legalRange) { + return getRanges(new String[] { text }, 0, 0, legalRange); + } + + /** + * Parse ranges from an array of elements. + * @param args - array of elements. + * @param offset - beginning offset. + * @param lastIndex - the last index of the array to read. + * @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; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/async/AsyncFilterManager.java b/ProtocolLib/src/main/java/com/comphenix/protocol/async/AsyncFilterManager.java index e0707229..7dd7b504 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/async/AsyncFilterManager.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/async/AsyncFilterManager.java @@ -20,8 +20,8 @@ package com.comphenix.protocol.async; import java.util.Collection; import java.util.List; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Logger; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitScheduler; @@ -29,12 +29,16 @@ import org.bukkit.scheduler.BukkitScheduler; import com.comphenix.protocol.AsynchronousManager; import com.comphenix.protocol.PacketStream; import com.comphenix.protocol.ProtocolManager; +import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.ListeningWhitelist; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.events.PacketListener; import com.comphenix.protocol.injector.PacketFilterManager; import com.comphenix.protocol.injector.PrioritizedListener; +import com.comphenix.protocol.injector.SortedPacketListenerList; import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; /** * Represents a filter manager for asynchronous packets. @@ -43,13 +47,18 @@ import com.google.common.base.Objects; */ public class AsyncFilterManager implements AsynchronousManager { + private SortedPacketListenerList serverTimeoutListeners; + private SortedPacketListenerList clientTimeoutListeners; + private Set timeoutListeners; + private PacketProcessingQueue serverProcessingQueue; private PacketSendingQueue serverQueue; + private PacketProcessingQueue clientProcessingQueue; private PacketSendingQueue clientQueue; - private Logger logger; + private ErrorReporter reporter; // The likely main thread private Thread mainThread; @@ -66,13 +75,32 @@ public class AsyncFilterManager implements AsynchronousManager { // Whether or not we're currently cleaning up private volatile boolean cleaningUp; - public AsyncFilterManager(Logger logger, BukkitScheduler scheduler, ProtocolManager manager) { + public AsyncFilterManager(ErrorReporter reporter, BukkitScheduler scheduler, ProtocolManager manager) { + + // Initialize timeout listeners + serverTimeoutListeners = new SortedPacketListenerList(); + clientTimeoutListeners = new SortedPacketListenerList(); + timeoutListeners = Sets.newSetFromMap(new ConcurrentHashMap()); // Server packets are synchronized already - this.serverQueue = new PacketSendingQueue(false); + this.serverQueue = new PacketSendingQueue(false) { + @Override + protected void onPacketTimeout(PacketEvent event) { + if (!cleaningUp) { + serverTimeoutListeners.invokePacketSending(AsyncFilterManager.this.reporter, event); + } + } + }; // Client packets must be synchronized - this.clientQueue = new PacketSendingQueue(true); + this.clientQueue = new PacketSendingQueue(true) { + @Override + protected void onPacketTimeout(PacketEvent event) { + if (!cleaningUp) { + clientTimeoutListeners.invokePacketSending(AsyncFilterManager.this.reporter, event); + } + } + }; this.serverProcessingQueue = new PacketProcessingQueue(serverQueue); this.clientProcessingQueue = new PacketProcessingQueue(clientQueue); @@ -80,7 +108,7 @@ public class AsyncFilterManager implements AsynchronousManager { this.scheduler = scheduler; this.manager = manager; - this.logger = logger; + this.reporter = reporter; this.mainThread = Thread.currentThread(); } @@ -89,6 +117,27 @@ public class AsyncFilterManager implements AsynchronousManager { return registerAsyncHandler(listener, true); } + @Override + public void registerTimeoutHandler(PacketListener listener) { + if (listener == null) + throw new IllegalArgumentException("listener cannot be NULL."); + if (!timeoutListeners.add(listener)) + return; + + ListeningWhitelist sending = listener.getSendingWhitelist(); + ListeningWhitelist receiving = listener.getReceivingWhitelist(); + + if (!ListeningWhitelist.isEmpty(sending)) + serverTimeoutListeners.addListener(listener, sending); + if (!ListeningWhitelist.isEmpty(receiving)) + serverTimeoutListeners.addListener(listener, receiving); + } + + @Override + public Set getTimeoutHandlers() { + return ImmutableSet.copyOf(timeoutListeners); + } + /** * Registers an asynchronous packet handler. *

@@ -131,6 +180,21 @@ public class AsyncFilterManager implements AsynchronousManager { return whitelist != null && whitelist.getWhitelist().size() > 0; } + @Override + public void unregisterTimeoutHandler(PacketListener listener) { + if (listener == null) + throw new IllegalArgumentException("listener cannot be NULL."); + + ListeningWhitelist sending = listener.getSendingWhitelist(); + ListeningWhitelist receiving = listener.getReceivingWhitelist(); + + // Do it in the opposite order + if (serverTimeoutListeners.removeListener(listener, sending).size() > 0 || + clientTimeoutListeners.removeListener(listener, receiving).size() > 0) { + timeoutListeners.remove(listener); + } + } + @Override public void unregisterAsyncHandler(AsyncListenerHandler handler) { if (handler == null) @@ -267,15 +331,19 @@ public class AsyncFilterManager implements AsynchronousManager { } @Override - public Logger getLogger() { - return logger; + public ErrorReporter getErrorReporter() { + return reporter; } - + @Override public void cleanupAll() { cleaningUp = true; serverProcessingQueue.cleanupAll(); serverQueue.cleanupAll(); + + timeoutListeners.clear(); + serverTimeoutListeners = null; + clientTimeoutListeners = null; } @Override @@ -333,7 +401,6 @@ public class AsyncFilterManager implements AsynchronousManager { * Send any due packets, or clean up packets that have expired. */ public void sendProcessedPackets(int tickCounter, boolean onMainThread) { - // The server queue is unlikely to need checking that often if (tickCounter % 10 == 0) { serverQueue.trySendPackets(onMainThread); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/async/AsyncListenerHandler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/async/AsyncListenerHandler.java index db01761b..0b1ef3ba 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/async/AsyncListenerHandler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/async/AsyncListenerHandler.java @@ -22,7 +22,6 @@ import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; import org.bukkit.plugin.Plugin; @@ -54,7 +53,7 @@ public class AsyncListenerHandler { private static final AtomicInteger nextID = new AtomicInteger(); // Default queue capacity - private static int DEFAULT_CAPACITY = 1024; + private static final int DEFAULT_CAPACITY = 1024; // Cancel the async handler private volatile boolean cancelled; @@ -113,10 +112,6 @@ public class AsyncListenerHandler { PacketListener getNullPacketListener() { return nullPacketListener; } - - private String getPluginName() { - return PacketAdapter.getPluginName(listener); - } /** * Retrieve the plugin associated with this async listener. @@ -442,17 +437,18 @@ public class AsyncListenerHandler { } catch (Throwable e) { // Minecraft doesn't want your Exception. - filterManager.getLogger().log(Level.SEVERE, - "Unhandled exception occured in onAsyncPacket() for " + getPluginName(), e); + filterManager.getErrorReporter().reportMinimal(listener.getPlugin(), "onAsyncPacket()", e); } // Now, get the next non-cancelled listener - for (; marker.getListenerTraversal().hasNext(); ) { - AsyncListenerHandler handler = marker.getListenerTraversal().next().getListener(); - - if (!handler.isCancelled()) { - handler.enqueuePacket(packet); - continue mainLoop; + if (!marker.hasExpired()) { + for (; marker.getListenerTraversal().hasNext(); ) { + AsyncListenerHandler handler = marker.getListenerTraversal().next().getListener(); + + if (!handler.isCancelled()) { + handler.enqueuePacket(packet); + continue mainLoop; + } } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/async/AsyncMarker.java b/ProtocolLib/src/main/java/com/comphenix/protocol/async/AsyncMarker.java index 36843939..febbbbcf 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/async/AsyncMarker.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/async/AsyncMarker.java @@ -51,7 +51,7 @@ public class AsyncMarker implements Serializable, Comparable { /** * Default number of milliseconds until a packet will rejected. */ - public static final int DEFAULT_TIMEOUT_DELTA = 60000; + public static final int DEFAULT_TIMEOUT_DELTA = 1800 * 1000; /** * Default number of packets to skip. @@ -418,7 +418,7 @@ public class AsyncMarker implements Serializable, Comparable { // We're in 1.2.5 alwaysSync = true; } else { - System.err.println("Cannot determine asynchronous state of packets!"); + System.err.println("[ProtocolLib] Cannot determine asynchronous state of packets!"); alwaysSync = true; } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketSendingQueue.java b/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketSendingQueue.java index 01fd8449..698c2def 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketSendingQueue.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketSendingQueue.java @@ -27,13 +27,14 @@ import org.bukkit.entity.Player; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.injector.PlayerLoggedOutException; +import com.comphenix.protocol.injector.SortedPacketListenerList; import com.comphenix.protocol.reflect.FieldAccessException; /** * Represents packets ready to be transmitted to a client. * @author Kristian */ -class PacketSendingQueue { +abstract class PacketSendingQueue { public static final int INITIAL_CAPACITY = 64; @@ -77,7 +78,7 @@ class PacketSendingQueue { AsyncMarker marker = packetUpdated.getAsyncMarker(); // Should we reorder the event? - if (marker.getQueuedSendingIndex() != marker.getNewSendingIndex()) { + if (marker.getQueuedSendingIndex() != marker.getNewSendingIndex() && !marker.hasExpired()) { PacketEvent copy = PacketEvent.fromSynchronous(packetUpdated, marker); // "Cancel" the original event @@ -127,6 +128,7 @@ class PacketSendingQueue { if (holder != null) { PacketEvent current = holder.getEvent(); AsyncMarker marker = current.getAsyncMarker(); + boolean hasExpired = marker.hasExpired(); // Abort if we're not on the main thread if (synchronizeMain) { @@ -144,8 +146,16 @@ class PacketSendingQueue { } } - if (marker.isProcessed() || marker.hasExpired()) { - if (marker.isProcessed() && !current.isCancelled()) { + if (marker.isProcessed() || hasExpired) { + if (hasExpired) { + // Notify timeout listeners + onPacketTimeout(current); + + // Recompute + marker = current.getAsyncMarker(); + hasExpired = marker.hasExpired(); + } + if (marker.isProcessed() && !current.isCancelled() && !hasExpired) { // Silently skip players that have logged out if (isOnline(current.getPlayer())) { sendPacket(current); @@ -162,6 +172,12 @@ class PacketSendingQueue { } } + /** + * Invoked when a packet has timed out. + * @param event - the timed out packet. + */ + protected abstract void onPacketTimeout(PacketEvent event); + private boolean isOnline(Player player) { return player != null && player.isOnline(); } @@ -205,7 +221,7 @@ class PacketSendingQueue { } catch (PlayerLoggedOutException e) { System.out.println(String.format( - "Warning: Dropped packet index %s of ID %s", + "[ProtocolLib] Warning: Dropped packet index %s of ID %s", marker.getOriginalSendingIndex(), event.getPacketID() )); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractIntervalTree.java b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractIntervalTree.java new file mode 100644 index 00000000..e2748cbc --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractIntervalTree.java @@ -0,0 +1,375 @@ +package com.comphenix.protocol.concurrency; + +import java.util.HashSet; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Set; +import java.util.TreeMap; + +import com.google.common.collect.Range; +import com.google.common.collect.Ranges; + +/** + * Represents a generic store of intervals and associated values. No two intervals + * can overlap in this representation. + *

+ * Note that this implementation is not thread safe. + * + * @author Kristian + * + * @param - type of the key. Must implement Comparable. + * @param - type of the value to associate. + */ +public abstract class AbstractIntervalTree, TValue> { + + protected enum State { + OPEN, + CLOSE, + BOTH + } + + /** + * Represents a range and a value in this interval tree. + */ + public class Entry implements Map.Entry, TValue> { + private final Range key; + 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.left = left; + this.right = right; + } + + @Override + public Range getKey() { + return key; + } + + @Override + public TValue getValue() { + return left.value; + } + + @Override + public TValue setValue(TValue value) { + TValue old = left.value; + + // Set both end points + left.value = value; + right.value = value; + return old; + } + } + + /** + * Represents a single end point (open, close or both) of a range. + */ + protected class EndPoint { + + // Whether or not the end-point is opening a range, closing a range or both. + public State state; + + // The value this range contains + public TValue value; + + public EndPoint(State state, TValue value) { + this.state = state; + this.value = value; + } + } + + // To quickly look up ranges we'll index them by endpoints + protected NavigableMap bounds = new TreeMap(); + + /** + * Removes every interval that intersects with the given range. + * @param lowerBound - lowest value to remove. + * @param upperBound - highest value to remove. + */ + public Set remove(TKey lowerBound, TKey upperBound) { + return remove(lowerBound, upperBound, false); + } + + /** + * Removes every interval that intersects with the given range. + * @param lowerBound - lowest value to remove. + * @param upperBound - highest value to remove. + * @param preserveOutside - whether or not to preserve the intervals that are partially outside. + */ + 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; + + Set resized = new HashSet(); + Set removed = new HashSet(); + + // Remove the previous element too. A close end-point must be preceded by an OPEN end-point. + if (first != null && range.get(first).state == State.CLOSE) { + TKey key = bounds.floorKey(first); + EndPoint removedPoint = removeIfNonNull(key); + + // Add the interval back + if (removedPoint != null && preserveDifference) { + resized.add(putUnsafe(key, decrementKey(lowerBound), removedPoint.value)); + } + } + + // Get the closing element too. + if (last != null && range.get(last).state == State.OPEN) { + TKey key = bounds.ceilingKey(last); + EndPoint removedPoint = removeIfNonNull(key); + + if (removedPoint != null && preserveDifference) { + resized.add(putUnsafe(incrementKey(upperBound), key, removedPoint.value)); + } + } + + // Get the removed entries too + getEntries(removed, range); + invokeEntryRemoved(removed); + + if (preserveDifference) { + invokeEntryRemoved(resized); + invokeEntryAdded(resized); + } + + // Remove the range as well + range.clear(); + return removed; + } + + // Helper + private EndPoint removeIfNonNull(TKey key) { + if (key != null) { + return bounds.remove(key); + } else { + return null; + } + } + + // Adds a given end point + protected EndPoint addEndPoint(TKey key, TValue value, State state) { + EndPoint endPoint = bounds.get(key); + + if (endPoint != null) { + endPoint.state = State.BOTH; + } else { + endPoint = new EndPoint(state, value); + bounds.put(key, endPoint); + } + return endPoint; + } + + /** + * Associates a given interval of keys with a certain value. Any previous + * association will be overwritten in the given interval. + *

+ * Overlapping intervals are not permitted. A key can only be associated with a single value. + * + * @param lowerBound - the minimum key (inclusive). + * @param upperBound - the maximum key (inclusive). + * @param value - the value, or NULL to reset this range. + */ + public void put(TKey lowerBound, TKey upperBound, TValue value) { + // While we don't permit overlapping intervals, we'll still allow overwriting existing intervals. + remove(lowerBound, upperBound, true); + invokeEntryAdded(putUnsafe(lowerBound, upperBound, value)); + } + + /** + * Associates a given interval without performing any interval checks. + * @param lowerBound - the minimum key (inclusive). + * @param upperBound - the maximum key (inclusive). + * @param value - the value, or NULL to reset the range. + */ + private Entry putUnsafe(TKey lowerBound, TKey upperBound, TValue value) { + // OK. Add the end points now + if (value != null) { + EndPoint left = addEndPoint(lowerBound, value, State.OPEN); + EndPoint right = addEndPoint(upperBound, value, State.CLOSE); + + Range range = Ranges.closed(lowerBound, upperBound); + return new Entry(range, left, right); + } else { + return null; + } + } + + /** + * Used to verify the validity of the given interval. + * @param lowerBound - lower bound (inclusive). + * @param upperBound - upper bound (inclusive). + */ + private void checkBounds(TKey lowerBound, TKey upperBound) { + if (lowerBound == null) + throw new IllegalAccessError("lowerbound cannot be NULL."); + if (upperBound == null) + throw new IllegalAccessError("upperBound cannot be NULL."); + if (upperBound.compareTo(lowerBound) < 0) + throw new IllegalArgumentException("upperBound cannot be less than lowerBound."); + } + + /** + * Determines if the given key is within an interval. + * @param key - key to check. + * @return TRUE if the given key is within an interval in this tree, FALSE otherwise. + */ + public boolean containsKey(TKey key) { + return getEndPoint(key) != null; + } + + /** + * Enumerates over every range in this interval tree. + * @return Number of ranges. + */ + public Set entrySet() { + // Don't mind the Java noise + Set result = new HashSet(); + getEntries(result, bounds); + return result; + } + + /** + * Remove every interval. + */ + public void clear() { + if (!bounds.isEmpty()) { + remove(bounds.firstKey(), bounds.lastKey()); + } + } + + /** + * Converts a map of end points into a set of entries. + * @param destination - set of entries. + * @param map - a map of end points. + */ + private void getEntries(Set destination, NavigableMap map) { + Map.Entry last = null; + + for (Map.Entry entry : bounds.entrySet()) { + switch (entry.getValue().state) { + case BOTH: + 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, last.getValue(), entry.getValue())); + break; + case OPEN: + // We don't know the full range yet + last = entry; + break; + default: + throw new IllegalStateException("Illegal open/close state detected."); + } + } + } + + /** + * Inserts every range from the given tree into the current tree. + * @param other - the other tree to read from. + */ + public void putAll(AbstractIntervalTree other) { + // Naively copy every range. + for (Entry entry : other.entrySet()) { + put(entry.key.lowerEndpoint(), entry.key.upperEndpoint(), entry.getValue()); + } + } + + /** + * Retrieves the value of the range that matches the given key, or NULL if nothing was found. + * @param key - the level to read for. + * @return The correct amount of experience, or NULL if nothing was recorded. + */ + public TValue get(TKey key) { + EndPoint point = getEndPoint(key); + + if (point != null) + return point.value; + else + return null; + } + + /** + * Get the end-point composite associated with this key. + * @param key - key to search for. + * @return The end point found, or NULL. + */ + protected EndPoint getEndPoint(TKey key) { + EndPoint ends = bounds.get(key); + + if (ends != null) { + // This is a piece of cake + return ends; + } else { + + // We need to determine if the point intersects with a range + TKey left = bounds.floorKey(key); + + // We only need to check to the left + if (left != null && bounds.get(left).state == State.OPEN) { + return bounds.get(left); + } else { + return null; + } + } + } + + private void invokeEntryAdded(Entry added) { + if (added != null) { + onEntryAdded(added); + } + } + + private void invokeEntryAdded(Set added) { + for (Entry entry : added) { + onEntryAdded(entry); + } + } + + private void invokeEntryRemoved(Set removed) { + for (Entry entry : removed) { + onEntryRemoved(entry); + } + } + + // Listeners for added or removed entries + /** + * Invoked when an entry is added. + * @param added - the entry that was added. + */ + protected void onEntryAdded(Entry added) { } + + /** + * Invoked when an entry is removed. + * @param removed - the removed entry. + */ + protected void onEntryRemoved(Entry removed) { } + + // Helpers for decrementing or incrementing key values + /** + * Decrement the given key by one unit. + * @param key - the key that should be decremented. + * @return The new decremented key. + */ + protected abstract TKey decrementKey(TKey key); + + /** + * Increment the given key by one unit. + * @param key - the key that should be incremented. + * @return The new incremented key. + */ + protected abstract TKey incrementKey(TKey key); +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java new file mode 100644 index 00000000..bceb7dca --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java @@ -0,0 +1,279 @@ +package com.comphenix.protocol.error; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; +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 org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.events.PacketAdapter; +import com.google.common.primitives.Primitives; + +/** + * Internal class used to handle exceptions. + * + * @author Kristian + */ +public class DetailedErrorReporter implements ErrorReporter { + + public static final String SECOND_LEVEL_PREFIX = " "; + public static final String DEFAULT_PREFIX = " "; + public static final String DEFAULT_SUPPORT_URL = "http://dev.bukkit.org/server-mods/protocollib/"; + public static final String PLUGIN_NAME = "ProtocolLib"; + + // Users that are informed about errors in the chat + public static final String ERROR_PERMISSION = "protocol.info"; + + // We don't want to spam the server + public static final int DEFAULT_MAX_ERROR_COUNT = 20; + + protected String prefix; + protected String supportURL; + + protected int errorCount; + protected int maxErrorCount; + protected Logger logger; + + // Whether or not Apache Commons is not present + protected boolean apacheCommonsMissing; + + // Map of global objects + protected Map globalParameters = new HashMap(); + + /** + * Create a default error reporting system. + */ + public DetailedErrorReporter() { + this(DEFAULT_PREFIX, DEFAULT_SUPPORT_URL); + } + + /** + * Create a central error reporting system. + * @param prefix - default line prefix. + * @param supportURL - URL to report the error. + */ + public DetailedErrorReporter(String prefix, String supportURL) { + this(prefix, supportURL, DEFAULT_MAX_ERROR_COUNT, getBukkitLogger()); + } + + // Attempt to get the logger. + private static Logger getBukkitLogger() { + try { + return Bukkit.getLogger(); + } catch (Throwable e) { + return Logger.getLogger("Minecraft"); + } + } + + /** + * Create a central error reporting system. + * @param prefix - default line prefix. + * @param supportURL - URL to report the error. + * @param maxErrorCount - number of errors to print before giving up. + * @param logger - current logger. + */ + public DetailedErrorReporter(String prefix, String supportURL, int maxErrorCount, Logger logger) { + this.prefix = prefix; + this.supportURL = supportURL; + this.maxErrorCount = maxErrorCount; + this.logger = logger; + } + + @Override + public void reportMinimal(Plugin sender, String methodName, Throwable error) { + logger.log(Level.SEVERE, "[" + PLUGIN_NAME + "] Unhandled exception occured in " + methodName + " for " + + PacketAdapter.getPluginName(sender), error); + } + + @Override + public void reportWarning(Object sender, String message) { + logger.log(Level.WARNING, "[" + PLUGIN_NAME + "] [" + getSenderName(sender) + "] " + message); + } + + @Override + public void reportWarning(Object sender, String message, Throwable error) { + logger.log(Level.WARNING, "[" + PLUGIN_NAME + "] [" + getSenderName(sender) + "] " + message, error); + } + + private String getSenderName(Object sender) { + if (sender != null) + return sender.getClass().getSimpleName(); + else + return "NULL"; + } + + @Override + public void reportDetailed(Object sender, String message, Throwable error, Object... parameters) { + + // Do not overtly spam the server! + if (++errorCount > maxErrorCount) { + String maxReached = String.format("Reached maxmimum error count. Cannot pass error %s from %s.", error, sender); + logger.severe(maxReached); + return; + } + + StringWriter text = new StringWriter(); + PrintWriter writer = new PrintWriter(text); + + // Helpful message + writer.println("[ProtocolLib] INTERNAL ERROR: " + message); + writer.println("If this problem hasn't already been reported, please open a ticket"); + writer.println("at " + supportURL + " with the following data:"); + + // Now, let us print important exception information + writer.println(" ===== STACK TRACE ====="); + + if (error != null) + error.printStackTrace(writer); + + // Data dump! + writer.println(" ===== DUMP ====="); + + // Relevant parameters + if (parameters != null && parameters.length > 0) { + writer.println("Parameters:"); + + // We *really* want to get as much information as possible + for (Object param : parameters) { + writer.println(addPrefix(getStringDescription(param), SECOND_LEVEL_PREFIX)); + } + } + + // Global parameters + for (String param : globalParameters()) { + writer.println(SECOND_LEVEL_PREFIX + param + ":"); + writer.println(addPrefix(getStringDescription(getGlobalParameter(param)), + SECOND_LEVEL_PREFIX + SECOND_LEVEL_PREFIX)); + } + + // Now, for the sender itself + writer.println("Sender:"); + writer.println(addPrefix(getStringDescription(sender), SECOND_LEVEL_PREFIX)); + + // Add the server version too + if (Bukkit.getServer() != null) { + writer.println("Server:"); + writer.println(addPrefix(Bukkit.getServer().getVersion(), SECOND_LEVEL_PREFIX)); + + // Inform of this occurrence + if (ERROR_PERMISSION != null) { + Bukkit.getServer().broadcast( + String.format("Error %s (%s) occured in %s.", message, error, sender), + ERROR_PERMISSION + ); + } + } + + // Make sure it is reported + logger.severe(addPrefix(text.toString(), prefix)); + } + + /** + * Adds the given prefix to every line in the text. + * @param text - text to modify. + * @param prefix - prefix added to every line in the text. + * @return The modified text. + */ + protected String addPrefix(String text, String prefix) { + return text.replaceAll("(?m)^", prefix); + } + + protected String getStringDescription(Object value) { + + // We can't only rely on toString. + if (value == null) { + return "[NULL]"; + } if (isSimpleType(value)) { + return value.toString(); + } else { + try { + if (!apacheCommonsMissing) + return (ToStringBuilder.reflectionToString(value, ToStringStyle.MULTI_LINE_STYLE, false, null)); + } catch (Throwable ex) { + // Apache is probably missing + logger.warning("Cannot find Apache Commons. Object introspection disabled."); + apacheCommonsMissing = true; + } + + // Just use toString() + return String.format("%s", value); + } + } + + /** + * Determine if the given object is a wrapper for a primitive/simple type or not. + * @param test - the object to test. + * @return TRUE if this object is simple enough to simply be printed, FALSE othewise. + */ + protected boolean isSimpleType(Object test) { + return test instanceof String || Primitives.isWrapperType(test.getClass()); + } + + public int getErrorCount() { + return errorCount; + } + + public void setErrorCount(int errorCount) { + this.errorCount = errorCount; + } + + public int getMaxErrorCount() { + return maxErrorCount; + } + + public void setMaxErrorCount(int maxErrorCount) { + this.maxErrorCount = maxErrorCount; + } + + /** + * Adds the given global parameter. It will be included in every error report. + * @param key - name of parameter. + * @param value - the global parameter itself. + */ + public void addGlobalParameter(String key, Object value) { + globalParameters.put(key, value); + } + + public Object getGlobalParameter(String key) { + return globalParameters.get(key); + } + + public void clearGlobalParameters() { + globalParameters.clear(); + } + + public Set globalParameters() { + return globalParameters.keySet(); + } + + public String getSupportURL() { + return supportURL; + } + + public void setSupportURL(String supportURL) { + this.supportURL = supportURL; + } + + public String getPrefix() { + return prefix; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + public Logger getLogger() { + return logger; + } + + public void setLogger(Logger logger) { + this.logger = logger; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/ErrorReporter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/ErrorReporter.java new file mode 100644 index 00000000..de6e1e36 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/ErrorReporter.java @@ -0,0 +1,39 @@ +package com.comphenix.protocol.error; + +import org.bukkit.plugin.Plugin; + +public interface ErrorReporter { + + /** + * Prints a small minimal error report about an exception from another plugin. + * @param sender - the other plugin. + * @param methodName - name of the caller method. + * @param error - the exception itself. + */ + public abstract void reportMinimal(Plugin sender, String methodName, Throwable error); + + /** + * Prints a warning message from the current plugin. + * @param sender - the object containing the caller method. + * @param message - error message. + */ + public abstract void reportWarning(Object sender, String message); + + /** + * Prints a warning message from the current plugin. + * @param sender - the object containing the caller method. + * @param message - error message. + * @param error - the exception that was thrown. + */ + public abstract void reportWarning(Object sender, String message, Throwable error); + + /** + * Prints a detailed error report about an unhandled exception. + * @param sender - the object containing the caller method. + * @param message - an error message to include. + * @param error - the exception that was thrown in the caller method. + * @param parameters - parameters from the caller method. + */ + public abstract void reportDetailed(Object sender, String message, Throwable error, Object... parameters); + +} \ No newline at end of file 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/events/PacketAdapter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketAdapter.java index fbe14775..056fe089 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketAdapter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketAdapter.java @@ -168,12 +168,20 @@ public abstract class PacketAdapter implements PacketListener { /** * Retrieves the name of the plugin that has been associated with the listener. + * @param listener - the listener. * @return Name of the associated plugin. */ public static String getPluginName(PacketListener listener) { - - Plugin plugin = listener.getPlugin(); - + return getPluginName(listener.getPlugin()); + } + + /** + * Retrieves the name of the given plugin. + * @param plugin - the plugin. + * @return Name of the given plugin. + */ + public static String getPluginName(Plugin plugin) { + // Try to get the plugin name try { if (plugin == null) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java index e1cf9b94..5ab389c2 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java @@ -26,6 +26,8 @@ import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import org.bukkit.Bukkit; +import org.bukkit.Server; import org.bukkit.World; import org.bukkit.WorldType; import org.bukkit.craftbukkit.CraftWorld; @@ -268,8 +270,7 @@ public class PacketContainer implements Serializable { final Object worldServer = ((CraftWorld) world).getHandle(); final Class nmsEntityClass = net.minecraft.server.Entity.class; - final World worldCopy = world; - + if (getEntity == null) getEntity = FuzzyReflection.fromObject(worldServer).getMethodByParameters( "getEntity", nmsEntityClass, new Class[] { int.class }); @@ -296,14 +297,17 @@ public class PacketContainer implements Serializable { if (nmsEntity != null) { return nmsEntity.getBukkitEntity(); } else { - // Maybe it's a player that's just logged in? Try a search - for (Player player : worldCopy.getPlayers()) { - if (player.getEntityId() == id) { - return player; + Server server = Bukkit.getServer(); + + // Maybe it's a player that has just logged in? Try a search + if (server != null) { + for (Player player : server.getOnlinePlayers()) { + if (player.getEntityId() == id) { + return player; + } } } - System.out.println("Entity doesn't exist."); return null; } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ListenerInvoker.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ListenerInvoker.java index fd0a8391..ae48ea6d 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ListenerInvoker.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ListenerInvoker.java @@ -46,4 +46,25 @@ public interface ListenerInvoker { * @return The packet ID. */ public abstract int getPacketID(Packet packet); + + /** + * Associate a given class with the given packet ID. Internal method. + * @param clazz - class to associate. + * @param packetID - the packet ID. + */ + public abstract void unregisterPacketClass(Class clazz); + + /** + * Remove a given class from the packet registry. Internal method. + * @param clazz - class to remove. + */ + public abstract void registerPacketClass(Class clazz, int packetID); + + /** + * Retrieves the correct packet class from a given packet ID. + * @param packetID - the packet ID. + * @param forceVanilla - whether or not to look for vanilla classes, not injected classes. + * @return The associated class. + */ + public abstract Class getPacketClassFromID(int packetID, boolean forceVanilla); } \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/MinecraftRegistry.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/MinecraftRegistry.java index 7b171e0d..34f7a341 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/MinecraftRegistry.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/MinecraftRegistry.java @@ -157,7 +157,7 @@ class MinecraftRegistry { /** * Retrieves the correct packet class from a given packet ID. * @param packetID - the packet ID. - * @param vanilla - whether or not to look for vanilla classes, not injected classes. + * @param forceVanilla - whether or not to look for vanilla classes, not injected classes. * @return The associated class. */ public static Class getPacketClassFromID(int packetID, boolean forceVanilla) { @@ -172,7 +172,9 @@ class MinecraftRegistry { // Will most likely not be used for (Map.Entry entry : getPacketToID().entrySet()) { if (Objects.equal(entry.getValue(), packetID)) { - return entry.getKey(); + // Attempt to get the vanilla class here too + if (!forceVanilla || entry.getKey().getName().startsWith("net.minecraft.server")) + return entry.getKey(); } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketConstructor.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketConstructor.java index c00c7f72..7c1f85e6 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketConstructor.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketConstructor.java @@ -27,6 +27,7 @@ import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.reflect.FieldAccessException; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; +import com.google.common.primitives.Primitives; /** * A packet constructor that uses an internal Minecraft. @@ -40,7 +41,7 @@ public class PacketConstructor { *

* Remember to call withPacket(). */ - public static final PacketConstructor DEFAULT = new PacketConstructor(null); + public static PacketConstructor DEFAULT = new PacketConstructor(null); // The constructor method that's actually responsible for creating the packet private Constructor constructorMethod; @@ -115,7 +116,7 @@ public class PacketConstructor { } } - Class packetType = MinecraftRegistry.getPacketClassFromID(id); + Class packetType = MinecraftRegistry.getPacketClassFromID(id, true); if (packetType == null) throw new IllegalArgumentException("Could not find a packet by the id " + id); @@ -176,7 +177,17 @@ public class PacketConstructor { // Determine if the types are similar if (params.length == types.length) { for (int i = 0; i < params.length; i++) { - if (!params[i].isAssignableFrom(types[i])) { + Class inputType = types[i]; + Class paramType = params[i]; + + // The input type is always wrapped + if (paramType.isPrimitive()) { + // Wrap it + paramType = Primitives.wrap(paramType); + } + + // Compare assignability + if (!paramType.isAssignableFrom(inputType)) { return false; } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java index 2aef7f5b..29ee6f9d 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java @@ -23,9 +23,8 @@ import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.annotation.Nullable; @@ -50,6 +49,7 @@ import com.comphenix.protocol.AsynchronousManager; import com.comphenix.protocol.ProtocolManager; import com.comphenix.protocol.async.AsyncFilterManager; import com.comphenix.protocol.async.AsyncMarker; +import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.*; import com.comphenix.protocol.injector.player.PlayerInjectionHandler; import com.comphenix.protocol.reflect.FieldAccessException; @@ -108,8 +108,8 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok private PlayerInjectionHandler playerInjection; // The two listener containers - private SortedPacketListenerList recievedListeners = new SortedPacketListenerList(); - private SortedPacketListenerList sendingListeners = new SortedPacketListenerList(); + private SortedPacketListenerList recievedListeners; + private SortedPacketListenerList sendingListeners; // Whether or not this class has been closed private volatile boolean hasClosed; @@ -117,8 +117,8 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok // The default class loader private ClassLoader classLoader; - // Error logger - private Logger logger; + // Error repoter + private ErrorReporter reporter; // The current server private Server server; @@ -134,24 +134,31 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok private AtomicInteger phaseLoginCount = new AtomicInteger(0); private AtomicInteger phasePlayingCount = new AtomicInteger(0); + // Whether or not plugins are using the send/receive methods + private AtomicBoolean packetCreation = new AtomicBoolean(); + /** * Only create instances of this class if protocol lib is disabled. * @param unhookTask */ - public PacketFilterManager(ClassLoader classLoader, Server server, DelayedSingleTask unhookTask, Logger logger) { - if (logger == null) - throw new IllegalArgumentException("logger cannot be NULL."); + public PacketFilterManager(ClassLoader classLoader, Server server, DelayedSingleTask unhookTask, ErrorReporter reporter) { + if (reporter == null) + throw new IllegalArgumentException("reporter cannot be NULL."); if (classLoader == null) throw new IllegalArgumentException("classLoader cannot be NULL."); // Just boilerplate final DelayedSingleTask finalUnhookTask = unhookTask; + // Listener containers + this.recievedListeners = new SortedPacketListenerList(); + this.sendingListeners = new SortedPacketListenerList(); + // References this.unhookTask = unhookTask; this.server = server; this.classLoader = classLoader; - this.logger = logger; + this.reporter = reporter; // Used to determine if injection is needed Predicate isInjectionNecessary = new Predicate() { @@ -170,20 +177,20 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok try { // Initialize injection mangers - this.playerInjection = new PlayerInjectionHandler(classLoader, logger, isInjectionNecessary, this, server); - this.packetInjector = new PacketInjector(classLoader, this, playerInjection); - this.asyncFilterManager = new AsyncFilterManager(logger, server.getScheduler(), this); + this.playerInjection = new PlayerInjectionHandler(classLoader, reporter, isInjectionNecessary, this, packetListeners, server); + this.packetInjector = new PacketInjector(classLoader, this, playerInjection, reporter); + this.asyncFilterManager = new AsyncFilterManager(reporter, server.getScheduler(), this); // Attempt to load the list of server and client packets try { this.serverPackets = MinecraftRegistry.getServerPackets(); this.clientPackets = MinecraftRegistry.getClientPackets(); } catch (FieldAccessException e) { - logger.log(Level.WARNING, "Cannot load server and client packet list.", e); + reporter.reportWarning(this, "Cannot load server and client packet list.", e); } } catch (IllegalAccessException e) { - logger.log(Level.SEVERE, "Unable to initialize packet injector.", e); + reporter.reportWarning(this, "Unable to initialize packet injector.", e); } } @@ -206,15 +213,8 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok */ public void setPlayerHook(PlayerInjectHooks playerHook) { playerInjection.setPlayerHook(playerHook); - - // Make sure the current listeners are compatible - playerInjection.checkListener(packetListeners); } - public Logger getLogger() { - return logger; - } - @Override public ImmutableSet getPacketListeners() { return ImmutableSet.copyOf(packetListeners); @@ -370,12 +370,16 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok @Override public void invokePacketRecieving(PacketEvent event) { - handlePacket(recievedListeners, event, false); + if (!hasClosed) { + handlePacket(recievedListeners, event, false); + } } @Override public void invokePacketSending(PacketEvent event) { - handlePacket(sendingListeners, event, true); + if (!hasClosed) { + handlePacket(sendingListeners, event, true); + } } /** @@ -394,9 +398,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok // Process synchronous events if (sending) - packetListeners.invokePacketSending(logger, event); + packetListeners.invokePacketSending(reporter, event); else - packetListeners.invokePacketRecieving(logger, event); + packetListeners.invokePacketRecieving(reporter, event); // To cancel asynchronous processing, use the async marker if (!event.isCancelled() && !hasAsyncCancelled(event.getAsyncMarker())) { @@ -434,7 +438,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok if (serverPackets != null && serverPackets.contains(packetID)) playerInjection.addPacketHandler(packetID); else - logger.warning(String.format( + reporter.reportWarning(this, String.format( "[%s] Unsupported server packet ID in current Minecraft version: %s", PacketAdapter.getPluginName(listener), packetID )); @@ -445,7 +449,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok if (clientPackets != null && clientPackets.contains(packetID)) packetInjector.addPacketHandler(packetID); else - logger.warning(String.format( + reporter.reportWarning(this, String.format( "[%s] Unsupported client packet ID in current Minecraft version: %s", PacketAdapter.getPluginName(listener), packetID )); @@ -481,6 +485,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok throw new IllegalArgumentException("reciever cannot be NULL."); if (packet == null) throw new IllegalArgumentException("packet cannot be NULL."); + // We may have to enable player injection indefinitely after this + if (packetCreation.compareAndSet(false, true)) + incrementPhases(GamePhase.PLAYING); playerInjection.sendServerPacket(reciever, packet, filters); } @@ -492,11 +499,13 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok @Override public void recieveClientPacket(Player sender, PacketContainer packet, boolean filters) throws IllegalAccessException, InvocationTargetException { - if (sender == null) throw new IllegalArgumentException("sender cannot be NULL."); if (packet == null) throw new IllegalArgumentException("packet cannot be NULL."); + // And here too + if (packetCreation.compareAndSet(false, true)) + incrementPhases(GamePhase.PLAYING); Packet mcPacket = packet.getHandle(); @@ -585,26 +594,43 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok manager.registerEvents(new Listener() { @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) public void onPrePlayerJoin(PlayerJoinEvent event) { - // Let's clean up the other injection first. - playerInjection.uninjectPlayer(event.getPlayer().getAddress()); + try { + // Let's clean up the other injection first. + playerInjection.uninjectPlayer(event.getPlayer().getAddress()); + } catch (Exception e) { + reporter.reportDetailed(PacketFilterManager.this, "Unable to uninject net handler for player.", e, event); + } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onPlayerJoin(PlayerJoinEvent event) { - // This call will be ignored if no listeners are registered - playerInjection.injectPlayer(event.getPlayer()); + try { + // This call will be ignored if no listeners are registered + playerInjection.injectPlayer(event.getPlayer()); + } catch (Exception e) { + reporter.reportDetailed(PacketFilterManager.this, "Unable to inject player.", e, event); + } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onPlayerQuit(PlayerQuitEvent event) { - playerInjection.uninjectPlayer(event.getPlayer()); + try { + playerInjection.handleDisconnect(event.getPlayer()); + playerInjection.uninjectPlayer(event.getPlayer()); + } catch (Exception e) { + reporter.reportDetailed(PacketFilterManager.this, "Unable to uninject logged off player.", e, event); + } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onPluginDisabled(PluginDisableEvent event) { - // Clean up in case the plugin forgets - if (event.getPlugin() != plugin) { - removePacketListeners(event.getPlugin()); + try { + // Clean up in case the plugin forgets + if (event.getPlugin() != plugin) { + removePacketListeners(event.getPlugin()); + } + } catch (Exception e) { + reporter.reportDetailed(PacketFilterManager.this, "Unable handle disabled plugin.", e, event); } } @@ -640,6 +666,21 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok return MinecraftRegistry.getPacketToID().get(packet.getClass()); } + @Override + public void registerPacketClass(Class clazz, int packetID) { + MinecraftRegistry.getPacketToID().put(clazz, packetID); + } + + @Override + public void unregisterPacketClass(Class clazz) { + MinecraftRegistry.getPacketToID().remove(clazz); + } + + @Override + public Class getPacketClassFromID(int packetID, boolean forceVanilla) { + return MinecraftRegistry.getPacketClassFromID(packetID, forceVanilla); + } + // Yes, this is crazy. @SuppressWarnings({ "unchecked", "rawtypes" }) private void registerOld(PluginManager manager, Plugin plugin) { @@ -680,10 +721,14 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok Object event = args[0]; // Check for the correct event - if (event instanceof PlayerJoinEvent) - playerInjection.injectPlayer(((PlayerJoinEvent) event).getPlayer()); - else if (event instanceof PlayerQuitEvent) - playerInjection.uninjectPlayer(((PlayerQuitEvent) event).getPlayer()); + if (event instanceof PlayerJoinEvent) { + Player player = ((PlayerJoinEvent) event).getPlayer(); + playerInjection.injectPlayer(player); + } else if (event instanceof PlayerQuitEvent) { + Player player = ((PlayerQuitEvent) event).getPlayer(); + playerInjection.handleDisconnect(player); + playerInjection.uninjectPlayer(player); + } } return null; } @@ -775,6 +820,8 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok // Remove listeners packetListeners.clear(); + recievedListeners = null; + sendingListeners = null; // Clean up async handlers. We have to do this last. asyncFilterManager.cleanupAll(); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketInjector.java index 1361d64c..951e4d79 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketInjector.java @@ -31,6 +31,7 @@ import net.minecraft.server.Packet; import net.sf.cglib.proxy.Callback; import net.sf.cglib.proxy.Enhancer; +import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.injector.player.PlayerInjectionHandler; @@ -51,6 +52,9 @@ class PacketInjector { // The packet filter manager private ListenerInvoker manager; + // Error reporter + private ErrorReporter reporter; + // Allows us to determine the sender private PlayerInjectionHandler playerInjection; @@ -61,11 +65,12 @@ class PacketInjector { private ClassLoader classLoader; public PacketInjector(ClassLoader classLoader, ListenerInvoker manager, - PlayerInjectionHandler playerInjection) throws IllegalAccessException { + PlayerInjectionHandler playerInjection, ErrorReporter reporter) throws IllegalAccessException { this.classLoader = classLoader; this.manager = manager; this.playerInjection = playerInjection; + this.reporter = reporter; this.readModifier = new ConcurrentHashMap(); initialize(); } @@ -129,12 +134,11 @@ class PacketInjector { // Subclass the specific packet class ex.setSuperclass(old); ex.setCallbackType(ReadPacketModifier.class); - ex.setUseCache(false); ex.setClassLoader(classLoader); Class proxy = ex.createClass(); // Create the proxy handler - ReadPacketModifier modifier = new ReadPacketModifier(packetID, this); + ReadPacketModifier modifier = new ReadPacketModifier(packetID, this, reporter); readModifier.put(packetID, modifier); // Add a static reference @@ -142,10 +146,10 @@ class PacketInjector { try { // Override values - putMethod.invoke(intHashMap, packetID, proxy); previous.put(packetID, old); registry.put(proxy, packetID); overwritten.put(packetID, proxy); + putMethod.invoke(intHashMap, packetID, proxy); return true; } catch (IllegalArgumentException e) { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ReadPacketModifier.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ReadPacketModifier.java index 54a75b54..6e1967e1 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ReadPacketModifier.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ReadPacketModifier.java @@ -20,9 +20,12 @@ package com.comphenix.protocol.injector; import java.io.DataInputStream; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.Collections; +import java.util.Map; import java.util.WeakHashMap; import com.comphenix.protocol.Packets; +import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; @@ -35,16 +38,23 @@ class ReadPacketModifier implements MethodInterceptor { @SuppressWarnings("rawtypes") private static Class[] parameters = { DataInputStream.class }; + // A cancel marker + private static final Object CANCEL_MARKER = new Object(); + // Common for all packets of the same type private PacketInjector packetInjector; private int packetID; - // Whether or not a packet has been cancelled - private static WeakHashMap override = new WeakHashMap(); + // Report errors + private ErrorReporter reporter; - public ReadPacketModifier(int packetID, PacketInjector packetInjector) { + // Whether or not a packet has been cancelled + private static Map override = Collections.synchronizedMap(new WeakHashMap()); + + public ReadPacketModifier(int packetID, PacketInjector packetInjector, ErrorReporter reporter) { this.packetID = packetID; this.packetInjector = packetInjector; + this.reporter = reporter; } /** @@ -75,11 +85,12 @@ class ReadPacketModifier implements MethodInterceptor { return proxy.invokeSuper(thisObj, args); } - if (override.containsKey(thisObj)) { - Object overridenObject = override.get(thisObj); - + // Atomic retrieval + Object overridenObject = override.get(thisObj); + + if (overridenObject != null) { // This packet has been cancelled - if (overridenObject == null) { + if (overridenObject == CANCEL_MARKER) { // So, cancel all void methods if (method.getReturnType().equals(Void.TYPE)) return null; @@ -96,27 +107,32 @@ class ReadPacketModifier implements MethodInterceptor { if (returnValue == null && Arrays.equals(method.getParameterTypes(), parameters)) { - // We need this in order to get the correct player - DataInputStream input = (DataInputStream) args[0]; - - // Let the people know - PacketContainer container = new PacketContainer(packetID, (Packet) thisObj); - PacketEvent event = packetInjector.packetRecieved(container, input); - - // Handle override - if (event != null) { - Packet result = event.getPacket().getHandle(); + try { + // We need this in order to get the correct player + DataInputStream input = (DataInputStream) args[0]; + + // Let the people know + PacketContainer container = new PacketContainer(packetID, (Packet) thisObj); + PacketEvent event = packetInjector.packetRecieved(container, input); - if (event.isCancelled()) { - override.put(thisObj, null); - } else if (!objectEquals(thisObj, result)) { - override.put(thisObj, result); - } - - // Update DataInputStream next time - if (!event.isCancelled() && packetID == Packets.Server.KEY_RESPONSE) { - packetInjector.scheduleDataInputRefresh(event.getPlayer()); + // Handle override + if (event != null) { + Packet result = event.getPacket().getHandle(); + + if (event.isCancelled()) { + override.put(thisObj, CANCEL_MARKER); + } else if (!objectEquals(thisObj, result)) { + override.put(thisObj, result); + } + + // Update DataInputStream next time + if (!event.isCancelled() && packetID == Packets.Server.KEY_RESPONSE) { + packetInjector.scheduleDataInputRefresh(event.getPlayer()); + } } + } catch (Throwable e) { + // Minecraft cannot handle this error + reporter.reportDetailed(this, "Cannot handle clienet packet.", e, args[0]); } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java index 94662c9b..d3184b10 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/SortedPacketListenerList.java @@ -18,11 +18,9 @@ package com.comphenix.protocol.injector; import java.util.Collection; -import java.util.logging.Level; -import java.util.logging.Logger; import com.comphenix.protocol.concurrency.AbstractConcurrentListenerMultimap; -import com.comphenix.protocol.events.PacketAdapter; +import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.events.PacketListener; @@ -31,14 +29,14 @@ import com.comphenix.protocol.events.PacketListener; * * @author Kristian */ -class SortedPacketListenerList extends AbstractConcurrentListenerMultimap { +public final class SortedPacketListenerList extends AbstractConcurrentListenerMultimap { /** * Invokes the given packet event for every registered listener. - * @param logger - the logger that will be used to inform about listener exceptions. + * @param reporter - the error reporter that will be used to inform about listener exceptions. * @param event - the packet event to invoke. */ - public void invokePacketRecieving(Logger logger, PacketEvent event) { + public void invokePacketRecieving(ErrorReporter reporter, PacketEvent event) { Collection> list = getListener(event.getPacketID()); if (list == null) @@ -50,19 +48,17 @@ class SortedPacketListenerList extends AbstractConcurrentListenerMultimap> list = getListener(event.getPacketID()); if (list == null) @@ -73,9 +69,7 @@ class SortedPacketListenerList extends AbstractConcurrentListenerMultimap { // We'll use the FakePacket marker instead of preventing the filters injector.sendServerPacket(createNegativePacket(packet), true); } - + // Collection.add contract return true; @@ -90,8 +91,10 @@ class InjectedArrayList extends ArrayList { * @return The inverted packet. */ Packet createNegativePacket(Packet source) { - Enhancer ex = new Enhancer(); - Class type = source.getClass(); + ListenerInvoker invoker = injector.getInvoker(); + + int packetID = invoker.getPacketID(source); + Class type = invoker.getPacketClassFromID(packetID, true); // We want to subtract the byte amount that were added to the running // total of outstanding packets. Otherwise, cancelling too many packets @@ -111,22 +114,38 @@ class InjectedArrayList extends ArrayList { // } // ect. // } + Enhancer ex = new Enhancer(); + ex.setSuperclass(type); ex.setInterfaces(new Class[] { FakePacket.class } ); ex.setUseCache(true); ex.setClassLoader(classLoader); - ex.setSuperclass(type); - ex.setCallback(new MethodInterceptor() { - @Override - public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { - if (method.getReturnType().equals(int.class) && args.length == 0) { - Integer result = (Integer) proxy.invokeSuper(obj, args); - return -result; - } else { - return proxy.invokeSuper(obj, args); - } - } - }); + ex.setCallbackType(InvertedIntegerCallback.class); + + Class proxyClass = ex.createClass(); + + // Temporarily associate the fake packet class + invoker.registerPacketClass(proxyClass, packetID); + + Packet fake = (Packet) Enhancer.create(proxyClass, new InvertedIntegerCallback()); - return (Packet) ex.create(); + // Remove this association + invoker.unregisterPacketClass(proxyClass); + return fake; + } + + /** + * Inverts the integer result of every integer method. + * @author Kristian + */ + private class InvertedIntegerCallback implements MethodInterceptor { + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + if (method.getReturnType().equals(int.class) && args.length == 0) { + Integer result = (Integer) proxy.invokeSuper(obj, args); + return -result; + } else { + return proxy.invokeSuper(obj, args); + } + } } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedServerConnection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedServerConnection.java index d65fd405..c9ae47ff 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedServerConnection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedServerConnection.java @@ -21,14 +21,13 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; import net.minecraft.server.NetLoginHandler; import net.sf.cglib.proxy.Factory; import org.bukkit.Server; +import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.reflect.FieldUtils; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.ObjectCloner; @@ -55,16 +54,16 @@ class InjectedServerConnection { private NetLoginInjector netLoginInjector; private Server server; - private Logger logger; + private ErrorReporter reporter; private boolean hasAttempted; private boolean hasSuccess; private Object minecraftServer = null; - public InjectedServerConnection(Logger logger, Server server, NetLoginInjector netLoginInjector) { + public InjectedServerConnection(ErrorReporter reporter, Server server, NetLoginInjector netLoginInjector) { this.listFields = new ArrayList(); this.replacedLists = new ArrayList>(); - this.logger = logger; + this.reporter = reporter; this.server = server; this.netLoginInjector = netLoginInjector; } @@ -83,7 +82,7 @@ class InjectedServerConnection { try { minecraftServer = FieldUtils.readField(minecraftServerField, server, true); } catch (IllegalAccessException e1) { - logger.log(Level.WARNING, "Cannot extract minecraft server from Bukkit."); + reporter.reportWarning(this, "Cannot extract minecraft server from Bukkit."); return; } @@ -95,15 +94,13 @@ class InjectedServerConnection { injectServerConnection(); } catch (IllegalArgumentException e) { - // DEBUG - logger.log(Level.WARNING, "Reverting to old 1.2.5 server connection injection.", e); - + // Minecraft 1.2.5 or lower injectListenerThread(); } catch (Exception e) { // Oh damn - inform the player - logger.log(Level.SEVERE, "Cannot inject into server connection. Bad things will happen.", e); + reporter.reportDetailed(this, "Cannot inject into server connection. Bad things will happen.", e); } } @@ -115,7 +112,7 @@ class InjectedServerConnection { listenerThreadField = FuzzyReflection.fromObject(minecraftServer). getFieldByType(".*NetworkListenThread"); } catch (RuntimeException e) { - logger.log(Level.SEVERE, "Cannot find listener thread in MinecraftServer.", e); + reporter.reportDetailed(this, "Cannot find listener thread in MinecraftServer.", e, minecraftServer); return; } @@ -125,7 +122,7 @@ class InjectedServerConnection { try { listenerThread = listenerThreadField.get(minecraftServer); } catch (Exception e) { - logger.log(Level.WARNING, "Unable to read the listener thread.", e); + reporter.reportWarning(this, "Unable to read the listener thread.", e); return; } @@ -142,7 +139,7 @@ class InjectedServerConnection { try { serverConnection = serverConnectionMethod.invoke(minecraftServer); } catch (Exception ex) { - logger.log(Level.WARNING, "Unable to retrieve server connection", ex); + reporter.reportDetailed(this, "Unable to retrieve server connection", ex, minecraftServer); return; } @@ -154,7 +151,7 @@ class InjectedServerConnection { // Verify the field count if (matches.size() != 1) - logger.log(Level.WARNING, "Unexpected number of threads in " + serverConnection.getClass().getName()); + reporter.reportWarning(this, "Unexpected number of threads in " + serverConnection.getClass().getName()); else dedicatedThreadField = matches.get(0); } @@ -164,7 +161,7 @@ class InjectedServerConnection { if (dedicatedThreadField != null) injectEveryListField(FieldUtils.readField(dedicatedThreadField, serverConnection, true), 1); } catch (IllegalAccessException e) { - logger.log(Level.WARNING, "Unable to retrieve net handler thread.", e); + reporter.reportWarning(this, "Unable to retrieve net handler thread.", e); } injectIntoList(serverConnection, listField); @@ -186,7 +183,7 @@ class InjectedServerConnection { // Warn about unexpected errors if (lists.size() < minimum) { - logger.log(Level.WARNING, "Unable to inject " + minimum + " lists in " + container.getClass().getName()); + reporter.reportWarning(this, "Unable to inject " + minimum + " lists in " + container.getClass().getName()); } } @@ -220,7 +217,12 @@ class InjectedServerConnection { // Is this a normal Minecraft object? if (!(inserting instanceof Factory)) { // If so, copy the content of the old element to the new - ObjectCloner.copyTo(inserting, replacement, inserting.getClass()); + try { + ObjectCloner.copyTo(inserting, replacement, inserting.getClass()); + } catch (Throwable e) { + reporter.reportDetailed(InjectedServerConnection.this, "Cannot copy old " + inserting + + " to new.", e, inserting, replacement); + } } } @@ -241,7 +243,7 @@ class InjectedServerConnection { // Clean up? if (removing instanceof NetLoginHandler) { netLoginInjector.cleanup(removing); - } + } } }; } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetLoginInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetLoginInjector.java index 7f753fbb..ac1c16d6 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetLoginInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetLoginInjector.java @@ -4,12 +4,11 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.logging.Level; -import java.util.logging.Logger; import org.bukkit.Server; import org.bukkit.entity.Player; +import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.injector.GamePhase; import com.comphenix.protocol.injector.player.TemporaryPlayerFactory.InjectContainer; import com.google.common.collect.Maps; @@ -27,16 +26,16 @@ class NetLoginInjector { private PlayerInjectionHandler injectionHandler; private Server server; - // The current logger - private Logger logger; + // The current error rerporter + private ErrorReporter reporter; private ReadWriteLock injectionLock = new ReentrantReadWriteLock(); // Used to create fake players private TemporaryPlayerFactory tempPlayerFactory = new TemporaryPlayerFactory(); - public NetLoginInjector(Logger logger, PlayerInjectionHandler injectionHandler, Server server) { - this.logger = logger; + public NetLoginInjector(ErrorReporter reporter, PlayerInjectionHandler injectionHandler, Server server) { + this.reporter = reporter; this.injectionHandler = injectionHandler; this.server = server; } @@ -63,12 +62,15 @@ class NetLoginInjector { InjectContainer container = (InjectContainer) fakePlayer; container.setInjector(injector); + // Save the login + injectedLogins.putIfAbsent(inserting, injector); + // NetServerInjector can never work (currently), so we don't need to replace the NetLoginHandler return inserting; } catch (Throwable e) { // Minecraft can't handle this, so we'll deal with it here - logger.log(Level.WARNING, "Unable to hook NetLoginHandler.", e); + reporter.reportDetailed(this, "Unable to hook NetLoginHandler.", e, inserting); return inserting; } finally { @@ -93,8 +95,34 @@ class NetLoginInjector { PlayerInjector injected = injectedLogins.get(removing); if (injected != null) { - injected.cleanupAll(); - injectedLogins.remove(removing); + try { + PlayerInjector newInjector = null; + Player player = injected.getPlayer(); + + // Clean up list + injectedLogins.remove(removing); + + // No need to clean up twice + if (injected.isClean()) + return; + + // Hack to clean up other references + newInjector = injectionHandler.getInjectorByNetworkHandler(injected.getNetworkManager()); + + // Update NetworkManager + if (newInjector == null) { + injectionHandler.uninjectPlayer(player); + } else { + injectionHandler.uninjectPlayer(player, false); + + if (injected instanceof NetworkObjectInjector) + newInjector.setNetworkManager(injected.getNetworkManager(), true); + } + + } catch (Throwable e) { + // Don't leak this to Minecraft + reporter.reportDetailed(this, "Cannot cleanup NetLoginHandler.", e, removing); + } } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkFieldInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkFieldInjector.java index d6c21dbb..dd65fa17 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkFieldInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkFieldInjector.java @@ -24,11 +24,11 @@ import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Logger; import org.bukkit.entity.Player; import com.comphenix.protocol.Packets; +import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.ListeningWhitelist; import com.comphenix.protocol.events.PacketListener; import com.comphenix.protocol.injector.GamePhase; @@ -73,10 +73,10 @@ class NetworkFieldInjector extends PlayerInjector { // Used to construct proxy objects private ClassLoader classLoader; - public NetworkFieldInjector(ClassLoader classLoader, Logger logger, Player player, + public NetworkFieldInjector(ClassLoader classLoader, ErrorReporter reporter, Player player, ListenerInvoker manager, IntegerSet sendingFilters) throws IllegalAccessException { - super(logger, player, manager); + super(reporter, player, manager); this.classLoader = classLoader; this.sendingFilters = sendingFilters; } @@ -123,11 +123,14 @@ class NetworkFieldInjector extends PlayerInjector { } @Override - public void checkListener(PacketListener listener) { + public UnsupportedListener checkListener(PacketListener listener) { + int[] unsupported = { Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK }; + // Unfortunately, we don't support chunk packets - if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), - Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK)) { - throw new IllegalStateException("The NETWORK_FIELD_INJECTOR hook doesn't support map chunk listeners."); + if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), unsupported)) { + return new UnsupportedListener("The NETWORK_FIELD_INJECTOR hook doesn't support map chunk listeners.", unsupported); + } else { + return null; } } @@ -166,7 +169,7 @@ class NetworkFieldInjector extends PlayerInjector { } @SuppressWarnings("unchecked") - public void cleanupAll() { + protected void cleanHook() { // Clean up for (VolatileField overriden : overridenLists) { List minecraftList = (List) overriden.getOldValue(); @@ -191,6 +194,11 @@ class NetworkFieldInjector extends PlayerInjector { overridenLists.clear(); } + @Override + public void handleDisconnect() { + // No need to do anything + } + @Override public boolean canInject(GamePhase phase) { // All phases should work diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkObjectInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkObjectInjector.java index e8d07dd8..e391d832 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkObjectInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkObjectInjector.java @@ -28,11 +28,11 @@ import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; -import java.util.logging.Logger; import org.bukkit.entity.Player; import com.comphenix.protocol.Packets; +import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.ListeningWhitelist; import com.comphenix.protocol.events.PacketListener; import com.comphenix.protocol.injector.GamePhase; @@ -50,10 +50,13 @@ class NetworkObjectInjector extends PlayerInjector { // Used to construct proxy objects private ClassLoader classLoader; + + // Shared callback filter - avoid creating a new class every time + private static CallbackFilter callbackFilter; - public NetworkObjectInjector(ClassLoader classLoader, Logger logger, Player player, + public NetworkObjectInjector(ClassLoader classLoader, ErrorReporter reporter, Player player, ListenerInvoker invoker, IntegerSet sendingFilters) throws IllegalAccessException { - super(logger, player, invoker); + super(reporter, player, invoker); this.sendingFilters = sendingFilters; this.classLoader = classLoader; } @@ -85,11 +88,14 @@ class NetworkObjectInjector extends PlayerInjector { } @Override - public void checkListener(PacketListener listener) { + public UnsupportedListener checkListener(PacketListener listener) { + int[] unsupported = { Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK }; + // Unfortunately, we don't support chunk packets - if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), - Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK)) { - throw new IllegalStateException("The NETWORK_FIELD_INJECTOR hook doesn't support map chunk listeners."); + if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), unsupported)) { + return new UnsupportedListener("The NETWORK_OBJECT_INJECTOR hook doesn't support map chunk listeners.", unsupported); + } else { + return null; } } @@ -131,20 +137,25 @@ class NetworkObjectInjector extends PlayerInjector { } }; + // Share callback filter - that way, we avoid generating a new class every time. + if (callbackFilter == null) { + callbackFilter = new CallbackFilter() { + @Override + public int accept(Method method) { + if (method.equals(queueMethod)) + return 0; + else + return 1; + } + }; + } + // Create our proxy object Enhancer ex = new Enhancer(); ex.setClassLoader(classLoader); ex.setSuperclass(networkInterface); ex.setCallbacks(new Callback[] { queueFilter, dispatch }); - ex.setCallbackFilter(new CallbackFilter() { - @Override - public int accept(Method method) { - if (method.equals(queueMethod)) - return 0; - else - return 1; - } - }); + ex.setCallbackFilter(callbackFilter); // Inject it, if we can. networkManagerRef.setValue(ex.create()); @@ -152,13 +163,18 @@ class NetworkObjectInjector extends PlayerInjector { } @Override - public void cleanupAll() { + protected void cleanHook() { // Clean up if (networkManagerRef != null && networkManagerRef.isCurrentSet()) { networkManagerRef.revertValue(); } } + @Override + public void handleDisconnect() { + // No need to do anything + } + @Override public boolean canInject(GamePhase phase) { // Works for all phases diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkServerInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkServerInjector.java index 5f8330d0..c97d9f50 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkServerInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkServerInjector.java @@ -17,9 +17,9 @@ package com.comphenix.protocol.injector.player; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.logging.Logger; import net.minecraft.server.Packet; import net.sf.cglib.proxy.Callback; @@ -32,6 +32,7 @@ import net.sf.cglib.proxy.NoOp; import org.bukkit.entity.Player; +import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.PacketListener; import com.comphenix.protocol.injector.GamePhase; import com.comphenix.protocol.injector.ListenerInvoker; @@ -50,6 +51,9 @@ import com.comphenix.protocol.reflect.instances.ExistingGenerator; */ public class NetworkServerInjector extends PlayerInjector { + private volatile static CallbackFilter callbackFilter; + + private static Field disconnectField; private static Method sendPacketMethod; private InjectedServerConnection serverInjection; @@ -59,12 +63,15 @@ public class NetworkServerInjector extends PlayerInjector { // Used to create proxy objects private ClassLoader classLoader; + // Whether or not the player has disconnected + private boolean hasDisconnected; + public NetworkServerInjector( - ClassLoader classLoader, Logger logger, Player player, + ClassLoader classLoader, ErrorReporter reporter, Player player, ListenerInvoker invoker, IntegerSet sendingFilters, InjectedServerConnection serverInjection) throws IllegalAccessException { - super(logger, player, invoker); + super(reporter, player, invoker); this.classLoader = classLoader; this.sendingFilters = sendingFilters; this.serverInjection = serverInjection; @@ -164,18 +171,24 @@ public class NetworkServerInjector extends PlayerInjector { }; Callback noOpCallback = NoOp.INSTANCE; + // Share callback filter - that way, we avoid generating a new class for + // every logged in player. + if (callbackFilter == null) { + callbackFilter = new CallbackFilter() { + @Override + public int accept(Method method) { + if (method.equals(sendPacketMethod)) + return 0; + else + return 1; + } + }; + } + ex.setClassLoader(classLoader); ex.setSuperclass(serverClass); ex.setCallbacks(new Callback[] { sendPacketCallback, noOpCallback }); - ex.setCallbackFilter(new CallbackFilter() { - @Override - public int accept(Method method) { - if (method.equals(sendPacketMethod)) - return 0; - else - return 1; - } - }); + ex.setCallbackFilter(callbackFilter); // Find the Minecraft NetServerHandler superclass Class minecraftSuperClass = getFirstMinecraftSuperClass(serverHandler.getClass()); @@ -202,6 +215,7 @@ public class NetworkServerInjector extends PlayerInjector { if (proxyObject != null) { // This will be done by InjectedServerConnection instead //copyTo(serverHandler, proxyObject); + serverInjection.replaceServerHandler(serverHandler, proxyObject); serverHandlerRef.setValue(proxyObject); return true; @@ -232,7 +246,7 @@ public class NetworkServerInjector extends PlayerInjector { } @Override - public void cleanupAll() { + protected void cleanHook() { if (serverHandlerRef != null && serverHandlerRef.isCurrentSet()) { ObjectCloner.copyTo(serverHandlerRef.getValue(), serverHandlerRef.getOldValue(), serverHandler.getClass()); serverHandlerRef.revertValue(); @@ -250,14 +264,46 @@ public class NetworkServerInjector extends PlayerInjector { } catch (IllegalAccessException e) { e.printStackTrace(); } + + // Prevent the PlayerQuitEvent from being sent twice + if (hasDisconnected) { + setDisconnect(serverHandlerRef.getValue(), true); + } } serverInjection.revertServerHandler(serverHandler); } @Override - public void checkListener(PacketListener listener) { + public void handleDisconnect() { + hasDisconnected = true; + } + + /** + * Set the disconnected field in a NetServerHandler. + * @param handler - the NetServerHandler. + * @param value - the new value. + */ + private void setDisconnect(Object handler, boolean value) { + // Set it + try { + // Load the field + if (disconnectField == null) { + disconnectField = FuzzyReflection.fromObject(handler).getFieldByName("disconnected.*"); + } + FieldUtils.writeField(disconnectField, handler, value); + + } catch (IllegalArgumentException e) { + reporter.reportDetailed(this, "Unable to find disconnect field. Is ProtocolLib up to date?", e, handler); + } catch (IllegalAccessException e) { + reporter.reportWarning(this, "Unable to update disconnected field. Player quit event may be sent twice."); + } + } + + @Override + public UnsupportedListener checkListener(PacketListener listener) { // We support everything + return null; } @Override diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java index d6a69316..b627810f 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java @@ -24,14 +24,13 @@ import java.net.Socket; import java.net.SocketAddress; import java.util.Map; import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; import net.minecraft.server.Packet; import org.bukkit.Server; import org.bukkit.entity.Player; +import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.PacketAdapter; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketListener; @@ -72,8 +71,8 @@ public class PlayerInjectionHandler { private volatile PlayerInjectHooks loginPlayerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT; private volatile PlayerInjectHooks playingPlayerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT; - // Error logger - private Logger logger; + // Error reporter + private ErrorReporter reporter; // Whether or not we're closing private boolean hasClosed; @@ -84,21 +83,25 @@ public class PlayerInjectionHandler { // Enabled packet filters private IntegerSet sendingFilters = new IntegerSet(MAXIMUM_PACKET_ID + 1); + // List of packet listeners + private Set packetListeners; + // The class loader we're using private ClassLoader classLoader; // Used to filter injection attempts private Predicate injectionFilter; - public PlayerInjectionHandler(ClassLoader classLoader, Logger logger, Predicate injectionFilter, - ListenerInvoker invoker, Server server) { + public PlayerInjectionHandler(ClassLoader classLoader, ErrorReporter reporter, Predicate injectionFilter, + ListenerInvoker invoker, Set packetListeners, Server server) { this.classLoader = classLoader; - this.logger = logger; + this.reporter = reporter; this.invoker = invoker; this.injectionFilter = injectionFilter; - this.netLoginInjector = new NetLoginInjector(logger, this, server); - this.serverInjection = new InjectedServerConnection(logger, server, netLoginInjector); + this.packetListeners = packetListeners; + this.netLoginInjector = new NetLoginInjector(reporter, this, server); + this.serverInjection = new InjectedServerConnection(reporter, server, netLoginInjector); serverInjection.injectList(); } @@ -144,6 +147,9 @@ public class PlayerInjectionHandler { loginPlayerHook = playerHook; if (phase.hasPlaying()) playingPlayerHook = playerHook; + + // Make sure the current listeners are compatible + checkListener(packetListeners); } /** @@ -173,11 +179,11 @@ public class PlayerInjectionHandler { // Construct the correct player hook switch (hook) { case NETWORK_HANDLER_FIELDS: - return new NetworkFieldInjector(classLoader, logger, player, invoker, sendingFilters); + return new NetworkFieldInjector(classLoader, reporter, player, invoker, sendingFilters); case NETWORK_MANAGER_OBJECT: - return new NetworkObjectInjector(classLoader, logger, player, invoker, sendingFilters); + return new NetworkObjectInjector(classLoader, reporter, player, invoker, sendingFilters); case NETWORK_SERVER_OBJECT: - return new NetworkServerInjector(classLoader, logger, player, invoker, sendingFilters, serverInjection); + return new NetworkServerInjector(classLoader, reporter, player, invoker, sendingFilters, serverInjection); default: throw new IllegalArgumentException("Cannot construct a player injector."); } @@ -198,7 +204,7 @@ public class PlayerInjectionHandler { if (injector != null) { return injector.getPlayer(); } else { - System.out.println("Unable to find stream: " + inputStream); + reporter.reportWarning(this, "Unable to find stream: " + inputStream); return null; } @@ -281,28 +287,22 @@ public class PlayerInjectionHandler { DataInputStream inputStream = injector.getInputStream(false); Socket socket = injector.getSocket(); - SocketAddress address = socket.getRemoteSocketAddress(); + SocketAddress address = socket != null ? socket.getRemoteSocketAddress() : null; - // Make sure the current player is not logged out - if (socket.isClosed()) { - throw new PlayerLoggedOutException(); - } - - PlayerInjector previous = addressLookup.get(address); + // Guard against NPE here too + PlayerInjector previous = address != null ? addressLookup.get(address) : null; // Close any previously associated hooks before we proceed if (previous != null) { - uninjectPlayer(previous.getPlayer()); - - // Remove the "hooked" network manager in our instance as well - if (previous instanceof NetworkObjectInjector) { - injector.setNetworkManager(previous.getNetworkManager(), true); - } + uninjectPlayer(previous.getPlayer(), false, true); } - + injector.injectManager(); - dataInputLookup.put(inputStream, injector); - addressLookup.put(address, injector); + + if (inputStream != null) + dataInputLookup.put(inputStream, injector); + if (address != null) + addressLookup.put(address, injector); break; } @@ -311,7 +311,8 @@ public class PlayerInjectionHandler { } catch (Exception e) { // Mark this injection attempt as a failure - logger.log(Level.SEVERE, "Player hook " + tempHook.toString() + " failed.", e); + reporter.reportDetailed(this, "Player hook " + tempHook.toString() + " failed.", + e, player, injectionPoint, phase); hookFailed = true; } @@ -319,7 +320,7 @@ public class PlayerInjectionHandler { tempHook = PlayerInjectHooks.values()[tempHook.ordinal() - 1]; if (hookFailed) - logger.log(Level.INFO, "Switching to " + tempHook.toString() + " instead."); + reporter.reportWarning(this, "Switching to " + tempHook.toString() + " instead."); // Check for UTTER FAILURE if (tempHook == PlayerInjectHooks.NONE) { @@ -340,8 +341,10 @@ public class PlayerInjectionHandler { if (permanentHook != getPlayerHook(phase)) setPlayerHook(phase, tempHook); - // Save last injector - playerInjection.put(player, injector); + // Save injector + if (injector != null) { + playerInjection.put(player, injector); + } } return injector; @@ -352,16 +355,50 @@ public class PlayerInjectionHandler { try { if (injector != null) injector.cleanupAll(); - } catch (Exception e2) { - logger.log(Level.WARNING, "Cleaing up after player hook failed.", e2); + } catch (Exception ex) { + reporter.reportDetailed(this, "Cleaing up after player hook failed.", ex, injector); + } + } + + /** + * Invoke special routines for handling disconnect before a player is uninjected. + * @param player - player to process. + */ + public void handleDisconnect(Player player) { + PlayerInjector injector = getInjector(player); + + if (injector != null) { + injector.handleDisconnect(); } } /** * Unregisters the given player. * @param player - player to unregister. + * @return TRUE if a player has been uninjected, FALSE otherwise. */ - public void uninjectPlayer(Player player) { + public boolean uninjectPlayer(Player player) { + return uninjectPlayer(player, true, false); + } + + /** + * Unregisters the given player. + * @param player - player to unregister. + * @param removeAuxiliary - TRUE to remove auxiliary information, such as input stream and address. + * @return TRUE if a player has been uninjected, FALSE otherwise. + */ + public boolean uninjectPlayer(Player player, boolean removeAuxiliary) { + return uninjectPlayer(player, removeAuxiliary, false); + } + + /** + * Unregisters the given player. + * @param player - player to unregister. + * @param removeAuxiliary - TRUE to remove auxiliary information, such as input stream and address. + * @param prepareNextHook - whether or not we need to fix any lingering hooks. + * @return TRUE if a player has been uninjected, FALSE otherwise. + */ + private boolean uninjectPlayer(Player player, boolean removeAuxiliary, boolean prepareNextHook) { if (!hasClosed && player != null) { PlayerInjector injector = playerInjection.remove(player); @@ -371,26 +408,54 @@ public class PlayerInjectionHandler { InetSocketAddress address = player.getAddress(); injector.cleanupAll(); - dataInputLookup.remove(input); + // Remove the "hooked" network manager in our instance as well + if (prepareNextHook && injector instanceof NetworkObjectInjector) { + try { + PlayerInjector dummyInjector = getHookInstance(player, PlayerInjectHooks.NETWORK_SERVER_OBJECT); + dummyInjector.initializePlayer(player); + dummyInjector.setNetworkManager(injector.getNetworkManager(), true); + + } catch (IllegalAccessException e) { + // Let the user know + reporter.reportWarning(this, "Unable to fully revert old injector. May cause conflicts.", e); + } + } - if (address != null) - addressLookup.remove(address); + // Clean up + if (removeAuxiliary) { + if (input != null) + dataInputLookup.remove(input); + if (address != null) + addressLookup.remove(address); + } + return true; } - } + } + + return false; } /** * Unregisters a player by the given address. + *

+ * If the server handler has been created before we've gotten a chance to unject the player, + * the method will try a workaround to remove the injected hook in the NetServerHandler. + * * @param address - address of the player to unregister. + * @param serverHandler - whether or not the net server handler has already been created. + * @return TRUE if a player has been uninjected, FALSE otherwise. */ - public void uninjectPlayer(InetSocketAddress address) { + public boolean uninjectPlayer(InetSocketAddress address) { if (!hasClosed && address != null) { PlayerInjector injector = addressLookup.get(address); // Clean up if (injector != null) - uninjectPlayer(injector.getPlayer()); + uninjectPlayer(injector.getPlayer(), false, true); + return true; } + + return false; } /** @@ -407,7 +472,7 @@ public class PlayerInjectionHandler { if (injector != null) injector.sendServerPacket(packet.getHandle(), filters); else - logger.log(Level.WARNING, String.format( + reporter.reportWarning(this, String.format( "Unable to send packet %s (%s): Player %s has logged out.", packet.getID(), packet, reciever.getName() )); @@ -428,7 +493,7 @@ public class PlayerInjectionHandler { if (injector != null) injector.processPacket(mcPacket); else - logger.log(Level.WARNING, String.format( + reporter.reportWarning(this, String.format( "Unable to receieve packet %s. Player %s has logged out.", mcPacket, player.getName() )); @@ -443,6 +508,26 @@ public class PlayerInjectionHandler { return playerInjection.get(player); } + /** + * Retrieve a player injector by looking for its NetworkManager. + * @param networkManager - current network manager. + * @return Related player injector. + */ + PlayerInjector getInjectorByNetworkHandler(Object networkManager) { + // That's not legal + if (networkManager == null) + return null; + + // O(n) is okay in this instance. This is only a backup solution. + for (PlayerInjector injector : playerInjection.values()) { + if (injector.getNetworkManager() == networkManager) + return injector; + } + + // None found + return null; + } + /** * Determine if the given listeners are valid. * @param listeners - listeners to check. @@ -451,26 +536,30 @@ public class PlayerInjectionHandler { // Make sure the current listeners are compatible if (lastSuccessfulHook != null) { for (PacketListener listener : listeners) { - try { - checkListener(listener); - } catch (IllegalStateException e) { - logger.log(Level.WARNING, "Unsupported listener.", e); - } + checkListener(listener); } } } /** * Determine if a listener is valid or not. + *

+ * If not, a warning will be printed to the console. * @param listener - listener to check. - * @throws IllegalStateException If the given listener's whitelist cannot be fulfilled. */ public void checkListener(PacketListener listener) { - try { - if (lastSuccessfulHook != null) - lastSuccessfulHook.checkListener(listener); - } catch (Exception e) { - throw new IllegalStateException("Registering listener " + PacketAdapter.getPluginName(listener) + " failed", e); + if (lastSuccessfulHook != null) { + UnsupportedListener result = lastSuccessfulHook.checkListener(listener); + + // We won't prevent the listener, as it may still have valid packets + if (result != null) { + reporter.reportWarning(this, "Cannot fully register listener for " + + PacketAdapter.getPluginName(listener) + ": " + result.toString()); + + // These are illegal + for (int packetID : result.getPackets()) + removePacketHandler(packetID); + } } } @@ -482,14 +571,6 @@ public class PlayerInjectionHandler { return sendingFilters.toSet(); } - /** - * Retrieve the current logger. - * @return Error logger. - */ - public Logger getLogger() { - return logger; - } - public void close() { // Guard diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjector.java index 3e2457e9..6eda9848 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjector.java @@ -23,8 +23,7 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.Socket; -import java.util.logging.Level; -import java.util.logging.Logger; +import java.net.SocketAddress; import net.minecraft.server.EntityPlayer; import net.minecraft.server.NetLoginHandler; @@ -35,6 +34,7 @@ import org.bukkit.craftbukkit.entity.CraftPlayer; import org.bukkit.entity.Player; import com.comphenix.protocol.Packets; +import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.events.PacketListener; @@ -99,17 +99,20 @@ abstract class PlayerInjector { protected DataInputStream cachedInput; // Handle errors - protected Logger logger; + protected ErrorReporter reporter; // Scheduled action on the next packet event protected Runnable scheduledAction; + // Whether or not the injector has been cleaned + private boolean clean; + // Whether or not to update the current player on the first Packet1Login boolean updateOnLogin; Player updatedPlayer; - public PlayerInjector(Logger logger, Player player, ListenerInvoker invoker) throws IllegalAccessException { - this.logger = logger; + public PlayerInjector(ErrorReporter reporter, Player player, ListenerInvoker invoker) throws IllegalAccessException { + this.reporter = reporter; this.player = player; this.invoker = invoker; } @@ -256,6 +259,21 @@ abstract class PlayerInjector { } } + /** + * Retrieve the associated address of this player. + * @return The associated address. + * @throws IllegalAccessException If we're unable to read the socket field. + */ + public SocketAddress getAddress() throws IllegalAccessException { + Socket socket = getSocket(); + + // Guard against NULL + if (socket != null) + return socket.getRemoteSocketAddress(); + else + return null; + } + /** * Attempt to disconnect the current client. * @param message - the message to display. @@ -284,19 +302,24 @@ abstract class PlayerInjector { disconnect.invoke(handler, message); return; } catch (IllegalArgumentException e) { - logger.log(Level.WARNING, "Invalid argument passed to disconnect method: " + message, e); + reporter.reportDetailed(this, "Invalid argument passed to disconnect method: " + message, e, handler); } catch (IllegalAccessException e) { - logger.log(Level.SEVERE, "Unable to access disconnect method.", e); + reporter.reportWarning(this, "Unable to access disconnect method.", e); } } - + // Fuck it try { - getSocket().close(); - } catch (IOException e) { - logger.log(Level.SEVERE, "Unable to close socket.", e); + Socket socket = getSocket(); + + try { + socket.close(); + } catch (IOException e) { + reporter.reportDetailed(this, "Unable to close socket.", e, socket); + } + } catch (IllegalAccessException e) { - logger.log(Level.SEVERE, "Insufficient permissions. Cannot close socket.", e); + reporter.reportWarning(this, "Insufficient permissions. Cannot close socket.", e); } } @@ -313,7 +336,7 @@ abstract class PlayerInjector { return null; hasProxyType = true; - logger.log(Level.WARNING, "Detected server handler proxy type by another plugin. Conflict may occur!"); + reporter.reportWarning(this, "Detected server handler proxy type by another plugin. Conflict may occur!"); // No? Is it a Proxy type? try { @@ -328,7 +351,7 @@ abstract class PlayerInjector { } } catch (IllegalAccessException e) { - logger.warning("Unable to load server handler from proxy type."); + reporter.reportWarning(this, "Unable to load server handler from proxy type."); } // Nope, just go with it @@ -427,7 +450,29 @@ abstract class PlayerInjector { /** * Remove all hooks and modifications. */ - public abstract void cleanupAll(); + public final void cleanupAll() { + if (!clean) + cleanHook(); + clean = true; + } + + /** + * Clean up after the player has disconnected. + */ + public abstract void handleDisconnect(); + + /** + * Override to add custom cleanup behavior. + */ + protected abstract void cleanHook(); + + /** + * Determine whether or not this hook has already been cleaned. + * @return TRUE if it has, FALSE otherwise. + */ + public boolean isClean() { + return clean; + } /** * Determine if this inject method can even be attempted. @@ -444,10 +489,12 @@ abstract class PlayerInjector { /** * Invoked before a new listener is registered. *

- * The player injector should throw an exception if this listener cannot be properly supplied with packet events. + * The player injector should only return a non-null value if some or all of the packet IDs are unsupported. + * * @param listener - the listener that is about to be registered. + * @return A error message with the unsupported packet IDs, or NULL if this listener is valid. */ - public abstract void checkListener(PacketListener listener); + public abstract UnsupportedListener checkListener(PacketListener listener); /** * Allows a packet to be sent by the listeners. @@ -455,43 +502,48 @@ abstract class PlayerInjector { * @return The given packet, or the packet replaced by the listeners. */ public Packet handlePacketSending(Packet packet) { - // Get the packet ID too - Integer id = invoker.getPacketID(packet); - Player currentPlayer = player; - - // Hack #1: Handle a single scheduled action - if (scheduledAction != null) { - scheduledAction.run(); - scheduledAction = null; - } - // Hack #2 - if (updateOnLogin) { - if (id == Packets.Server.LOGIN) { - try { - updatedPlayer = getEntityPlayer(getNetHandler()).getBukkitEntity(); - } catch (IllegalAccessException e) { - logger.log(Level.WARNING, "Cannot update player in PlayerEvent.", e); + try { + // Get the packet ID too + Integer id = invoker.getPacketID(packet); + Player currentPlayer = player; + + // Hack #1: Handle a single scheduled action + if (scheduledAction != null) { + scheduledAction.run(); + scheduledAction = null; + } + // Hack #2 + if (updateOnLogin) { + if (id == Packets.Server.LOGIN) { + try { + updatedPlayer = getEntityPlayer(getNetHandler()).getBukkitEntity(); + } catch (IllegalAccessException e) { + reporter.reportDetailed(this, "Cannot update player in PlayerEvent.", e, packet); + } } + + // This will only occur in the NetLoginHandler injection + if (updatedPlayer != null) + currentPlayer = updatedPlayer; } - // This will only occur in the NetLoginHandler injection - if (updatedPlayer != null) - currentPlayer = updatedPlayer; - } - - // Make sure we're listening - if (id != null && hasListener(id)) { - // A packet has been sent guys! - PacketContainer container = new PacketContainer(id, packet); - PacketEvent event = PacketEvent.fromServer(invoker, container, currentPlayer); - invoker.invokePacketSending(event); + // Make sure we're listening + if (id != null && hasListener(id)) { + // A packet has been sent guys! + PacketContainer container = new PacketContainer(id, packet); + PacketEvent event = PacketEvent.fromServer(invoker, container, currentPlayer); + invoker.invokePacketSending(event); + + // Cancelling is pretty simple. Just ignore the packet. + if (event.isCancelled()) + return null; + + // Right, remember to replace the packet again + return event.getPacket().getHandle(); + } - // Cancelling is pretty simple. Just ignore the packet. - if (event.isCancelled()) - return null; - - // Right, remember to replace the packet again - return event.getPacket().getHandle(); + } catch (Throwable e) { + reporter.reportDetailed(this, "Cannot handle server packet.", e, packet); } return packet; @@ -544,6 +596,14 @@ abstract class PlayerInjector { return player; } + /** + * Object that can invoke the packet events. + * @return Packet event invoker. + */ + public ListenerInvoker getInvoker() { + return invoker; + } + /** * Retrieve the hooked player object OR the more up-to-date player instance. * @return The hooked player, or a more up-to-date instance. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ReplacedArrayList.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ReplacedArrayList.java index 6ebb9cb7..8fe8b3e3 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ReplacedArrayList.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ReplacedArrayList.java @@ -68,7 +68,7 @@ class ReplacedArrayList extends ArrayList { } /** - * Invoksed when an element is being removed. + * Invoked when an element is being removed. * @param removing - the element being removed. */ protected void onRemoved(TKey removing) { @@ -264,6 +264,15 @@ class ReplacedArrayList extends ArrayList { addMapping(target, replacement, false); } + /** + * Retrieve the old value, if it exists. + * @param target - the key. + * @return The value that was replaced, or NULL. + */ + public TKey getMapping(TKey target) { + return replaceMap.get(target); + } + /** * Add a replace rule. *

@@ -284,8 +293,9 @@ class ReplacedArrayList extends ArrayList { /** * Revert the given mapping. * @param target - the instance we replaced. + * @return The old mapped value, or NULL if nothing was replaced. */ - public synchronized void removeMapping(TKey target) { + public synchronized TKey removeMapping(TKey target) { // Make sure the mapping exist if (replaceMap.containsKey(target)) { TKey replacement = replaceMap.get(target); @@ -293,7 +303,25 @@ class ReplacedArrayList extends ArrayList { // Revert existing elements replaceAll(replacement, target); + return replacement; } + return null; + } + + /** + * Swap the new replaced value with its old value. + * @param target - the instance we replaced. + * @param The old mapped value, or NULL if nothing was swapped. + */ + public synchronized TKey swapMapping(TKey target) { + // Make sure the mapping exist + TKey replacement = removeMapping(target); + + // Add the reverse + if (replacement != null) { + replaceMap.put(replacement, target); + } + return replacement; } /** diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/TemporaryPlayerFactory.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/TemporaryPlayerFactory.java index bb1d6a96..891edc7c 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/TemporaryPlayerFactory.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/TemporaryPlayerFactory.java @@ -42,6 +42,9 @@ class TemporaryPlayerFactory { // Helpful constructors private final PacketConstructor chatPacket; + // Prevent too many class creations + private static CallbackFilter callbackFilter; + public TemporaryPlayerFactory() { chatPacket = PacketConstructor.DEFAULT.withPacket(3, new Object[] { "DEMO" }); } @@ -76,13 +79,16 @@ class TemporaryPlayerFactory { String methodName = method.getName(); PlayerInjector injector = ((InjectContainer) obj).getInjector(); + if (injector == null) + throw new IllegalStateException("Unable to find injector."); + // Use the socket to get the address if (methodName.equalsIgnoreCase("getName")) return "UNKNOWN[" + injector.getSocket().getRemoteSocketAddress() + "]"; if (methodName.equalsIgnoreCase("getPlayer")) return injector.getUpdatedPlayer(); if (methodName.equalsIgnoreCase("getAddress")) - return injector.getSocket().getRemoteSocketAddress(); + return injector.getAddress(); if (methodName.equalsIgnoreCase("getServer")) return server; @@ -117,22 +123,27 @@ class TemporaryPlayerFactory { } }; + // Shared callback filter + if (callbackFilter == null) { + callbackFilter = new CallbackFilter() { + @Override + public int accept(Method method) { + // Do not override the object method or the superclass methods + if (method.getDeclaringClass().equals(Object.class) || + method.getDeclaringClass().equals(InjectContainer.class)) + return 0; + else + return 1; + } + }; + } + // CGLib is amazing Enhancer ex = new Enhancer(); ex.setSuperclass(InjectContainer.class); ex.setInterfaces(new Class[] { Player.class }); ex.setCallbacks(new Callback[] { NoOp.INSTANCE, implementation }); - ex.setCallbackFilter(new CallbackFilter() { - @Override - public int accept(Method method) { - // Do not override the object method or the superclass methods - if (method.getDeclaringClass().equals(Object.class) || - method.getDeclaringClass().equals(InjectContainer.class)) - return 0; - else - return 1; - } - }); + ex.setCallbackFilter(callbackFilter); return (Player) ex.create(); } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/UnsupportedListener.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/UnsupportedListener.java new file mode 100644 index 00000000..2e4cbe5a --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/UnsupportedListener.java @@ -0,0 +1,47 @@ +package com.comphenix.protocol.injector.player; + +import java.util.Arrays; + +import com.google.common.base.Joiner; + +/** + * Represents an error message from a player injector. + * + * @author Kristian + */ +class UnsupportedListener { + private String message; + private int[] packets; + + /** + * Create a new error message. + * @param message - the message. + * @param packets - unsupported packets. + */ + public UnsupportedListener(String message, int[] packets) { + super(); + this.message = message; + this.packets = packets; + } + + /** + * Retrieve the error message. + * @return Error message. + */ + public String getMessage() { + return message; + } + + /** + * Retrieve all unsupported packets. + * @return Unsupported packets. + */ + public int[] getPackets() { + return packets; + } + + @Override + public String toString() { + return String.format("%s (%s)", message, Joiner.on(", ").join(Arrays.asList(packets))); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Statistics.java b/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Statistics.java index 5bf12478..0cc29faa 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Statistics.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Statistics.java @@ -36,10 +36,11 @@ public class Statistics { public Statistics(Plugin plugin) throws IOException { metrics = new Metrics(plugin); - metrics.start(); // Determine who is using this library addPluginUserGraph(metrics); + + metrics.start(); } private void addPluginUserGraph(Metrics metrics) { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Updater.java b/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Updater.java new file mode 100644 index 00000000..eaea4a7b --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Updater.java @@ -0,0 +1,688 @@ +package com.comphenix.protocol.metrics; + +/* + * Updater for Bukkit. + * + * This class provides the means to safetly and easily update a plugin, or check to see if it is updated using dev.bukkit.org + */ +import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.XMLEvent; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; + +/** + * Check dev.bukkit.org to find updates for a given plugin, and download the updates if needed. + *

+ * VERY, VERY IMPORTANT: Because there are no standards for adding auto-update toggles in your plugin's config, this system provides NO CHECK WITH YOUR CONFIG to make sure the user has allowed auto-updating. + *
+ * It is a BUKKIT POLICY that you include a boolean value in your config that prevents the auto-updater from running AT ALL. + *
+ * If you fail to include this option in your config, your plugin will be REJECTED when you attempt to submit it to dev.bukkit.org. + *

+ * An example of a good configuration option would be something similar to 'auto-update: true' - if this value is set to false you may NOT run the auto-updater. + *
+ * If you are unsure about these rules, please read the plugin submission guidelines: http://goo.gl/8iU5l + * + * @author H31IX + */ +public class Updater +{ + // If the version number contains one of these, don't update. + private static final String[] noUpdateTag = {"-DEV", "-PRE", "-SNAPSHOT"}; + + // Slugs will be appended to this to get to the project's RSS feed + private static final String DBOUrl = "http://dev.bukkit.org/server-mods/"; + private static final int BYTE_SIZE = 1024; // Used for downloading files + + private final Plugin plugin; + private final String slug; + + private volatile long totalSize; // Holds the total size of the file + private volatile int sizeLine; // Used for detecting file size + private volatile int multiplier; // Used for determining when to broadcast download updates + + private volatile URL url; // Connecting to RSS + + private volatile String updateFolder = YamlConfiguration.loadConfiguration(new File("bukkit.yml")).getString("settings.update-folder"); // The folder that downloads will be placed in + + // Used for determining the outcome of the update process + private volatile Updater.UpdateResult result = Updater.UpdateResult.SUCCESS; + + // Whether to announce file downloads + private volatile boolean announce; + + private volatile UpdateType type; + private volatile String downloadedVersion; + private volatile String versionTitle; + private volatile String versionLink; + private volatile File file; + + // Used to announce progress + private volatile Logger logger; + + // Strings for reading RSS + private static final String TITLE = "title"; + private static final String LINK = "link"; + private static final String ITEM = "item"; + + /** + * Gives the dev the result of the update process. Can be obtained by called getResult(). + */ + public enum UpdateResult + { + /** + * The updater found an update, and has readied it to be loaded the next time the server restarts/reloads. + */ + SUCCESS(1, "The updater found an update, and has readied it to be loaded the next time the server restarts/reloads."), + + /** + * The updater did not find an update, and nothing was downloaded. + */ + NO_UPDATE(2, "The updater did not find an update, and nothing was downloaded."), + + /** + * The updater found an update, but was unable to download it. + */ + FAIL_DOWNLOAD(3, "The updater found an update, but was unable to download it."), + + /** + * For some reason, the updater was unable to contact dev.bukkit.org to download the file. + */ + FAIL_DBO(4, "For some reason, the updater was unable to contact dev.bukkit.org to download the file."), + + /** + * When running the version check, the file on DBO did not contain the a version in the format 'vVersion' such as 'v1.0'. + */ + FAIL_NOVERSION(5, "When running the version check, the file on DBO did not contain the a version in the format 'vVersion' such as 'v1.0'."), + + /** + * The slug provided by the plugin running the updater was invalid and doesn't exist on DBO. + */ + FAIL_BADSLUG(6, "The slug provided by the plugin running the updater was invalid and doesn't exist on DBO."), + + /** + * The updater found an update, but because of the UpdateType being set to NO_DOWNLOAD, it wasn't downloaded. + */ + UPDATE_AVAILABLE(7, "The updater found an update, but because of the UpdateType being set to NO_DOWNLOAD, it wasn't downloaded."); + + private static final Map valueList = new HashMap(); + private final int value; + private final String description; + + private UpdateResult(int value, String description) + { + this.value = value; + this.description = description; + } + + public int getValue() + { + return this.value; + } + + public static Updater.UpdateResult getResult(int value) + { + return valueList.get(value); + } + + @Override + public String toString() { + return description; + } + + static + { + for(Updater.UpdateResult result : Updater.UpdateResult.values()) + { + valueList.put(result.value, result); + } + } + } + + /** + * Allows the dev to specify the type of update that will be run. + */ + public enum UpdateType + { + /** + * Run a version check, and then if the file is out of date, download the newest version. + */ + DEFAULT(1), + /** + * Don't run a version check, just find the latest update and download it. + */ + NO_VERSION_CHECK(2), + /** + * Get information about the version and the download size, but don't actually download anything. + */ + NO_DOWNLOAD(3); + + private static final Map valueList = new HashMap(); + private final int value; + + private UpdateType(int value) + { + this.value = value; + } + + public int getValue() + { + return this.value; + } + + public static Updater.UpdateType getResult(int value) + { + return valueList.get(value); + } + + static + { + for(Updater.UpdateType result : Updater.UpdateType.values()) + { + valueList.put(result.value, result); + } + } + } + + /** + * Initialize the updater + * + * @param plugin + * The plugin that is checking for an update. + * @param slug + * The dev.bukkit.org slug of the project (http://dev.bukkit.org/server-mods/SLUG_IS_HERE) + * @param file + * The file that the plugin is running from, get this by doing this.getFile() from within your main class. + * @param permission + * Permission needed to read the output of the update process. + */ + public Updater(Plugin plugin, Logger logger, String slug, File file, String permission) + { + this.plugin = plugin; + this.file = file; + this.slug = slug; + this.logger = logger; + } + + /** + * Update the plugin. + * + * @param type + * Specify the type of update this will be. See {@link UpdateType} + * @param announce + * True if the program should announce the progress of new updates in console + * @return The result of the update process. + */ + public synchronized UpdateResult update(UpdateType type, boolean announce) + { + this.type = type; + this.announce = announce; + + try + { + // Obtain the results of the project's file feed + url = null; + url = new URL(DBOUrl + slug + "/files.rss"); + } + catch (MalformedURLException ex) + { + // The slug doesn't exist + logger.warning("The author of this plugin has misconfigured their Auto Update system"); + logger.warning("The project slug added ('" + slug + "') is invalid, and does not exist on dev.bukkit.org"); + result = Updater.UpdateResult.FAIL_BADSLUG; // Bad slug! Bad! + } + if (url != null) + { + // Obtain the results of the project's file feed + readFeed(); + if(versionCheck(versionTitle)) + { + String fileLink = getFile(versionLink); + if(fileLink != null && type != UpdateType.NO_DOWNLOAD) + { + String name = file.getName(); + // If it's a zip file, it shouldn't be downloaded as the plugin's name + if(fileLink.endsWith(".zip")) + { + String [] split = fileLink.split("/"); + name = split[split.length-1]; + } + + // Never download the same file twice + if (!downloadedVersion.equalsIgnoreCase(versionLink)) { + saveFile(new File("plugins/" + updateFolder), name, fileLink); + downloadedVersion = versionLink; + result = UpdateResult.SUCCESS; + + } else { + result = UpdateResult.UPDATE_AVAILABLE; + } + } + else + { + result = UpdateResult.UPDATE_AVAILABLE; + } + } + } + + return result; + } + + /** + * Get the result of the update process. + */ + public Updater.UpdateResult getResult() + { + return result; + } + + /** + * Get the total bytes of the file (can only be used after running a version check or a normal run). + */ + public long getFileSize() + { + return totalSize; + } + + /** + * Get the version string latest file avaliable online. + */ + public String getLatestVersionString() + { + return versionTitle; + } + + /** + * Save an update from dev.bukkit.org into the server's update folder. + */ + private void saveFile(File folder, String file, String u) + { + if(!folder.exists()) + { + folder.mkdir(); + } + BufferedInputStream in = null; + FileOutputStream fout = null; + try + { + // Download the file + URL url = new URL(u); + int fileLength = url.openConnection().getContentLength(); + in = new BufferedInputStream(url.openStream()); + fout = new FileOutputStream(folder.getAbsolutePath() + "/" + file); + + byte[] data = new byte[BYTE_SIZE]; + int count; + if(announce) logger.info("About to download a new update: " + versionTitle); + long downloaded = 0; + while ((count = in.read(data, 0, BYTE_SIZE)) != -1) + { + downloaded += count; + fout.write(data, 0, count); + int percent = (int) (downloaded * 100 / fileLength); + if(announce && (percent % 10 == 0)) + { + logger.info("Downloading update: " + percent + "% of " + fileLength + " bytes."); + } + } + //Just a quick check to make sure we didn't leave any files from last time... + for(File xFile : new File("plugins/" + updateFolder).listFiles()) + { + if(xFile.getName().endsWith(".zip")) + { + xFile.delete(); + } + } + // Check to see if it's a zip file, if it is, unzip it. + File dFile = new File(folder.getAbsolutePath() + "/" + file); + if(dFile.getName().endsWith(".zip")) + { + // Unzip + unzip(dFile.getCanonicalPath()); + } + if(announce) logger.info("Finished updating."); + } + catch (Exception ex) + { + logger.warning("The auto-updater tried to download a new update, but was unsuccessful."); + result = Updater.UpdateResult.FAIL_DOWNLOAD; + } + finally + { + try + { + if (in != null) + { + in.close(); + } + if (fout != null) + { + fout.close(); + } + } + catch (Exception ex) + { + } + } + } + + /** + * Part of Zip-File-Extractor, modified by H31IX for use with Bukkit + */ + private void unzip(String file) + { + try + { + File fSourceZip = new File(file); + String zipPath = file.substring(0, file.length()-4); + ZipFile zipFile = new ZipFile(fSourceZip); + Enumeration e = zipFile.entries(); + while(e.hasMoreElements()) + { + ZipEntry entry = (ZipEntry)e.nextElement(); + File destinationFilePath = new File(zipPath,entry.getName()); + destinationFilePath.getParentFile().mkdirs(); + if(entry.isDirectory()) + { + continue; + } + else + { + BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry)); + int b; + byte buffer[] = new byte[BYTE_SIZE]; + FileOutputStream fos = new FileOutputStream(destinationFilePath); + BufferedOutputStream bos = new BufferedOutputStream(fos, BYTE_SIZE); + while((b = bis.read(buffer, 0, BYTE_SIZE)) != -1) + { + bos.write(buffer, 0, b); + } + bos.flush(); + bos.close(); + bis.close(); + String name = destinationFilePath.getName(); + if(name.endsWith(".jar") && pluginFile(name)) + { + destinationFilePath.renameTo(new File("plugins/" + updateFolder + "/" + name)); + } + } + entry = null; + destinationFilePath = null; + } + e = null; + zipFile.close(); + zipFile = null; + // Move any plugin data folders that were included to the right place, Bukkit won't do this for us. + for(File dFile : new File(zipPath).listFiles()) + { + if(dFile.isDirectory()) + { + if(pluginFile(dFile.getName())) + { + File oFile = new File("plugins/" + dFile.getName()); // Get current dir + File [] contents = oFile.listFiles(); // List of existing files in the current dir + for(File cFile : dFile.listFiles()) // Loop through all the files in the new dir + { + boolean found = false; + for(File xFile : contents) // Loop through contents to see if it exists + { + if(xFile.getName().equals(cFile.getName())) + { + found = true; + break; + } + } + if(!found) + { + // Move the new file into the current dir + cFile.renameTo(new File(oFile.getCanonicalFile() + "/" + cFile.getName())); + } + else + { + // This file already exists, so we don't need it anymore. + cFile.delete(); + } + } + } + } + dFile.delete(); + } + new File(zipPath).delete(); + fSourceZip.delete(); + } + catch(IOException ex) + { + ex.printStackTrace(); + logger.warning("The auto-updater tried to unzip a new update file, but was unsuccessful."); + result = Updater.UpdateResult.FAIL_DOWNLOAD; + } + new File(file).delete(); + } + + /** + * Check if the name of a jar is one of the plugins currently installed, used for extracting the correct files out of a zip. + */ + public boolean pluginFile(String name) + { + for(File file : new File("plugins").listFiles()) + { + if(file.getName().equals(name)) + { + return true; + } + } + return false; + } + + /** + * Obtain the direct download file url from the file's page. + */ + private String getFile(String link) + { + String download = null; + try + { + // Open a connection to the page + URL url = new URL(link); + URLConnection urlConn = url.openConnection(); + InputStreamReader inStream = new InputStreamReader(urlConn.getInputStream()); + BufferedReader buff = new BufferedReader(inStream); + + int counter = 0; + String line; + while((line = buff.readLine()) != null) + { + counter++; + // Search for the download link + if(line.contains("

  • ")) + { + // Get the raw link + download = line.split("Download")[0]; + } + // Search for size + else if (line.contains("
    Size
    ")) + { + sizeLine = counter+1; + } + else if(counter == sizeLine) + { + String size = line.replaceAll("
    ", "").replaceAll("
    ", ""); + multiplier = size.contains("MiB") ? 1048576 : 1024; + size = size.replace(" KiB", "").replace(" MiB", ""); + totalSize = (long)(Double.parseDouble(size)*multiplier); + } + } + urlConn = null; + inStream = null; + buff.close(); + buff = null; + } + catch (Exception ex) + { + ex.printStackTrace(); + logger.warning("The auto-updater tried to contact dev.bukkit.org, but was unsuccessful."); + result = Updater.UpdateResult.FAIL_DBO; + return null; + } + return download; + } + + /** + * Check to see if the program should continue by evaluation whether the plugin is already updated, or shouldn't be updated + */ + private boolean versionCheck(String title) + { + if (type != UpdateType.NO_VERSION_CHECK) + { + String[] parts = title.split(" "); + String version = plugin.getDescription().getVersion(); + + if(parts.length == 2) + { + String remoteVersion = parts[1].split(" ")[0]; // Get the newest file's version number + int remVer = -1, curVer=0; + try + { + remVer = calVer(remoteVersion); + curVer = calVer(version); + } + catch(NumberFormatException nfe) + { + remVer=-1; + } + + if(hasTag(version)||version.equalsIgnoreCase(remoteVersion)||curVer>=remVer) + { + // We already have the latest version, or this build is tagged for no-update + result = Updater.UpdateResult.NO_UPDATE; + return false; + } + } + else + { + // The file's name did not contain the string 'vVersion' + logger.warning("The author of this plugin has misconfigured their Auto Update system"); + logger.warning("Files uploaded to BukkitDev should contain the version number, seperated from the name by a 'v', such as PluginName v1.0"); + logger.warning("Please notify the author (" + plugin.getDescription().getAuthors().get(0) + ") of this error."); + result = Updater.UpdateResult.FAIL_NOVERSION; + return false; + } + } + return true; + } + /** + * Used to calculate the version string as an Integer + */ + private Integer calVer(String s) throws NumberFormatException + { + if(s.contains(".")) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i values() { - return members.keySet(); + return new HashSet(members.keySet()); } } 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..51f22558 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/PrettyPrinter.java @@ -0,0 +1,174 @@ +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)) { + output.append(value); + } else if (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/reflect/PrimitiveUtils.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/PrimitiveUtils.java deleted file mode 100644 index 3597920f..00000000 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/PrimitiveUtils.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.comphenix.protocol.reflect; - -/* - * Copyright (C) 2008 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import java.lang.reflect.Type; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -/** - * Contains static utility methods pertaining to primitive types and their - * corresponding wrapper types. - * - * @author Kevin Bourrillion - */ -public final class PrimitiveUtils { - private PrimitiveUtils() { - } - - /** A map from primitive types to their corresponding wrapper types. */ - private static final Map, Class> PRIMITIVE_TO_WRAPPER_TYPE; - - /** A map from wrapper types to their corresponding primitive types. */ - private static final Map, Class> WRAPPER_TO_PRIMITIVE_TYPE; - - // Sad that we can't use a BiMap. :( - static { - Map, Class> primToWrap = new HashMap, Class>(16); - Map, Class> wrapToPrim = new HashMap, Class>(16); - - add(primToWrap, wrapToPrim, boolean.class, Boolean.class); - add(primToWrap, wrapToPrim, byte.class, Byte.class); - add(primToWrap, wrapToPrim, char.class, Character.class); - add(primToWrap, wrapToPrim, double.class, Double.class); - add(primToWrap, wrapToPrim, float.class, Float.class); - add(primToWrap, wrapToPrim, int.class, Integer.class); - add(primToWrap, wrapToPrim, long.class, Long.class); - add(primToWrap, wrapToPrim, short.class, Short.class); - add(primToWrap, wrapToPrim, void.class, Void.class); - - PRIMITIVE_TO_WRAPPER_TYPE = Collections.unmodifiableMap(primToWrap); - WRAPPER_TO_PRIMITIVE_TYPE = Collections.unmodifiableMap(wrapToPrim); - } - - private static void add(Map, Class> forward, - Map, Class> backward, Class key, Class value) { - forward.put(key, value); - backward.put(value, key); - } - - /** - * Returns true if this type is a primitive. - */ - public static boolean isPrimitive(Type type) { - return PRIMITIVE_TO_WRAPPER_TYPE.containsKey(type); - } - - /** - * Returns {@code true} if {@code type} is one of the nine primitive-wrapper - * types, such as {@link Integer}. - * - * @see Class#isPrimitive - */ - public static boolean isWrapperType(Type type) { - return WRAPPER_TO_PRIMITIVE_TYPE.containsKey(checkNotNull(type)); - } - - /** - * Returns the corresponding wrapper type of {@code type} if it is a - * primitive type; otherwise returns {@code type} itself. Idempotent. - * - *
    -	 *     wrap(int.class) == Integer.class
    -	 *     wrap(Integer.class) == Integer.class
    -	 *     wrap(String.class) == String.class
    -	 * 
    - */ - public static Class wrap(Class type) { - // cast is safe: long.class and Long.class are both of type Class - @SuppressWarnings("unchecked") - Class wrapped = (Class) PRIMITIVE_TO_WRAPPER_TYPE - .get(checkNotNull(type)); - return (wrapped == null) ? type : wrapped; - } - - /** - * Returns the corresponding primitive type of {@code type} if it is a - * wrapper type; otherwise returns {@code type} itself. Idempotent. - * - *
    -	 *     unwrap(Integer.class) == int.class
    -	 *     unwrap(int.class) == int.class
    -	 *     unwrap(String.class) == String.class
    -	 * 
    - */ - public static Class unwrap(Class type) { - // cast is safe: long.class and Long.class are both of type Class - @SuppressWarnings("unchecked") - Class unwrapped = (Class) WRAPPER_TO_PRIMITIVE_TYPE - .get(checkNotNull(type)); - return (unwrapped == null) ? type : unwrapped; - } - - public static T checkNotNull(T obj) { - if (obj == null) { - throw new NullPointerException(); - } - return obj; - } -} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java index f4bb07f3..2665dc21 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java @@ -426,7 +426,7 @@ public class StructureModifier { Class type = field.getType(); // First, ignore primitive fields - if (!PrimitiveUtils.isPrimitive(type)) { + if (!type.isPrimitive()) { // Next, see if we actually can generate a default value if (generator.getDefault(type) != null) { // If so, require it diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/CompiledStructureModifier.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/CompiledStructureModifier.java index caea3167..602815f8 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/CompiledStructureModifier.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/CompiledStructureModifier.java @@ -67,6 +67,16 @@ public abstract class CompiledStructureModifier extends StructureModifie else return (TField) result; } + + /** + * Read the given field index using reflection. + * @param index - index of field. + * @return Resulting value. + * @throws FieldAccessException The field doesn't exist, or it cannot be accessed under the current security contraints. + */ + protected Object readReflected(int index) throws FieldAccessException { + return super.read(index); + } protected abstract Object readGenerated(int fieldIndex) throws FieldAccessException; @@ -78,6 +88,17 @@ public abstract class CompiledStructureModifier extends StructureModifie return writeGenerated(index, value); } + /** + * Write the given field using reflection. + * @param index - index of field. + * @param value - new value. + * @throws FieldAccessException The field doesn't exist, or it cannot be accessed under the current security contraints. + */ + @SuppressWarnings("unchecked") + protected void writeReflected(int index, Object value) throws FieldAccessException { + super.write(index, (TField) value); + } + protected abstract StructureModifier writeGenerated(int index, Object value) throws FieldAccessException; @Override diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/StructureCompiler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/StructureCompiler.java index 14f8bdb2..10ceaa72 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/StructureCompiler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/StructureCompiler.java @@ -25,9 +25,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import com.comphenix.protocol.reflect.PrimitiveUtils; import com.comphenix.protocol.reflect.StructureModifier; import com.google.common.base.Objects; +import com.google.common.primitives.Primitives; import net.sf.cglib.asm.*; @@ -306,7 +306,7 @@ public final class StructureCompiler { for (int i = 0; i < fields.size(); i++) { Class outputType = fields.get(i).getType(); - Class inputType = PrimitiveUtils.wrap(outputType); + Class inputType = Primitives.wrap(outputType); String typeDescriptor = Type.getDescriptor(outputType); String inputPath = inputType.getName().replace('.', '/'); @@ -323,7 +323,7 @@ public final class StructureCompiler { mv.visitVarInsn(Opcodes.ALOAD, 3); mv.visitVarInsn(Opcodes.ALOAD, 2); - if (!PrimitiveUtils.isPrimitive(outputType)) + if (!outputType.isPrimitive()) mv.visitTypeInsn(Opcodes.CHECKCAST, inputPath); else boxingHelper.unbox(Type.getType(outputType)); @@ -335,8 +335,7 @@ public final class StructureCompiler { mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitVarInsn(Opcodes.ILOAD, 1); mv.visitVarInsn(Opcodes.ALOAD, 2); - mv.visitMethodInsn(Opcodes.INVOKESPECIAL, COMPILED_CLASS, "write", "(ILjava/lang/Object;)L" + SUPER_CLASS + ";"); - mv.visitInsn(Opcodes.POP); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, COMPILED_CLASS, "writeReflected", "(ILjava/lang/Object;)V;"); } mv.visitJumpInsn(Opcodes.GOTO, returnLabel); @@ -408,7 +407,7 @@ public final class StructureCompiler { // We have to use reflection for private and protected fields. mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitVarInsn(Opcodes.ILOAD, 1); - mv.visitMethodInsn(Opcodes.INVOKESPECIAL, COMPILED_CLASS, "read", "(I)Ljava/lang/Object;"); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, COMPILED_CLASS, "readReflected", "(I)Ljava/lang/Object;"); } mv.visitInsn(Opcodes.ARETURN); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java index 46505891..38066613 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java @@ -206,7 +206,6 @@ public class DefaultInstances { // Just check if any of them are NULL for (Class type : types) { if (getDefaultInternal(type, providers, recursionLevel) == null) { - System.out.println(type.getName() + " is NULL!"); return true; } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/PrimitiveGenerator.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/PrimitiveGenerator.java index 526846a0..5563acc6 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/PrimitiveGenerator.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/PrimitiveGenerator.java @@ -21,8 +21,8 @@ import java.lang.reflect.Array; import javax.annotation.Nullable; -import com.comphenix.protocol.reflect.PrimitiveUtils; import com.google.common.base.Defaults; +import com.google.common.primitives.Primitives; /** * Provides constructors for primtive types, wrappers, arrays and strings. @@ -58,10 +58,10 @@ public class PrimitiveGenerator implements InstanceProvider { @Override public Object create(@Nullable Class type) { - if (PrimitiveUtils.isPrimitive(type)) { + if (type.isPrimitive()) { return Defaults.defaultValue(type); - } else if (PrimitiveUtils.isWrapperType(type)) { - return Defaults.defaultValue(PrimitiveUtils.unwrap(type)); + } else if (Primitives.isWrapperType(type)) { + return Defaults.defaultValue(Primitives.unwrap(type)); } else if (type.isArray()) { Class arrayType = type.getComponentType(); return Array.newInstance(arrayType, 0); 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..ff7621a5 --- /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 receiver - the receiver. + * @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/java/plugin.yml b/ProtocolLib/src/main/java/plugin.yml deleted file mode 100644 index 9d100284..00000000 --- a/ProtocolLib/src/main/java/plugin.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: ProtocolLib -version: 1.4.0 -description: Provides read/write access to the Minecraft protocol. -author: Comphenix -website: http://www.comphenix.net/ProtocolLib - -main: com.comphenix.protocol.ProtocolLibrary -database: false \ No newline at end of file diff --git a/ProtocolLib/src/main/resources/config.yml b/ProtocolLib/src/main/resources/config.yml new file mode 100644 index 00000000..8bc3ca2d --- /dev/null +++ b/ProtocolLib/src/main/resources/config.yml @@ -0,0 +1,12 @@ +global: + # Settings for the automatic version updater + auto updater: + notify: true + download: true + + # Number of seconds to wait until a new update is downloaded + delay: 43200 # 12 hours + # Last update time + last: 0 + + metrics: true \ No newline at end of file diff --git a/ProtocolLib/src/main/resources/plugin.yml b/ProtocolLib/src/main/resources/plugin.yml new file mode 100644 index 00000000..78a71f62 --- /dev/null +++ b/ProtocolLib/src/main/resources/plugin.yml @@ -0,0 +1,33 @@ +name: ProtocolLib +version: 1.5.1 +description: Provides read/write access to the Minecraft protocol. +author: Comphenix +website: http://www.comphenix.net/ProtocolLib + +main: com.comphenix.protocol.ProtocolLibrary +database: false + +commands: + protocol: + description: Performs administrative tasks regarding ProtocolLib. + usage: / config|check|update + permission: experiencemod.admin + permission-message: You don't have + packet: + description: Add or remove a simple packet listener. + usage: / add|remove|names client|server [ID start]-[ID stop] [detailed] + permission: experiencemod.admin + permission-message: You don't have + +permissions: + protocol.*: + description: Gives access to everything. + children: + protocol.admin: true + protocol.info: true + protocol.admin: + description: Able to initiate the update process, and can configure debug mode. + default: op + protocol.info: + description: Can read update notifications and error reports. + default: op \ No newline at end of file