diff --git a/ItemDisguise/.classpath b/ItemDisguise/.classpath
index 71e70473..2bda6dc7 100644
--- a/ItemDisguise/.classpath
+++ b/ItemDisguise/.classpath
@@ -7,6 +7,12 @@
+
+
+
+
+
+
diff --git a/ProtocolLib/pom.xml b/ProtocolLib/pom.xml
index ac7a8b99..414e2599 100644
--- a/ProtocolLib/pom.xml
+++ b/ProtocolLib/pom.xml
@@ -2,7 +2,7 @@
4.0.0com.comphenix.protocolProtocolLib
- 2.3.0
+ 2.4.3jarProvides read/write access to the Minecraft protocol.
@@ -57,7 +57,7 @@
false
- true
+ false
@@ -203,7 +203,7 @@
org.bukkitcraftbukkit
- 1.4.7-R0.1
+ 1.5.1-R0.2-SNAPSHOTprovided
@@ -219,16 +219,16 @@
test
- org.powermock
- powermock-module-junit4
- ${powermock.version}
- test
-
-
- org.powermock
- powermock-api-mockito
- ${powermock.version}
- test
-
+ org.powermock
+ powermock-module-junit4
+ ${powermock.version}
+ test
+
+
+ org.powermock
+ powermock-api-mockito
+ ${powermock.version}
+ test
+
\ 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
index dd7c5bbe..74bb63c2 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java
@@ -1,160 +1,167 @@
-/*
- * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
- * Copyright (C) 2012 Kristian S. Stangeland
- *
- * This program is free software; you can redistribute it and/or modify it under the terms of the
- * GNU General Public License as published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
- * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
- * See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with this program;
- * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
- * 02111-1307 USA
- */
-
-package com.comphenix.protocol;
-
-import java.lang.reflect.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.injector.server.AbstractInputStreamLookup;
-import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
-import com.comphenix.protocol.injector.spigot.SpigotPacketInjector;
-import com.comphenix.protocol.reflect.FieldUtils;
-import com.comphenix.protocol.reflect.FuzzyReflection;
-import com.comphenix.protocol.reflect.MethodUtils;
-import com.comphenix.protocol.reflect.ObjectWriter;
-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;
-import com.comphenix.protocol.utility.MinecraftReflection;
-import com.comphenix.protocol.wrappers.ChunkPosition;
-import com.comphenix.protocol.wrappers.WrappedDataWatcher;
-import com.comphenix.protocol.wrappers.WrappedWatchableObject;
-import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer;
-
-/**
- * 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,
- ObjectWriter.class, Packets.Server.class, Packets.Client.class,
- ChunkPosition.class, WrappedDataWatcher.class, WrappedWatchableObject.class,
- AbstractInputStreamLookup.class, TemporaryPlayerFactory.class, SpigotPacketInjector.class,
- MinecraftReflection.class, NbtBinarySerializer.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.EntityUtilities",
- "com.comphenix.protocol.injector.packet.PacketRegistry",
- "com.comphenix.protocol.injector.packet.PacketInjector",
- "com.comphenix.protocol.injector.packet.ReadPacketModifier",
- "com.comphenix.protocol.injector.StructureCache",
- "com.comphenix.protocol.reflect.compiler.BoxingHelper",
- "com.comphenix.protocol.reflect.compiler.MethodDescriptor",
- "com.comphenix.protocol.wrappers.nbt.WrappedElement",
- };
-
- 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]);
- }
-}
+/*
+ * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
+ * Copyright (C) 2012 Kristian S. Stangeland
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program;
+ * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+ * 02111-1307 USA
+ */
+
+package com.comphenix.protocol;
+
+import java.lang.reflect.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.error.Report;
+import com.comphenix.protocol.error.ReportType;
+import com.comphenix.protocol.events.ListeningWhitelist;
+import com.comphenix.protocol.events.PacketContainer;
+import com.comphenix.protocol.injector.BukkitUnwrapper;
+import com.comphenix.protocol.injector.server.AbstractInputStreamLookup;
+import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
+import com.comphenix.protocol.injector.spigot.SpigotPacketInjector;
+import com.comphenix.protocol.reflect.FieldUtils;
+import com.comphenix.protocol.reflect.FuzzyReflection;
+import com.comphenix.protocol.reflect.MethodUtils;
+import com.comphenix.protocol.reflect.ObjectWriter;
+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;
+import com.comphenix.protocol.utility.MinecraftReflection;
+import com.comphenix.protocol.wrappers.ChunkPosition;
+import com.comphenix.protocol.wrappers.WrappedDataWatcher;
+import com.comphenix.protocol.wrappers.WrappedWatchableObject;
+import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer;
+
+/**
+ * Used to fix ClassLoader leaks that may lead to filling up the permanent generation.
+ *
+ * @author Kristian
+ */
+class CleanupStaticMembers {
+ // Reports
+ public final static ReportType REPORT_CANNOT_RESET_FIELD = new ReportType("Unable to reset field %s: %s");
+ public final static ReportType REPORT_CANNOT_UNLOAD_CLASS = new ReportType("Unable to unload class %s.");
+
+ 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,
+ ObjectWriter.class, Packets.Server.class, Packets.Client.class,
+ ChunkPosition.class, WrappedDataWatcher.class, WrappedWatchableObject.class,
+ AbstractInputStreamLookup.class, TemporaryPlayerFactory.class, SpigotPacketInjector.class,
+ MinecraftReflection.class, NbtBinarySerializer.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.EntityUtilities",
+ "com.comphenix.protocol.injector.packet.PacketRegistry",
+ "com.comphenix.protocol.injector.packet.PacketInjector",
+ "com.comphenix.protocol.injector.packet.ReadPacketModifier",
+ "com.comphenix.protocol.injector.StructureCache",
+ "com.comphenix.protocol.reflect.compiler.BoxingHelper",
+ "com.comphenix.protocol.reflect.compiler.MethodDescriptor",
+ "com.comphenix.protocol.wrappers.nbt.WrappedElement",
+ };
+
+ 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,
+ Report.newBuilder(REPORT_CANNOT_RESET_FIELD).error(e).messageParam(field.getName(), e.getMessage())
+ );
+ }
+ }
+ }
+ }
+
+ // 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, Report.newBuilder(REPORT_CANNOT_UNLOAD_CLASS).error(e).messageParam(name));
+ }
+ }
+
+ 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
index 18ddb25e..7ca949bd 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandBase.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandBase.java
@@ -1,109 +1,117 @@
-/*
- * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
- * Copyright (C) 2012 Kristian S. Stangeland
- *
- * This program is free software; you can redistribute it and/or modify it under the terms of the
- * GNU General Public License as published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
- * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
- * See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with this program;
- * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
- * 02111-1307 USA
- */
-
-package com.comphenix.protocol;
-
-import org.bukkit.ChatColor;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandExecutor;
-import org.bukkit.command.CommandSender;
-
-import com.comphenix.protocol.error.ErrorReporter;
-
-/**
- * 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;
-
- protected ErrorReporter reporter;
-
- public CommandBase(ErrorReporter reporter, String permission, String name) {
- this(reporter, permission, name, 0);
- }
-
- public CommandBase(ErrorReporter reporter, String permission, String name, int minimumArgumentCount) {
- this.reporter = reporter;
- this.name = name;
- this.permission = permission;
- this.minimumArgumentCount = minimumArgumentCount;
- }
-
- @Override
- public final boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
- try {
- // Make sure we're dealing with the correct command
- if (!command.getName().equalsIgnoreCase(name)) {
- return false;
- }
- if (permission != null && !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;
- }
-
- } catch (Exception e) {
- reporter.reportDetailed(this, "Cannot execute command " + name, e, sender, label, args);
- return true;
- }
- }
-
- /**
- * Retrieve the permission necessary to execute this command.
- * @return The permission, or NULL if not needed.
- */
- public String getPermission() {
- return permission;
- }
-
- /**
- * Retrieve the primary name of this command.
- * @return Primary name.
- */
- public String getName() {
- return name;
- }
-
- /**
- * Retrieve the error reporter.
- * @return Error reporter.
- */
- protected ErrorReporter getReporter() {
- return reporter;
- }
-
- /**
- * Main implementation of this command.
- * @param sender - command sender.
- * @param args
- * @return
- */
- protected abstract boolean handleCommand(CommandSender sender, String[] args);
-}
+/*
+ * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
+ * Copyright (C) 2012 Kristian S. Stangeland
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program;
+ * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+ * 02111-1307 USA
+ */
+
+package com.comphenix.protocol;
+
+import org.bukkit.ChatColor;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+
+import com.comphenix.protocol.error.ErrorReporter;
+import com.comphenix.protocol.error.Report;
+import com.comphenix.protocol.error.ReportType;
+
+/**
+ * Base class for all our commands.
+ *
+ * @author Kristian
+ */
+abstract class CommandBase implements CommandExecutor {
+ public static final ReportType REPORT_COMMAND_ERROR = new ReportType("Cannot execute command %s.");
+ public static final ReportType REPORT_UNEXPECTED_COMMAND = new ReportType("Incorrect command assigned to %s.");
+
+ public static final String PERMISSION_ADMIN = "protocol.admin";
+
+ private String permission;
+ private String name;
+ private int minimumArgumentCount;
+
+ protected ErrorReporter reporter;
+
+ public CommandBase(ErrorReporter reporter, String permission, String name) {
+ this(reporter, permission, name, 0);
+ }
+
+ public CommandBase(ErrorReporter reporter, String permission, String name, int minimumArgumentCount) {
+ this.reporter = reporter;
+ this.name = name;
+ this.permission = permission;
+ this.minimumArgumentCount = minimumArgumentCount;
+ }
+
+ @Override
+ public final boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ try {
+ // Make sure we're dealing with the correct command
+ if (!command.getName().equalsIgnoreCase(name)) {
+ reporter.reportWarning(this, Report.newBuilder(REPORT_UNEXPECTED_COMMAND).messageParam(this));
+ return false;
+ }
+ if (permission != null && !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 {
+ sender.sendMessage(ChatColor.RED + "Insufficient commands. You need at least " + minimumArgumentCount);
+ return false;
+ }
+
+ } catch (Exception e) {
+ reporter.reportDetailed(this,
+ Report.newBuilder(REPORT_COMMAND_ERROR).error(e).messageParam(name).callerParam(sender, label, args)
+ );
+ return true;
+ }
+ }
+
+ /**
+ * Retrieve the permission necessary to execute this command.
+ * @return The permission, or NULL if not needed.
+ */
+ public String getPermission() {
+ return permission;
+ }
+
+ /**
+ * Retrieve the primary name of this command.
+ * @return Primary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Retrieve the error reporter.
+ * @return Error reporter.
+ */
+ protected ErrorReporter getReporter() {
+ return reporter;
+ }
+
+ /**
+ * Main implementation of this command.
+ * @param sender - command sender.
+ * @param args
+ * @return
+ */
+ protected abstract boolean handleCommand(CommandSender sender, String[] args);
+}
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java
new file mode 100644
index 00000000..392b1efc
--- /dev/null
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java
@@ -0,0 +1,482 @@
+package com.comphenix.protocol;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import javax.script.Invocable;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+
+import org.bukkit.ChatColor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.conversations.Conversable;
+import org.bukkit.conversations.Conversation;
+import org.bukkit.conversations.ConversationAbandonedEvent;
+import org.bukkit.conversations.ConversationAbandonedListener;
+import org.bukkit.conversations.ConversationContext;
+import org.bukkit.conversations.ConversationFactory;
+import org.bukkit.plugin.Plugin;
+
+import com.comphenix.protocol.MultipleLinesPrompt.MultipleConversationCanceller;
+import com.comphenix.protocol.concurrency.IntegerSet;
+import com.comphenix.protocol.error.ErrorReporter;
+import com.comphenix.protocol.error.Report;
+import com.comphenix.protocol.error.ReportType;
+import com.comphenix.protocol.events.PacketEvent;
+import com.google.common.collect.DiscreteDomains;
+import com.google.common.collect.Range;
+import com.google.common.collect.Ranges;
+
+/**
+ * A command to apply JavaScript filtering to the packet command.
+ *
+ * @author Kristian
+ */
+public class CommandFilter extends CommandBase {
+ public static final ReportType REPORT_FALLBACK_ENGINE = new ReportType("Falling back to the Rhino engine.");
+ public static final ReportType REPORT_CANNOT_LOAD_FALLBACK_ENGINE = new ReportType("Could not load Rhino either. Please upgrade your JVM or OS.");
+ public static final ReportType REPORT_PACKAGES_UNSUPPORTED_IN_ENGINE = new ReportType("Unable to initialize packages for JavaScript engine.");
+ public static final ReportType REPORT_FILTER_REMOVED_FOR_ERROR = new ReportType("Removing filter %s for causing %s.");
+ public static final ReportType REPORT_CANNOT_HANDLE_CONVERSATION = new ReportType("Cannot handle conversation.");
+
+ public interface FilterFailedHandler{
+ /**
+ * Invoked when a given filter has failed.
+ * @param event - the packet event.
+ * @param filter - the filter that failed.
+ * @param ex - the failure.
+ * @returns TRUE to keep processing this filter, FALSE to remove it.
+ */
+ public boolean handle(PacketEvent event, Filter filter, Exception ex);
+ }
+
+ /**
+ * Possible sub commands.
+ *
+ * @author Kristian
+ */
+ private enum SubCommand {
+ ADD, REMOVE;
+ }
+
+ /**
+ * A filter that will be used to process a packet event.
+ * @author Kristian
+ */
+ public static class Filter {
+ private final String name;
+ private final String predicate;
+
+ private final IntegerSet ranges;
+
+ /**
+ * Construct a new immutable filter.
+ * @param name - the unique name of the filter.
+ * @param predicate - the JavaScript predicate that will be used to filter packet events.
+ * @param ranges - a list of valid packet ID ranges that this filter applies to.
+ */
+ public Filter(String name, String predicate, Set packets) {
+ this.name = name;
+ this.predicate = predicate;
+ this.ranges = new IntegerSet(Packets.MAXIMUM_PACKET_ID + 1);
+ this.ranges.addAll(packets);
+ }
+
+ /**
+ * Retrieve the unique name of the filter.
+ * @return Unique name of the filter.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Retrieve the JavaScript predicate that will be used to filter packet events.
+ * @return Predicate itself.
+ */
+ public String getPredicate() {
+ return predicate;
+ }
+
+ /**
+ * Retrieve a copy of the set of packets this filter applies to.
+ * @return Set of packets this filter applies to.
+ */
+ public Set getRanges() {
+ return ranges.toSet();
+ }
+
+ /**
+ * Determine whether or not a packet event needs to be passed to this filter.
+ * @param event - the event to test.
+ * @return TRUE if it does, FALSE otherwise.
+ */
+ private boolean isApplicable(PacketEvent event) {
+ return ranges.contains(event.getPacketID());
+ }
+
+ /**
+ * Evaluate the current filter using the provided ScriptEngine as context.
+ *
+ * This context may be modified with additional code.
+ * @param context - the current script context.
+ * @param event - the packet event to evaluate.
+ * @return TRUE to pass this packet event on to the debug listeners, FALSE otherwise.
+ * @throws ScriptException If the compilation failed or the filter is not valid.
+ */
+ public boolean evaluate(ScriptEngine context, PacketEvent event) throws ScriptException {
+ if (!isApplicable(event))
+ return true;
+ // Ensure that the predicate has been compiled
+ compile(context);
+
+ try {
+ Object result = ((Invocable) context).invokeFunction(name, event, event.getPacket().getHandle());
+
+ if (result instanceof Boolean)
+ return (Boolean) result;
+ else
+ throw new ScriptException("Filter result wasn't a boolean: " + result);
+
+ } catch (NoSuchMethodException e) {
+ // Must be a fault with the script engine itself
+ throw new IllegalStateException("Unable to compile " + name + " into current script engine.", e);
+ }
+ }
+
+ /**
+ * Force the compilation of a specific filter.
+ * @param context - the current script context.
+ * @throws ScriptException If the compilation failed.
+ */
+ public void compile(ScriptEngine context) throws ScriptException {
+ if (context.get(name) == null) {
+ context.eval("var " + name + " = function(event, packet) {\n" + predicate);
+ }
+ }
+
+ /**
+ * Clean up all associated code from this filter in the provided script engine.
+ * @param context - the current script context.
+ */
+ public void close(ScriptEngine context) {
+ context.put(name, null);
+ }
+ }
+
+ private class CompilationSuccessCanceller implements MultipleConversationCanceller {
+ @Override
+ public boolean cancelBasedOnInput(ConversationContext context, String in) {
+ throw new UnsupportedOperationException("Cannot cancel on the last line alone.");
+ }
+
+ @Override
+ public void setConversation(Conversation conversation) {
+ // Ignore
+ }
+
+ @Override
+ public boolean cancelBasedOnInput(ConversationContext context, String currentLine, StringBuilder lines, int lineCount) {
+ try {
+ engine.eval("function(event, packet) {\n" + lines.toString());
+
+ // It compiles - accept the filter!
+ return true;
+ } catch (ScriptException e) {
+ // We also have the function() line
+ int realLineCount = lineCount + 1;
+
+ // Only possible to recover from an error on the last line.
+ return e.getLineNumber() < realLineCount;
+ }
+ }
+
+ @Override
+ public CompilationSuccessCanceller clone() {
+ return new CompilationSuccessCanceller();
+ }
+ }
+
+ /**
+ * Name of this command.
+ */
+ public static final String NAME = "filter";
+
+ // Default error handler
+ private FilterFailedHandler defaultFailedHandler;
+
+ // Currently registered filters
+ private List filters = new ArrayList();
+
+ // Owner plugin
+ private final Plugin plugin;
+
+ // Whether or not the command is enabled
+ private ProtocolConfig config;
+
+ // Script engine
+ private ScriptEngine engine;
+
+ public CommandFilter(ErrorReporter reporter, Plugin plugin, ProtocolConfig config) {
+ super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 2);
+ this.plugin = plugin;
+ this.config = config;
+
+ // Start the engine
+ initalizeScript();
+ }
+
+ private void initalizeScript() {
+ try {
+ // First attempt
+ initializeEngine();
+
+ // Oh for ..
+ if (!isInitialized()) {
+ throw new ScriptException("A JavaScript engine could not be found.");
+ }
+ } catch (ScriptException e1) {
+ // It's not a huge deal
+ printPackageWarning(e1);
+
+ if (!config.getScriptEngineName().equals("rhino")) {
+ reporter.reportWarning(this, Report.newBuilder(REPORT_FALLBACK_ENGINE));
+ config.setScriptEngineName("rhino");
+ config.saveAll();
+
+ try {
+ initializeEngine();
+
+ if (!isInitialized()) {
+ reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_LOAD_FALLBACK_ENGINE));
+ }
+ } catch (ScriptException e2) {
+ // And again ..
+ printPackageWarning(e2);
+ }
+ }
+ }
+ }
+
+ private void printPackageWarning(ScriptException e) {
+ reporter.reportWarning(this, Report.newBuilder(REPORT_PACKAGES_UNSUPPORTED_IN_ENGINE).error(e));
+ }
+
+ /**
+ * Initialize the current configured engine.
+ * @throws ScriptException If we are unable to import packages.
+ */
+ private void initializeEngine() throws ScriptException {
+ ScriptEngineManager manager = new ScriptEngineManager();
+ engine = manager.getEngineByName(config.getScriptEngineName());
+
+ // Import useful packages
+ if (engine != null) {
+ engine.eval("importPackage(org.bukkit);");
+ engine.eval("importPackage(com.comphenix.protocol.reflect);");
+ }
+ }
+
+ /**
+ * Determine if the filter engine has been successfully initialized.
+ * @return TRUE if it has, FALSE otherwise.
+ */
+ public boolean isInitialized() {
+ return engine != null;
+ }
+
+ private FilterFailedHandler getDefaultErrorHandler() {
+ // No need to create a new object every time
+ if (defaultFailedHandler == null) {
+ defaultFailedHandler = new FilterFailedHandler() {
+ @Override
+ public boolean handle(PacketEvent event, Filter filter, Exception ex) {
+ reporter.reportMinimal(plugin, "filterEvent(PacketEvent)", ex, event);
+ reporter.reportWarning(this,
+ Report.newBuilder(REPORT_FILTER_REMOVED_FOR_ERROR).messageParam(filter.getName(), ex.getClass().getSimpleName())
+ );
+ return false;
+ }
+ };
+ }
+ return defaultFailedHandler;
+ }
+
+ /**
+ * Determine whether or not to pass the given packet event to the packet listeners.
+ *
+ * Uses a default filter failure handler that simply prints the error message and removes the filter.
+ * @param event - the event.
+ * @return TRUE if we should, FALSE otherwise.
+ */
+ public boolean filterEvent(PacketEvent event) {
+ return filterEvent(event, getDefaultErrorHandler());
+ }
+
+ /**
+ * Determine whether or not to pass the given packet event to the packet listeners.
+ * @param event - the event.
+ * @param handler - failure handler.
+ * @return TRUE if we should, FALSE otherwise.
+ * @throws FilterFailedException If one of the filters failed.
+ */
+ public boolean filterEvent(PacketEvent event, FilterFailedHandler handler) {
+ for (Iterator it = filters.iterator(); it.hasNext(); ) {
+ Filter filter = it.next();
+
+ try {
+ if (!filter.evaluate(engine, event)) {
+ return false;
+ }
+ } catch (Exception ex) {
+ if (!handler.handle(event, filter, ex)) {
+ it.remove();
+ }
+ }
+ }
+ // Pass!
+ return true;
+ }
+
+ /*
+ * Description: Adds or removes a simple packet filter.
+ Usage: / add|remove name [packet IDs]
+ */
+ @Override
+ protected boolean handleCommand(CommandSender sender, String[] args) {
+ if (!config.isDebug()) {
+ sender.sendMessage(ChatColor.RED + "Debug mode must be enabled in the configuration first!");
+ return true;
+ }
+ if (!isInitialized()) {
+ sender.sendMessage(ChatColor.RED + "JavaScript engine was not present. Filter system is disabled.");
+ return true;
+ }
+
+ final SubCommand command = parseCommand(args, 0);
+ final String name = args[1];
+
+ switch (command) {
+ case ADD:
+ // Never overwrite an existing filter
+ if (findFilter(name) != null) {
+ sender.sendMessage(ChatColor.RED + "Filter " + name + " already exists. Remove it first.");
+ return true;
+ }
+
+ final Set packets = parseRanges(args, 2);
+ sender.sendMessage("Enter filter program ('}' to complete or CANCEL):");
+
+ // Make sure we can use the conversable interface
+ if (sender instanceof Conversable) {
+ final MultipleLinesPrompt prompt =
+ new MultipleLinesPrompt(new CompilationSuccessCanceller(), "function(event, packet) {");
+
+ new ConversationFactory(plugin).
+ withFirstPrompt(prompt).
+ withEscapeSequence("CANCEL").
+ withLocalEcho(false).
+ addConversationAbandonedListener(new ConversationAbandonedListener() {
+ @Override
+ public void conversationAbandoned(ConversationAbandonedEvent event) {
+ try {
+ final Conversable whom = event.getContext().getForWhom();
+
+ if (event.gracefulExit()) {
+ String predicate = prompt.removeAccumulatedInput(event.getContext());
+ Filter filter = new Filter(name, predicate, packets);
+
+ // Print the last line as well
+ whom.sendRawMessage(prompt.getPromptText(event.getContext()));
+
+ try {
+ // Force early compilation
+ filter.compile(engine);
+
+ filters.add(filter);
+ whom.sendRawMessage(ChatColor.GOLD + "Added filter " + name);
+ } catch (ScriptException e) {
+ e.printStackTrace();
+ whom.sendRawMessage(ChatColor.GOLD + "Compilation error: " + e.getMessage());
+ }
+ } else {
+ // Too bad
+ whom.sendRawMessage(ChatColor.RED + "Cancelled filter.");
+ }
+ } catch (Exception e) {
+ reporter.reportDetailed(this,
+ Report.newBuilder(REPORT_CANNOT_HANDLE_CONVERSATION).error(e).callerParam(event)
+ );
+ }
+ }
+ }).
+ buildConversation((Conversable) sender).
+ begin();
+ } else {
+ sender.sendMessage(ChatColor.RED + "Only console and players are supported!");
+ }
+
+ break;
+
+ case REMOVE:
+ Filter filter = findFilter(name);
+
+ // See if it exists before we remove it
+ if (filter != null) {
+ filter.close(engine);
+ filters.remove(filter);
+ sender.sendMessage(ChatColor.GOLD + "Removed filter " + name);
+ } else {
+ sender.sendMessage(ChatColor.RED + "Unable to find a filter by the name " + name);
+ }
+ break;
+ }
+
+ return true;
+ }
+
+ private Set parseRanges(String[] args, int start) {
+ List> ranges = RangeParser.getRanges(args, 2, args.length - 1, Ranges.closed(0, 255));
+ Set flatten = new HashSet();
+
+ if (ranges.isEmpty()) {
+ // Use every packet ID
+ ranges.add(Ranges.closed(0, 255));
+ }
+
+ // Finally, flatten it all
+ for (Range range : ranges) {
+ flatten.addAll(range.asSet(DiscreteDomains.integers()));
+ }
+ return flatten;
+ }
+
+ /**
+ * Lookup a filter by its name.
+ * @param name - the filter name.
+ * @return The filter, or NULL if not found.
+ */
+ private Filter findFilter(String name) {
+ // We'll just use a linear scan for now - we don't expect that many filters
+ for (Filter filter : filters) {
+ if (filter.getName().equalsIgnoreCase(name)) {
+ return filter;
+ }
+ }
+ return null;
+ }
+
+ private SubCommand parseCommand(String[] args, int index) {
+ String text = args[index].toUpperCase();
+
+ try {
+ return SubCommand.valueOf(text);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(text + " is not a valid sub command. Must be add or remove.", e);
+ }
+ }
+}
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java
index 6f098599..72eac4c4 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java
@@ -1,533 +1,544 @@
-/*
- * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
- * Copyright (C) 2012 Kristian S. Stangeland
- *
- * This program is free software; you can redistribute it and/or modify it under the terms of the
- * GNU General Public License as published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
- * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
- * See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with this program;
- * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
- * 02111-1307 USA
- */
-
-package com.comphenix.protocol;
-
-import java.lang.reflect.InvocationTargetException;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.WeakHashMap;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import net.sf.cglib.proxy.Factory;
-
-import org.bukkit.ChatColor;
-import org.bukkit.command.CommandSender;
-import org.bukkit.entity.Player;
-import org.bukkit.plugin.Plugin;
-
-import com.comphenix.protocol.concurrency.AbstractIntervalTree;
-import com.comphenix.protocol.error.ErrorReporter;
-import com.comphenix.protocol.events.ConnectionSide;
-import com.comphenix.protocol.events.ListenerPriority;
-import com.comphenix.protocol.events.ListeningWhitelist;
-import com.comphenix.protocol.events.PacketEvent;
-import com.comphenix.protocol.events.PacketListener;
-import com.comphenix.protocol.injector.GamePhase;
-import com.comphenix.protocol.reflect.FieldAccessException;
-import com.comphenix.protocol.reflect.PrettyPrinter;
-import com.comphenix.protocol.utility.ChatExtensions;
-import com.comphenix.protocol.utility.MinecraftReflection;
-import com.google.common.collect.DiscreteDomains;
-import com.google.common.collect.Range;
-import com.google.common.collect.Ranges;
-import com.google.common.collect.Sets;
-
-/**
- * Handles the "packet" debug command.
- *
- * @author Kristian
- */
-class CommandPacket extends CommandBase {
-
- private interface DetailedPacketListener extends PacketListener {
- /**
- * Determine whether or not the given packet listener is detailed or not.
- * @return TRUE if it is detailed, FALSE otherwise.
- */
- public boolean isDetailed();
- }
-
- private enum SubCommand {
- ADD, REMOVE, NAMES, PAGE;
- }
-
- /**
- * Name of this command.
- */
- public static final String NAME = "packet";
-
- /**
- * Number of lines per page.
- */
- public static final int PAGE_LINE_COUNT = 9;
-
- private Plugin plugin;
- private Logger logger;
- private ProtocolManager manager;
-
- private ChatExtensions chatter;
-
- // Paged message
- private Map> pagedMessage = new WeakHashMap>();
-
- // Registered packet listeners
- private AbstractIntervalTree clientListeners = createTree(ConnectionSide.CLIENT_SIDE);
- private AbstractIntervalTree serverListeners = createTree(ConnectionSide.SERVER_SIDE);
-
- public CommandPacket(ErrorReporter reporter, Plugin plugin, Logger logger, ProtocolManager manager) {
- super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 1);
- this.plugin = plugin;
- this.logger = logger;
- 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, permission);
- }
- }
-
- private void printPage(CommandSender sender, int pageIndex) {
- List paged = pagedMessage.get(sender);
-
- // Make sure the player has any pages
- if (paged != null) {
- int lastPage = ((paged.size() - 1) / PAGE_LINE_COUNT) + 1;
-
- for (int i = PAGE_LINE_COUNT * (pageIndex - 1); i < PAGE_LINE_COUNT * pageIndex; i++) {
- if (i < paged.size()) {
- sendMessageSilently(sender, " " + paged.get(i));
- }
- }
-
- // More data?
- if (pageIndex < lastPage) {
- sendMessageSilently(sender, "Send /packet page " + (pageIndex + 1) + " for the next page.");
- }
-
- } else {
- sendMessageSilently(sender, ChatColor.RED + "No pages found.");
- }
- }
-
- /*
- * Description: Adds or removes a simple packet listener.
- Usage: / add|remove client|server|both [ID start] [ID stop] [detailed]
- */
- @Override
- protected boolean handleCommand(CommandSender sender, String[] args) {
- try {
- SubCommand subCommand = parseCommand(args, 0);
-
- // Commands with different parameters
- if (subCommand == SubCommand.PAGE) {
- int page = Integer.parseInt(args[1]);
-
- if (page > 0)
- printPage(sender, page);
- else
- sendMessageSilently(sender, ChatColor.RED + "Page index must be greater than zero.");
- return true;
- }
-
- ConnectionSide side = parseSide(args, 1, ConnectionSide.BOTH);
-
- Integer lastIndex = args.length - 1;
- Boolean detailed = parseBoolean(args, "detailed", lastIndex);
-
- // See if the last element is a boolean
- if (detailed == null) {
- detailed = false;
- } else {
- lastIndex--;
- }
-
- // Make sure the packet IDs are valid
- List> ranges = RangeParser.getRanges(args, 2, lastIndex, Ranges.closed(0, 255));
-
- if (ranges.isEmpty()) {
- // Use every packet ID
- ranges.add(Ranges.closed(0, 255));
- }
-
- // Perform commands
- if (subCommand == SubCommand.ADD) {
- // The add command is dangerous - don't default on the connection side
- if (args.length == 1) {
- sender.sendMessage(ChatColor.RED + "Please specify a connectionn side.");
- return false;
- }
-
- executeAddCommand(sender, side, detailed, ranges);
- } else if (subCommand == SubCommand.REMOVE) {
- executeRemoveCommand(sender, side, detailed, ranges);
- } else if (subCommand == SubCommand.NAMES) {
- executeNamesCommand(sender, side, ranges);
- }
-
- } catch (NumberFormatException e) {
- sendMessageSilently(sender, ChatColor.RED + "Cannot parse number: " + e.getMessage());
- } catch (IllegalArgumentException e) {
- sendMessageSilently(sender, ChatColor.RED + e.getMessage());
- }
-
- return true;
- }
-
- private void executeAddCommand(CommandSender sender, ConnectionSide side, Boolean detailed, List> ranges) {
- for (Range range : ranges) {
- DetailedPacketListener listener = addPacketListeners(side, range.lowerEndpoint(), range.upperEndpoint(), detailed);
- sendMessageSilently(sender, ChatColor.BLUE + "Added listener " + getWhitelistInfo(listener));
- }
- }
-
- private void executeRemoveCommand(CommandSender sender, ConnectionSide side, Boolean detailed, List> ranges) {
- int count = 0;
-
- // Remove each packet listener
- for (Range range : ranges) {
- count += removePacketListeners(side, range.lowerEndpoint(), range.upperEndpoint(), detailed).size();
- }
-
- sendMessageSilently(sender, ChatColor.BLUE + "Fully removed " + count + " listeners.");
- }
-
- private void executeNamesCommand(CommandSender sender, ConnectionSide side, List> ranges) {
- Set named = getNamedPackets(side);
- List messages = new ArrayList();
-
- // Print the equivalent name of every given ID
- for (Range range : ranges) {
- for (int id : range.asSet(DiscreteDomains.integers())) {
- if (named.contains(id)) {
- messages.add(ChatColor.WHITE + "" + id + ": " + ChatColor.BLUE + Packets.getDeclaredName(id));
- }
- }
- }
-
- if (sender instanceof Player && messages.size() > 0 && messages.size() > PAGE_LINE_COUNT) {
- // Divide the messages into chuncks
- pagedMessage.put(sender, messages);
- printPage(sender, 1);
-
- } else {
- // Just print the damn thing
- for (String message : messages) {
- sendMessageSilently(sender, message);
- }
- }
- }
-
- /**
- * Retrieve whitelist information about a given listener.
- * @param listener - the given listener.
- * @return Whitelist information.
- */
- private String getWhitelistInfo(PacketListener listener) {
- boolean sendingEmpty = ListeningWhitelist.isEmpty(listener.getSendingWhitelist());
- boolean receivingEmpty = ListeningWhitelist.isEmpty(listener.getReceivingWhitelist());
-
- if (!sendingEmpty && !receivingEmpty)
- return String.format("Sending: %s, Receiving: %s", listener.getSendingWhitelist(), listener.getReceivingWhitelist());
- else if (!sendingEmpty)
- return listener.getSendingWhitelist().toString();
- else if (!receivingEmpty)
- return listener.getReceivingWhitelist().toString();
- else
- return "[None]";
- }
-
- private Set getValidPackets(ConnectionSide side) throws FieldAccessException {
- HashSet supported = Sets.newHashSet();
-
- if (side.isForClient())
- supported.addAll(Packets.Client.getSupported());
- else if (side.isForServer())
- supported.addAll(Packets.Server.getSupported());
- return supported;
- }
-
- private Set getNamedPackets(ConnectionSide side) {
-
- Set valids = null;
- Set result = Sets.newHashSet();
-
- try {
- valids = getValidPackets(side);
- } catch (FieldAccessException e) {
- valids = Ranges.closed(0, 255).asSet(DiscreteDomains.integers());
- }
-
- // Check connection side
- if (side.isForClient())
- result.addAll(Packets.Client.getRegistry().values());
- if (side.isForServer())
- result.addAll(Packets.Server.getRegistry().values());
-
- // Remove invalid packets
- result.retainAll(valids);
- return result;
- }
-
- public DetailedPacketListener createPacketListener(final ConnectionSide side, int idStart, int idStop, final boolean detailed) {
-
- Set range = Ranges.closed(idStart, idStop).asSet(DiscreteDomains.integers());
- Set packets;
-
- try {
- // Only use supported packet IDs
- packets = new HashSet(getValidPackets(side));
- packets.retainAll(range);
-
- } catch (FieldAccessException e) {
- // Don't filter anything then
- packets = range;
- }
-
- // Ignore empty sets
- if (packets.isEmpty())
- return null;
-
- // Create the listener we will be using
- final ListeningWhitelist whitelist = new ListeningWhitelist(ListenerPriority.MONITOR, packets, GamePhase.BOTH);
-
- return new DetailedPacketListener() {
- @Override
- public void onPacketSending(PacketEvent event) {
- if (side.isForServer()) {
- printInformation(event);
- }
- }
-
- @Override
- public void onPacketReceiving(PacketEvent event) {
- if (side.isForClient()) {
- printInformation(event);
- }
- }
-
- private void printInformation(PacketEvent event) {
- String format = side.isForClient() ?
- "Received %s (%s) from %s" :
- "Sent %s (%s) to %s";
- String shortDescription = String.format(format,
- Packets.getDeclaredName(event.getPacketID()),
- event.getPacketID(),
- event.getPlayer().getName()
- );
-
- // Detailed will print the packet's content too
- if (detailed) {
- try {
- Object packet = event.getPacket().getHandle();
- Class> clazz = packet.getClass();
-
- // Get the first Minecraft super class
- while ((!MinecraftReflection.isMinecraftClass(clazz) ||
- Factory.class.isAssignableFrom(clazz)) && clazz != Object.class) {
- clazz = clazz.getSuperclass();
- }
-
- logger.info(shortDescription + ":\n" +
- PrettyPrinter.printObject(packet, clazz, MinecraftReflection.getPacketClass())
- );
-
- } catch (IllegalAccessException e) {
- logger.log(Level.WARNING, "Unable to use reflection.", e);
- }
- } else {
- logger.info(shortDescription + ".");
- }
- }
-
- @Override
- public ListeningWhitelist getSendingWhitelist() {
- return side.isForServer() ? whitelist : ListeningWhitelist.EMPTY_WHITELIST;
- }
-
- @Override
- public ListeningWhitelist getReceivingWhitelist() {
- return side.isForClient() ? whitelist : ListeningWhitelist.EMPTY_WHITELIST;
- }
-
- @Override
- public Plugin getPlugin() {
- return plugin;
- }
-
- @Override
- public boolean isDetailed() {
- return detailed;
- }
- };
- }
-
- public DetailedPacketListener addPacketListeners(ConnectionSide side, int idStart, int idStop, boolean detailed) {
- DetailedPacketListener listener = createPacketListener(side, idStart, idStop, detailed);
-
- // The trees will manage the listeners for us
- if (listener != null) {
- if (side.isForClient())
- clientListeners.put(idStart, idStop, listener);
- if (side.isForServer())
- serverListeners.put(idStart, idStop, listener);
- return listener;
- } else {
- throw new IllegalArgumentException("No packets found in the range " + idStart + " - " + idStop + ".");
- }
- }
-
- public Set.Entry> removePacketListeners(
- ConnectionSide side, int idStart, int idStop, boolean detailed) {
-
- HashSet.Entry> result = Sets.newHashSet();
-
- // The interval tree will automatically remove the listeners for us
- if (side.isForClient())
- result.addAll(clientListeners.remove(idStart, idStop, true));
- if (side.isForServer())
- result.addAll(serverListeners.remove(idStart, idStop, true));
- return result;
- }
-
- private SubCommand parseCommand(String[] args, int index) {
- String text = args[index].toLowerCase();
-
- // Parse this too
- if ("add".startsWith(text))
- return SubCommand.ADD;
- else if ("remove".startsWith(text))
- return SubCommand.REMOVE;
- else if ("names".startsWith(text))
- return SubCommand.NAMES;
- else if ("page".startsWith(text))
- return SubCommand.PAGE;
- else
- throw new IllegalArgumentException(text + " is not a valid sub command. Must be add or remove.");
- }
-
- private ConnectionSide parseSide(String[] args, int index, ConnectionSide defaultValue) {
- if (index < args.length) {
- String text = args[index].toLowerCase();
-
- // Parse the side gracefully
- if ("client".startsWith(text))
- return ConnectionSide.CLIENT_SIDE;
- else if ("server".startsWith(text))
- return ConnectionSide.SERVER_SIDE;
- else
- throw new IllegalArgumentException(text + " is not a connection side.");
-
- } else {
- return defaultValue;
- }
- }
-
- // Parse a boolean
- private Boolean parseBoolean(String[] args, String parameterName, int index) {
- if (index < args.length) {
- if (args[index].equalsIgnoreCase("true"))
- return true;
- else if (args[index].equalsIgnoreCase(parameterName))
- return true;
- else if (args[index].equalsIgnoreCase("false"))
- return false;
- else
- return null;
- } else {
- return null;
- }
- }
-}
+/*
+ * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
+ * Copyright (C) 2012 Kristian S. Stangeland
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program;
+ * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+ * 02111-1307 USA
+ */
+
+package com.comphenix.protocol;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.sf.cglib.proxy.Factory;
+
+import org.bukkit.ChatColor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.Plugin;
+
+import com.comphenix.protocol.concurrency.AbstractIntervalTree;
+import com.comphenix.protocol.error.ErrorReporter;
+import com.comphenix.protocol.error.Report;
+import com.comphenix.protocol.error.ReportType;
+import com.comphenix.protocol.events.ConnectionSide;
+import com.comphenix.protocol.events.ListenerPriority;
+import com.comphenix.protocol.events.ListeningWhitelist;
+import com.comphenix.protocol.events.PacketEvent;
+import com.comphenix.protocol.events.PacketListener;
+import com.comphenix.protocol.injector.GamePhase;
+import com.comphenix.protocol.reflect.FieldAccessException;
+import com.comphenix.protocol.reflect.PrettyPrinter;
+import com.comphenix.protocol.utility.ChatExtensions;
+import com.comphenix.protocol.utility.MinecraftReflection;
+import com.google.common.collect.DiscreteDomains;
+import com.google.common.collect.Range;
+import com.google.common.collect.Ranges;
+import com.google.common.collect.Sets;
+
+/**
+ * Handles the "packet" debug command.
+ *
+ * @author Kristian
+ */
+class CommandPacket extends CommandBase {
+ public static final ReportType REPORT_CANNOT_SEND_MESSAGE = new ReportType("Cannot send chat message.");
+
+ private interface DetailedPacketListener extends PacketListener {
+ /**
+ * Determine whether or not the given packet listener is detailed or not.
+ * @return TRUE if it is detailed, FALSE otherwise.
+ */
+ public boolean isDetailed();
+ }
+
+ private enum SubCommand {
+ ADD, REMOVE, NAMES, PAGE;
+ }
+
+ /**
+ * Name of this command.
+ */
+ public static final String NAME = "packet";
+
+ /**
+ * Number of lines per page.
+ */
+ public static final int PAGE_LINE_COUNT = 9;
+
+ private Plugin plugin;
+ private Logger logger;
+ private ProtocolManager manager;
+
+ private ChatExtensions chatter;
+
+ // Paged message
+ private Map> pagedMessage = new WeakHashMap>();
+
+ // Registered packet listeners
+ private AbstractIntervalTree clientListeners = createTree(ConnectionSide.CLIENT_SIDE);
+ private AbstractIntervalTree serverListeners = createTree(ConnectionSide.SERVER_SIDE);
+
+ // Filter packet events
+ private CommandFilter filter;
+
+ public CommandPacket(ErrorReporter reporter, Plugin plugin, Logger logger, CommandFilter filter, ProtocolManager manager) {
+ super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 1);
+ this.plugin = plugin;
+ this.logger = logger;
+ this.manager = manager;
+ this.filter = filter;
+ this.chatter = new ChatExtensions(manager);
+ }
+
+ /**
+ * Construct a packet listener interval tree.
+ * @return Construct the tree.
+ */
+ private AbstractIntervalTree createTree(final ConnectionSide side) {
+ return new AbstractIntervalTree() {
+ @Override
+ protected Integer decrementKey(Integer key) {
+ return key != null ? key - 1 : null;
+ }
+
+ @Override
+ protected Integer incrementKey(Integer key) {
+ return key != null ? key + 1 : null;
+ }
+
+ @Override
+ protected void onEntryAdded(Entry added) {
+ // Ensure that the starting ID and the ending ID is correct
+ // This is necessary because the interval tree may change the range.
+ if (added != null) {
+ Range key = added.getKey();
+ DetailedPacketListener listener = added.getValue();
+ DetailedPacketListener corrected = createPacketListener(
+ side, key.lowerEndpoint(), key.upperEndpoint(), listener.isDetailed());
+
+ added.setValue(corrected);
+
+ if (corrected != null) {
+ manager.addPacketListener(corrected);
+ } else {
+ // Never mind
+ remove(key.lowerEndpoint(), key.upperEndpoint());
+ }
+ }
+ }
+
+ @Override
+ protected void onEntryRemoved(Entry removed) {
+ // Remove the listener
+ if (removed != null) {
+ DetailedPacketListener listener = removed.getValue();
+
+ if (listener != null) {
+ manager.removePacketListener(listener);
+ }
+ }
+ }
+ };
+ }
+
+ /**
+ * Send a message without invoking the packet listeners.
+ * @param receiver - the player to send it to.
+ * @param message - the message to send.
+ * @return TRUE if the message was sent successfully, FALSE otherwise.
+ */
+ public void sendMessageSilently(CommandSender receiver, String message) {
+ try {
+ chatter.sendMessageSilently(receiver, message);
+ } catch (InvocationTargetException e) {
+ reporter.reportDetailed(this,
+ Report.newBuilder(REPORT_CANNOT_SEND_MESSAGE).error(e).callerParam(receiver, message)
+ );
+ }
+ }
+
+ /**
+ * Broadcast a message without invoking any packet listeners.
+ * @param message - message to send.
+ * @param permission - permission required to receieve the message. NULL to target everyone.
+ */
+ public void broadcastMessageSilently(String message, String permission) {
+ try {
+ chatter.broadcastMessageSilently(message, permission);
+ } catch (InvocationTargetException e) {
+ reporter.reportDetailed(this,
+ Report.newBuilder(REPORT_CANNOT_SEND_MESSAGE).error(e).callerParam(message, permission)
+ );
+ }
+ }
+
+ private void printPage(CommandSender sender, int pageIndex) {
+ List paged = pagedMessage.get(sender);
+
+ // Make sure the player has any pages
+ if (paged != null) {
+ int lastPage = ((paged.size() - 1) / PAGE_LINE_COUNT) + 1;
+
+ for (int i = PAGE_LINE_COUNT * (pageIndex - 1); i < PAGE_LINE_COUNT * pageIndex; i++) {
+ if (i < paged.size()) {
+ sendMessageSilently(sender, " " + paged.get(i));
+ }
+ }
+
+ // More data?
+ if (pageIndex < lastPage) {
+ sendMessageSilently(sender, "Send /packet page " + (pageIndex + 1) + " for the next page.");
+ }
+
+ } else {
+ sendMessageSilently(sender, ChatColor.RED + "No pages found.");
+ }
+ }
+
+ /*
+ * Description: Adds or removes a simple packet listener.
+ Usage: / add|remove client|server|both [ID start] [ID stop] [detailed]
+ */
+ @Override
+ protected boolean handleCommand(CommandSender sender, String[] args) {
+ try {
+ SubCommand subCommand = parseCommand(args, 0);
+
+ // Commands with different parameters
+ if (subCommand == SubCommand.PAGE) {
+ int page = Integer.parseInt(args[1]);
+
+ if (page > 0)
+ printPage(sender, page);
+ else
+ sendMessageSilently(sender, ChatColor.RED + "Page index must be greater than zero.");
+ return true;
+ }
+
+ ConnectionSide side = parseSide(args, 1, ConnectionSide.BOTH);
+
+ Integer lastIndex = args.length - 1;
+ Boolean detailed = parseBoolean(args, "detailed", lastIndex);
+
+ // See if the last element is a boolean
+ if (detailed == null) {
+ detailed = false;
+ } else {
+ lastIndex--;
+ }
+
+ // Make sure the packet IDs are valid
+ List> ranges = RangeParser.getRanges(args, 2, lastIndex, Ranges.closed(0, 255));
+
+ if (ranges.isEmpty()) {
+ // Use every packet ID
+ ranges.add(Ranges.closed(0, 255));
+ }
+
+ // Perform commands
+ if (subCommand == SubCommand.ADD) {
+ // The add command is dangerous - don't default on the connection side
+ if (args.length == 1) {
+ sender.sendMessage(ChatColor.RED + "Please specify a connectionn side.");
+ return false;
+ }
+
+ executeAddCommand(sender, side, detailed, ranges);
+ } else if (subCommand == SubCommand.REMOVE) {
+ executeRemoveCommand(sender, side, detailed, ranges);
+ } else if (subCommand == SubCommand.NAMES) {
+ executeNamesCommand(sender, side, ranges);
+ }
+
+ } catch (NumberFormatException e) {
+ sendMessageSilently(sender, ChatColor.RED + "Cannot parse number: " + e.getMessage());
+ } catch (IllegalArgumentException e) {
+ sendMessageSilently(sender, ChatColor.RED + e.getMessage());
+ }
+
+ return true;
+ }
+
+ private void executeAddCommand(CommandSender sender, ConnectionSide side, Boolean detailed, List> ranges) {
+ for (Range range : ranges) {
+ DetailedPacketListener listener = addPacketListeners(side, range.lowerEndpoint(), range.upperEndpoint(), detailed);
+ sendMessageSilently(sender, ChatColor.BLUE + "Added listener " + getWhitelistInfo(listener));
+ }
+ }
+
+ private void executeRemoveCommand(CommandSender sender, ConnectionSide side, Boolean detailed, List> ranges) {
+ int count = 0;
+
+ // Remove each packet listener
+ for (Range range : ranges) {
+ count += removePacketListeners(side, range.lowerEndpoint(), range.upperEndpoint(), detailed).size();
+ }
+
+ sendMessageSilently(sender, ChatColor.BLUE + "Fully removed " + count + " listeners.");
+ }
+
+ private void executeNamesCommand(CommandSender sender, ConnectionSide side, List> ranges) {
+ Set named = getNamedPackets(side);
+ List messages = new ArrayList();
+
+ // Print the equivalent name of every given ID
+ for (Range range : ranges) {
+ for (int id : range.asSet(DiscreteDomains.integers())) {
+ if (named.contains(id)) {
+ messages.add(ChatColor.WHITE + "" + id + ": " + ChatColor.BLUE + Packets.getDeclaredName(id));
+ }
+ }
+ }
+
+ if (sender instanceof Player && messages.size() > 0 && messages.size() > PAGE_LINE_COUNT) {
+ // Divide the messages into chuncks
+ pagedMessage.put(sender, messages);
+ printPage(sender, 1);
+
+ } else {
+ // Just print the damn thing
+ for (String message : messages) {
+ sendMessageSilently(sender, message);
+ }
+ }
+ }
+
+ /**
+ * Retrieve whitelist information about a given listener.
+ * @param listener - the given listener.
+ * @return Whitelist information.
+ */
+ private String getWhitelistInfo(PacketListener listener) {
+ boolean sendingEmpty = ListeningWhitelist.isEmpty(listener.getSendingWhitelist());
+ boolean receivingEmpty = ListeningWhitelist.isEmpty(listener.getReceivingWhitelist());
+
+ if (!sendingEmpty && !receivingEmpty)
+ return String.format("Sending: %s, Receiving: %s", listener.getSendingWhitelist(), listener.getReceivingWhitelist());
+ else if (!sendingEmpty)
+ return listener.getSendingWhitelist().toString();
+ else if (!receivingEmpty)
+ return listener.getReceivingWhitelist().toString();
+ else
+ return "[None]";
+ }
+
+ private Set getValidPackets(ConnectionSide side) throws FieldAccessException {
+ HashSet supported = Sets.newHashSet();
+
+ if (side.isForClient())
+ supported.addAll(Packets.Client.getSupported());
+ else if (side.isForServer())
+ supported.addAll(Packets.Server.getSupported());
+
+ return supported;
+ }
+
+ private Set getNamedPackets(ConnectionSide side) {
+
+ Set valids = null;
+ Set result = Sets.newHashSet();
+
+ try {
+ valids = getValidPackets(side);
+ } catch (FieldAccessException e) {
+ valids = Ranges.closed(0, 255).asSet(DiscreteDomains.integers());
+ }
+
+ // Check connection side
+ if (side.isForClient())
+ result.addAll(Packets.Client.getRegistry().values());
+ if (side.isForServer())
+ result.addAll(Packets.Server.getRegistry().values());
+
+ // Remove invalid packets
+ result.retainAll(valids);
+ return result;
+ }
+
+ public DetailedPacketListener createPacketListener(final ConnectionSide side, int idStart, int idStop, final boolean detailed) {
+ Set range = Ranges.closed(idStart, idStop).asSet(DiscreteDomains.integers());
+ Set packets;
+
+ try {
+ // Only use supported packet IDs
+ packets = new HashSet(getValidPackets(side));
+ packets.retainAll(range);
+
+ } catch (FieldAccessException e) {
+ // Don't filter anything then
+ packets = range;
+ }
+
+ // Ignore empty sets
+ if (packets.isEmpty())
+ return null;
+
+ // Create the listener we will be using
+ final ListeningWhitelist whitelist = new ListeningWhitelist(ListenerPriority.MONITOR, packets, GamePhase.BOTH);
+
+ return new DetailedPacketListener() {
+ @Override
+ public void onPacketSending(PacketEvent event) {
+ if (side.isForServer() && filter.filterEvent(event)) {
+ printInformation(event);
+ }
+ }
+
+ @Override
+ public void onPacketReceiving(PacketEvent event) {
+ if (side.isForClient() && filter.filterEvent(event)) {
+ printInformation(event);
+ }
+ }
+
+ private void printInformation(PacketEvent event) {
+ String format = side.isForClient() ?
+ "Received %s (%s) from %s" :
+ "Sent %s (%s) to %s";
+ String shortDescription = String.format(format,
+ Packets.getDeclaredName(event.getPacketID()),
+ event.getPacketID(),
+ event.getPlayer().getName()
+ );
+
+ // Detailed will print the packet's content too
+ if (detailed) {
+ try {
+ Object packet = event.getPacket().getHandle();
+ Class> clazz = packet.getClass();
+
+ // Get the first Minecraft super class
+ while ((!MinecraftReflection.isMinecraftClass(clazz) ||
+ Factory.class.isAssignableFrom(clazz)) && clazz != Object.class) {
+ clazz = clazz.getSuperclass();
+ }
+
+ logger.info(shortDescription + ":\n" +
+ PrettyPrinter.printObject(packet, clazz, MinecraftReflection.getPacketClass())
+ );
+
+ } catch (IllegalAccessException e) {
+ logger.log(Level.WARNING, "Unable to use reflection.", e);
+ }
+ } else {
+ logger.info(shortDescription + ".");
+ }
+ }
+
+ @Override
+ public ListeningWhitelist getSendingWhitelist() {
+ return side.isForServer() ? whitelist : ListeningWhitelist.EMPTY_WHITELIST;
+ }
+
+ @Override
+ public ListeningWhitelist getReceivingWhitelist() {
+ return side.isForClient() ? whitelist : ListeningWhitelist.EMPTY_WHITELIST;
+ }
+
+ @Override
+ public Plugin getPlugin() {
+ return plugin;
+ }
+
+ @Override
+ public boolean isDetailed() {
+ return detailed;
+ }
+ };
+ }
+
+ public DetailedPacketListener addPacketListeners(ConnectionSide side, int idStart, int idStop, boolean detailed) {
+ DetailedPacketListener listener = createPacketListener(side, idStart, idStop, detailed);
+
+ // The trees will manage the listeners for us
+ if (listener != null) {
+ if (side.isForClient())
+ clientListeners.put(idStart, idStop, listener);
+ if (side.isForServer())
+ serverListeners.put(idStart, idStop, listener);
+ return listener;
+ } else {
+ throw new IllegalArgumentException("No packets found in the range " + idStart + " - " + idStop + ".");
+ }
+ }
+
+ public Set.Entry> removePacketListeners(
+ ConnectionSide side, int idStart, int idStop, boolean detailed) {
+
+ HashSet.Entry> result = Sets.newHashSet();
+
+ // The interval tree will automatically remove the listeners for us
+ if (side.isForClient())
+ result.addAll(clientListeners.remove(idStart, idStop, true));
+ if (side.isForServer())
+ result.addAll(serverListeners.remove(idStart, idStop, true));
+ return result;
+ }
+
+ private SubCommand parseCommand(String[] args, int index) {
+ String text = args[index].toLowerCase();
+
+ // Parse this too
+ if ("add".startsWith(text))
+ return SubCommand.ADD;
+ else if ("remove".startsWith(text))
+ return SubCommand.REMOVE;
+ else if ("names".startsWith(text))
+ return SubCommand.NAMES;
+ else if ("page".startsWith(text))
+ return SubCommand.PAGE;
+ else
+ throw new IllegalArgumentException(text + " is not a valid sub command. Must be add or remove.");
+ }
+
+ private ConnectionSide parseSide(String[] args, int index, ConnectionSide defaultValue) {
+ if (index < args.length) {
+ String text = args[index].toLowerCase();
+
+ // Parse the side gracefully
+ if ("client".startsWith(text))
+ return ConnectionSide.CLIENT_SIDE;
+ else if ("server".startsWith(text))
+ return ConnectionSide.SERVER_SIDE;
+ else
+ throw new IllegalArgumentException(text + " is not a connection side.");
+
+ } else {
+ return defaultValue;
+ }
+ }
+
+ // Parse a boolean
+ private Boolean parseBoolean(String[] args, String parameterName, int index) {
+ if (index < args.length) {
+ if (args[index].equalsIgnoreCase("true"))
+ return true;
+ else if (args[index].equalsIgnoreCase(parameterName))
+ return true;
+ else if (args[index].equalsIgnoreCase("false"))
+ return false;
+ else
+ return null;
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandProtocol.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandProtocol.java
index 672c4127..4509a504 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandProtocol.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandProtocol.java
@@ -1,137 +1,147 @@
-/*
- * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
- * Copyright (C) 2012 Kristian S. Stangeland
- *
- * This program is free software; you can redistribute it and/or modify it under the terms of the
- * GNU General Public License as published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
- * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
- * See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with this program;
- * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
- * 02111-1307 USA
- */
-
-package com.comphenix.protocol;
-
-import java.io.IOException;
-
-import org.bukkit.ChatColor;
-import org.bukkit.command.CommandSender;
-import org.bukkit.plugin.Plugin;
-
-import com.comphenix.protocol.error.ErrorReporter;
-import com.comphenix.protocol.metrics.Updater;
-import com.comphenix.protocol.metrics.Updater.UpdateResult;
-import com.comphenix.protocol.metrics.Updater.UpdateType;
-import com.comphenix.protocol.utility.WrappedScheduler;
-
-/**
- * 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(ErrorReporter reporter, Plugin plugin, Updater updater, ProtocolConfig config) {
- super(reporter, 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
- WrappedScheduler.runAsynchronouslyOnce(plugin, new Runnable() {
- @Override
- public void run() {
- try {
- UpdateResult result = updater.update(UpdateType.NO_DOWNLOAD, true);
- sender.sendMessage(ChatColor.BLUE + "[ProtocolLib] " + result.toString());
- } catch (Exception e) {
- if (isHttpError(e)) {
- getReporter().reportWarning(this, "Http error: " + e.getCause().getMessage());
- } else {
- getReporter().reportDetailed(this, "Cannot check updates for ProtocolLib.", e, sender);
- }
- }
- }
- }, 0L);
-
- updateFinished();
- }
-
- public void updateVersion(final CommandSender sender) {
- // Perform on an async thread
- WrappedScheduler.runAsynchronouslyOnce(plugin, new Runnable() {
- @Override
- public void run() {
- try {
- UpdateResult result = updater.update(UpdateType.DEFAULT, true);
- sender.sendMessage(ChatColor.BLUE + "[ProtocolLib] " + result.toString());
- } catch (Exception e) {
- if (isHttpError(e)) {
- getReporter().reportWarning(this, "Http error: " + e.getCause().getMessage());
- } else {
- getReporter().reportDetailed(this, "Cannot update ProtocolLib.", e, sender);
- }
- }
- }
- }, 0L);
-
- updateFinished();
- }
-
- private boolean isHttpError(Exception e) {
- Throwable cause = e.getCause();
-
- if (cause instanceof IOException) {
- // Thanks for making the message a part of the API ...
- return cause.getMessage().contains("HTTP response");
- } else {
- return false;
- }
- }
-
- /**
- * 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!");
- }
-}
+/*
+ * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
+ * Copyright (C) 2012 Kristian S. Stangeland
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program;
+ * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+ * 02111-1307 USA
+ */
+
+package com.comphenix.protocol;
+
+import java.io.IOException;
+
+import org.bukkit.ChatColor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.plugin.Plugin;
+
+import com.comphenix.protocol.error.ErrorReporter;
+import com.comphenix.protocol.error.Report;
+import com.comphenix.protocol.error.ReportType;
+import com.comphenix.protocol.metrics.Updater;
+import com.comphenix.protocol.metrics.Updater.UpdateResult;
+import com.comphenix.protocol.metrics.Updater.UpdateType;
+import com.comphenix.protocol.utility.WrappedScheduler;
+
+/**
+ * Handles the "protocol" administration command.
+ *
+ * @author Kristian
+ */
+class CommandProtocol extends CommandBase {
+ /**
+ * Name of this command.
+ */
+ public static final String NAME = "protocol";
+
+ public static final ReportType REPORT_HTTP_ERROR = new ReportType("Http error: %s");
+ public static final ReportType REPORT_CANNOT_CHECK_FOR_UPDATES = new ReportType("Cannot check updates for ProtocolLib.");
+ public static final ReportType REPORT_CANNOT_UPDATE_PLUGIN = new ReportType("Cannot update ProtocolLib.");
+
+ private Plugin plugin;
+ private Updater updater;
+ private ProtocolConfig config;
+
+ public CommandProtocol(ErrorReporter reporter, Plugin plugin, Updater updater, ProtocolConfig config) {
+ super(reporter, 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
+ WrappedScheduler.runAsynchronouslyOnce(plugin, new Runnable() {
+ @Override
+ public void run() {
+ try {
+ UpdateResult result = updater.update(UpdateType.NO_DOWNLOAD, true);
+ sender.sendMessage(ChatColor.BLUE + "[ProtocolLib] " + result.toString());
+ } catch (Exception e) {
+ if (isHttpError(e)) {
+ getReporter().reportWarning(this,
+ Report.newBuilder(REPORT_HTTP_ERROR).messageParam(e.getCause().getMessage())
+ );
+ } else {
+ getReporter().reportDetailed(this, Report.newBuilder(REPORT_CANNOT_CHECK_FOR_UPDATES).error(e).callerParam(sender));
+ }
+ }
+ }
+ }, 0L);
+
+ updateFinished();
+ }
+
+ public void updateVersion(final CommandSender sender) {
+ // Perform on an async thread
+ WrappedScheduler.runAsynchronouslyOnce(plugin, new Runnable() {
+ @Override
+ public void run() {
+ try {
+ UpdateResult result = updater.update(UpdateType.DEFAULT, true);
+ sender.sendMessage(ChatColor.BLUE + "[ProtocolLib] " + result.toString());
+ } catch (Exception e) {
+ if (isHttpError(e)) {
+ getReporter().reportWarning(this,
+ Report.newBuilder(REPORT_HTTP_ERROR).messageParam(e.getCause().getMessage())
+ );
+ } else {
+ getReporter().reportDetailed(this,Report.newBuilder(REPORT_CANNOT_UPDATE_PLUGIN).error(e).callerParam(sender));
+ }
+ }
+ }
+ }, 0L);
+
+ updateFinished();
+ }
+
+ private boolean isHttpError(Exception e) {
+ Throwable cause = e.getCause();
+
+ if (cause instanceof IOException) {
+ // Thanks for making the message a part of the API ...
+ return cause.getMessage().contains("HTTP response");
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * 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/MultipleLinesPrompt.java b/ProtocolLib/src/main/java/com/comphenix/protocol/MultipleLinesPrompt.java
new file mode 100644
index 00000000..53f1229e
--- /dev/null
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/MultipleLinesPrompt.java
@@ -0,0 +1,163 @@
+package com.comphenix.protocol;
+
+import org.bukkit.conversations.Conversation;
+import org.bukkit.conversations.ConversationCanceller;
+import org.bukkit.conversations.ConversationContext;
+import org.bukkit.conversations.ExactMatchConversationCanceller;
+import org.bukkit.conversations.Prompt;
+import org.bukkit.conversations.StringPrompt;
+
+/**
+ * Represents a conversation prompt that accepts a list of lines.
+ *
+ * @author Kristian
+ */
+class MultipleLinesPrompt extends StringPrompt {
+ /**
+ * Represents a canceller that determines if the multiple lines prompt is finished.
+ * @author Kristian
+ */
+ public static interface MultipleConversationCanceller extends ConversationCanceller {
+ @Override
+ public boolean cancelBasedOnInput(ConversationContext context, String currentLine);
+
+ /**
+ * Determine if the current prompt is done based on the context, last
+ * line and collected lines.
+ *
+ * @param context - current context.
+ * @param currentLine - current (last) line.
+ * @param lines - collected lines.
+ * @param lineCount - number of lines.
+ * @return TRUE if we are done, FALSE otherwise.
+ */
+ public boolean cancelBasedOnInput(ConversationContext context, String currentLine,
+ StringBuilder lines, int lineCount);
+ }
+
+ /**
+ * A wrapper class for turning a ConversationCanceller into a MultipleConversationCanceller.
+ * @author Kristian
+ */
+ private static class MultipleWrapper implements MultipleConversationCanceller {
+ private ConversationCanceller canceller;
+
+ public MultipleWrapper(ConversationCanceller canceller) {
+ this.canceller = canceller;
+ }
+
+ @Override
+ public boolean cancelBasedOnInput(ConversationContext context, String currentLine) {
+ return canceller.cancelBasedOnInput(context, currentLine);
+ }
+
+ @Override
+ public boolean cancelBasedOnInput(ConversationContext context, String currentLine,
+ StringBuilder lines, int lineCount) {
+ return cancelBasedOnInput(context, currentLine);
+ }
+
+ @Override
+ public void setConversation(Conversation conversation) {
+ canceller.setConversation(conversation);
+ }
+
+ @Override
+ public MultipleWrapper clone() {
+ return new MultipleWrapper(canceller.clone());
+ }
+ }
+
+ // Feels a bit like Android
+ private static final String KEY = "multiple_lines_prompt";
+ private static final String KEY_LAST = KEY + ".last_line";
+ private static final String KEY_LINES = KEY + ".linecount";
+
+ private final MultipleConversationCanceller endMarker;
+ private final String initialPrompt;
+
+ /**
+ * Retrieve and remove the current accumulated input.
+ *
+ * @param context
+ * - conversation context.
+ * @return The accumulated input, or NULL if not found.
+ */
+ public String removeAccumulatedInput(ConversationContext context) {
+ Object result = context.getSessionData(KEY);
+
+ if (result instanceof StringBuilder) {
+ context.setSessionData(KEY, null);
+ context.setSessionData(KEY_LINES, null);
+ return ((StringBuilder) result).toString();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Construct a multiple lines input prompt with a specific end marker.
+ *
+ * This is usually an empty string.
+ *
+ * @param endMarker - the end marker.
+ */
+ public MultipleLinesPrompt(String endMarker, String initialPrompt) {
+ this(new ExactMatchConversationCanceller(endMarker), initialPrompt);
+ }
+
+ /**
+ * Construct a multiple lines input prompt with a specific end marker implementation.
+ *
+ * Note: Use {@link #MultipleLinesPrompt(MultipleConversationCanceller, String)} if implementing a custom canceller.
+ * @param endMarker - the end marker.
+ * @param initialPrompt - the initial prompt text.
+ */
+ public MultipleLinesPrompt(ConversationCanceller endMarker, String initialPrompt) {
+ this.endMarker = new MultipleWrapper(endMarker);
+ this.initialPrompt = initialPrompt;
+ }
+
+ /**
+ * Construct a multiple lines input prompt with a specific end marker implementation.
+ * @param endMarker - the end marker.
+ * @param initialPrompt - the initial prompt text.
+ */
+ public MultipleLinesPrompt(MultipleConversationCanceller endMarker, String initialPrompt) {
+ this.endMarker = endMarker;
+ this.initialPrompt = initialPrompt;
+ }
+
+ @Override
+ public Prompt acceptInput(ConversationContext context, String in) {
+ StringBuilder result = (StringBuilder) context.getSessionData(KEY);
+ Integer count = (Integer) context.getSessionData(KEY_LINES);
+
+ // Handle first run
+ if (result == null)
+ context.setSessionData(KEY, result = new StringBuilder());
+ if (count == null)
+ count = 0;
+
+ // Save the last line as well
+ context.setSessionData(KEY_LAST, in);
+ context.setSessionData(KEY_LINES, ++count);
+ result.append(in + "\n");
+
+ // And we're done
+ if (endMarker.cancelBasedOnInput(context, in, result, count))
+ return Prompt.END_OF_CONVERSATION;
+ else
+ return this;
+ }
+
+ @Override
+ public String getPromptText(ConversationContext context) {
+ Object last = context.getSessionData(KEY_LAST);
+
+ if (last instanceof String)
+ return (String) last;
+ else
+ return initialPrompt;
+ }
+}
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/PacketStream.java b/ProtocolLib/src/main/java/com/comphenix/protocol/PacketStream.java
index 6d07232c..a243452a 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/PacketStream.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/PacketStream.java
@@ -21,6 +21,7 @@ import java.lang.reflect.InvocationTargetException;
import org.bukkit.entity.Player;
+import com.comphenix.protocol.events.ListenerPriority;
import com.comphenix.protocol.events.PacketContainer;
/**
@@ -43,7 +44,7 @@ public interface PacketStream {
* Send a packet to the given player.
* @param reciever - the reciever.
* @param packet - packet to send.
- * @param filters - whether or not to invoke any packet filters.
+ * @param filters - whether or not to invoke any packet filters below {@link ListenerPriority#MONITOR}.
* @throws InvocationTargetException - if an error occured when sending the packet.
*/
public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters)
@@ -63,7 +64,7 @@ public interface PacketStream {
* Simulate recieving a certain packet from a given player.
* @param sender - the sender.
* @param packet - the packet that was sent.
- * @param filters - whether or not to invoke any packet filters.
+ * @param filters - whether or not to invoke any packet filters below {@link ListenerPriority#MONITOR}.
* @throws InvocationTargetException If the reflection machinery failed.
* @throws IllegalAccessException If the underlying method caused an error.
*/
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java b/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java
index ada35964..8b543e32 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java
@@ -114,6 +114,10 @@ public final class Packets {
public static final int PLAYER_INFO = 201;
public static final int ABILITIES = 202;
public static final int TAB_COMPLETE = 203;
+ public static final int SCOREBOARD_OBJECTIVE = 206;
+ public static final int UPDATE_SCORE = 207;
+ public static final int DISPLAY_SCOREBOARD = 208;
+ public static final int TEAMS = 209;
public static final int CUSTOM_PAYLOAD = 250;
public static final int KEY_RESPONSE = 252;
public static final int KEY_REQUEST = 253;
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java
index 87e5b523..269bcb04 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java
@@ -18,12 +18,15 @@
package com.comphenix.protocol;
import java.io.File;
+import java.io.IOException;
import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.Plugin;
import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
/**
* Represents the configuration of ProtocolLib.
@@ -31,6 +34,7 @@ import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks;
* @author Kristian
*/
class ProtocolConfig {
+ private static final String LAST_UPDATE_FILE = "lastupdate";
private static final String SECTION_GLOBAL = "global";
private static final String SECTION_AUTOUPDATER = "auto updater";
@@ -40,12 +44,14 @@ class ProtocolConfig {
private static final String IGNORE_VERSION_CHECK = "ignore version check";
private static final String BACKGROUND_COMPILER_ENABLED = "background compiler";
+ private static final String DEBUG_MODE_ENABLED = "debug";
private static final String INJECTION_METHOD = "injection method";
+ private static final String SCRIPT_ENGINE_NAME = "script engine";
+
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 = 43200;
@@ -57,6 +63,11 @@ class ProtocolConfig {
private ConfigurationSection global;
private ConfigurationSection updater;
+ // Last update time
+ private long lastUpdateTime;
+ private boolean configChanged;
+ private boolean valuesChanged;
+
public ProtocolConfig(Plugin plugin) {
this(plugin, plugin.getConfig());
}
@@ -70,10 +81,64 @@ class ProtocolConfig {
* Reload configuration file.
*/
public void reloadConfig() {
+ // Reset
+ configChanged = false;
+ valuesChanged = false;
+
this.config = plugin.getConfig();
+ this.lastUpdateTime = loadLastUpdate();
loadSections(!loadingSections);
}
+ /**
+ * Load the last update time stamp from the file system.
+ * @return Last update time stamp.
+ */
+ private long loadLastUpdate() {
+ File dataFile = getLastUpdateFile();
+
+ if (dataFile.exists()) {
+ try {
+ return Long.parseLong(Files.toString(dataFile, Charsets.UTF_8));
+ } catch (NumberFormatException e) {
+ throw new RuntimeException("Cannot parse " + dataFile + " as a number.", e);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot read " + dataFile, e);
+ }
+ } else {
+ // Default last update
+ return 0;
+ }
+ }
+
+ /**
+ * Store the given time stamp.
+ * @param value - time stamp to store.
+ */
+ private void saveLastUpdate(long value) {
+ File dataFile = getLastUpdateFile();
+
+ // The data folder must exist
+ dataFile.getParentFile().mkdirs();
+
+ if (dataFile.exists())
+ dataFile.delete();
+
+ try {
+ Files.write(Long.toString(value), dataFile, Charsets.UTF_8);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot write " + dataFile, e);
+ }
+ }
+
+ /**
+ * Retrieve the file that is used to store the update time stamp.
+ * @return File storing the update time stamp.
+ */
+ private File getLastUpdateFile() {
+ return new File(plugin.getDataFolder(), LAST_UPDATE_FILE);
+ }
+
/**
* Load data sections.
* @param copyDefaults - whether or not to copy configuration defaults.
@@ -100,6 +165,17 @@ class ProtocolConfig {
System.out.println("[ProtocolLib] Created default configuration.");
}
}
+
+ /**
+ * Set a particular configuration key value pair.
+ * @param section - the configuration root.
+ * @param path - the path to the key.
+ * @param value - the value to set.
+ */
+ private void setConfig(ConfigurationSection section, String path, Object value) {
+ configChanged = true;
+ section.set(path, value);
+ }
/**
* Retrieve a reference to the configuration file.
@@ -122,7 +198,7 @@ class ProtocolConfig {
* @param value - TRUE to do this automatically, FALSE otherwise.
*/
public void setAutoNotify(boolean value) {
- updater.set(UPDATER_NOTIFY, value);
+ setConfig(updater, UPDATER_NOTIFY, value);
}
/**
@@ -138,7 +214,25 @@ class ProtocolConfig {
* @param value - TRUE if it should. FALSE otherwise.
*/
public void setAutoDownload(boolean value) {
- updater.set(UPDATER_DOWNLAD, value);
+ setConfig(updater, UPDATER_DOWNLAD, value);
+ }
+
+ /**
+ * Determine whether or not debug mode is enabled.
+ *
+ * This grants access to the filter command.
+ * @return TRUE if it is, FALSE otherwise.
+ */
+ public boolean isDebug() {
+ return global.getBoolean(DEBUG_MODE_ENABLED, false);
+ }
+
+ /**
+ * Set whether or not debug mode is enabled.
+ * @param value - TRUE if it is enabled, FALSE otherwise.
+ */
+ public void setDebug(boolean value) {
+ setConfig(global, DEBUG_MODE_ENABLED, value);
}
/**
@@ -160,17 +254,9 @@ class ProtocolConfig {
// Silently fix the delay
if (delaySeconds < DEFAULT_UPDATER_DELAY)
delaySeconds = DEFAULT_UPDATER_DELAY;
- updater.set(UPDATER_DELAY, delaySeconds);
+ setConfig(updater, 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);
- }
-
/**
* The version of Minecraft to ignore the built-in safety feature.
* @return The version to ignore ProtocolLib's satefy.
@@ -188,7 +274,7 @@ class ProtocolConfig {
* @param ignoreVersion - the version of Minecraft where the satefy will be disabled.
*/
public void setIgnoreVersionCheck(String ignoreVersion) {
- global.set(IGNORE_VERSION_CHECK, ignoreVersion);
+ setConfig(global, IGNORE_VERSION_CHECK, ignoreVersion);
}
/**
@@ -207,7 +293,7 @@ class ProtocolConfig {
* @param enabled - whether or not metrics is enabled.
*/
public void setMetricsEnabled(boolean enabled) {
- global.set(METRICS_ENABLED, enabled);
+ setConfig(global, METRICS_ENABLED, enabled);
}
/**
@@ -226,7 +312,15 @@ class ProtocolConfig {
* @param enabled - TRUE if is enabled/running, FALSE otherwise.
*/
public void setBackgroundCompilerEnabled(boolean enabled) {
- global.set(BACKGROUND_COMPILER_ENABLED, enabled);
+ setConfig(global, BACKGROUND_COMPILER_ENABLED, enabled);
+ }
+
+ /**
+ * Retrieve the last time we updated, in seconds since 1970.01.01 00:00.
+ * @return Last update time.
+ */
+ public long getAutoLastTime() {
+ return lastUpdateTime;
}
/**
@@ -234,7 +328,26 @@ class ProtocolConfig {
* @param lastTimeSeconds - new last update time.
*/
public void setAutoLastTime(long lastTimeSeconds) {
- updater.set(UPDATER_LAST_TIME, lastTimeSeconds);
+ this.valuesChanged = true;
+ this.lastUpdateTime = lastTimeSeconds;
+ }
+
+ /**
+ * Retrieve the unique name of the script engine to use for filtering.
+ * @return Script engine to use.
+ */
+ public String getScriptEngineName() {
+ return global.getString(SCRIPT_ENGINE_NAME, "JavaScript");
+ }
+
+ /**
+ * Set the unique name of the script engine to use for filtering.
+ *
+ * This setting will take effect next time ProtocolLib is started.
+ * @param name - name of the script engine to use.
+ */
+ public void setScriptEngineName(String name) {
+ setConfig(global, SCRIPT_ENGINE_NAME, name);
}
/**
@@ -266,13 +379,20 @@ class ProtocolConfig {
* @return Injection method.
*/
public void setInjectionMethod(PlayerInjectHooks hook) {
- global.set(INJECTION_METHOD, hook.name());
+ setConfig(global, INJECTION_METHOD, hook.name());
}
/**
* Save the current configuration file.
*/
public void saveAll() {
- plugin.saveConfig();
+ if (valuesChanged)
+ saveLastUpdate(lastUpdateTime);
+ if (configChanged)
+ plugin.saveConfig();
+
+ // And we're done
+ valuesChanged = false;
+ configChanged = false;
}
}
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java
index 271c1181..b93c662a 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java
@@ -33,8 +33,11 @@ import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import com.comphenix.protocol.async.AsyncFilterManager;
+import com.comphenix.protocol.error.BasicErrorReporter;
import com.comphenix.protocol.error.DetailedErrorReporter;
import com.comphenix.protocol.error.ErrorReporter;
+import com.comphenix.protocol.error.Report;
+import com.comphenix.protocol.error.ReportType;
import com.comphenix.protocol.injector.DelayedSingleTask;
import com.comphenix.protocol.injector.PacketFilterManager;
import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks;
@@ -43,6 +46,7 @@ import com.comphenix.protocol.metrics.Updater;
import com.comphenix.protocol.metrics.Updater.UpdateResult;
import com.comphenix.protocol.reflect.compiler.BackgroundCompiler;
import com.comphenix.protocol.utility.ChatExtensions;
+import com.comphenix.protocol.utility.MinecraftVersion;
/**
* The main entry point for ProtocolLib.
@@ -50,6 +54,24 @@ import com.comphenix.protocol.utility.ChatExtensions;
* @author Kristian
*/
public class ProtocolLibrary extends JavaPlugin {
+ // Every possible error or warning report type
+ public static final ReportType REPORT_CANNOT_LOAD_CONFIG = new ReportType("Cannot load configuration");
+ public static final ReportType REPORT_CANNOT_DELETE_CONFIG = new ReportType("Cannot delete old ProtocolLib configuration.");
+ public static final ReportType REPORT_CANNOT_PARSE_INJECTION_METHOD = new ReportType("Cannot parse injection method. Using default.");
+
+ public static final ReportType REPORT_PLUGIN_LOAD_ERROR = new ReportType("Cannot load ProtocolLib.");
+ public static final ReportType REPORT_PLUGIN_ENABLE_ERROR = new ReportType("Cannot enable ProtocolLib.");
+
+ public static final ReportType REPORT_METRICS_IO_ERROR = new ReportType("Unable to enable metrics due to network problems.");
+ public static final ReportType REPORT_METRICS_GENERIC_ERROR = new ReportType("Unable to enable metrics due to network problems.");
+
+ public static final ReportType REPORT_CANNOT_PARSE_MINECRAFT_VERSION = new ReportType("Unable to retrieve current Minecraft version.");
+ public static final ReportType REPORT_CANNOT_DETECT_CONFLICTING_PLUGINS = new ReportType("Unable to detect conflicting plugin versions.");
+ public static final ReportType REPORT_CANNOT_REGISTER_COMMAND = new ReportType("Cannot register command %s: %s");
+
+ public static final ReportType REPORT_CANNOT_CREATE_TIMEOUT_TASK = new ReportType("Unable to create packet timeout task.");
+ public static final ReportType REPORT_CANNOT_UPDATE_PLUGIN = new ReportType("Cannot perform automatic updates.");
+
/**
* The minimum version ProtocolLib has been tested with.
*/
@@ -58,7 +80,7 @@ public class ProtocolLibrary extends JavaPlugin {
/**
* The maximum version ProtocolLib has been tested with,
*/
- private static final String MAXIMUM_MINECRAFT_VERSION = "1.4.7";
+ private static final String MAXIMUM_MINECRAFT_VERSION = "1.5.2";
/**
* The number of milliseconds per second.
@@ -71,7 +93,7 @@ public class ProtocolLibrary extends JavaPlugin {
private static PacketFilterManager protocolManager;
// Error reporter
- private static ErrorReporter reporter;
+ private static ErrorReporter reporter = new BasicErrorReporter();
// Metrics and statistisc
private Statistics statistisc;
@@ -102,6 +124,7 @@ public class ProtocolLibrary extends JavaPlugin {
// Commands
private CommandProtocol commandProtocol;
private CommandPacket commandPacket;
+ private CommandFilter commandFilter;
// Whether or not disable is not needed
private boolean skipDisable;
@@ -118,26 +141,34 @@ public class ProtocolLibrary extends JavaPlugin {
try {
config = new ProtocolConfig(this);
} catch (Exception e) {
- detailedReporter.reportWarning(this, "Cannot load configuration", e);
+ detailedReporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_LOAD_CONFIG).error(e));
// Load it again
if (deleteConfig()) {
config = new ProtocolConfig(this);
} else {
- reporter.reportWarning(this, "Cannot delete old ProtocolLib configuration.");
+ reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_DELETE_CONFIG));
}
}
+ // Print the state of the debug mode
+ if (config.isDebug()) {
+ logger.warning("Debug mode is enabled!");
+ }
+
try {
// Check for other versions
checkConflictingVersions();
+ // Handle unexpected Minecraft versions
+ MinecraftVersion version = verifyMinecraftVersion();
+
// Set updater
updater = new Updater(this, logger, "protocollib", getFile(), "protocol.info");
unhookTask = new DelayedSingleTask(this);
protocolManager = new PacketFilterManager(
- getClassLoader(), getServer(), unhookTask, detailedReporter);
+ getClassLoader(), getServer(), this, version, unhookTask, detailedReporter);
// Setup error reporter
detailedReporter.addGlobalParameter("manager", protocolManager);
@@ -152,18 +183,19 @@ public class ProtocolLibrary extends JavaPlugin {
protocolManager.setPlayerHook(hook);
}
} catch (IllegalArgumentException e) {
- detailedReporter.reportWarning(config, "Cannot parse injection method. Using default.", e);
+ detailedReporter.reportWarning(config, Report.newBuilder(REPORT_CANNOT_PARSE_INJECTION_METHOD).error(e));
}
// Initialize command handlers
commandProtocol = new CommandProtocol(detailedReporter, this, updater, config);
- commandPacket = new CommandPacket(detailedReporter, this, logger, protocolManager);
+ commandFilter = new CommandFilter(detailedReporter, this, config);
+ commandPacket = new CommandPacket(detailedReporter, this, logger, commandFilter, protocolManager);
// Send logging information to player listeners too
setupBroadcastUsers(PERMISSION_INFO);
} catch (Throwable e) {
- detailedReporter.reportDetailed(this, "Cannot load ProtocolLib.", e, protocolManager);
+ detailedReporter.reportDetailed(this, Report.newBuilder(REPORT_PLUGIN_LOAD_ERROR).error(e).callerParam(protocolManager));
disablePlugin();
}
}
@@ -249,12 +281,10 @@ public class ProtocolLibrary extends JavaPlugin {
logger.info("Structure compiler thread has been disabled.");
}
- // Handle unexpected Minecraft versions
- verifyMinecraftVersion();
-
// Set up command handlers
registerCommand(CommandProtocol.NAME, commandProtocol);
registerCommand(CommandPacket.NAME, commandPacket);
+ registerCommand(CommandFilter.NAME, commandFilter);
// Player login and logout events
protocolManager.registerEvents(manager, this);
@@ -264,7 +294,7 @@ public class ProtocolLibrary extends JavaPlugin {
createAsyncTask(server);
} catch (Throwable e) {
- reporter.reportDetailed(this, "Cannot enable ProtocolLib.", e);
+ reporter.reportDetailed(this, Report.newBuilder(REPORT_PLUGIN_ENABLE_ERROR).error(e));
disablePlugin();
return;
}
@@ -275,14 +305,14 @@ public class ProtocolLibrary extends JavaPlugin {
statistisc = new Statistics(this);
}
} catch (IOException e) {
- reporter.reportDetailed(this, "Unable to enable metrics.", e, statistisc);
+ reporter.reportDetailed(this, Report.newBuilder(REPORT_METRICS_IO_ERROR).error(e).callerParam(statistisc));
} catch (Throwable e) {
- reporter.reportDetailed(this, "Metrics cannot be enabled. Incompatible Bukkit version.", e, statistisc);
+ reporter.reportDetailed(this, Report.newBuilder(REPORT_METRICS_GENERIC_ERROR).error(e).callerParam(statistisc));
}
}
// Used to check Minecraft version
- private void verifyMinecraftVersion() {
+ private MinecraftVersion verifyMinecraftVersion() {
try {
MinecraftVersion minimum = new MinecraftVersion(MINIMUM_MINECRAFT_VERSION);
MinecraftVersion maximum = new MinecraftVersion(MAXIMUM_MINECRAFT_VERSION);
@@ -296,9 +326,14 @@ public class ProtocolLibrary extends JavaPlugin {
if (current.compareTo(maximum) > 0)
logger.warning("Version " + current + " has not yet been tested! Proceed with caution.");
}
+ return current;
+
} catch (Exception e) {
- reporter.reportWarning(this, "Unable to retrieve current Minecraft version.", e);
+ reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_PARSE_MINECRAFT_VERSION).error(e));
}
+
+ // Unknown version
+ return null;
}
private void checkConflictingVersions() {
@@ -331,7 +366,7 @@ public class ProtocolLibrary extends JavaPlugin {
}
} catch (Exception e) {
- reporter.reportWarning(this, "Unable to detect conflicting plugin versions.", e);
+ reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_DETECT_CONFLICTING_PLUGINS).error(e));
}
// See if the newest version is actually higher
@@ -360,7 +395,9 @@ public class ProtocolLibrary extends JavaPlugin {
throw new RuntimeException("plugin.yml might be corrupt.");
} catch (RuntimeException e) {
- reporter.reportWarning(this, "Cannot register command " + name + ": " + e.getMessage());
+ reporter.reportWarning(this,
+ Report.newBuilder(REPORT_CANNOT_REGISTER_COMMAND).messageParam(name, e.getMessage()).error(e)
+ );
}
}
@@ -394,7 +431,7 @@ public class ProtocolLibrary extends JavaPlugin {
} catch (Throwable e) {
if (asyncPacketTask == -1) {
- reporter.reportDetailed(this, "Unable to create packet timeout task.", e);
+ reporter.reportDetailed(this, Report.newBuilder(REPORT_CANNOT_CREATE_TIMEOUT_TASK).error(e));
}
}
}
@@ -417,7 +454,7 @@ public class ProtocolLibrary extends JavaPlugin {
commandProtocol.updateFinished();
}
} catch (Exception e) {
- reporter.reportDetailed(this, "Cannot perform automatic updates.", e);
+ reporter.reportDetailed(this, Report.newBuilder(REPORT_CANNOT_UPDATE_PLUGIN).error(e));
updateDisabled = true;
}
}
@@ -454,7 +491,9 @@ public class ProtocolLibrary extends JavaPlugin {
unhookTask.close();
protocolManager = null;
statistisc = null;
- reporter = null;
+
+ // To clean up global parameters
+ reporter = new BasicErrorReporter();
// Leaky ClassLoader begone!
if (updater == null || updater.getResult() != UpdateResult.SUCCESS) {
@@ -479,6 +518,8 @@ public class ProtocolLibrary extends JavaPlugin {
/**
* Retrieve the current error reporter.
+ *
+ * This is guaranteed to not be NULL in 2.5.0 and later.
* @return Current error reporter.
*/
public static ErrorReporter getErrorReporter() {
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolManager.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolManager.java
index 741c54bc..a9da948f 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolManager.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolManager.java
@@ -27,6 +27,7 @@ import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import com.comphenix.protocol.async.AsyncMarker;
+import com.comphenix.protocol.events.ListenerPriority;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketListener;
import com.comphenix.protocol.injector.PacketConstructor;
@@ -47,7 +48,7 @@ public interface ProtocolManager extends PacketStream {
*
* @param reciever - the reciever.
* @param packet - packet to send.
- * @param filters - whether or not to invoke any packet filters.
+ * @param filters - whether or not to invoke any packet filters below {@link ListenerPriority#MONITOR}.
* @throws InvocationTargetException - if an error occured when sending the packet.
*/
@Override
@@ -62,7 +63,7 @@ public interface ProtocolManager extends PacketStream {
*
* @param sender - the sender.
* @param packet - the packet that was sent.
- * @param filters - whether or not to invoke any packet filters.
+ * @param filters - whether or not to invoke any packet filters below {@link ListenerPriority#MONITOR}.
* @throws InvocationTargetException If the reflection machinery failed.
* @throws IllegalAccessException If the underlying method caused an error.
*/
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketProcessingQueue.java b/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketProcessingQueue.java
index 800124f2..3671ba0e 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketProcessingQueue.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketProcessingQueue.java
@@ -23,6 +23,7 @@ import java.util.PriorityQueue;
import java.util.Queue;
import java.util.concurrent.Semaphore;
+import com.comphenix.protocol.Packets;
import com.comphenix.protocol.concurrency.AbstractConcurrentListenerMultimap;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.injector.PrioritizedListener;
@@ -66,7 +67,7 @@ class PacketProcessingQueue extends AbstractConcurrentListenerMultimap sendingQueue;
-
- // Asynchronous packet sending
- private Executor asynchronousSender;
-
- // Whether or not packet transmission must occur on a specific thread
- private final boolean notThreadSafe;
-
- // Whether or not we've run the cleanup procedure
- private boolean cleanedUp = false;
-
- /**
- * Create a packet sending queue.
- * @param notThreadSafe - whether or not to synchronize with the main thread or a background thread.
- */
- public PacketSendingQueue(boolean notThreadSafe, Executor asynchronousSender) {
- this.sendingQueue = new PriorityBlockingQueue(INITIAL_CAPACITY);
- this.notThreadSafe = notThreadSafe;
- this.asynchronousSender = asynchronousSender;
- }
-
- /**
- * Number of packet events in the queue.
- * @return The number of packet events in the queue.
- */
- public int size() {
- return sendingQueue.size();
- }
-
- /**
- * Enqueue a packet for sending.
- * @param packet - packet to queue.
- */
- public void enqueue(PacketEvent packet) {
- sendingQueue.add(new PacketEventHolder(packet));
- }
-
- /**
- * Invoked when one of the packets have finished processing.
- * @param packetUpdated - the packet that has now been updated.
- * @param onMainThread - whether or not this is occuring on the main thread.
- */
- public synchronized void signalPacketUpdate(PacketEvent packetUpdated, boolean onMainThread) {
-
- AsyncMarker marker = packetUpdated.getAsyncMarker();
-
- // Should we reorder the event?
- if (marker.getQueuedSendingIndex() != marker.getNewSendingIndex() && !marker.hasExpired()) {
- PacketEvent copy = PacketEvent.fromSynchronous(packetUpdated, marker);
-
- // "Cancel" the original event
- packetUpdated.setCancelled(true);
-
- // Enqueue the copy with the new sending index
- enqueue(copy);
- }
-
- // Mark this packet as finished
- marker.setProcessed(true);
- trySendPackets(onMainThread);
- }
-
- /***
- * Invoked when a list of packet IDs are no longer associated with any listeners.
- * @param packetsRemoved - packets that no longer have any listeners.
- * @param onMainThread - whether or not this is occuring on the main thread.
- */
- public synchronized void signalPacketUpdate(List packetsRemoved, boolean onMainThread) {
-
- Set lookup = new HashSet(packetsRemoved);
-
- // Note that this is O(n), so it might be expensive
- for (PacketEventHolder holder : sendingQueue) {
- PacketEvent event = holder.getEvent();
-
- if (lookup.contains(event.getPacketID())) {
- event.getAsyncMarker().setProcessed(true);
- }
- }
-
- // This is likely to have changed the situation a bit
- trySendPackets(onMainThread);
- }
-
- /**
- * Attempt to send any remaining packets.
- * @param onMainThread - whether or not this is occuring on the main thread.
- */
- public void trySendPackets(boolean onMainThread) {
- // Whether or not to continue sending packets
- boolean sending = true;
-
- // Transmit as many packets as we can
- while (sending) {
- PacketEventHolder holder = sendingQueue.poll();
-
- if (holder != null) {
- sending = processPacketHolder(onMainThread, holder);
-
- if (!sending) {
- // Add it back again
- sendingQueue.add(holder);
- }
-
- } else {
- // No more packets to send
- sending = false;
- }
- }
- }
-
- /**
- * Invoked when a packet might be ready for transmission.
- * @param onMainThread - TRUE if we're on the main thread, FALSE otherwise.
- * @param holder - packet container.
- * @return TRUE to continue sending packets, FALSE otherwise.
- */
- private boolean processPacketHolder(boolean onMainThread, final PacketEventHolder holder) {
- PacketEvent current = holder.getEvent();
- AsyncMarker marker = current.getAsyncMarker();
- boolean hasExpired = marker.hasExpired();
-
- // Guard in cause the queue is closed
- if (cleanedUp) {
- return true;
- }
-
- // End condition?
- if (marker.isProcessed() || hasExpired) {
- if (hasExpired) {
- // Notify timeout listeners
- onPacketTimeout(current);
-
- // Recompute
- marker = current.getAsyncMarker();
- hasExpired = marker.hasExpired();
-
- // Could happen due to the timeout listeners
- if (!marker.isProcessed() && !hasExpired) {
- return false;
- }
- }
-
- // Is it okay to send the packet?
- if (!current.isCancelled() && !hasExpired) {
- // Make sure we're on the main thread
- if (notThreadSafe) {
- try {
- boolean wantAsync = marker.isMinecraftAsync(current);
- boolean wantSync = !wantAsync;
-
- // Wait for the next main thread heartbeat if we haven't fulfilled our promise
- if (!onMainThread && wantSync) {
- return false;
- }
-
- // Let's give it what it wants
- if (onMainThread && wantAsync) {
- asynchronousSender.execute(new Runnable() {
- @Override
- public void run() {
- // We know this isn't on the main thread
- processPacketHolder(false, holder);
- }
- });
-
- // Scheduler will do the rest
- return true;
- }
-
- } catch (FieldAccessException e) {
- e.printStackTrace();
-
- // Just drop the packet
- return true;
- }
- }
-
- // Silently skip players that have logged out
- if (isOnline(current.getPlayer())) {
- sendPacket(current);
- }
- }
-
- // Drop the packet
- return true;
- }
-
- // Add it back and stop sending
- return false;
- }
-
- /**
- * 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();
- }
-
- /**
- * Send every packet, regardless of the processing state.
- */
- private void forceSend() {
- while (true) {
- PacketEventHolder holder = sendingQueue.poll();
-
- if (holder != null) {
- sendPacket(holder.getEvent());
- } else {
- break;
- }
- }
- }
-
- /**
- * Whether or not the packet transmission must synchronize with the main thread.
- * @return TRUE if it must, FALSE otherwise.
- */
- public boolean isSynchronizeMain() {
- return notThreadSafe;
- }
-
- /**
- * Transmit a packet, if it hasn't already.
- * @param event - the packet to transmit.
- */
- private void sendPacket(PacketEvent event) {
-
- AsyncMarker marker = event.getAsyncMarker();
-
- try {
- // Don't send a packet twice
- if (marker != null && !marker.isTransmitted()) {
- marker.sendPacket(event);
- }
-
- } catch (PlayerLoggedOutException e) {
- System.out.println(String.format(
- "[ProtocolLib] Warning: Dropped packet index %s of ID %s",
- marker.getOriginalSendingIndex(), event.getPacketID()
- ));
-
- } catch (IOException e) {
- // Just print the error
- e.printStackTrace();
- }
- }
-
- /**
- * Automatically transmits every delayed packet.
- */
- public void cleanupAll() {
- if (!cleanedUp) {
- // Note that the cleanup itself will always occur on the main thread
- forceSend();
-
- // And we're done
- cleanedUp = true;
- }
- }
-}
+/*
+ * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
+ * Copyright (C) 2012 Kristian S. Stangeland
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program;
+ * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+ * 02111-1307 USA
+ */
+
+package com.comphenix.protocol.async;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.PriorityBlockingQueue;
+
+import org.bukkit.entity.Player;
+
+import com.comphenix.protocol.events.PacketEvent;
+import com.comphenix.protocol.injector.PlayerLoggedOutException;
+import com.comphenix.protocol.reflect.FieldAccessException;
+
+/**
+ * Represents packets ready to be transmitted to a client.
+ * @author Kristian
+ */
+abstract class PacketSendingQueue {
+
+ public static final int INITIAL_CAPACITY = 10;
+
+ private PriorityBlockingQueue sendingQueue;
+
+ // Asynchronous packet sending
+ private Executor asynchronousSender;
+
+ // Whether or not packet transmission must occur on a specific thread
+ private final boolean notThreadSafe;
+
+ // Whether or not we've run the cleanup procedure
+ private boolean cleanedUp = false;
+
+ /**
+ * Create a packet sending queue.
+ * @param notThreadSafe - whether or not to synchronize with the main thread or a background thread.
+ */
+ public PacketSendingQueue(boolean notThreadSafe, Executor asynchronousSender) {
+ this.sendingQueue = new PriorityBlockingQueue(INITIAL_CAPACITY);
+ this.notThreadSafe = notThreadSafe;
+ this.asynchronousSender = asynchronousSender;
+ }
+
+ /**
+ * Number of packet events in the queue.
+ * @return The number of packet events in the queue.
+ */
+ public int size() {
+ return sendingQueue.size();
+ }
+
+ /**
+ * Enqueue a packet for sending.
+ * @param packet - packet to queue.
+ */
+ public void enqueue(PacketEvent packet) {
+ sendingQueue.add(new PacketEventHolder(packet));
+ }
+
+ /**
+ * Invoked when one of the packets have finished processing.
+ * @param packetUpdated - the packet that has now been updated.
+ * @param onMainThread - whether or not this is occuring on the main thread.
+ */
+ public synchronized void signalPacketUpdate(PacketEvent packetUpdated, boolean onMainThread) {
+
+ AsyncMarker marker = packetUpdated.getAsyncMarker();
+
+ // Should we reorder the event?
+ if (marker.getQueuedSendingIndex() != marker.getNewSendingIndex() && !marker.hasExpired()) {
+ PacketEvent copy = PacketEvent.fromSynchronous(packetUpdated, marker);
+
+ // "Cancel" the original event
+ packetUpdated.setReadOnly(false);
+ packetUpdated.setCancelled(true);
+
+ // Enqueue the copy with the new sending index
+ enqueue(copy);
+ }
+
+ // Mark this packet as finished
+ marker.setProcessed(true);
+ trySendPackets(onMainThread);
+ }
+
+ /***
+ * Invoked when a list of packet IDs are no longer associated with any listeners.
+ * @param packetsRemoved - packets that no longer have any listeners.
+ * @param onMainThread - whether or not this is occuring on the main thread.
+ */
+ public synchronized void signalPacketUpdate(List packetsRemoved, boolean onMainThread) {
+
+ Set lookup = new HashSet(packetsRemoved);
+
+ // Note that this is O(n), so it might be expensive
+ for (PacketEventHolder holder : sendingQueue) {
+ PacketEvent event = holder.getEvent();
+
+ if (lookup.contains(event.getPacketID())) {
+ event.getAsyncMarker().setProcessed(true);
+ }
+ }
+
+ // This is likely to have changed the situation a bit
+ trySendPackets(onMainThread);
+ }
+
+ /**
+ * Attempt to send any remaining packets.
+ * @param onMainThread - whether or not this is occuring on the main thread.
+ */
+ public void trySendPackets(boolean onMainThread) {
+ // Whether or not to continue sending packets
+ boolean sending = true;
+
+ // Transmit as many packets as we can
+ while (sending) {
+ PacketEventHolder holder = sendingQueue.poll();
+
+ if (holder != null) {
+ sending = processPacketHolder(onMainThread, holder);
+
+ if (!sending) {
+ // Add it back again
+ sendingQueue.add(holder);
+ }
+
+ } else {
+ // No more packets to send
+ sending = false;
+ }
+ }
+ }
+
+ /**
+ * Invoked when a packet might be ready for transmission.
+ * @param onMainThread - TRUE if we're on the main thread, FALSE otherwise.
+ * @param holder - packet container.
+ * @return TRUE to continue sending packets, FALSE otherwise.
+ */
+ private boolean processPacketHolder(boolean onMainThread, final PacketEventHolder holder) {
+ PacketEvent current = holder.getEvent();
+ AsyncMarker marker = current.getAsyncMarker();
+ boolean hasExpired = marker.hasExpired();
+
+ // Guard in cause the queue is closed
+ if (cleanedUp) {
+ return true;
+ }
+
+ // End condition?
+ if (marker.isProcessed() || hasExpired) {
+ if (hasExpired) {
+ // Notify timeout listeners
+ onPacketTimeout(current);
+
+ // Recompute
+ marker = current.getAsyncMarker();
+ hasExpired = marker.hasExpired();
+
+ // Could happen due to the timeout listeners
+ if (!marker.isProcessed() && !hasExpired) {
+ return false;
+ }
+ }
+
+ // Is it okay to send the packet?
+ if (!current.isCancelled() && !hasExpired) {
+ // Make sure we're on the main thread
+ if (notThreadSafe) {
+ try {
+ boolean wantAsync = marker.isMinecraftAsync(current);
+ boolean wantSync = !wantAsync;
+
+ // Wait for the next main thread heartbeat if we haven't fulfilled our promise
+ if (!onMainThread && wantSync) {
+ return false;
+ }
+
+ // Let's give it what it wants
+ if (onMainThread && wantAsync) {
+ asynchronousSender.execute(new Runnable() {
+ @Override
+ public void run() {
+ // We know this isn't on the main thread
+ processPacketHolder(false, holder);
+ }
+ });
+
+ // Scheduler will do the rest
+ return true;
+ }
+
+ } catch (FieldAccessException e) {
+ e.printStackTrace();
+
+ // Just drop the packet
+ return true;
+ }
+ }
+
+ // Silently skip players that have logged out
+ if (isOnline(current.getPlayer())) {
+ sendPacket(current);
+ }
+ }
+
+ // Drop the packet
+ return true;
+ }
+
+ // Add it back and stop sending
+ return false;
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * Send every packet, regardless of the processing state.
+ */
+ private void forceSend() {
+ while (true) {
+ PacketEventHolder holder = sendingQueue.poll();
+
+ if (holder != null) {
+ sendPacket(holder.getEvent());
+ } else {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Whether or not the packet transmission must synchronize with the main thread.
+ * @return TRUE if it must, FALSE otherwise.
+ */
+ public boolean isSynchronizeMain() {
+ return notThreadSafe;
+ }
+
+ /**
+ * Transmit a packet, if it hasn't already.
+ * @param event - the packet to transmit.
+ */
+ private void sendPacket(PacketEvent event) {
+
+ AsyncMarker marker = event.getAsyncMarker();
+
+ try {
+ // Don't send a packet twice
+ if (marker != null && !marker.isTransmitted()) {
+ marker.sendPacket(event);
+ }
+
+ } catch (PlayerLoggedOutException e) {
+ System.out.println(String.format(
+ "[ProtocolLib] Warning: Dropped packet index %s of ID %s",
+ marker.getOriginalSendingIndex(), event.getPacketID()
+ ));
+
+ } catch (IOException e) {
+ // Just print the error
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Automatically transmits every delayed packet.
+ */
+ public void cleanupAll() {
+ if (!cleanedUp) {
+ // Note that the cleanup itself will always occur on the main thread
+ forceSend();
+
+ // And we're done
+ cleanedUp = true;
+ }
+ }
+}
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractConcurrentListenerMultimap.java b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractConcurrentListenerMultimap.java
index 52d869db..826e6b0c 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractConcurrentListenerMultimap.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractConcurrentListenerMultimap.java
@@ -23,6 +23,7 @@ import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicReferenceArray;
import com.comphenix.protocol.events.ListeningWhitelist;
import com.comphenix.protocol.injector.PrioritizedListener;
@@ -34,10 +35,14 @@ import com.google.common.collect.Iterables;
* @author Kristian
*/
public abstract class AbstractConcurrentListenerMultimap {
-
// The core of our map
- private ConcurrentMap>> listeners =
- new ConcurrentHashMap>>();
+ private AtomicReferenceArray>> arrayListeners;
+ private ConcurrentMap>> mapListeners;
+
+ public AbstractConcurrentListenerMultimap(int maximumPacketID) {
+ arrayListeners = new AtomicReferenceArray>>(maximumPacketID + 1);
+ mapListeners = new ConcurrentHashMap>>();
+ }
/**
* Adds a listener to its requested list of packet recievers.
@@ -45,7 +50,6 @@ public abstract class AbstractConcurrentListenerMultimap {
* @param whitelist - the packet whitelist to use.
*/
public void addListener(TListener listener, ListeningWhitelist whitelist) {
-
PrioritizedListener prioritized = new PrioritizedListener(listener, whitelist.getPriority());
for (Integer packetID : whitelist.getWhitelist()) {
@@ -55,21 +59,20 @@ public abstract class AbstractConcurrentListenerMultimap {
// Add the listener to a specific packet notifcation list
private void addListener(Integer packetID, PrioritizedListener listener) {
-
- SortedCopyOnWriteArray> list = listeners.get(packetID);
+ SortedCopyOnWriteArray> list = arrayListeners.get(packetID);
// We don't want to create this for every lookup
if (list == null) {
// It would be nice if we could use a PriorityBlockingQueue, but it doesn't preseve iterator order,
// which is a essential feature for our purposes.
final SortedCopyOnWriteArray> value = new SortedCopyOnWriteArray>();
-
- list = listeners.putIfAbsent(packetID, value);
-
- // We may end up creating multiple multisets, but we'll agree
- // on the one to use.
- if (list == null) {
+
+ // We may end up creating multiple multisets, but we'll agree on which to use
+ if (arrayListeners.compareAndSet(packetID, null, value)) {
+ mapListeners.put(packetID, value);
list = value;
+ } else {
+ list = arrayListeners.get(packetID);
}
}
@@ -84,13 +87,11 @@ public abstract class AbstractConcurrentListenerMultimap {
* @return Every packet ID that was removed due to no listeners.
*/
public List removeListener(TListener listener, ListeningWhitelist whitelist) {
-
List removedPackets = new ArrayList();
// Again, not terribly efficient. But adding or removing listeners should be a rare event.
for (Integer packetID : whitelist.getWhitelist()) {
-
- SortedCopyOnWriteArray> list = listeners.get(packetID);
+ SortedCopyOnWriteArray> list = arrayListeners.get(packetID);
// Remove any listeners
if (list != null) {
@@ -100,7 +101,8 @@ public abstract class AbstractConcurrentListenerMultimap {
list.remove(new PrioritizedListener(listener, whitelist.getPriority()));
if (list.size() == 0) {
- listeners.remove(packetID);
+ arrayListeners.set(packetID, null);
+ mapListeners.remove(packetID);
removedPackets.add(packetID);
}
}
@@ -120,7 +122,7 @@ public abstract class AbstractConcurrentListenerMultimap {
* @return Registered listeners.
*/
public Collection> getListener(int packetID) {
- return listeners.get(packetID);
+ return arrayListeners.get(packetID);
}
/**
@@ -128,7 +130,7 @@ public abstract class AbstractConcurrentListenerMultimap {
* @return Every listener.
*/
public Iterable> values() {
- return Iterables.concat(listeners.values());
+ return Iterables.concat(mapListeners.values());
}
/**
@@ -136,13 +138,15 @@ public abstract class AbstractConcurrentListenerMultimap {
* @return Registered packet ID.
*/
public Set keySet() {
- return listeners.keySet();
+ return mapListeners.keySet();
}
/**
* Remove all packet listeners.
*/
protected void clearListeners() {
- listeners.clear();
+ arrayListeners = new AtomicReferenceArray<
+ SortedCopyOnWriteArray>>(arrayListeners.length());
+ mapListeners.clear();
}
}
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/IntegerSet.java b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/IntegerSet.java
index e2aaa3f6..c99e18e1 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/IntegerSet.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/IntegerSet.java
@@ -18,6 +18,7 @@
package com.comphenix.protocol.concurrency;
import java.util.Arrays;
+import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@@ -60,6 +61,16 @@ public class IntegerSet {
array[element] = true;
}
+ /**
+ * Add the given collection of elements to the set.
+ * @param packets - elements to add.
+ */
+ public void addAll(Collection packets) {
+ for (Integer id : packets) {
+ add(id);
+ }
+ }
+
/**
* Remove the given element from the set, or do nothing if it's already removed.
* @param element - element to remove.
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/BasicErrorReporter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/BasicErrorReporter.java
new file mode 100644
index 00000000..69226d57
--- /dev/null
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/BasicErrorReporter.java
@@ -0,0 +1,96 @@
+package com.comphenix.protocol.error;
+
+import java.io.PrintStream;
+import org.bukkit.plugin.Plugin;
+
+import com.comphenix.protocol.error.Report.ReportBuilder;
+import com.comphenix.protocol.reflect.PrettyPrinter;
+
+/**
+ * Represents a basic error reporter that prints error reports to the standard error stream.
+ *
+ * Note that this implementation doesn't distinguish between {@link #reportWarning(Object, Report)}
+ * and {@link #reportDetailed(Object, Report)} - they both have the exact same behavior.
+ * @author Kristian
+ */
+public class BasicErrorReporter implements ErrorReporter {
+ private final PrintStream output;
+
+ /**
+ * Construct a new basic error reporter that prints directly the standard error stream.
+ */
+ public BasicErrorReporter() {
+ this(System.err);
+ }
+
+ /**
+ * Construct a error reporter that prints to the given output stream.
+ * @param output - the output stream.
+ */
+ public BasicErrorReporter(PrintStream output) {
+ this.output = output;
+ }
+
+ @Override
+ public void reportMinimal(Plugin sender, String methodName, Throwable error) {
+ output.println("Unhandled exception occured in " + methodName + " for " + sender.getName());
+ error.printStackTrace(output);
+ }
+
+ @Override
+ public void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters) {
+ reportMinimal(sender, methodName, error);
+
+ // Also print parameters
+ printParameters(parameters);
+ }
+
+ @Override
+ public void reportWarning(Object sender, Report report) {
+ // Basic warning
+ output.println("[" + sender.getClass().getSimpleName() + "] " + report.getReportMessage());
+
+ if (report.getException() != null) {
+ report.getException().printStackTrace(output);
+ }
+ printParameters(report.getCallerParameters());
+ }
+
+ @Override
+ public void reportWarning(Object sender, ReportBuilder reportBuilder) {
+ reportWarning(sender, reportBuilder.build());
+ }
+
+ @Override
+ public void reportDetailed(Object sender, Report report) {
+ // No difference from warning
+ reportWarning(sender, report);
+ }
+
+ @Override
+ public void reportDetailed(Object sender, ReportBuilder reportBuilder) {
+ reportWarning(sender, reportBuilder);
+ }
+
+ /**
+ * Print the given parameters to the standard error stream.
+ * @param parameters - the output parameters.
+ */
+ private void printParameters(Object[] parameters) {
+ if (parameters != null && parameters.length > 0) {
+ output.println("Parameters: ");
+
+ try {
+ for (Object parameter : parameters) {
+ if (parameter == null)
+ output.println("[NULL]");
+ else
+ output.println(PrettyPrinter.printObject(parameter));
+ }
+ } catch (IllegalAccessException e) {
+ // Damn it
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/DelegatedErrorReporter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DelegatedErrorReporter.java
new file mode 100644
index 00000000..7584f5d0
--- /dev/null
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DelegatedErrorReporter.java
@@ -0,0 +1,80 @@
+package com.comphenix.protocol.error;
+
+import org.bukkit.plugin.Plugin;
+
+import com.comphenix.protocol.error.Report.ReportBuilder;
+
+/**
+ * Construct an error reporter that delegates to another error reporter.
+ * @author Kristian
+ */
+public class DelegatedErrorReporter implements ErrorReporter {
+ private final ErrorReporter delegated;
+
+ /**
+ * Construct a new error reporter that forwards all reports to a given reporter.
+ * @param delegated - the delegated reporter.
+ */
+ public DelegatedErrorReporter(ErrorReporter delegated) {
+ this.delegated = delegated;
+ }
+
+ /**
+ * Retrieve the underlying error reporter.
+ * @return Underlying error reporter.
+ */
+ public ErrorReporter getDelegated() {
+ return delegated;
+ }
+
+ @Override
+ public void reportMinimal(Plugin sender, String methodName, Throwable error) {
+ delegated.reportMinimal(sender, methodName, error);
+ }
+
+ @Override
+ public void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters) {
+ delegated.reportMinimal(sender, methodName, error, parameters);
+ }
+
+ @Override
+ public void reportWarning(Object sender, Report report) {
+ Report transformed = filterReport(sender, report, false);
+
+ if (transformed != null) {
+ delegated.reportWarning(sender, transformed);
+ }
+ }
+
+ @Override
+ public void reportDetailed(Object sender, Report report) {
+ Report transformed = filterReport(sender, report, true);
+
+ if (transformed != null) {
+ delegated.reportDetailed(sender, transformed);
+ }
+ }
+
+ /**
+ * Invoked before an error report is passed on to the underlying error reporter.
+ *
+ * To cancel a report, return NULL.
+ * @param sender - the sender component.
+ * @param report - the error report.
+ * @param detailed - whether or not the report will be displayed in detail.
+ * @return The report to pass on, or NULL to cancel it.
+ */
+ protected Report filterReport(Object sender, Report report, boolean detailed) {
+ return report;
+ }
+
+ @Override
+ public void reportWarning(Object sender, ReportBuilder reportBuilder) {
+ reportWarning(sender, reportBuilder.build());
+ }
+
+ @Override
+ public void reportDetailed(Object sender, ReportBuilder reportBuilder) {
+ reportDetailed(sender, reportBuilder.build());
+ }
+}
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java
index 09786d1a..6953d437 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java
@@ -1,387 +1,499 @@
-/*
- * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
- * Copyright (C) 2012 Kristian S. Stangeland
- *
- * This program is free software; you can redistribute it and/or modify it under the terms of the
- * GNU General Public License as published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
- * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
- * See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with this program;
- * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
- * 02111-1307 USA
- */
-
-package com.comphenix.protocol.error;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.lang.ref.WeakReference;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.atomic.AtomicInteger;
-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.comphenix.protocol.reflect.PrettyPrinter;
-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;
-
- // Prevent spam per plugin too
- private ConcurrentMap warningCount = new ConcurrentHashMap();
-
- protected String prefix;
- protected String supportURL;
-
- protected AtomicInteger internalErrorCount = new AtomicInteger();
-
- protected int maxErrorCount;
- protected Logger logger;
-
- protected WeakReference pluginReference;
-
- // 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(Plugin plugin) {
- this(plugin, DEFAULT_PREFIX, DEFAULT_SUPPORT_URL);
- }
-
- /**
- * Create a central error reporting system.
- * @param plugin - the plugin owner.
- * @param prefix - default line prefix.
- * @param supportURL - URL to report the error.
- */
- public DetailedErrorReporter(Plugin plugin, String prefix, String supportURL) {
- this(plugin, 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 plugin - the plugin owner.
- * @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(Plugin plugin, String prefix, String supportURL, int maxErrorCount, Logger logger) {
- if (plugin == null)
- throw new IllegalArgumentException("Plugin cannot be NULL.");
-
- this.pluginReference = new WeakReference(plugin);
- this.prefix = prefix;
- this.supportURL = supportURL;
- this.maxErrorCount = maxErrorCount;
- this.logger = logger;
- }
-
- @Override
- public void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters) {
- if (reportMinimalNoSpam(sender, methodName, error)) {
- // Print parameters, if they are given
- if (parameters != null && parameters.length > 0) {
- logger.log(Level.SEVERE, " Parameters:");
-
- // Print each parameter
- for (Object parameter : parameters) {
- logger.log(Level.SEVERE, " " + getStringDescription(parameter));
- }
- }
- }
- }
-
- @Override
- public void reportMinimal(Plugin sender, String methodName, Throwable error) {
- reportMinimalNoSpam(sender, methodName, error);
- }
-
- public boolean reportMinimalNoSpam(Plugin sender, String methodName, Throwable error) {
- String pluginName = PacketAdapter.getPluginName(sender);
- AtomicInteger counter = warningCount.get(pluginName);
-
- // Thread safe pattern
- if (counter == null) {
- AtomicInteger created = new AtomicInteger();
- counter = warningCount.putIfAbsent(pluginName, created);
-
- if (counter == null) {
- counter = created;
- }
- }
-
- final int errorCount = counter.incrementAndGet();
-
- // See if we should print the full error
- if (errorCount < getMaxErrorCount()) {
- logger.log(Level.SEVERE, "[" + PLUGIN_NAME + "] Unhandled exception occured in " +
- methodName + " for " + pluginName, error);
- return true;
-
- } else {
- // Nope - only print the error count occationally
- if (isPowerOfTwo(errorCount)) {
- logger.log(Level.SEVERE, "[" + PLUGIN_NAME + "] Unhandled exception number " + errorCount + " occured in " +
- methodName + " for " + pluginName, error);
- }
- return false;
- }
- }
-
- /**
- * Determine if a given number is a power of two.
- *
- * That is, if there exists an N such that 2^N = number.
- * @param number - the number to check.
- * @return TRUE if the given number is a power of two, FALSE otherwise.
- */
- private boolean isPowerOfTwo(int number) {
- return (number & (number - 1)) == 0;
- }
-
- @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) {
-
- final Plugin plugin = pluginReference.get();
- final int errorCount = internalErrorCount.incrementAndGet();
-
- // Do not overtly spam the server!
- if (errorCount > getMaxErrorCount()) {
- // Only allow the error count at rare occations
- if (isPowerOfTwo(errorCount)) {
- // Permit it - but print the number of exceptions first
- reportWarning(this, "Internal exception count: " + errorCount + "!");
- } else {
- // NEVER SPAM THE CONSOLE
- 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));
-
- // And plugin
- if (plugin != null) {
- writer.println("Version:");
- writer.println(addPrefix(plugin.toString(), 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
- apacheCommonsMissing = true;
- }
-
- // Use our custom object printer instead
- try {
- return PrettyPrinter.printObject(value, value.getClass(), Object.class);
- } catch (IllegalAccessException e) {
- return "[Error: " + e.getMessage() + "]";
- }
- }
- }
-
- /**
- * 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 internalErrorCount.get();
- }
-
- public void setErrorCount(int errorCount) {
- internalErrorCount.set(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;
- }
-}
+/*
+ * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
+ * Copyright (C) 2012 Kristian S. Stangeland
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program;
+ * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+ * 02111-1307 USA
+ */
+
+package com.comphenix.protocol.error;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+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.error.Report.ReportBuilder;
+import com.comphenix.protocol.events.PacketAdapter;
+import com.comphenix.protocol.reflect.PrettyPrinter;
+import com.google.common.primitives.Primitives;
+
+/**
+ * Internal class used to handle exceptions.
+ *
+ * @author Kristian
+ */
+public class DetailedErrorReporter implements ErrorReporter {
+ /**
+ * Report format for printing the current exception count.
+ */
+ public static final ReportType REPORT_EXCEPTION_COUNT = new ReportType("Internal exception count: %s!");
+
+ 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/";
+
+ // 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;
+
+ // Prevent spam per plugin too
+ private ConcurrentMap warningCount = new ConcurrentHashMap();
+
+ protected String prefix;
+ protected String supportURL;
+
+ protected AtomicInteger internalErrorCount = new AtomicInteger();
+
+ protected int maxErrorCount;
+ protected Logger logger;
+
+ protected WeakReference pluginReference;
+ protected String pluginName;
+
+ // 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(Plugin plugin) {
+ this(plugin, DEFAULT_PREFIX, DEFAULT_SUPPORT_URL);
+ }
+
+ /**
+ * Create a central error reporting system.
+ * @param plugin - the plugin owner.
+ * @param prefix - default line prefix.
+ * @param supportURL - URL to report the error.
+ */
+ public DetailedErrorReporter(Plugin plugin, String prefix, String supportURL) {
+ this(plugin, prefix, supportURL, DEFAULT_MAX_ERROR_COUNT, getBukkitLogger());
+ }
+
+ /**
+ * Create a central error reporting system.
+ * @param plugin - the plugin owner.
+ * @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(Plugin plugin, String prefix, String supportURL, int maxErrorCount, Logger logger) {
+ if (plugin == null)
+ throw new IllegalArgumentException("Plugin cannot be NULL.");
+
+ this.pluginReference = new WeakReference(plugin);
+ this.pluginName = plugin.getName();
+ this.prefix = prefix;
+ this.supportURL = supportURL;
+ this.maxErrorCount = maxErrorCount;
+ this.logger = logger;
+ }
+
+ // Attempt to get the logger.
+ private static Logger getBukkitLogger() {
+ try {
+ return Bukkit.getLogger();
+ } catch (Throwable e) {
+ return Logger.getLogger("Minecraft");
+ }
+ }
+
+ @Override
+ public void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters) {
+ if (reportMinimalNoSpam(sender, methodName, error)) {
+ // Print parameters, if they are given
+ if (parameters != null && parameters.length > 0) {
+ logger.log(Level.SEVERE, printParameters(parameters));
+ }
+ }
+ }
+
+ @Override
+ public void reportMinimal(Plugin sender, String methodName, Throwable error) {
+ reportMinimalNoSpam(sender, methodName, error);
+ }
+
+ /**
+ * Report a problem with a given method and plugin, ensuring that we don't exceed the maximum number of error reports.
+ * @param sender - the component that observed this exception.
+ * @param methodName - the method name.
+ * @param error - the error itself.
+ * @return TRUE if the error was printed, FALSE if it was suppressed.
+ */
+ public boolean reportMinimalNoSpam(Plugin sender, String methodName, Throwable error) {
+ String pluginName = PacketAdapter.getPluginName(sender);
+ AtomicInteger counter = warningCount.get(pluginName);
+
+ // Thread safe pattern
+ if (counter == null) {
+ AtomicInteger created = new AtomicInteger();
+ counter = warningCount.putIfAbsent(pluginName, created);
+
+ if (counter == null) {
+ counter = created;
+ }
+ }
+
+ final int errorCount = counter.incrementAndGet();
+
+ // See if we should print the full error
+ if (errorCount < getMaxErrorCount()) {
+ logger.log(Level.SEVERE, "[" + pluginName + "] Unhandled exception occured in " +
+ methodName + " for " + pluginName, error);
+ return true;
+
+ } else {
+ // Nope - only print the error count occationally
+ if (isPowerOfTwo(errorCount)) {
+ logger.log(Level.SEVERE, "[" + pluginName + "] Unhandled exception number " + errorCount + " occured in " +
+ methodName + " for " + pluginName, error);
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Determine if a given number is a power of two.
+ *
+ * That is, if there exists an N such that 2^N = number.
+ * @param number - the number to check.
+ * @return TRUE if the given number is a power of two, FALSE otherwise.
+ */
+ private boolean isPowerOfTwo(int number) {
+ return (number & (number - 1)) == 0;
+ }
+
+ @Override
+ public void reportWarning(Object sender, ReportBuilder reportBuilder) {
+ if (reportBuilder == null)
+ throw new IllegalArgumentException("reportBuilder cannot be NULL.");
+
+ reportWarning(sender, reportBuilder.build());
+ }
+
+ @Override
+ public void reportWarning(Object sender, Report report) {
+ String message = "[" + pluginName + "] [" + getSenderName(sender) + "] " + report.getReportMessage();
+
+ // Print the main warning
+ if (report.getException() != null) {
+ logger.log(Level.WARNING, message, report.getException());
+ } else {
+ logger.log(Level.WARNING, message);
+ }
+
+ // Parameters?
+ if (report.hasCallerParameters()) {
+ // Write it
+ logger.log(Level.WARNING, printParameters(report.getCallerParameters()));
+ }
+ }
+
+ /**
+ * Retrieve the name of a sender class.
+ * @param sender - sender object.
+ * @return The name of the sender's class.
+ */
+ private String getSenderName(Object sender) {
+ if (sender != null)
+ return sender.getClass().getSimpleName();
+ else
+ return "NULL";
+ }
+
+ @Override
+ public void reportDetailed(Object sender, ReportBuilder reportBuilder) {
+ reportDetailed(sender, reportBuilder.build());
+ }
+
+ @Override
+ public void reportDetailed(Object sender, Report report) {
+ final Plugin plugin = pluginReference.get();
+ final int errorCount = internalErrorCount.incrementAndGet();
+
+ // Do not overtly spam the server!
+ if (errorCount > getMaxErrorCount()) {
+ // Only allow the error count at rare occations
+ if (isPowerOfTwo(errorCount)) {
+ // Permit it - but print the number of exceptions first
+ reportWarning(this, Report.newBuilder(REPORT_EXCEPTION_COUNT).messageParam(errorCount).build());
+ } else {
+ // NEVER SPAM THE CONSOLE
+ return;
+ }
+ }
+
+ StringWriter text = new StringWriter();
+ PrintWriter writer = new PrintWriter(text);
+
+ // Helpful message
+ writer.println("[" + pluginName + "] INTERNAL ERROR: " + report.getReportMessage());
+ 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 (report.getException() != null) {
+ report.getException().printStackTrace(writer);
+ }
+
+ // Data dump!
+ writer.println(" ===== DUMP =====");
+
+ // Relevant parameters
+ if (report.hasCallerParameters()) {
+ printParameters(writer, report.getCallerParameters());
+ }
+
+ // 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));
+
+ // And plugin
+ if (plugin != null) {
+ writer.println("Version:");
+ writer.println(addPrefix(plugin.toString(), 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.", report.getReportMessage(), report.getException(), sender),
+ ERROR_PERMISSION
+ );
+ }
+ }
+
+ // Make sure it is reported
+ logger.severe(addPrefix(text.toString(), prefix));
+ }
+
+ private String printParameters(Object... parameters) {
+ StringWriter writer = new StringWriter();
+
+ // Print and retrieve the string buffer
+ printParameters(new PrintWriter(writer), parameters);
+ return writer.toString();
+ }
+
+ private void printParameters(PrintWriter writer, Object[] parameters) {
+ 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));
+ }
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Retrieve a string representation of the given object.
+ * @param value - object to convert.
+ * @return String representation.
+ */
+ 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
+ apacheCommonsMissing = true;
+ }
+
+ // Use our custom object printer instead
+ try {
+ return PrettyPrinter.printObject(value, value.getClass(), Object.class);
+ } catch (IllegalAccessException e) {
+ return "[Error: " + e.getMessage() + "]";
+ }
+ }
+ }
+
+ /**
+ * 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());
+ }
+
+ /**
+ * Retrieve the current number of errors printed through {@link #reportDetailed(Object, Report)}.
+ * @return Number of errors printed.
+ */
+ public int getErrorCount() {
+ return internalErrorCount.get();
+ }
+
+ /**
+ * Set the number of errors printed.
+ * @param errorCount - new number of errors printed.
+ */
+ public void setErrorCount(int errorCount) {
+ internalErrorCount.set(errorCount);
+ }
+
+ /**
+ * Retrieve the maximum number of errors we can print before we begin suppressing errors.
+ * @return Maximum number of errors.
+ */
+ public int getMaxErrorCount() {
+ return maxErrorCount;
+ }
+
+ /**
+ * Set the maximum number of errors we can print before we begin suppressing errors.
+ * @param maxErrorCount - new max count.
+ */
+ public void setMaxErrorCount(int maxErrorCount) {
+ this.maxErrorCount = maxErrorCount;
+ }
+
+ /**
+ * Adds the given global parameter. It will be included in every error report.
+ *
+ * Both key and value must be non-null.
+ * @param key - name of parameter.
+ * @param value - the global parameter itself.
+ */
+ public void addGlobalParameter(String key, Object value) {
+ if (key == null)
+ throw new IllegalArgumentException("key cannot be NULL.");
+ if (value == null)
+ throw new IllegalArgumentException("value cannot be NULL.");
+
+ globalParameters.put(key, value);
+ }
+
+ /**
+ * Retrieve a global parameter by its key.
+ * @param key - key of the parameter to retrieve.
+ * @return The value of the global parameter, or NULL if not found.
+ */
+ public Object getGlobalParameter(String key) {
+ if (key == null)
+ throw new IllegalArgumentException("key cannot be NULL.");
+
+ return globalParameters.get(key);
+ }
+
+ /**
+ * Reset all global parameters.
+ */
+ public void clearGlobalParameters() {
+ globalParameters.clear();
+ }
+
+ /**
+ * Retrieve a set of every registered global parameter.
+ * @return Set of all registered global parameters.
+ */
+ public Set globalParameters() {
+ return globalParameters.keySet();
+ }
+
+ /**
+ * Retrieve the support URL that will be added to all detailed reports.
+ * @return Support URL.
+ */
+ public String getSupportURL() {
+ return supportURL;
+ }
+
+ /**
+ * Set the support URL that will be added to all detailed reports.
+ * @param supportURL - the new support URL.
+ */
+ public void setSupportURL(String supportURL) {
+ this.supportURL = supportURL;
+ }
+
+ /**
+ * Retrieve the prefix to apply to every line in the error reports.
+ * @return Error report prefix.
+ */
+ public String getPrefix() {
+ return prefix;
+ }
+
+ /**
+ * Set the prefix to apply to every line in the error reports.
+ * @param prefix - new prefix.
+ */
+ public void setPrefix(String prefix) {
+ this.prefix = prefix;
+ }
+
+ /**
+ * Retrieve the current logger that is used to print all reports.
+ * @return The current logger.
+ */
+ public Logger getLogger() {
+ return logger;
+ }
+
+ /**
+ * Set the current logger that is used to print all reports.
+ * @param logger - new 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
index e1b7f927..fde3334f 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/error/ErrorReporter.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/ErrorReporter.java
@@ -1,65 +1,74 @@
-/*
- * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
- * Copyright (C) 2012 Kristian S. Stangeland
- *
- * This program is free software; you can redistribute it and/or modify it under the terms of the
- * GNU General Public License as published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
- * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
- * See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with this program;
- * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
- * 02111-1307 USA
- */
-
-package com.comphenix.protocol.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 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.
- * @param parameters - any relevant parameters to print.
- */
- public abstract void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters);
-
- /**
- * 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);
-
+/*
+ * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
+ * Copyright (C) 2012 Kristian S. Stangeland
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program;
+ * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+ * 02111-1307 USA
+ */
+
+package com.comphenix.protocol.error;
+
+import org.bukkit.plugin.Plugin;
+
+import com.comphenix.protocol.error.Report.ReportBuilder;
+
+/**
+ * Represents an object that can forward an error {@link Report} to the display and permanent storage.
+ *
+ * @author Kristian
+ */
+public interface ErrorReporter {
+ /**
+ * Prints a small minimal error report regarding 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 small minimal error report regarding an exception from another plugin.
+ * @param sender - the other plugin.
+ * @param methodName - name of the caller method.
+ * @param error - the exception itself.
+ * @param parameters - any relevant parameters to print.
+ */
+ public abstract void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters);
+
+ /**
+ * Prints a warning message from the current plugin.
+ * @param sender - the object containing the caller method.
+ * @param report - an error report to include.
+ */
+ public abstract void reportWarning(Object sender, Report report);
+
+ /**
+ * Prints a warning message from the current plugin.
+ * @param sender - the object containing the caller method.
+ * @param reportBuilder - an error report builder that will be used to get the report.
+ */
+ public abstract void reportWarning(Object sender, ReportBuilder reportBuilder);
+
+ /**
+ * Prints a detailed error report about an unhandled exception.
+ * @param sender - the object containing the caller method.
+ * @param report - an error report to include.
+ */
+ public abstract void reportDetailed(Object sender, Report report);
+
+ /**
+ * Prints a detailed error report about an unhandled exception.
+ * @param sender - the object containing the caller method.
+ * @param reportBuilder - an error report builder that will be used to get the report.
+ */
+ public abstract void reportDetailed(Object sender, ReportBuilder reportBuilder);
}
\ No newline at end of file
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/Report.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/Report.java
new file mode 100644
index 00000000..177c0407
--- /dev/null
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/Report.java
@@ -0,0 +1,162 @@
+package com.comphenix.protocol.error;
+
+import javax.annotation.Nullable;
+
+/**
+ * Represents a error or warning report.
+ *
+ * @author Kristian
+ */
+public class Report {
+ private final ReportType type;
+ private final Throwable exception;
+ private final Object[] messageParameters;
+ private final Object[] callerParameters;
+
+ /**
+ * Must be constructed through the factory method in Report.
+ */
+ public static class ReportBuilder {
+ private ReportType type;
+ private Throwable exception;
+ private Object[] messageParameters;
+ private Object[] callerParameters;
+
+ private ReportBuilder() {
+ // Don't allow
+ }
+
+ /**
+ * Set the current report type. Cannot be NULL.
+ * @param type - report type.
+ * @return This builder, for chaining.
+ */
+ public ReportBuilder type(ReportType type) {
+ if (type == null)
+ throw new IllegalArgumentException("Report type cannot be set to NULL.");
+ this.type = type;
+ return this;
+ }
+
+ /**
+ * Set the current exception that occured.
+ * @param exception - exception that occured.
+ * @return This builder, for chaining.
+ */
+ public ReportBuilder error(@Nullable Throwable exception) {
+ this.exception = exception;
+ return this;
+ }
+
+ /**
+ * Set the message parameters that are used to construct a message text.
+ * @param messageParameters - parameters for the report type.
+ * @return This builder, for chaining.
+ */
+ public ReportBuilder messageParam(@Nullable Object... messageParameters) {
+ this.messageParameters = messageParameters;
+ return this;
+ }
+
+ /**
+ * Set the parameters in the caller method. This is optional.
+ * @param callerParameters - parameters of the caller method.
+ * @return This builder, for chaining.
+ */
+ public ReportBuilder callerParam(@Nullable Object... callerParameters) {
+ this.callerParameters = callerParameters;
+ return this;
+ }
+
+ /**
+ * Construct a new report with the provided input.
+ * @return A new report.
+ */
+ public Report build() {
+ return new Report(type, exception, messageParameters, callerParameters);
+ }
+ }
+
+ /**
+ * Construct a new report builder.
+ * @param type - the initial report type.
+ * @return Report builder.
+ */
+ public static ReportBuilder newBuilder(ReportType type) {
+ return new ReportBuilder().type(type);
+ }
+
+ /**
+ * Construct a new report with the given type and parameters.
+ * @param exception - exception that occured in the caller method.
+ * @param type - the report type that will be used to construct the message.
+ * @param messageParameters - parameters used to construct the report message.
+ * @param callerParameters - parameters from the caller method.
+ */
+ protected Report(ReportType type, @Nullable Throwable exception, @Nullable Object[] messageParameters, @Nullable Object[] callerParameters) {
+ if (type == null)
+ throw new IllegalArgumentException("type cannot be NULL.");
+ this.type = type;
+ this.exception = exception;
+ this.messageParameters = messageParameters;
+ this.callerParameters = callerParameters;
+ }
+
+ /**
+ * Format the current report type with the provided message parameters.
+ * @return The formated report message.
+ */
+ public String getReportMessage() {
+ return type.getMessage(messageParameters);
+ }
+
+ /**
+ * Retrieve the message parameters that will be used to construc the report message.
+ *
0;
+ }
+
+ /**
+ * Determine if we have any caller parameters.
+ * @return TRUE if there are any caller parameters, FALSE otherwise.
+ */
+ public boolean hasCallerParameters() {
+ return callerParameters != null && callerParameters.length > 0;
+ }
+}
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/ReportType.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/ReportType.java
new file mode 100644
index 00000000..cd4490e0
--- /dev/null
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/ReportType.java
@@ -0,0 +1,66 @@
+package com.comphenix.protocol.error;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.comphenix.protocol.reflect.FieldAccessException;
+
+/**
+ * Represents a strongly-typed report. Subclasses should be immutable.
+ *
+ * By convention, a report must be declared as a static field publicly accessible from the sender class.
+ * @author Kristian
+ */
+public class ReportType {
+ private final String errorFormat;
+
+ /**
+ * Construct a new report type.
+ * @param errorFormat - string used to format the underlying report.
+ */
+ public ReportType(String errorFormat) {
+ this.errorFormat = errorFormat;
+ }
+
+ /**
+ * Convert the given report to a string, using the provided parameters.
+ * @param parameters - parameters to insert, or NULL to insert nothing.
+ * @return The full report in string format.
+ */
+ public String getMessage(Object[] parameters) {
+ if (parameters == null || parameters.length == 0)
+ return toString();
+ else
+ return String.format(errorFormat, parameters);
+ }
+
+ @Override
+ public String toString() {
+ return errorFormat;
+ }
+
+ /**
+ * Retrieve all publicly associated reports.
+ * @param clazz - sender class.
+ * @return All associated reports.
+ */
+ public static ReportType[] getReports(Class> clazz) {
+ if (clazz == null)
+ throw new IllegalArgumentException("clazz cannot be NULL.");
+ List result = new ArrayList();
+
+ for (Field field : clazz.getFields()) {
+ if (Modifier.isStatic(field.getModifiers()) &&
+ ReportType.class.isAssignableFrom(field.getDeclaringClass())) {
+ try {
+ result.add((ReportType) field.get(null));
+ } catch (IllegalAccessException e) {
+ throw new FieldAccessException("Unable to access field.", e);
+ }
+ }
+ }
+ return result.toArray(new ReportType[0]);
+ }
+}
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketEvent.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketEvent.java
index c35330cb..5d43ff50 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketEvent.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketEvent.java
@@ -44,6 +44,9 @@ public class PacketEvent extends EventObject implements Cancellable {
private AsyncMarker asyncMarker;
private boolean asynchronous;
+ // Whether or not a packet event is read only
+ private boolean readOnly;
+
/**
* Use the static constructors to create instances of this event.
* @param source - the event source.
@@ -114,6 +117,8 @@ public class PacketEvent extends EventObject implements Cancellable {
* @param packet - the packet that will be sent instead.
*/
public void setPacket(PacketContainer packet) {
+ if (readOnly)
+ throw new IllegalStateException("The packet event is read-only.");
this.packet = packet;
}
@@ -147,6 +152,8 @@ public class PacketEvent extends EventObject implements Cancellable {
* @param cancel - TRUE if it should be cancelled, FALSE otherwise.
*/
public void setCancelled(boolean cancel) {
+ if (readOnly)
+ throw new IllegalStateException("The packet event is read-only.");
this.cancel = cancel;
}
@@ -193,9 +200,34 @@ public class PacketEvent extends EventObject implements Cancellable {
public void setAsyncMarker(AsyncMarker asyncMarker) {
if (isAsynchronous())
throw new IllegalStateException("The marker is immutable for asynchronous events");
+ if (readOnly)
+ throw new IllegalStateException("The packet event is read-only.");
this.asyncMarker = asyncMarker;
}
+ /**
+ * Determine if the current packet event is read only.
+ *
+ * This is used to ensure that a monitor listener doesn't accidentally alter the state of the event. However,
+ * it is still possible to modify the packet itself, as it would require too many resources to verify its integrity.
+ *
+ * Thus, the packet is considered immutable if the packet event is read only.
+ * @return TRUE if it is, FALSE otherwise.
+ */
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ /**
+ * Set the read-only state of this packet event.
+ *
+ * This will be reset for every packet listener.
+ * @param readOnly - TRUE if it is read-only, FALSE otherwise.
+ */
+ public void setReadOnly(boolean readOnly) {
+ this.readOnly = readOnly;
+ }
+
/**
* Determine if the packet event has been executed asynchronously or not.
* @return TRUE if this packet event is asynchronous, FALSE otherwise.
@@ -203,7 +235,7 @@ public class PacketEvent extends EventObject implements Cancellable {
public boolean isAsynchronous() {
return asynchronous;
}
-
+
private void writeObject(ObjectOutputStream output) throws IOException {
// Default serialization
output.defaultWriteObject();
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/BukkitUnwrapper.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/BukkitUnwrapper.java
index ae444147..7a1a724d 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/BukkitUnwrapper.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/BukkitUnwrapper.java
@@ -1,181 +1,213 @@
-/*
- * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
- * Copyright (C) 2012 Kristian S. Stangeland
- *
- * This program is free software; you can redistribute it and/or modify it under the terms of the
- * GNU General Public License as published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
- * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
- * See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with this program;
- * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
- * 02111-1307 USA
- */
-
-package com.comphenix.protocol.injector;
-
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.Collection;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-import com.comphenix.protocol.ProtocolLibrary;
-import com.comphenix.protocol.injector.PacketConstructor.Unwrapper;
-import com.comphenix.protocol.reflect.FieldUtils;
-import com.comphenix.protocol.reflect.instances.DefaultInstances;
-import com.google.common.primitives.Primitives;
-
-/**
- * Represents an object capable of converting wrapped Bukkit objects into NMS objects.
- *