Merge branch 'master' into gh-pages
Conflicts: ProtocolLib/.classpath
Dieser Commit ist enthalten in:
Commit
23fb21d234
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<classpath>
|
<classpath>
|
||||||
<classpathentry including="**/*.java" kind="src" output="target/classes" path="src/main/java">
|
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||||
<attributes>
|
<attributes>
|
||||||
<attribute name="optional" value="true"/>
|
<attribute name="optional" value="true"/>
|
||||||
<attribute name="maven.pomderived" value="true"/>
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
@ -13,6 +13,11 @@
|
|||||||
</attributes>
|
</attributes>
|
||||||
</classpathentry>
|
</classpathentry>
|
||||||
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
|
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
|
||||||
|
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6">
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6">
|
||||||
<attributes>
|
<attributes>
|
||||||
<attribute name="maven.pomderived" value="true"/>
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
eclipse.preferences.version=1
|
eclipse.preferences.version=1
|
||||||
encoding//src/main/java=cp1252
|
encoding//src/main/java=cp1252
|
||||||
|
encoding//src/main/resources=cp1252
|
||||||
encoding//src/test/java=cp1252
|
encoding//src/test/java=cp1252
|
||||||
encoding/<project>=cp1252
|
encoding/<project>=cp1252
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<groupId>com.comphenix.protocol</groupId>
|
<groupId>com.comphenix.protocol</groupId>
|
||||||
<artifactId>ProtocolLib</artifactId>
|
<artifactId>ProtocolLib</artifactId>
|
||||||
<name>ProtocolLib</name>
|
<name>ProtocolLib</name>
|
||||||
<version>1.4.0</version>
|
<version>1.5.1</version>
|
||||||
<description>Provides read/write access to the Minecraft protocol.</description>
|
<description>Provides read/write access to the Minecraft protocol.</description>
|
||||||
<url>http://dev.bukkit.org/server-mods/protocollib/</url>
|
<url>http://dev.bukkit.org/server-mods/protocollib/</url>
|
||||||
<developers>
|
<developers>
|
||||||
@ -38,7 +38,7 @@
|
|||||||
<defaultGoal>clean install</defaultGoal>
|
<defaultGoal>clean install</defaultGoal>
|
||||||
<resources>
|
<resources>
|
||||||
<resource>
|
<resource>
|
||||||
<directory>src/main/java</directory>
|
<directory>src/main/resources</directory>
|
||||||
<excludes>
|
<excludes>
|
||||||
<exclude>**/*.java</exclude>
|
<exclude>**/*.java</exclude>
|
||||||
</excludes>
|
</excludes>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>com.comphenix.protocol</groupId>
|
<groupId>com.comphenix.protocol</groupId>
|
||||||
<artifactId>ProtocolLib</artifactId>
|
<artifactId>ProtocolLib</artifactId>
|
||||||
<version>1.4.0</version>
|
<version>1.5.1</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<description>Provides read/write access to the Minecraft protocol.</description>
|
<description>Provides read/write access to the Minecraft protocol.</description>
|
||||||
|
|
||||||
@ -36,7 +36,7 @@
|
|||||||
<testSourceDirectory>src/test/java</testSourceDirectory>
|
<testSourceDirectory>src/test/java</testSourceDirectory>
|
||||||
<resources>
|
<resources>
|
||||||
<resource>
|
<resource>
|
||||||
<directory>src/main/java</directory>
|
<directory>src/main/resources</directory>
|
||||||
<excludes>
|
<excludes>
|
||||||
<exclude>**/*.java</exclude>
|
<exclude>**/*.java</exclude>
|
||||||
</excludes>
|
</excludes>
|
||||||
|
@ -18,11 +18,11 @@
|
|||||||
package com.comphenix.protocol;
|
package com.comphenix.protocol;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
import com.comphenix.protocol.async.AsyncListenerHandler;
|
import com.comphenix.protocol.async.AsyncListenerHandler;
|
||||||
|
import com.comphenix.protocol.error.ErrorReporter;
|
||||||
import com.comphenix.protocol.events.PacketEvent;
|
import com.comphenix.protocol.events.PacketEvent;
|
||||||
import com.comphenix.protocol.events.PacketListener;
|
import com.comphenix.protocol.events.PacketListener;
|
||||||
|
|
||||||
@ -81,10 +81,10 @@ public interface AsynchronousManager {
|
|||||||
public abstract PacketStream getPacketStream();
|
public abstract PacketStream getPacketStream();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the default error logger.
|
* Retrieve the default error reporter.
|
||||||
* @return Default logger.
|
* @return Default reporter.
|
||||||
*/
|
*/
|
||||||
public abstract Logger getLogger();
|
public abstract ErrorReporter getErrorReporter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove listeners, close threads and transmit every delayed packet.
|
* Remove listeners, close threads and transmit every delayed packet.
|
||||||
@ -99,4 +99,22 @@ public interface AsynchronousManager {
|
|||||||
* @param packet - packet to signal.
|
* @param packet - packet to signal.
|
||||||
*/
|
*/
|
||||||
public abstract void signalPacketTransmission(PacketEvent packet);
|
public abstract void signalPacketTransmission(PacketEvent packet);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a synchronous listener that handles packets when they time out.
|
||||||
|
* @param listener - synchronous listener that will handle timed out packets.
|
||||||
|
*/
|
||||||
|
public abstract void registerTimeoutHandler(PacketListener listener);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters a given timeout listener.
|
||||||
|
* @param listener - the timeout listener to unregister.
|
||||||
|
*/
|
||||||
|
public abstract void unregisterTimeoutHandler(PacketListener listener);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a immutable list of every registered timeout handler.
|
||||||
|
* @return List of every registered timeout handler.
|
||||||
|
*/
|
||||||
|
public abstract Set<PacketListener> getTimeoutHandlers();
|
||||||
}
|
}
|
@ -0,0 +1,132 @@
|
|||||||
|
package com.comphenix.protocol;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.comphenix.protocol.async.AsyncListenerHandler;
|
||||||
|
import com.comphenix.protocol.error.ErrorReporter;
|
||||||
|
import com.comphenix.protocol.events.ListeningWhitelist;
|
||||||
|
import com.comphenix.protocol.events.PacketContainer;
|
||||||
|
import com.comphenix.protocol.injector.BukkitUnwrapper;
|
||||||
|
import com.comphenix.protocol.reflect.FieldUtils;
|
||||||
|
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||||
|
import com.comphenix.protocol.reflect.MethodUtils;
|
||||||
|
import com.comphenix.protocol.reflect.ObjectCloner;
|
||||||
|
import com.comphenix.protocol.reflect.compiler.BackgroundCompiler;
|
||||||
|
import com.comphenix.protocol.reflect.compiler.StructureCompiler;
|
||||||
|
import com.comphenix.protocol.reflect.instances.CollectionGenerator;
|
||||||
|
import com.comphenix.protocol.reflect.instances.DefaultInstances;
|
||||||
|
import com.comphenix.protocol.reflect.instances.PrimitiveGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to fix ClassLoader leaks that may lead to filling up the permanent generation.
|
||||||
|
*
|
||||||
|
* @author Kristian
|
||||||
|
*/
|
||||||
|
class CleanupStaticMembers {
|
||||||
|
|
||||||
|
private ClassLoader loader;
|
||||||
|
private ErrorReporter reporter;
|
||||||
|
|
||||||
|
public CleanupStaticMembers(ClassLoader loader, ErrorReporter reporter) {
|
||||||
|
this.loader = loader;
|
||||||
|
this.reporter = reporter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that the previous ClassLoader is not leaking.
|
||||||
|
*/
|
||||||
|
public void resetAll() {
|
||||||
|
// This list must always be updated
|
||||||
|
Class<?>[] publicClasses = {
|
||||||
|
AsyncListenerHandler.class, ListeningWhitelist.class, PacketContainer.class,
|
||||||
|
BukkitUnwrapper.class, DefaultInstances.class, CollectionGenerator.class,
|
||||||
|
PrimitiveGenerator.class, FuzzyReflection.class, MethodUtils.class,
|
||||||
|
BackgroundCompiler.class, StructureCompiler.class,
|
||||||
|
ObjectCloner.class, Packets.Server.class, Packets.Client.class
|
||||||
|
};
|
||||||
|
|
||||||
|
String[] internalClasses = {
|
||||||
|
"com.comphenix.protocol.events.SerializedOfflinePlayer",
|
||||||
|
"com.comphenix.protocol.injector.player.InjectedServerConnection",
|
||||||
|
"com.comphenix.protocol.injector.player.NetworkFieldInjector",
|
||||||
|
"com.comphenix.protocol.injector.player.NetworkObjectInjector",
|
||||||
|
"com.comphenix.protocol.injector.player.NetworkServerInjector",
|
||||||
|
"com.comphenix.protocol.injector.player.PlayerInjector",
|
||||||
|
"com.comphenix.protocol.injector.player.TemporaryPlayerFactory",
|
||||||
|
"com.comphenix.protocol.injector.EntityUtilities",
|
||||||
|
"com.comphenix.protocol.injector.MinecraftRegistry",
|
||||||
|
"com.comphenix.protocol.injector.PacketInjector",
|
||||||
|
"com.comphenix.protocol.injector.ReadPacketModifier",
|
||||||
|
"com.comphenix.protocol.injector.StructureCache",
|
||||||
|
"com.comphenix.protocol.reflect.compiler.BoxingHelper",
|
||||||
|
"com.comphenix.protocol.reflect.compiler.MethodDescriptor"
|
||||||
|
};
|
||||||
|
|
||||||
|
resetClasses(publicClasses);
|
||||||
|
resetClasses(getClasses(loader, internalClasses));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetClasses(Class<?>[] classes) {
|
||||||
|
// Reset each class one by one
|
||||||
|
for (Class<?> clazz : classes) {
|
||||||
|
resetClass(clazz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetClass(Class<?> clazz) {
|
||||||
|
for (Field field : clazz.getFields()) {
|
||||||
|
Class<?> type = field.getType();
|
||||||
|
|
||||||
|
// Only check static non-primitive fields. We also skip strings.
|
||||||
|
if (Modifier.isStatic(field.getModifiers()) &&
|
||||||
|
!type.isPrimitive() && !type.equals(String.class)) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
setFinalStatic(field, null);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
// Just inform the player
|
||||||
|
reporter.reportWarning(this, "Unable to reset field " + field.getName() + ": " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HACK! HAACK!
|
||||||
|
private static void setFinalStatic(Field field, Object newValue) throws IllegalAccessException {
|
||||||
|
int modifier = field.getModifiers();
|
||||||
|
boolean isFinal = Modifier.isFinal(modifier);
|
||||||
|
|
||||||
|
Field modifiersField = isFinal ? FieldUtils.getField(Field.class, "modifiers", true) : null;
|
||||||
|
|
||||||
|
// We have to remove the final field first
|
||||||
|
if (isFinal) {
|
||||||
|
FieldUtils.writeField(modifiersField, field, modifier & ~Modifier.FINAL, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we can safely modify the field
|
||||||
|
FieldUtils.writeStaticField(field, newValue, true);
|
||||||
|
|
||||||
|
// Revert modifier
|
||||||
|
if (isFinal) {
|
||||||
|
FieldUtils.writeField(modifiersField, field, modifier, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Class<?>[] getClasses(ClassLoader loader, String[] names) {
|
||||||
|
List<Class<?>> output = new ArrayList<Class<?>>();
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
51
ProtocolLib/src/main/java/com/comphenix/protocol/CommandBase.java
Normale Datei
51
ProtocolLib/src/main/java/com/comphenix/protocol/CommandBase.java
Normale Datei
@ -0,0 +1,51 @@
|
|||||||
|
package com.comphenix.protocol;
|
||||||
|
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for all our commands.
|
||||||
|
*
|
||||||
|
* @author Kristian
|
||||||
|
*/
|
||||||
|
abstract class CommandBase implements CommandExecutor {
|
||||||
|
|
||||||
|
public static final String PERMISSION_ADMIN = "protocol.admin";
|
||||||
|
|
||||||
|
private String permission;
|
||||||
|
private String name;
|
||||||
|
private int minimumArgumentCount;
|
||||||
|
|
||||||
|
public CommandBase(String permission, String name) {
|
||||||
|
this(permission, name, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandBase(String permission, String name, int minimumArgumentCount) {
|
||||||
|
this.name = name;
|
||||||
|
this.permission = permission;
|
||||||
|
this.minimumArgumentCount = minimumArgumentCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
// Make sure we're dealing with the correct command
|
||||||
|
if (!command.getName().equalsIgnoreCase(name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!sender.hasPermission(permission)) {
|
||||||
|
sender.sendMessage(ChatColor.RED + "You haven't got permission to run this command.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check argument length
|
||||||
|
if (args != null && args.length >= minimumArgumentCount) {
|
||||||
|
return handleCommand(sender, args);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract boolean handleCommand(CommandSender sender, String[] args);
|
||||||
|
}
|
512
ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java
Normale Datei
512
ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java
Normale Datei
@ -0,0 +1,512 @@
|
|||||||
|
package com.comphenix.protocol;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import net.minecraft.server.Packet;
|
||||||
|
import net.sf.cglib.proxy.Factory;
|
||||||
|
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
|
import com.comphenix.protocol.concurrency.AbstractIntervalTree;
|
||||||
|
import com.comphenix.protocol.error.ErrorReporter;
|
||||||
|
import com.comphenix.protocol.events.ConnectionSide;
|
||||||
|
import com.comphenix.protocol.events.ListenerPriority;
|
||||||
|
import com.comphenix.protocol.events.ListeningWhitelist;
|
||||||
|
import com.comphenix.protocol.events.PacketEvent;
|
||||||
|
import com.comphenix.protocol.events.PacketListener;
|
||||||
|
import com.comphenix.protocol.injector.GamePhase;
|
||||||
|
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||||
|
import com.comphenix.protocol.reflect.PrettyPrinter;
|
||||||
|
import com.comphenix.protocol.utility.ChatExtensions;
|
||||||
|
import com.google.common.collect.DiscreteDomains;
|
||||||
|
import com.google.common.collect.Range;
|
||||||
|
import com.google.common.collect.Ranges;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the "packet" debug command.
|
||||||
|
*
|
||||||
|
* @author Kristian
|
||||||
|
*/
|
||||||
|
class CommandPacket extends CommandBase {
|
||||||
|
|
||||||
|
private interface DetailedPacketListener extends PacketListener {
|
||||||
|
/**
|
||||||
|
* Determine whether or not the given packet listener is detailed or not.
|
||||||
|
* @return TRUE if it is detailed, FALSE otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isDetailed();
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SubCommand {
|
||||||
|
ADD, REMOVE, NAMES, PAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of this command.
|
||||||
|
*/
|
||||||
|
public static final String NAME = "packet";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of lines per page.
|
||||||
|
*/
|
||||||
|
public static final int PAGE_LINE_COUNT = 9;
|
||||||
|
|
||||||
|
private Plugin plugin;
|
||||||
|
private Logger logger;
|
||||||
|
private ErrorReporter reporter;
|
||||||
|
private ProtocolManager manager;
|
||||||
|
|
||||||
|
private ChatExtensions chatter;
|
||||||
|
|
||||||
|
// Paged message
|
||||||
|
private Map<CommandSender, List<String>> pagedMessage = new WeakHashMap<CommandSender, List<String>>();
|
||||||
|
|
||||||
|
// Registered packet listeners
|
||||||
|
private AbstractIntervalTree<Integer, DetailedPacketListener> clientListeners = createTree(ConnectionSide.CLIENT_SIDE);
|
||||||
|
private AbstractIntervalTree<Integer, DetailedPacketListener> serverListeners = createTree(ConnectionSide.SERVER_SIDE);
|
||||||
|
|
||||||
|
public CommandPacket(Plugin plugin, Logger logger, ErrorReporter reporter, ProtocolManager manager) {
|
||||||
|
super(CommandBase.PERMISSION_ADMIN, NAME, 2);
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.logger = logger;
|
||||||
|
this.reporter = reporter;
|
||||||
|
this.manager = manager;
|
||||||
|
this.chatter = new ChatExtensions(manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a packet listener interval tree.
|
||||||
|
* @return Construct the tree.
|
||||||
|
*/
|
||||||
|
private AbstractIntervalTree<Integer, DetailedPacketListener> createTree(final ConnectionSide side) {
|
||||||
|
return new AbstractIntervalTree<Integer, DetailedPacketListener>() {
|
||||||
|
@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<Integer> key = added.getKey();
|
||||||
|
DetailedPacketListener listener = added.getValue();
|
||||||
|
DetailedPacketListener corrected = createPacketListener(
|
||||||
|
side, key.lowerEndpoint(), key.upperEndpoint(), listener.isDetailed());
|
||||||
|
|
||||||
|
added.setValue(corrected);
|
||||||
|
|
||||||
|
if (corrected != null) {
|
||||||
|
manager.addPacketListener(corrected);
|
||||||
|
} else {
|
||||||
|
// Never mind
|
||||||
|
remove(key.lowerEndpoint(), key.upperEndpoint());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onEntryRemoved(Entry removed) {
|
||||||
|
// Remove the listener
|
||||||
|
if (removed != null) {
|
||||||
|
DetailedPacketListener listener = removed.getValue();
|
||||||
|
|
||||||
|
if (listener != null) {
|
||||||
|
manager.removePacketListener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message without invoking the packet listeners.
|
||||||
|
* @param receiver - the player to send it to.
|
||||||
|
* @param message - the message to send.
|
||||||
|
* @return TRUE if the message was sent successfully, FALSE otherwise.
|
||||||
|
*/
|
||||||
|
public void sendMessageSilently(CommandSender receiver, String message) {
|
||||||
|
try {
|
||||||
|
chatter.sendMessageSilently(receiver, message);
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
reporter.reportDetailed(this, "Cannot send chat message.", e, receiver, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast a message without invoking any packet listeners.
|
||||||
|
* @param message - message to send.
|
||||||
|
* @param permission - permission required to receieve the message. NULL to target everyone.
|
||||||
|
*/
|
||||||
|
public void broadcastMessageSilently(String message, String permission) {
|
||||||
|
try {
|
||||||
|
chatter.broadcastMessageSilently(message, permission);
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
reporter.reportDetailed(this, "Cannot send chat message.", e, message, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printPage(CommandSender sender, int pageIndex) {
|
||||||
|
List<String> 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: /<command> 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<Range<Integer>> ranges = RangeParser.getRanges(args, 2, lastIndex, Ranges.closed(0, 255));
|
||||||
|
|
||||||
|
if (ranges.isEmpty()) {
|
||||||
|
// Use every packet ID
|
||||||
|
ranges.add(Ranges.closed(0, 255));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform commands
|
||||||
|
if (subCommand == SubCommand.ADD) {
|
||||||
|
executeAddCommand(sender, side, detailed, ranges);
|
||||||
|
} else if (subCommand == SubCommand.REMOVE) {
|
||||||
|
executeRemoveCommand(sender, side, detailed, ranges);
|
||||||
|
} else if (subCommand == SubCommand.NAMES) {
|
||||||
|
executeNamesCommand(sender, side, ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
sendMessageSilently(sender, ChatColor.RED + "Cannot parse number: " + e.getMessage());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
sendMessageSilently(sender, ChatColor.RED + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeAddCommand(CommandSender sender, ConnectionSide side, Boolean detailed, List<Range<Integer>> ranges) {
|
||||||
|
for (Range<Integer> 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<Range<Integer>> ranges) {
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
// Remove each packet listener
|
||||||
|
for (Range<Integer> 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<Range<Integer>> ranges) {
|
||||||
|
Set<Integer> named = getNamedPackets(side);
|
||||||
|
List<String> messages = new ArrayList<String>();
|
||||||
|
|
||||||
|
// Print the equivalent name of every given ID
|
||||||
|
for (Range<Integer> 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<Integer> getValidPackets(ConnectionSide side) throws FieldAccessException {
|
||||||
|
if (side.isForClient())
|
||||||
|
return Packets.Client.getSupported();
|
||||||
|
else if (side.isForServer())
|
||||||
|
return Packets.Server.getSupported();
|
||||||
|
else
|
||||||
|
throw new IllegalArgumentException("Illegal side: " + side);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Integer> getNamedPackets(ConnectionSide side) {
|
||||||
|
|
||||||
|
Set<Integer> valids = null;
|
||||||
|
Set<Integer> result = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
valids = getValidPackets(side);
|
||||||
|
} catch (FieldAccessException e) {
|
||||||
|
valids = Ranges.closed(0, 255).asSet(DiscreteDomains.integers());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check connection side
|
||||||
|
if (side.isForClient())
|
||||||
|
result = Packets.Client.getRegistry().values();
|
||||||
|
else if (side.isForServer())
|
||||||
|
result = Packets.Server.getRegistry().values();
|
||||||
|
else
|
||||||
|
throw new IllegalArgumentException("Illegal side: " + side);
|
||||||
|
|
||||||
|
// Remove invalid packets
|
||||||
|
result.retainAll(valids);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DetailedPacketListener createPacketListener(final ConnectionSide side, int idStart, int idStop, final boolean detailed) {
|
||||||
|
|
||||||
|
Set<Integer> range = Ranges.closed(idStart, idStop).asSet(DiscreteDomains.integers());
|
||||||
|
Set<Integer> packets;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Only use supported packet IDs
|
||||||
|
packets = new HashSet<Integer>(getValidPackets(side));
|
||||||
|
packets.retainAll(range);
|
||||||
|
|
||||||
|
} catch (FieldAccessException e) {
|
||||||
|
// Don't filter anything then
|
||||||
|
packets = range;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore empty sets
|
||||||
|
if (packets.isEmpty())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Create the listener we will be using
|
||||||
|
final ListeningWhitelist whitelist = new ListeningWhitelist(ListenerPriority.MONITOR, packets, GamePhase.BOTH);
|
||||||
|
|
||||||
|
return new DetailedPacketListener() {
|
||||||
|
@Override
|
||||||
|
public void onPacketSending(PacketEvent event) {
|
||||||
|
if (side.isForServer()) {
|
||||||
|
printInformation(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPacketReceiving(PacketEvent event) {
|
||||||
|
if (side.isForClient()) {
|
||||||
|
printInformation(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printInformation(PacketEvent event) {
|
||||||
|
String verb = side.isForClient() ? "Received" : "Sent";
|
||||||
|
String shortDescription = String.format(
|
||||||
|
"%s %s (%s) from %s",
|
||||||
|
verb,
|
||||||
|
Packets.getDeclaredName(event.getPacketID()),
|
||||||
|
event.getPacketID(),
|
||||||
|
event.getPlayer().getName()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Detailed will print the packet's content too
|
||||||
|
if (detailed) {
|
||||||
|
try {
|
||||||
|
Packet packet = event.getPacket().getHandle();
|
||||||
|
Class<?> clazz = packet.getClass();
|
||||||
|
|
||||||
|
// Get the first Minecraft super class
|
||||||
|
while ((!clazz.getName().startsWith("net.minecraft.server") ||
|
||||||
|
Factory.class.isAssignableFrom(clazz)) && clazz != Object.class) {
|
||||||
|
clazz = clazz.getSuperclass();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(shortDescription + ":\n" +
|
||||||
|
PrettyPrinter.printObject(packet, clazz, Packet.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
logger.log(Level.WARNING, "Unable to use reflection.", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info(shortDescription + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListeningWhitelist getSendingWhitelist() {
|
||||||
|
return side.isForServer() ? whitelist : ListeningWhitelist.EMPTY_WHITELIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListeningWhitelist getReceivingWhitelist() {
|
||||||
|
return side.isForClient() ? whitelist : ListeningWhitelist.EMPTY_WHITELIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Plugin getPlugin() {
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDetailed() {
|
||||||
|
return detailed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public DetailedPacketListener addPacketListeners(ConnectionSide side, int idStart, int idStop, boolean detailed) {
|
||||||
|
DetailedPacketListener listener = createPacketListener(side, idStart, idStop, detailed);
|
||||||
|
|
||||||
|
// The trees will manage the listeners for us
|
||||||
|
if (listener != null) {
|
||||||
|
getListenerTree(side).put(idStart, idStop, listener);
|
||||||
|
return listener;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("No packets found in the range " + idStart + " - " + idStop + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<AbstractIntervalTree<Integer, DetailedPacketListener>.Entry> removePacketListeners(
|
||||||
|
ConnectionSide side, int idStart, int idStop, boolean detailed) {
|
||||||
|
|
||||||
|
// The interval tree will automatically remove the listeners for us
|
||||||
|
return getListenerTree(side).remove(idStart, idStop);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AbstractIntervalTree<Integer, DetailedPacketListener> getListenerTree(ConnectionSide side) {
|
||||||
|
if (side.isForClient())
|
||||||
|
return clientListeners;
|
||||||
|
else if (side.isForServer())
|
||||||
|
return serverListeners;
|
||||||
|
else
|
||||||
|
throw new IllegalArgumentException("Not a legal connection side.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private SubCommand parseCommand(String[] args, int index) {
|
||||||
|
String text = args[index].toLowerCase();
|
||||||
|
|
||||||
|
// Parse this too
|
||||||
|
if ("add".startsWith(text))
|
||||||
|
return SubCommand.ADD;
|
||||||
|
else if ("remove".startsWith(text))
|
||||||
|
return SubCommand.REMOVE;
|
||||||
|
else if ("names".startsWith(text))
|
||||||
|
return SubCommand.NAMES;
|
||||||
|
else if ("page".startsWith(text))
|
||||||
|
return SubCommand.PAGE;
|
||||||
|
else
|
||||||
|
throw new IllegalArgumentException(text + " is not a valid sub command. Must be add or remove.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConnectionSide parseSide(String[] args, int index, ConnectionSide defaultValue) {
|
||||||
|
if (index < args.length) {
|
||||||
|
String text = args[index].toLowerCase();
|
||||||
|
|
||||||
|
// Parse the side gracefully
|
||||||
|
if ("client".startsWith(text))
|
||||||
|
return ConnectionSide.CLIENT_SIDE;
|
||||||
|
else if ("server".startsWith(text))
|
||||||
|
return ConnectionSide.SERVER_SIDE;
|
||||||
|
else
|
||||||
|
throw new IllegalArgumentException(text + " is not a connection side.");
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a boolean
|
||||||
|
private Boolean parseBoolean(String[] args, String parameterName, int index) {
|
||||||
|
if (index < args.length) {
|
||||||
|
if (args[index].equalsIgnoreCase("true"))
|
||||||
|
return true;
|
||||||
|
else if (args[index].equalsIgnoreCase(parameterName))
|
||||||
|
return true;
|
||||||
|
else if (args[index].equalsIgnoreCase("false"))
|
||||||
|
return false;
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package com.comphenix.protocol;
|
||||||
|
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
|
import com.comphenix.protocol.metrics.Updater;
|
||||||
|
import com.comphenix.protocol.metrics.Updater.UpdateResult;
|
||||||
|
import com.comphenix.protocol.metrics.Updater.UpdateType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the "protocol" administration command.
|
||||||
|
*
|
||||||
|
* @author Kristian
|
||||||
|
*/
|
||||||
|
class CommandProtocol extends CommandBase {
|
||||||
|
/**
|
||||||
|
* Name of this command.
|
||||||
|
*/
|
||||||
|
public static final String NAME = "protocol";
|
||||||
|
|
||||||
|
private Plugin plugin;
|
||||||
|
private Updater updater;
|
||||||
|
private ProtocolConfig config;
|
||||||
|
|
||||||
|
public CommandProtocol(Plugin plugin, Updater updater, ProtocolConfig config) {
|
||||||
|
super(CommandBase.PERMISSION_ADMIN, NAME, 1);
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.updater = updater;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean handleCommand(CommandSender sender, String[] args) {
|
||||||
|
String subCommand = args[0];
|
||||||
|
|
||||||
|
// Only return TRUE if we executed the correct command
|
||||||
|
if (subCommand.equalsIgnoreCase("config") || subCommand.equalsIgnoreCase("reload"))
|
||||||
|
reloadConfiguration(sender);
|
||||||
|
else if (subCommand.equalsIgnoreCase("check"))
|
||||||
|
checkVersion(sender);
|
||||||
|
else if (subCommand.equalsIgnoreCase("update"))
|
||||||
|
updateVersion(sender);
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkVersion(final CommandSender sender) {
|
||||||
|
// Perform on an async thread
|
||||||
|
plugin.getServer().getScheduler().scheduleAsyncDelayedTask(plugin, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
UpdateResult result = updater.update(UpdateType.NO_DOWNLOAD, true);
|
||||||
|
sender.sendMessage(ChatColor.BLUE + "[ProtocolLib] " + result.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateVersion(final CommandSender sender) {
|
||||||
|
// Perform on an async thread
|
||||||
|
plugin.getServer().getScheduler().scheduleAsyncDelayedTask(plugin, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
UpdateResult result = updater.update(UpdateType.DEFAULT, true);
|
||||||
|
sender.sendMessage(ChatColor.BLUE + "[ProtocolLib] " + result.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent further automatic updates until the next delay.
|
||||||
|
*/
|
||||||
|
public void updateFinished() {
|
||||||
|
long currentTime = System.currentTimeMillis() / ProtocolLibrary.MILLI_PER_SECOND;
|
||||||
|
|
||||||
|
config.setAutoLastTime(currentTime);
|
||||||
|
config.saveAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reloadConfiguration(CommandSender sender) {
|
||||||
|
plugin.reloadConfig();
|
||||||
|
sender.sendMessage(ChatColor.BLUE + "Reloaded configuration!");
|
||||||
|
}
|
||||||
|
}
|
188
ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java
Normale Datei
188
ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java
Normale Datei
@ -0,0 +1,188 @@
|
|||||||
|
package com.comphenix.protocol;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import org.bukkit.configuration.Configuration;
|
||||||
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the configuration of ProtocolLib.
|
||||||
|
*
|
||||||
|
* @author Kristian
|
||||||
|
*/
|
||||||
|
class ProtocolConfig {
|
||||||
|
|
||||||
|
private static final String SECTION_GLOBAL = "global";
|
||||||
|
private static final String SECTION_AUTOUPDATER = "auto updater";
|
||||||
|
|
||||||
|
private static final String METRICS_ENABLED = "metrics";
|
||||||
|
|
||||||
|
private static final String UPDATER_NOTIFY = "notify";
|
||||||
|
private static final String UPDATER_DOWNLAD = "download";
|
||||||
|
private static final String UPDATER_DELAY = "delay";
|
||||||
|
private static final String UPDATER_LAST_TIME = "last";
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
private static final long DEFAULT_UPDATER_DELAY = 60;
|
||||||
|
|
||||||
|
private Plugin plugin;
|
||||||
|
private Configuration config;
|
||||||
|
private boolean loadingSections;
|
||||||
|
|
||||||
|
private ConfigurationSection global;
|
||||||
|
private ConfigurationSection updater;
|
||||||
|
|
||||||
|
public ProtocolConfig(Plugin plugin) {
|
||||||
|
this(plugin, plugin.getConfig());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProtocolConfig(Plugin plugin, Configuration config) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
reloadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload configuration file.
|
||||||
|
*/
|
||||||
|
public void reloadConfig() {
|
||||||
|
this.config = plugin.getConfig();
|
||||||
|
loadSections(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load data sections.
|
||||||
|
* @param copyDefaults - whether or not to copy configuration defaults.
|
||||||
|
*/
|
||||||
|
private void loadSections(boolean copyDefaults) {
|
||||||
|
if (loadingSections)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (config != null) {
|
||||||
|
global = config.getConfigurationSection(SECTION_GLOBAL);
|
||||||
|
}
|
||||||
|
if (global != null) {
|
||||||
|
updater = global.getConfigurationSection(SECTION_AUTOUPDATER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Automatically copy defaults
|
||||||
|
if (copyDefaults && (!getFile().exists() || global == null || updater == null)) {
|
||||||
|
loadingSections = true;
|
||||||
|
|
||||||
|
if (config != null)
|
||||||
|
config.options().copyDefaults(true);
|
||||||
|
plugin.saveDefaultConfig();
|
||||||
|
config = plugin.getConfig();
|
||||||
|
|
||||||
|
loadingSections = false;
|
||||||
|
loadSections(false);
|
||||||
|
|
||||||
|
// Inform the user
|
||||||
|
System.out.println("[ProtocolLib] Created default configuration.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a reference to the configuration file.
|
||||||
|
* @return Configuration file on disk.
|
||||||
|
*/
|
||||||
|
public File getFile() {
|
||||||
|
return new File(plugin.getDataFolder(), "config.yml");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve whether or not ProtocolLib should determine if a new version has been released.
|
||||||
|
* @return TRUE if it should do this automatically, FALSE otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isAutoNotify() {
|
||||||
|
return updater.getBoolean(UPDATER_NOTIFY, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether or not ProtocolLib should determine if a new version has been released.
|
||||||
|
* @param value - TRUE to do this automatically, FALSE otherwise.
|
||||||
|
*/
|
||||||
|
public void setAutoNotify(boolean value) {
|
||||||
|
updater.set(UPDATER_NOTIFY, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve whether or not ProtocolLib should automatically download the new version.
|
||||||
|
* @return TRUE if it should, FALSE otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isAutoDownload() {
|
||||||
|
return updater != null && updater.getBoolean(UPDATER_DOWNLAD, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether or not ProtocolLib should automatically download the new version.
|
||||||
|
* @param value - TRUE if it should. FALSE otherwise.
|
||||||
|
*/
|
||||||
|
public void setAutoDownload(boolean value) {
|
||||||
|
updater.set(UPDATER_DOWNLAD, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the amount of time to wait until checking for a new update.
|
||||||
|
* @return The amount of time to wait.
|
||||||
|
*/
|
||||||
|
public long getAutoDelay() {
|
||||||
|
// Note that the delay must be greater than 59 seconds
|
||||||
|
return Math.max(updater.getInt(UPDATER_DELAY, 0), DEFAULT_UPDATER_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the amount of time to wait until checking for a new update.
|
||||||
|
* <p>
|
||||||
|
* This time must be greater than 59 seconds.
|
||||||
|
* @param delaySeconds - the amount of time to wait.
|
||||||
|
*/
|
||||||
|
public void setAutoDelay(long delaySeconds) {
|
||||||
|
// Silently fix the delay
|
||||||
|
if (delaySeconds < DEFAULT_UPDATER_DELAY)
|
||||||
|
delaySeconds = DEFAULT_UPDATER_DELAY;
|
||||||
|
updater.set(UPDATER_DELAY, delaySeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the last time we updated, in seconds since 1970.01.01 00:00.
|
||||||
|
* @return Last update time.
|
||||||
|
*/
|
||||||
|
public long getAutoLastTime() {
|
||||||
|
return updater.getLong(UPDATER_LAST_TIME, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve whether or not metrics is enabled.
|
||||||
|
* @return TRUE if metrics is enabled, FALSE otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isMetricsEnabled() {
|
||||||
|
return global.getBoolean(METRICS_ENABLED, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether or not metrics is enabled.
|
||||||
|
* <p>
|
||||||
|
* This setting will take effect next time ProtocolLib is started.
|
||||||
|
*
|
||||||
|
* @param enabled - whether or not metrics is enabled.
|
||||||
|
*/
|
||||||
|
public void setMetricsEnabled(boolean enabled) {
|
||||||
|
global.set(METRICS_ENABLED, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the last time we updated, in seconds since 1970.01.01 00:00.
|
||||||
|
* @param lastTimeSeconds - new last update time.
|
||||||
|
*/
|
||||||
|
public void setAutoLastTime(long lastTimeSeconds) {
|
||||||
|
updater.set(UPDATER_LAST_TIME, lastTimeSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the current configuration file.
|
||||||
|
*/
|
||||||
|
public void saveAll() {
|
||||||
|
plugin.saveConfig();
|
||||||
|
}
|
||||||
|
}
|
@ -18,7 +18,8 @@
|
|||||||
package com.comphenix.protocol;
|
package com.comphenix.protocol;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Handler;
|
||||||
|
import java.util.logging.LogRecord;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import org.bukkit.Server;
|
import org.bukkit.Server;
|
||||||
@ -26,12 +27,12 @@ import org.bukkit.plugin.PluginManager;
|
|||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
import com.comphenix.protocol.async.AsyncFilterManager;
|
import com.comphenix.protocol.async.AsyncFilterManager;
|
||||||
import com.comphenix.protocol.events.ConnectionSide;
|
import com.comphenix.protocol.error.DetailedErrorReporter;
|
||||||
import com.comphenix.protocol.events.MonitorAdapter;
|
import com.comphenix.protocol.error.ErrorReporter;
|
||||||
import com.comphenix.protocol.events.PacketEvent;
|
|
||||||
import com.comphenix.protocol.injector.DelayedSingleTask;
|
import com.comphenix.protocol.injector.DelayedSingleTask;
|
||||||
import com.comphenix.protocol.injector.PacketFilterManager;
|
import com.comphenix.protocol.injector.PacketFilterManager;
|
||||||
import com.comphenix.protocol.metrics.Statistics;
|
import com.comphenix.protocol.metrics.Statistics;
|
||||||
|
import com.comphenix.protocol.metrics.Updater;
|
||||||
import com.comphenix.protocol.reflect.compiler.BackgroundCompiler;
|
import com.comphenix.protocol.reflect.compiler.BackgroundCompiler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,11 +42,18 @@ import com.comphenix.protocol.reflect.compiler.BackgroundCompiler;
|
|||||||
*/
|
*/
|
||||||
public class ProtocolLibrary extends JavaPlugin {
|
public class ProtocolLibrary extends JavaPlugin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of milliseconds per second.
|
||||||
|
*/
|
||||||
|
static final long MILLI_PER_SECOND = 1000;
|
||||||
|
|
||||||
|
private static final String PERMISSION_INFO = "protocol.info";
|
||||||
|
|
||||||
// There should only be one protocol manager, so we'll make it static
|
// There should only be one protocol manager, so we'll make it static
|
||||||
private static PacketFilterManager protocolManager;
|
private static PacketFilterManager protocolManager;
|
||||||
|
|
||||||
// Error logger
|
// Error reporter
|
||||||
private Logger logger;
|
private ErrorReporter reporter;
|
||||||
|
|
||||||
// Metrics and statistisc
|
// Metrics and statistisc
|
||||||
private Statistics statistisc;
|
private Statistics statistisc;
|
||||||
@ -62,27 +70,109 @@ public class ProtocolLibrary extends JavaPlugin {
|
|||||||
// Used to unhook players after a delay
|
// Used to unhook players after a delay
|
||||||
private DelayedSingleTask unhookTask;
|
private DelayedSingleTask unhookTask;
|
||||||
|
|
||||||
// Used for debugging
|
// Settings/options
|
||||||
private boolean debugListener;
|
private ProtocolConfig config;
|
||||||
|
|
||||||
|
// Updater
|
||||||
|
private Updater updater;
|
||||||
|
|
||||||
|
// Logger
|
||||||
|
private Logger logger;
|
||||||
|
|
||||||
|
// Commands
|
||||||
|
private CommandProtocol commandProtocol;
|
||||||
|
private CommandPacket commandPacket;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoad() {
|
public void onLoad() {
|
||||||
|
// Load configuration
|
||||||
logger = getLoggerSafely();
|
logger = getLoggerSafely();
|
||||||
|
|
||||||
|
// Add global parameters
|
||||||
|
DetailedErrorReporter reporter = new DetailedErrorReporter();
|
||||||
|
updater = new Updater(this, logger, "protocollib", getFile(), "protocol.info");
|
||||||
|
|
||||||
|
try {
|
||||||
|
config = new ProtocolConfig(this);
|
||||||
|
} catch (Exception e) {
|
||||||
|
reporter.reportWarning(this, "Cannot load configuration", e);
|
||||||
|
|
||||||
|
// Load it again
|
||||||
|
deleteConfig();
|
||||||
|
config = new ProtocolConfig(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
unhookTask = new DelayedSingleTask(this);
|
unhookTask = new DelayedSingleTask(this);
|
||||||
protocolManager = new PacketFilterManager(getClassLoader(), getServer(), unhookTask, logger);
|
protocolManager = new PacketFilterManager(getClassLoader(), getServer(), unhookTask, reporter);
|
||||||
|
reporter.addGlobalParameter("manager", protocolManager);
|
||||||
|
|
||||||
|
// Initialize command handlers
|
||||||
|
commandProtocol = new CommandProtocol(this, updater, config);
|
||||||
|
commandPacket = new CommandPacket(this, logger, reporter, protocolManager);
|
||||||
|
|
||||||
|
// Send logging information to player listeners too
|
||||||
|
broadcastUsers(PERMISSION_INFO);
|
||||||
|
|
||||||
|
} catch (Throwable e) {
|
||||||
|
reporter.reportDetailed(this, "Cannot load ProtocolLib.", e, protocolManager);
|
||||||
|
disablePlugin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteConfig() {
|
||||||
|
config.getFile().delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reloadConfig() {
|
||||||
|
super.reloadConfig();
|
||||||
|
// Reload configuration
|
||||||
|
if (config != null) {
|
||||||
|
config.reloadConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void broadcastUsers(final String permission) {
|
||||||
|
// Broadcast information to every user too
|
||||||
|
logger.addHandler(new Handler() {
|
||||||
|
@Override
|
||||||
|
public void publish(LogRecord record) {
|
||||||
|
commandPacket.broadcastMessageSilently(record.getMessage(), permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flush() {
|
||||||
|
// Not needed.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws SecurityException {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
|
try {
|
||||||
Server server = getServer();
|
Server server = getServer();
|
||||||
PluginManager manager = server.getPluginManager();
|
PluginManager manager = server.getPluginManager();
|
||||||
|
|
||||||
|
// Don't do anything else!
|
||||||
|
if (manager == null)
|
||||||
|
return;
|
||||||
|
|
||||||
// Initialize background compiler
|
// Initialize background compiler
|
||||||
if (backgroundCompiler == null) {
|
if (backgroundCompiler == null) {
|
||||||
backgroundCompiler = new BackgroundCompiler(getClassLoader());
|
backgroundCompiler = new BackgroundCompiler(getClassLoader());
|
||||||
BackgroundCompiler.setInstance(backgroundCompiler);
|
BackgroundCompiler.setInstance(backgroundCompiler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up command handlers
|
||||||
|
getCommand(CommandProtocol.NAME).setExecutor(commandProtocol);
|
||||||
|
getCommand(CommandPacket.NAME).setExecutor(commandPacket);
|
||||||
|
|
||||||
// Notify server managers of incompatible plugins
|
// Notify server managers of incompatible plugins
|
||||||
checkForIncompatibility(manager);
|
checkForIncompatibility(manager);
|
||||||
|
|
||||||
@ -90,50 +180,32 @@ public class ProtocolLibrary extends JavaPlugin {
|
|||||||
protocolManager.registerEvents(manager, this);
|
protocolManager.registerEvents(manager, this);
|
||||||
|
|
||||||
// Worker that ensures that async packets are eventually sent
|
// Worker that ensures that async packets are eventually sent
|
||||||
|
// It also performs the update check.
|
||||||
createAsyncTask(server);
|
createAsyncTask(server);
|
||||||
//toggleDebugListener();
|
|
||||||
|
} catch (Throwable e) {
|
||||||
|
reporter.reportDetailed(this, "Cannot enable ProtocolLib.", e);
|
||||||
|
disablePlugin();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Try to enable statistics
|
// Try to enable statistics
|
||||||
try {
|
try {
|
||||||
|
if (config.isMetricsEnabled()) {
|
||||||
statistisc = new Statistics(this);
|
statistisc = new Statistics(this);
|
||||||
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.log(Level.SEVERE, "Unable to enable metrics.", e);
|
reporter.reportDetailed(this, "Unable to enable metrics.", e, statistisc);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
logger.log(Level.SEVERE, "Metrics cannot be enabled. Incompatible Bukkit version.", e);
|
reporter.reportDetailed(this, "Metrics cannot be enabled. Incompatible Bukkit version.", e, statistisc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle a listener that prints every sent and received packet.
|
* Disable the current plugin.
|
||||||
*/
|
*/
|
||||||
void toggleDebugListener() {
|
private void disablePlugin() {
|
||||||
|
getServer().getPluginManager().disablePlugin(this);
|
||||||
if (debugListener) {
|
|
||||||
protocolManager.removePacketListeners(this);
|
|
||||||
} else {
|
|
||||||
// DEBUG DEBUG
|
|
||||||
protocolManager.addPacketListener(new MonitorAdapter(this, ConnectionSide.BOTH, logger) {
|
|
||||||
@Override
|
|
||||||
public void onPacketReceiving(PacketEvent event) {
|
|
||||||
Object handle = event.getPacket().getHandle();
|
|
||||||
|
|
||||||
System.out.println(String.format(
|
|
||||||
"RECEIVING %s@%s from %s.",
|
|
||||||
handle.getClass().getSimpleName(), handle.hashCode(), event.getPlayer().getName()
|
|
||||||
));
|
|
||||||
};
|
|
||||||
@Override
|
|
||||||
public void onPacketSending(PacketEvent event) {
|
|
||||||
Object handle = event.getPacket().getHandle();
|
|
||||||
|
|
||||||
System.out.println(String.format(
|
|
||||||
"SENDING %s@%s from %s.",
|
|
||||||
handle.getClass().getSimpleName(), handle.hashCode(), event.getPlayer().getName()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
debugListener = !debugListener;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createAsyncTask(Server server) {
|
private void createAsyncTask(Server server) {
|
||||||
@ -149,16 +221,35 @@ public class ProtocolLibrary extends JavaPlugin {
|
|||||||
|
|
||||||
// We KNOW we're on the main thread at the moment
|
// We KNOW we're on the main thread at the moment
|
||||||
manager.sendProcessedPackets(tickCounter++, true);
|
manager.sendProcessedPackets(tickCounter++, true);
|
||||||
|
|
||||||
|
// Check for updates too
|
||||||
|
checkUpdates();
|
||||||
}
|
}
|
||||||
}, ASYNC_PACKET_DELAY, ASYNC_PACKET_DELAY);
|
}, ASYNC_PACKET_DELAY, ASYNC_PACKET_DELAY);
|
||||||
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
if (asyncPacketTask == -1) {
|
if (asyncPacketTask == -1) {
|
||||||
logger.log(Level.SEVERE, "Unable to create packet timeout task.", e);
|
reporter.reportDetailed(this, "Unable to create packet timeout task.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void checkUpdates() {
|
||||||
|
// Ignore milliseconds - it's pointless
|
||||||
|
long currentTime = System.currentTimeMillis() / MILLI_PER_SECOND;
|
||||||
|
|
||||||
|
// Should we update?
|
||||||
|
if (currentTime > config.getAutoLastTime() + config.getAutoDelay()) {
|
||||||
|
// Initiate the update as if it came from the console
|
||||||
|
if (config.isAutoDownload())
|
||||||
|
commandProtocol.updateVersion(getServer().getConsoleSender());
|
||||||
|
else if (config.isAutoNotify())
|
||||||
|
commandProtocol.checkVersion(getServer().getConsoleSender());
|
||||||
|
else
|
||||||
|
commandProtocol.updateFinished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void checkForIncompatibility(PluginManager manager) {
|
private void checkForIncompatibility(PluginManager manager) {
|
||||||
// Plugin authors: Notify me to remove these
|
// Plugin authors: Notify me to remove these
|
||||||
String[] incompatiblePlugins = {};
|
String[] incompatiblePlugins = {};
|
||||||
@ -166,7 +257,7 @@ public class ProtocolLibrary extends JavaPlugin {
|
|||||||
for (String plugin : incompatiblePlugins) {
|
for (String plugin : incompatiblePlugins) {
|
||||||
if (manager.getPlugin(plugin) != null) {
|
if (manager.getPlugin(plugin) != null) {
|
||||||
// Check for versions, ect.
|
// Check for versions, ect.
|
||||||
logger.severe("Detected incompatible plugin: " + plugin);
|
reporter.reportWarning(this, "Detected incompatible plugin: " + plugin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,6 +281,24 @@ public class ProtocolLibrary extends JavaPlugin {
|
|||||||
protocolManager.close();
|
protocolManager.close();
|
||||||
protocolManager = null;
|
protocolManager = null;
|
||||||
statistisc = null;
|
statistisc = null;
|
||||||
|
|
||||||
|
// Leaky ClassLoader begone!
|
||||||
|
CleanupStaticMembers cleanup = new CleanupStaticMembers(getClassLoader(), reporter);
|
||||||
|
cleanup.resetAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the Bukkit logger first, before we try to create our own
|
||||||
|
private Logger getLoggerSafely() {
|
||||||
|
Logger log = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
log = getLogger();
|
||||||
|
} catch (Throwable e) { }
|
||||||
|
|
||||||
|
// Use the default logger instead
|
||||||
|
if (log == null)
|
||||||
|
log = Logger.getLogger("Minecraft");
|
||||||
|
return log;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -203,26 +312,11 @@ public class ProtocolLibrary extends JavaPlugin {
|
|||||||
/**
|
/**
|
||||||
* Retrieve the metrics instance used to measure users of this library.
|
* Retrieve the metrics instance used to measure users of this library.
|
||||||
* <p>
|
* <p>
|
||||||
* Note that this method may return NULL when the server is reloading or shutting down.
|
* Note that this method may return NULL when the server is reloading or shutting down. It is also
|
||||||
|
* NULL if metrics has been disabled.
|
||||||
* @return Metrics instance container.
|
* @return Metrics instance container.
|
||||||
*/
|
*/
|
||||||
public Statistics getStatistics() {
|
public Statistics getStatistics() {
|
||||||
return statistisc;
|
return statistisc;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the Bukkit logger first, before we try to create our own
|
|
||||||
private Logger getLoggerSafely() {
|
|
||||||
|
|
||||||
Logger log = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
log = getLogger();
|
|
||||||
} catch (Throwable e) {
|
|
||||||
// We'll handle it
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log == null)
|
|
||||||
log = Logger.getLogger("Minecraft");
|
|
||||||
return log;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
142
ProtocolLib/src/main/java/com/comphenix/protocol/RangeParser.java
Normale Datei
142
ProtocolLib/src/main/java/com/comphenix/protocol/RangeParser.java
Normale Datei
@ -0,0 +1,142 @@
|
|||||||
|
package com.comphenix.protocol;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.google.common.collect.DiscreteDomains;
|
||||||
|
import com.google.common.collect.Range;
|
||||||
|
import com.google.common.collect.Ranges;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to parse ranges in CommandPacket.
|
||||||
|
*
|
||||||
|
* @author Kristian
|
||||||
|
*/
|
||||||
|
class RangeParser {
|
||||||
|
/**
|
||||||
|
* Parse a range from a given text.
|
||||||
|
* @param text - the text.
|
||||||
|
* @param legalRange - range of legal values.
|
||||||
|
* @return The parsed ranges.
|
||||||
|
*/
|
||||||
|
public static List<Range<Integer>> getRanges(String text, Range<Integer> legalRange) {
|
||||||
|
return getRanges(new String[] { text }, 0, 0, legalRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse ranges from an array of elements.
|
||||||
|
* @param args - array of elements.
|
||||||
|
* @param offset - beginning offset.
|
||||||
|
* @param lastIndex - the last index of the array to read.
|
||||||
|
* @param legalRange - range of legal values.
|
||||||
|
* @return The parsed ranges.
|
||||||
|
*/
|
||||||
|
public static List<Range<Integer>> getRanges(String[] args, int offset, int lastIndex, Range<Integer> legalRange) {
|
||||||
|
List<String> tokens = tokenizeInput(args, offset, lastIndex);
|
||||||
|
List<Range<Integer>> ranges = new ArrayList<Range<Integer>>();
|
||||||
|
|
||||||
|
for (int i = 0; i < tokens.size(); i++) {
|
||||||
|
Range<Integer> range;
|
||||||
|
String current = tokens.get(i);
|
||||||
|
String next = i + 1 < tokens.size() ? tokens.get(i + 1) : null;
|
||||||
|
|
||||||
|
// Yoda equality is done for null-safety
|
||||||
|
if ("-".equals(current)) {
|
||||||
|
throw new IllegalArgumentException("A hyphen must appear between two numbers.");
|
||||||
|
} else if ("-".equals(next)) {
|
||||||
|
if (i + 2 >= tokens.size())
|
||||||
|
throw new IllegalArgumentException("Cannot form a range without a upper limit.");
|
||||||
|
|
||||||
|
// This is a proper range
|
||||||
|
range = Ranges.closed(Integer.parseInt(current), Integer.parseInt(tokens.get(i + 2)));
|
||||||
|
ranges.add(range);
|
||||||
|
|
||||||
|
// Skip the two next tokens
|
||||||
|
i += 2;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Just a single number
|
||||||
|
range = Ranges.singleton(Integer.parseInt(current));
|
||||||
|
ranges.add(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ranges
|
||||||
|
if (!legalRange.encloses(range)) {
|
||||||
|
throw new IllegalArgumentException(range + " is not in the range " + range.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return simplify(ranges, legalRange.upperEndpoint());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplify a list of ranges by assuming a maximum value.
|
||||||
|
* @param ranges - the list of ranges to simplify.
|
||||||
|
* @param maximum - the maximum value (minimum value is always 0).
|
||||||
|
* @return A simplified list of ranges.
|
||||||
|
*/
|
||||||
|
private static List<Range<Integer>> simplify(List<Range<Integer>> ranges, int maximum) {
|
||||||
|
List<Range<Integer>> result = new ArrayList<Range<Integer>>();
|
||||||
|
boolean[] set = new boolean[maximum + 1];
|
||||||
|
int start = -1;
|
||||||
|
|
||||||
|
// Set every ID
|
||||||
|
for (Range<Integer> range : ranges) {
|
||||||
|
for (int id : range.asSet(DiscreteDomains.integers())) {
|
||||||
|
set[id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate ranges from this set
|
||||||
|
for (int i = 0; i <= set.length; i++) {
|
||||||
|
if (i < set.length && set[i]) {
|
||||||
|
if (start < 0) {
|
||||||
|
start = i;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (start >= 0) {
|
||||||
|
result.add(Ranges.closed(start, i - 1));
|
||||||
|
start = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> tokenizeInput(String[] args, int offset, int lastIndex) {
|
||||||
|
List<String> tokens = new ArrayList<String>();
|
||||||
|
|
||||||
|
// Tokenize the input
|
||||||
|
for (int i = offset; i <= lastIndex; i++) {
|
||||||
|
String text = args[i];
|
||||||
|
StringBuilder number = new StringBuilder();
|
||||||
|
|
||||||
|
for (int j = 0; j < text.length(); j++) {
|
||||||
|
char current = text.charAt(j);
|
||||||
|
|
||||||
|
if (Character.isDigit(current)) {
|
||||||
|
number.append(current);
|
||||||
|
} else if (Character.isWhitespace(current)) {
|
||||||
|
// That's ok
|
||||||
|
} else if (current == '-') {
|
||||||
|
// Add the number token first
|
||||||
|
if (number.length() > 0) {
|
||||||
|
tokens.add(number.toString());
|
||||||
|
number.setLength(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.add(Character.toString(current));
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Illegal character '" + current + "' found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the number token, if it hasn't already
|
||||||
|
if (number.length() > 0)
|
||||||
|
tokens.add(number.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
}
|
@ -20,8 +20,8 @@ package com.comphenix.protocol.async;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
import org.bukkit.scheduler.BukkitScheduler;
|
import org.bukkit.scheduler.BukkitScheduler;
|
||||||
@ -29,12 +29,16 @@ import org.bukkit.scheduler.BukkitScheduler;
|
|||||||
import com.comphenix.protocol.AsynchronousManager;
|
import com.comphenix.protocol.AsynchronousManager;
|
||||||
import com.comphenix.protocol.PacketStream;
|
import com.comphenix.protocol.PacketStream;
|
||||||
import com.comphenix.protocol.ProtocolManager;
|
import com.comphenix.protocol.ProtocolManager;
|
||||||
|
import com.comphenix.protocol.error.ErrorReporter;
|
||||||
import com.comphenix.protocol.events.ListeningWhitelist;
|
import com.comphenix.protocol.events.ListeningWhitelist;
|
||||||
import com.comphenix.protocol.events.PacketEvent;
|
import com.comphenix.protocol.events.PacketEvent;
|
||||||
import com.comphenix.protocol.events.PacketListener;
|
import com.comphenix.protocol.events.PacketListener;
|
||||||
import com.comphenix.protocol.injector.PacketFilterManager;
|
import com.comphenix.protocol.injector.PacketFilterManager;
|
||||||
import com.comphenix.protocol.injector.PrioritizedListener;
|
import com.comphenix.protocol.injector.PrioritizedListener;
|
||||||
|
import com.comphenix.protocol.injector.SortedPacketListenerList;
|
||||||
import com.google.common.base.Objects;
|
import com.google.common.base.Objects;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a filter manager for asynchronous packets.
|
* Represents a filter manager for asynchronous packets.
|
||||||
@ -43,13 +47,18 @@ import com.google.common.base.Objects;
|
|||||||
*/
|
*/
|
||||||
public class AsyncFilterManager implements AsynchronousManager {
|
public class AsyncFilterManager implements AsynchronousManager {
|
||||||
|
|
||||||
|
private SortedPacketListenerList serverTimeoutListeners;
|
||||||
|
private SortedPacketListenerList clientTimeoutListeners;
|
||||||
|
private Set<PacketListener> timeoutListeners;
|
||||||
|
|
||||||
private PacketProcessingQueue serverProcessingQueue;
|
private PacketProcessingQueue serverProcessingQueue;
|
||||||
private PacketSendingQueue serverQueue;
|
private PacketSendingQueue serverQueue;
|
||||||
|
|
||||||
|
|
||||||
private PacketProcessingQueue clientProcessingQueue;
|
private PacketProcessingQueue clientProcessingQueue;
|
||||||
private PacketSendingQueue clientQueue;
|
private PacketSendingQueue clientQueue;
|
||||||
|
|
||||||
private Logger logger;
|
private ErrorReporter reporter;
|
||||||
|
|
||||||
// The likely main thread
|
// The likely main thread
|
||||||
private Thread mainThread;
|
private Thread mainThread;
|
||||||
@ -66,13 +75,32 @@ public class AsyncFilterManager implements AsynchronousManager {
|
|||||||
// Whether or not we're currently cleaning up
|
// Whether or not we're currently cleaning up
|
||||||
private volatile boolean cleaningUp;
|
private volatile boolean cleaningUp;
|
||||||
|
|
||||||
public AsyncFilterManager(Logger logger, BukkitScheduler scheduler, ProtocolManager manager) {
|
public AsyncFilterManager(ErrorReporter reporter, BukkitScheduler scheduler, ProtocolManager manager) {
|
||||||
|
|
||||||
|
// Initialize timeout listeners
|
||||||
|
serverTimeoutListeners = new SortedPacketListenerList();
|
||||||
|
clientTimeoutListeners = new SortedPacketListenerList();
|
||||||
|
timeoutListeners = Sets.newSetFromMap(new ConcurrentHashMap<PacketListener, Boolean>());
|
||||||
|
|
||||||
// Server packets are synchronized already
|
// Server packets are synchronized already
|
||||||
this.serverQueue = new PacketSendingQueue(false);
|
this.serverQueue = new PacketSendingQueue(false) {
|
||||||
|
@Override
|
||||||
|
protected void onPacketTimeout(PacketEvent event) {
|
||||||
|
if (!cleaningUp) {
|
||||||
|
serverTimeoutListeners.invokePacketSending(AsyncFilterManager.this.reporter, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Client packets must be synchronized
|
// Client packets must be synchronized
|
||||||
this.clientQueue = new PacketSendingQueue(true);
|
this.clientQueue = new PacketSendingQueue(true) {
|
||||||
|
@Override
|
||||||
|
protected void onPacketTimeout(PacketEvent event) {
|
||||||
|
if (!cleaningUp) {
|
||||||
|
clientTimeoutListeners.invokePacketSending(AsyncFilterManager.this.reporter, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.serverProcessingQueue = new PacketProcessingQueue(serverQueue);
|
this.serverProcessingQueue = new PacketProcessingQueue(serverQueue);
|
||||||
this.clientProcessingQueue = new PacketProcessingQueue(clientQueue);
|
this.clientProcessingQueue = new PacketProcessingQueue(clientQueue);
|
||||||
@ -80,7 +108,7 @@ public class AsyncFilterManager implements AsynchronousManager {
|
|||||||
this.scheduler = scheduler;
|
this.scheduler = scheduler;
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
|
|
||||||
this.logger = logger;
|
this.reporter = reporter;
|
||||||
this.mainThread = Thread.currentThread();
|
this.mainThread = Thread.currentThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,6 +117,27 @@ public class AsyncFilterManager implements AsynchronousManager {
|
|||||||
return registerAsyncHandler(listener, true);
|
return registerAsyncHandler(listener, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerTimeoutHandler(PacketListener listener) {
|
||||||
|
if (listener == null)
|
||||||
|
throw new IllegalArgumentException("listener cannot be NULL.");
|
||||||
|
if (!timeoutListeners.add(listener))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ListeningWhitelist sending = listener.getSendingWhitelist();
|
||||||
|
ListeningWhitelist receiving = listener.getReceivingWhitelist();
|
||||||
|
|
||||||
|
if (!ListeningWhitelist.isEmpty(sending))
|
||||||
|
serverTimeoutListeners.addListener(listener, sending);
|
||||||
|
if (!ListeningWhitelist.isEmpty(receiving))
|
||||||
|
serverTimeoutListeners.addListener(listener, receiving);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<PacketListener> getTimeoutHandlers() {
|
||||||
|
return ImmutableSet.copyOf(timeoutListeners);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers an asynchronous packet handler.
|
* Registers an asynchronous packet handler.
|
||||||
* <p>
|
* <p>
|
||||||
@ -131,6 +180,21 @@ public class AsyncFilterManager implements AsynchronousManager {
|
|||||||
return whitelist != null && whitelist.getWhitelist().size() > 0;
|
return whitelist != null && whitelist.getWhitelist().size() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unregisterTimeoutHandler(PacketListener listener) {
|
||||||
|
if (listener == null)
|
||||||
|
throw new IllegalArgumentException("listener cannot be NULL.");
|
||||||
|
|
||||||
|
ListeningWhitelist sending = listener.getSendingWhitelist();
|
||||||
|
ListeningWhitelist receiving = listener.getReceivingWhitelist();
|
||||||
|
|
||||||
|
// Do it in the opposite order
|
||||||
|
if (serverTimeoutListeners.removeListener(listener, sending).size() > 0 ||
|
||||||
|
clientTimeoutListeners.removeListener(listener, receiving).size() > 0) {
|
||||||
|
timeoutListeners.remove(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void unregisterAsyncHandler(AsyncListenerHandler handler) {
|
public void unregisterAsyncHandler(AsyncListenerHandler handler) {
|
||||||
if (handler == null)
|
if (handler == null)
|
||||||
@ -267,8 +331,8 @@ public class AsyncFilterManager implements AsynchronousManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Logger getLogger() {
|
public ErrorReporter getErrorReporter() {
|
||||||
return logger;
|
return reporter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -276,6 +340,10 @@ public class AsyncFilterManager implements AsynchronousManager {
|
|||||||
cleaningUp = true;
|
cleaningUp = true;
|
||||||
serverProcessingQueue.cleanupAll();
|
serverProcessingQueue.cleanupAll();
|
||||||
serverQueue.cleanupAll();
|
serverQueue.cleanupAll();
|
||||||
|
|
||||||
|
timeoutListeners.clear();
|
||||||
|
serverTimeoutListeners = null;
|
||||||
|
clientTimeoutListeners = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -333,7 +401,6 @@ public class AsyncFilterManager implements AsynchronousManager {
|
|||||||
* Send any due packets, or clean up packets that have expired.
|
* Send any due packets, or clean up packets that have expired.
|
||||||
*/
|
*/
|
||||||
public void sendProcessedPackets(int tickCounter, boolean onMainThread) {
|
public void sendProcessedPackets(int tickCounter, boolean onMainThread) {
|
||||||
|
|
||||||
// The server queue is unlikely to need checking that often
|
// The server queue is unlikely to need checking that often
|
||||||
if (tickCounter % 10 == 0) {
|
if (tickCounter % 10 == 0) {
|
||||||
serverQueue.trySendPackets(onMainThread);
|
serverQueue.trySendPackets(onMainThread);
|
||||||
|
@ -22,7 +22,6 @@ import java.util.Set;
|
|||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.logging.Level;
|
|
||||||
|
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
@ -54,7 +53,7 @@ public class AsyncListenerHandler {
|
|||||||
private static final AtomicInteger nextID = new AtomicInteger();
|
private static final AtomicInteger nextID = new AtomicInteger();
|
||||||
|
|
||||||
// Default queue capacity
|
// Default queue capacity
|
||||||
private static int DEFAULT_CAPACITY = 1024;
|
private static final int DEFAULT_CAPACITY = 1024;
|
||||||
|
|
||||||
// Cancel the async handler
|
// Cancel the async handler
|
||||||
private volatile boolean cancelled;
|
private volatile boolean cancelled;
|
||||||
@ -114,10 +113,6 @@ public class AsyncListenerHandler {
|
|||||||
return nullPacketListener;
|
return nullPacketListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getPluginName() {
|
|
||||||
return PacketAdapter.getPluginName(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the plugin associated with this async listener.
|
* Retrieve the plugin associated with this async listener.
|
||||||
* @return The plugin.
|
* @return The plugin.
|
||||||
@ -442,11 +437,11 @@ public class AsyncListenerHandler {
|
|||||||
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
// Minecraft doesn't want your Exception.
|
// Minecraft doesn't want your Exception.
|
||||||
filterManager.getLogger().log(Level.SEVERE,
|
filterManager.getErrorReporter().reportMinimal(listener.getPlugin(), "onAsyncPacket()", e);
|
||||||
"Unhandled exception occured in onAsyncPacket() for " + getPluginName(), e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now, get the next non-cancelled listener
|
// Now, get the next non-cancelled listener
|
||||||
|
if (!marker.hasExpired()) {
|
||||||
for (; marker.getListenerTraversal().hasNext(); ) {
|
for (; marker.getListenerTraversal().hasNext(); ) {
|
||||||
AsyncListenerHandler handler = marker.getListenerTraversal().next().getListener();
|
AsyncListenerHandler handler = marker.getListenerTraversal().next().getListener();
|
||||||
|
|
||||||
@ -455,6 +450,7 @@ public class AsyncListenerHandler {
|
|||||||
continue mainLoop;
|
continue mainLoop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// There are no more listeners - queue the packet for transmission
|
// There are no more listeners - queue the packet for transmission
|
||||||
filterManager.signalFreeProcessingSlot(packet);
|
filterManager.signalFreeProcessingSlot(packet);
|
||||||
|
@ -51,7 +51,7 @@ public class AsyncMarker implements Serializable, Comparable<AsyncMarker> {
|
|||||||
/**
|
/**
|
||||||
* Default number of milliseconds until a packet will rejected.
|
* Default number of milliseconds until a packet will rejected.
|
||||||
*/
|
*/
|
||||||
public static final int DEFAULT_TIMEOUT_DELTA = 60000;
|
public static final int DEFAULT_TIMEOUT_DELTA = 1800 * 1000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default number of packets to skip.
|
* Default number of packets to skip.
|
||||||
@ -418,7 +418,7 @@ public class AsyncMarker implements Serializable, Comparable<AsyncMarker> {
|
|||||||
// We're in 1.2.5
|
// We're in 1.2.5
|
||||||
alwaysSync = true;
|
alwaysSync = true;
|
||||||
} else {
|
} else {
|
||||||
System.err.println("Cannot determine asynchronous state of packets!");
|
System.err.println("[ProtocolLib] Cannot determine asynchronous state of packets!");
|
||||||
alwaysSync = true;
|
alwaysSync = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,13 +27,14 @@ import org.bukkit.entity.Player;
|
|||||||
|
|
||||||
import com.comphenix.protocol.events.PacketEvent;
|
import com.comphenix.protocol.events.PacketEvent;
|
||||||
import com.comphenix.protocol.injector.PlayerLoggedOutException;
|
import com.comphenix.protocol.injector.PlayerLoggedOutException;
|
||||||
|
import com.comphenix.protocol.injector.SortedPacketListenerList;
|
||||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents packets ready to be transmitted to a client.
|
* Represents packets ready to be transmitted to a client.
|
||||||
* @author Kristian
|
* @author Kristian
|
||||||
*/
|
*/
|
||||||
class PacketSendingQueue {
|
abstract class PacketSendingQueue {
|
||||||
|
|
||||||
public static final int INITIAL_CAPACITY = 64;
|
public static final int INITIAL_CAPACITY = 64;
|
||||||
|
|
||||||
@ -77,7 +78,7 @@ class PacketSendingQueue {
|
|||||||
AsyncMarker marker = packetUpdated.getAsyncMarker();
|
AsyncMarker marker = packetUpdated.getAsyncMarker();
|
||||||
|
|
||||||
// Should we reorder the event?
|
// Should we reorder the event?
|
||||||
if (marker.getQueuedSendingIndex() != marker.getNewSendingIndex()) {
|
if (marker.getQueuedSendingIndex() != marker.getNewSendingIndex() && !marker.hasExpired()) {
|
||||||
PacketEvent copy = PacketEvent.fromSynchronous(packetUpdated, marker);
|
PacketEvent copy = PacketEvent.fromSynchronous(packetUpdated, marker);
|
||||||
|
|
||||||
// "Cancel" the original event
|
// "Cancel" the original event
|
||||||
@ -127,6 +128,7 @@ class PacketSendingQueue {
|
|||||||
if (holder != null) {
|
if (holder != null) {
|
||||||
PacketEvent current = holder.getEvent();
|
PacketEvent current = holder.getEvent();
|
||||||
AsyncMarker marker = current.getAsyncMarker();
|
AsyncMarker marker = current.getAsyncMarker();
|
||||||
|
boolean hasExpired = marker.hasExpired();
|
||||||
|
|
||||||
// Abort if we're not on the main thread
|
// Abort if we're not on the main thread
|
||||||
if (synchronizeMain) {
|
if (synchronizeMain) {
|
||||||
@ -144,8 +146,16 @@ class PacketSendingQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (marker.isProcessed() || marker.hasExpired()) {
|
if (marker.isProcessed() || hasExpired) {
|
||||||
if (marker.isProcessed() && !current.isCancelled()) {
|
if (hasExpired) {
|
||||||
|
// Notify timeout listeners
|
||||||
|
onPacketTimeout(current);
|
||||||
|
|
||||||
|
// Recompute
|
||||||
|
marker = current.getAsyncMarker();
|
||||||
|
hasExpired = marker.hasExpired();
|
||||||
|
}
|
||||||
|
if (marker.isProcessed() && !current.isCancelled() && !hasExpired) {
|
||||||
// Silently skip players that have logged out
|
// Silently skip players that have logged out
|
||||||
if (isOnline(current.getPlayer())) {
|
if (isOnline(current.getPlayer())) {
|
||||||
sendPacket(current);
|
sendPacket(current);
|
||||||
@ -162,6 +172,12 @@ class PacketSendingQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked when a packet has timed out.
|
||||||
|
* @param event - the timed out packet.
|
||||||
|
*/
|
||||||
|
protected abstract void onPacketTimeout(PacketEvent event);
|
||||||
|
|
||||||
private boolean isOnline(Player player) {
|
private boolean isOnline(Player player) {
|
||||||
return player != null && player.isOnline();
|
return player != null && player.isOnline();
|
||||||
}
|
}
|
||||||
@ -205,7 +221,7 @@ class PacketSendingQueue {
|
|||||||
|
|
||||||
} catch (PlayerLoggedOutException e) {
|
} catch (PlayerLoggedOutException e) {
|
||||||
System.out.println(String.format(
|
System.out.println(String.format(
|
||||||
"Warning: Dropped packet index %s of ID %s",
|
"[ProtocolLib] Warning: Dropped packet index %s of ID %s",
|
||||||
marker.getOriginalSendingIndex(), event.getPacketID()
|
marker.getOriginalSendingIndex(), event.getPacketID()
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -0,0 +1,375 @@
|
|||||||
|
package com.comphenix.protocol.concurrency;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NavigableMap;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
import com.google.common.collect.Range;
|
||||||
|
import com.google.common.collect.Ranges;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a generic store of intervals and associated values. No two intervals
|
||||||
|
* can overlap in this representation.
|
||||||
|
* <p>
|
||||||
|
* Note that this implementation is not thread safe.
|
||||||
|
*
|
||||||
|
* @author Kristian
|
||||||
|
*
|
||||||
|
* @param <TKey> - type of the key. Must implement Comparable.
|
||||||
|
* @param <TValue> - type of the value to associate.
|
||||||
|
*/
|
||||||
|
public abstract class AbstractIntervalTree<TKey extends Comparable<TKey>, TValue> {
|
||||||
|
|
||||||
|
protected enum State {
|
||||||
|
OPEN,
|
||||||
|
CLOSE,
|
||||||
|
BOTH
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a range and a value in this interval tree.
|
||||||
|
*/
|
||||||
|
public class Entry implements Map.Entry<Range<TKey>, TValue> {
|
||||||
|
private final Range<TKey> key;
|
||||||
|
private EndPoint left;
|
||||||
|
private EndPoint right;
|
||||||
|
|
||||||
|
Entry(Range<TKey> key, EndPoint left, EndPoint right) {
|
||||||
|
if (left == null)
|
||||||
|
throw new IllegalAccessError("left cannot be NUll");
|
||||||
|
if (right == null)
|
||||||
|
throw new IllegalAccessError("right cannot be NUll");
|
||||||
|
|
||||||
|
this.key = key;
|
||||||
|
this.left = left;
|
||||||
|
this.right = right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Range<TKey> getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TValue getValue() {
|
||||||
|
return left.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TValue setValue(TValue value) {
|
||||||
|
TValue old = left.value;
|
||||||
|
|
||||||
|
// Set both end points
|
||||||
|
left.value = value;
|
||||||
|
right.value = value;
|
||||||
|
return old;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single end point (open, close or both) of a range.
|
||||||
|
*/
|
||||||
|
protected class EndPoint {
|
||||||
|
|
||||||
|
// Whether or not the end-point is opening a range, closing a range or both.
|
||||||
|
public State state;
|
||||||
|
|
||||||
|
// The value this range contains
|
||||||
|
public TValue value;
|
||||||
|
|
||||||
|
public EndPoint(State state, TValue value) {
|
||||||
|
this.state = state;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// To quickly look up ranges we'll index them by endpoints
|
||||||
|
protected NavigableMap<TKey, EndPoint> bounds = new TreeMap<TKey, EndPoint>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes every interval that intersects with the given range.
|
||||||
|
* @param lowerBound - lowest value to remove.
|
||||||
|
* @param upperBound - highest value to remove.
|
||||||
|
*/
|
||||||
|
public Set<Entry> remove(TKey lowerBound, TKey upperBound) {
|
||||||
|
return remove(lowerBound, upperBound, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes every interval that intersects with the given range.
|
||||||
|
* @param lowerBound - lowest value to remove.
|
||||||
|
* @param upperBound - highest value to remove.
|
||||||
|
* @param preserveOutside - whether or not to preserve the intervals that are partially outside.
|
||||||
|
*/
|
||||||
|
public Set<Entry> remove(TKey lowerBound, TKey upperBound, boolean preserveDifference) {
|
||||||
|
checkBounds(lowerBound, upperBound);
|
||||||
|
NavigableMap<TKey, EndPoint> range = bounds.subMap(lowerBound, true, upperBound, true);
|
||||||
|
|
||||||
|
boolean emptyRange = range.isEmpty();
|
||||||
|
TKey first = !emptyRange ? range.firstKey() : null;
|
||||||
|
TKey last = !emptyRange ? range.lastKey() : null;
|
||||||
|
|
||||||
|
Set<Entry> resized = new HashSet<Entry>();
|
||||||
|
Set<Entry> removed = new HashSet<Entry>();
|
||||||
|
|
||||||
|
// Remove the previous element too. A close end-point must be preceded by an OPEN end-point.
|
||||||
|
if (first != null && range.get(first).state == State.CLOSE) {
|
||||||
|
TKey key = bounds.floorKey(first);
|
||||||
|
EndPoint removedPoint = removeIfNonNull(key);
|
||||||
|
|
||||||
|
// Add the interval back
|
||||||
|
if (removedPoint != null && preserveDifference) {
|
||||||
|
resized.add(putUnsafe(key, decrementKey(lowerBound), removedPoint.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the closing element too.
|
||||||
|
if (last != null && range.get(last).state == State.OPEN) {
|
||||||
|
TKey key = bounds.ceilingKey(last);
|
||||||
|
EndPoint removedPoint = removeIfNonNull(key);
|
||||||
|
|
||||||
|
if (removedPoint != null && preserveDifference) {
|
||||||
|
resized.add(putUnsafe(incrementKey(upperBound), key, removedPoint.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the removed entries too
|
||||||
|
getEntries(removed, range);
|
||||||
|
invokeEntryRemoved(removed);
|
||||||
|
|
||||||
|
if (preserveDifference) {
|
||||||
|
invokeEntryRemoved(resized);
|
||||||
|
invokeEntryAdded(resized);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the range as well
|
||||||
|
range.clear();
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper
|
||||||
|
private EndPoint removeIfNonNull(TKey key) {
|
||||||
|
if (key != null) {
|
||||||
|
return bounds.remove(key);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a given end point
|
||||||
|
protected EndPoint addEndPoint(TKey key, TValue value, State state) {
|
||||||
|
EndPoint endPoint = bounds.get(key);
|
||||||
|
|
||||||
|
if (endPoint != null) {
|
||||||
|
endPoint.state = State.BOTH;
|
||||||
|
} else {
|
||||||
|
endPoint = new EndPoint(state, value);
|
||||||
|
bounds.put(key, endPoint);
|
||||||
|
}
|
||||||
|
return endPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates a given interval of keys with a certain value. Any previous
|
||||||
|
* association will be overwritten in the given interval.
|
||||||
|
* <p>
|
||||||
|
* Overlapping intervals are not permitted. A key can only be associated with a single value.
|
||||||
|
*
|
||||||
|
* @param lowerBound - the minimum key (inclusive).
|
||||||
|
* @param upperBound - the maximum key (inclusive).
|
||||||
|
* @param value - the value, or NULL to reset this range.
|
||||||
|
*/
|
||||||
|
public void put(TKey lowerBound, TKey upperBound, TValue value) {
|
||||||
|
// While we don't permit overlapping intervals, we'll still allow overwriting existing intervals.
|
||||||
|
remove(lowerBound, upperBound, true);
|
||||||
|
invokeEntryAdded(putUnsafe(lowerBound, upperBound, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates a given interval without performing any interval checks.
|
||||||
|
* @param lowerBound - the minimum key (inclusive).
|
||||||
|
* @param upperBound - the maximum key (inclusive).
|
||||||
|
* @param value - the value, or NULL to reset the range.
|
||||||
|
*/
|
||||||
|
private Entry putUnsafe(TKey lowerBound, TKey upperBound, TValue value) {
|
||||||
|
// OK. Add the end points now
|
||||||
|
if (value != null) {
|
||||||
|
EndPoint left = addEndPoint(lowerBound, value, State.OPEN);
|
||||||
|
EndPoint right = addEndPoint(upperBound, value, State.CLOSE);
|
||||||
|
|
||||||
|
Range<TKey> range = Ranges.closed(lowerBound, upperBound);
|
||||||
|
return new Entry(range, left, right);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to verify the validity of the given interval.
|
||||||
|
* @param lowerBound - lower bound (inclusive).
|
||||||
|
* @param upperBound - upper bound (inclusive).
|
||||||
|
*/
|
||||||
|
private void checkBounds(TKey lowerBound, TKey upperBound) {
|
||||||
|
if (lowerBound == null)
|
||||||
|
throw new IllegalAccessError("lowerbound cannot be NULL.");
|
||||||
|
if (upperBound == null)
|
||||||
|
throw new IllegalAccessError("upperBound cannot be NULL.");
|
||||||
|
if (upperBound.compareTo(lowerBound) < 0)
|
||||||
|
throw new IllegalArgumentException("upperBound cannot be less than lowerBound.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the given key is within an interval.
|
||||||
|
* @param key - key to check.
|
||||||
|
* @return TRUE if the given key is within an interval in this tree, FALSE otherwise.
|
||||||
|
*/
|
||||||
|
public boolean containsKey(TKey key) {
|
||||||
|
return getEndPoint(key) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumerates over every range in this interval tree.
|
||||||
|
* @return Number of ranges.
|
||||||
|
*/
|
||||||
|
public Set<Entry> entrySet() {
|
||||||
|
// Don't mind the Java noise
|
||||||
|
Set<Entry> result = new HashSet<Entry>();
|
||||||
|
getEntries(result, bounds);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove every interval.
|
||||||
|
*/
|
||||||
|
public void clear() {
|
||||||
|
if (!bounds.isEmpty()) {
|
||||||
|
remove(bounds.firstKey(), bounds.lastKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a map of end points into a set of entries.
|
||||||
|
* @param destination - set of entries.
|
||||||
|
* @param map - a map of end points.
|
||||||
|
*/
|
||||||
|
private void getEntries(Set<Entry> destination, NavigableMap<TKey, EndPoint> map) {
|
||||||
|
Map.Entry<TKey, EndPoint> last = null;
|
||||||
|
|
||||||
|
for (Map.Entry<TKey, EndPoint> entry : bounds.entrySet()) {
|
||||||
|
switch (entry.getValue().state) {
|
||||||
|
case BOTH:
|
||||||
|
EndPoint point = entry.getValue();
|
||||||
|
destination.add(new Entry(Ranges.singleton(entry.getKey()), point, point));
|
||||||
|
break;
|
||||||
|
case CLOSE:
|
||||||
|
Range<TKey> range = Ranges.closed(last.getKey(), entry.getKey());
|
||||||
|
destination.add(new Entry(range, last.getValue(), entry.getValue()));
|
||||||
|
break;
|
||||||
|
case OPEN:
|
||||||
|
// We don't know the full range yet
|
||||||
|
last = entry;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Illegal open/close state detected.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts every range from the given tree into the current tree.
|
||||||
|
* @param other - the other tree to read from.
|
||||||
|
*/
|
||||||
|
public void putAll(AbstractIntervalTree<TKey, TValue> other) {
|
||||||
|
// Naively copy every range.
|
||||||
|
for (Entry entry : other.entrySet()) {
|
||||||
|
put(entry.key.lowerEndpoint(), entry.key.upperEndpoint(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the value of the range that matches the given key, or NULL if nothing was found.
|
||||||
|
* @param key - the level to read for.
|
||||||
|
* @return The correct amount of experience, or NULL if nothing was recorded.
|
||||||
|
*/
|
||||||
|
public TValue get(TKey key) {
|
||||||
|
EndPoint point = getEndPoint(key);
|
||||||
|
|
||||||
|
if (point != null)
|
||||||
|
return point.value;
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the end-point composite associated with this key.
|
||||||
|
* @param key - key to search for.
|
||||||
|
* @return The end point found, or NULL.
|
||||||
|
*/
|
||||||
|
protected EndPoint getEndPoint(TKey key) {
|
||||||
|
EndPoint ends = bounds.get(key);
|
||||||
|
|
||||||
|
if (ends != null) {
|
||||||
|
// This is a piece of cake
|
||||||
|
return ends;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// We need to determine if the point intersects with a range
|
||||||
|
TKey left = bounds.floorKey(key);
|
||||||
|
|
||||||
|
// We only need to check to the left
|
||||||
|
if (left != null && bounds.get(left).state == State.OPEN) {
|
||||||
|
return bounds.get(left);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invokeEntryAdded(Entry added) {
|
||||||
|
if (added != null) {
|
||||||
|
onEntryAdded(added);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invokeEntryAdded(Set<Entry> added) {
|
||||||
|
for (Entry entry : added) {
|
||||||
|
onEntryAdded(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invokeEntryRemoved(Set<Entry> removed) {
|
||||||
|
for (Entry entry : removed) {
|
||||||
|
onEntryRemoved(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listeners for added or removed entries
|
||||||
|
/**
|
||||||
|
* Invoked when an entry is added.
|
||||||
|
* @param added - the entry that was added.
|
||||||
|
*/
|
||||||
|
protected void onEntryAdded(Entry added) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked when an entry is removed.
|
||||||
|
* @param removed - the removed entry.
|
||||||
|
*/
|
||||||
|
protected void onEntryRemoved(Entry removed) { }
|
||||||
|
|
||||||
|
// Helpers for decrementing or incrementing key values
|
||||||
|
/**
|
||||||
|
* Decrement the given key by one unit.
|
||||||
|
* @param key - the key that should be decremented.
|
||||||
|
* @return The new decremented key.
|
||||||
|
*/
|
||||||
|
protected abstract TKey decrementKey(TKey key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the given key by one unit.
|
||||||
|
* @param key - the key that should be incremented.
|
||||||
|
* @return The new incremented key.
|
||||||
|
*/
|
||||||
|
protected abstract TKey incrementKey(TKey key);
|
||||||
|
}
|
@ -0,0 +1,279 @@
|
|||||||
|
package com.comphenix.protocol.error;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.apache.commons.lang.builder.ToStringBuilder;
|
||||||
|
import org.apache.commons.lang.builder.ToStringStyle;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
|
import com.comphenix.protocol.events.PacketAdapter;
|
||||||
|
import com.google.common.primitives.Primitives;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal class used to handle exceptions.
|
||||||
|
*
|
||||||
|
* @author Kristian
|
||||||
|
*/
|
||||||
|
public class DetailedErrorReporter implements ErrorReporter {
|
||||||
|
|
||||||
|
public static final String SECOND_LEVEL_PREFIX = " ";
|
||||||
|
public static final String DEFAULT_PREFIX = " ";
|
||||||
|
public static final String DEFAULT_SUPPORT_URL = "http://dev.bukkit.org/server-mods/protocollib/";
|
||||||
|
public static final String PLUGIN_NAME = "ProtocolLib";
|
||||||
|
|
||||||
|
// Users that are informed about errors in the chat
|
||||||
|
public static final String ERROR_PERMISSION = "protocol.info";
|
||||||
|
|
||||||
|
// We don't want to spam the server
|
||||||
|
public static final int DEFAULT_MAX_ERROR_COUNT = 20;
|
||||||
|
|
||||||
|
protected String prefix;
|
||||||
|
protected String supportURL;
|
||||||
|
|
||||||
|
protected int errorCount;
|
||||||
|
protected int maxErrorCount;
|
||||||
|
protected Logger logger;
|
||||||
|
|
||||||
|
// Whether or not Apache Commons is not present
|
||||||
|
protected boolean apacheCommonsMissing;
|
||||||
|
|
||||||
|
// Map of global objects
|
||||||
|
protected Map<String, Object> globalParameters = new HashMap<String, Object>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a default error reporting system.
|
||||||
|
*/
|
||||||
|
public DetailedErrorReporter() {
|
||||||
|
this(DEFAULT_PREFIX, DEFAULT_SUPPORT_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a central error reporting system.
|
||||||
|
* @param prefix - default line prefix.
|
||||||
|
* @param supportURL - URL to report the error.
|
||||||
|
*/
|
||||||
|
public DetailedErrorReporter(String prefix, String supportURL) {
|
||||||
|
this(prefix, supportURL, DEFAULT_MAX_ERROR_COUNT, getBukkitLogger());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to get the logger.
|
||||||
|
private static Logger getBukkitLogger() {
|
||||||
|
try {
|
||||||
|
return Bukkit.getLogger();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return Logger.getLogger("Minecraft");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a central error reporting system.
|
||||||
|
* @param prefix - default line prefix.
|
||||||
|
* @param supportURL - URL to report the error.
|
||||||
|
* @param maxErrorCount - number of errors to print before giving up.
|
||||||
|
* @param logger - current logger.
|
||||||
|
*/
|
||||||
|
public DetailedErrorReporter(String prefix, String supportURL, int maxErrorCount, Logger logger) {
|
||||||
|
this.prefix = prefix;
|
||||||
|
this.supportURL = supportURL;
|
||||||
|
this.maxErrorCount = maxErrorCount;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reportMinimal(Plugin sender, String methodName, Throwable error) {
|
||||||
|
logger.log(Level.SEVERE, "[" + PLUGIN_NAME + "] Unhandled exception occured in " + methodName + " for " +
|
||||||
|
PacketAdapter.getPluginName(sender), error);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reportWarning(Object sender, String message) {
|
||||||
|
logger.log(Level.WARNING, "[" + PLUGIN_NAME + "] [" + getSenderName(sender) + "] " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reportWarning(Object sender, String message, Throwable error) {
|
||||||
|
logger.log(Level.WARNING, "[" + PLUGIN_NAME + "] [" + getSenderName(sender) + "] " + message, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getSenderName(Object sender) {
|
||||||
|
if (sender != null)
|
||||||
|
return sender.getClass().getSimpleName();
|
||||||
|
else
|
||||||
|
return "NULL";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reportDetailed(Object sender, String message, Throwable error, Object... parameters) {
|
||||||
|
|
||||||
|
// Do not overtly spam the server!
|
||||||
|
if (++errorCount > maxErrorCount) {
|
||||||
|
String maxReached = String.format("Reached maxmimum error count. Cannot pass error %s from %s.", error, sender);
|
||||||
|
logger.severe(maxReached);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringWriter text = new StringWriter();
|
||||||
|
PrintWriter writer = new PrintWriter(text);
|
||||||
|
|
||||||
|
// Helpful message
|
||||||
|
writer.println("[ProtocolLib] INTERNAL ERROR: " + message);
|
||||||
|
writer.println("If this problem hasn't already been reported, please open a ticket");
|
||||||
|
writer.println("at " + supportURL + " with the following data:");
|
||||||
|
|
||||||
|
// Now, let us print important exception information
|
||||||
|
writer.println(" ===== STACK TRACE =====");
|
||||||
|
|
||||||
|
if (error != null)
|
||||||
|
error.printStackTrace(writer);
|
||||||
|
|
||||||
|
// Data dump!
|
||||||
|
writer.println(" ===== DUMP =====");
|
||||||
|
|
||||||
|
// Relevant parameters
|
||||||
|
if (parameters != null && parameters.length > 0) {
|
||||||
|
writer.println("Parameters:");
|
||||||
|
|
||||||
|
// We *really* want to get as much information as possible
|
||||||
|
for (Object param : parameters) {
|
||||||
|
writer.println(addPrefix(getStringDescription(param), SECOND_LEVEL_PREFIX));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global parameters
|
||||||
|
for (String param : globalParameters()) {
|
||||||
|
writer.println(SECOND_LEVEL_PREFIX + param + ":");
|
||||||
|
writer.println(addPrefix(getStringDescription(getGlobalParameter(param)),
|
||||||
|
SECOND_LEVEL_PREFIX + SECOND_LEVEL_PREFIX));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, for the sender itself
|
||||||
|
writer.println("Sender:");
|
||||||
|
writer.println(addPrefix(getStringDescription(sender), SECOND_LEVEL_PREFIX));
|
||||||
|
|
||||||
|
// Add the server version too
|
||||||
|
if (Bukkit.getServer() != null) {
|
||||||
|
writer.println("Server:");
|
||||||
|
writer.println(addPrefix(Bukkit.getServer().getVersion(), SECOND_LEVEL_PREFIX));
|
||||||
|
|
||||||
|
// Inform of this occurrence
|
||||||
|
if (ERROR_PERMISSION != null) {
|
||||||
|
Bukkit.getServer().broadcast(
|
||||||
|
String.format("Error %s (%s) occured in %s.", message, error, sender),
|
||||||
|
ERROR_PERMISSION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure it is reported
|
||||||
|
logger.severe(addPrefix(text.toString(), prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the given prefix to every line in the text.
|
||||||
|
* @param text - text to modify.
|
||||||
|
* @param prefix - prefix added to every line in the text.
|
||||||
|
* @return The modified text.
|
||||||
|
*/
|
||||||
|
protected String addPrefix(String text, String prefix) {
|
||||||
|
return text.replaceAll("(?m)^", prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getStringDescription(Object value) {
|
||||||
|
|
||||||
|
// We can't only rely on toString.
|
||||||
|
if (value == null) {
|
||||||
|
return "[NULL]";
|
||||||
|
} if (isSimpleType(value)) {
|
||||||
|
return value.toString();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
if (!apacheCommonsMissing)
|
||||||
|
return (ToStringBuilder.reflectionToString(value, ToStringStyle.MULTI_LINE_STYLE, false, null));
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
// Apache is probably missing
|
||||||
|
logger.warning("Cannot find Apache Commons. Object introspection disabled.");
|
||||||
|
apacheCommonsMissing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just use toString()
|
||||||
|
return String.format("%s", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the given object is a wrapper for a primitive/simple type or not.
|
||||||
|
* @param test - the object to test.
|
||||||
|
* @return TRUE if this object is simple enough to simply be printed, FALSE othewise.
|
||||||
|
*/
|
||||||
|
protected boolean isSimpleType(Object test) {
|
||||||
|
return test instanceof String || Primitives.isWrapperType(test.getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getErrorCount() {
|
||||||
|
return errorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setErrorCount(int errorCount) {
|
||||||
|
this.errorCount = errorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxErrorCount() {
|
||||||
|
return maxErrorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxErrorCount(int maxErrorCount) {
|
||||||
|
this.maxErrorCount = maxErrorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the given global parameter. It will be included in every error report.
|
||||||
|
* @param key - name of parameter.
|
||||||
|
* @param value - the global parameter itself.
|
||||||
|
*/
|
||||||
|
public void addGlobalParameter(String key, Object value) {
|
||||||
|
globalParameters.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getGlobalParameter(String key) {
|
||||||
|
return globalParameters.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearGlobalParameters() {
|
||||||
|
globalParameters.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package com.comphenix.protocol.error;
|
||||||
|
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
|
public interface ErrorReporter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints a small minimal error report about an exception from another plugin.
|
||||||
|
* @param sender - the other plugin.
|
||||||
|
* @param methodName - name of the caller method.
|
||||||
|
* @param error - the exception itself.
|
||||||
|
*/
|
||||||
|
public abstract void reportMinimal(Plugin sender, String methodName, Throwable error);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints a warning message from the current plugin.
|
||||||
|
* @param sender - the object containing the caller method.
|
||||||
|
* @param message - error message.
|
||||||
|
*/
|
||||||
|
public abstract void reportWarning(Object sender, String message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints a warning message from the current plugin.
|
||||||
|
* @param sender - the object containing the caller method.
|
||||||
|
* @param message - error message.
|
||||||
|
* @param error - the exception that was thrown.
|
||||||
|
*/
|
||||||
|
public abstract void reportWarning(Object sender, String message, Throwable error);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints a detailed error report about an unhandled exception.
|
||||||
|
* @param sender - the object containing the caller method.
|
||||||
|
* @param message - an error message to include.
|
||||||
|
* @param error - the exception that was thrown in the caller method.
|
||||||
|
* @param parameters - parameters from the caller method.
|
||||||
|
*/
|
||||||
|
public abstract void reportDetailed(Object sender, String message, Throwable error, Object... parameters);
|
||||||
|
|
||||||
|
}
|
@ -137,6 +137,20 @@ public class ListeningWhitelist {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the given whitelist is empty or not.
|
||||||
|
* @param whitelist - the whitelist to test.
|
||||||
|
* @return TRUE if the whitelist is empty, FALSE otherwise.
|
||||||
|
*/
|
||||||
|
public static boolean isEmpty(ListeningWhitelist whitelist) {
|
||||||
|
if (whitelist == EMPTY_WHITELIST)
|
||||||
|
return true;
|
||||||
|
else if (whitelist == null)
|
||||||
|
return true;
|
||||||
|
else
|
||||||
|
return whitelist.getWhitelist().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(final Object obj){
|
public boolean equals(final Object obj){
|
||||||
if(obj instanceof ListeningWhitelist){
|
if(obj instanceof ListeningWhitelist){
|
||||||
@ -157,5 +171,4 @@ public class ListeningWhitelist {
|
|||||||
.add("priority", priority)
|
.add("priority", priority)
|
||||||
.add("packets", whitelist).toString();
|
.add("packets", whitelist).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -168,11 +168,19 @@ public abstract class PacketAdapter implements PacketListener {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the name of the plugin that has been associated with the listener.
|
* Retrieves the name of the plugin that has been associated with the listener.
|
||||||
|
* @param listener - the listener.
|
||||||
* @return Name of the associated plugin.
|
* @return Name of the associated plugin.
|
||||||
*/
|
*/
|
||||||
public static String getPluginName(PacketListener listener) {
|
public static String getPluginName(PacketListener listener) {
|
||||||
|
return getPluginName(listener.getPlugin());
|
||||||
|
}
|
||||||
|
|
||||||
Plugin plugin = listener.getPlugin();
|
/**
|
||||||
|
* Retrieves the name of the given plugin.
|
||||||
|
* @param plugin - the plugin.
|
||||||
|
* @return Name of the given plugin.
|
||||||
|
*/
|
||||||
|
public static String getPluginName(Plugin plugin) {
|
||||||
|
|
||||||
// Try to get the plugin name
|
// Try to get the plugin name
|
||||||
try {
|
try {
|
||||||
|
@ -26,6 +26,8 @@ import java.io.Serializable;
|
|||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Server;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.WorldType;
|
import org.bukkit.WorldType;
|
||||||
import org.bukkit.craftbukkit.CraftWorld;
|
import org.bukkit.craftbukkit.CraftWorld;
|
||||||
@ -268,7 +270,6 @@ public class PacketContainer implements Serializable {
|
|||||||
|
|
||||||
final Object worldServer = ((CraftWorld) world).getHandle();
|
final Object worldServer = ((CraftWorld) world).getHandle();
|
||||||
final Class<?> nmsEntityClass = net.minecraft.server.Entity.class;
|
final Class<?> nmsEntityClass = net.minecraft.server.Entity.class;
|
||||||
final World worldCopy = world;
|
|
||||||
|
|
||||||
if (getEntity == null)
|
if (getEntity == null)
|
||||||
getEntity = FuzzyReflection.fromObject(worldServer).getMethodByParameters(
|
getEntity = FuzzyReflection.fromObject(worldServer).getMethodByParameters(
|
||||||
@ -296,14 +297,17 @@ public class PacketContainer implements Serializable {
|
|||||||
if (nmsEntity != null) {
|
if (nmsEntity != null) {
|
||||||
return nmsEntity.getBukkitEntity();
|
return nmsEntity.getBukkitEntity();
|
||||||
} else {
|
} else {
|
||||||
// Maybe it's a player that's just logged in? Try a search
|
Server server = Bukkit.getServer();
|
||||||
for (Player player : worldCopy.getPlayers()) {
|
|
||||||
|
// Maybe it's a player that has just logged in? Try a search
|
||||||
|
if (server != null) {
|
||||||
|
for (Player player : server.getOnlinePlayers()) {
|
||||||
if (player.getEntityId() == id) {
|
if (player.getEntityId() == id) {
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
System.out.println("Entity doesn't exist.");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,4 +46,25 @@ public interface ListenerInvoker {
|
|||||||
* @return The packet ID.
|
* @return The packet ID.
|
||||||
*/
|
*/
|
||||||
public abstract int getPacketID(Packet packet);
|
public abstract int getPacketID(Packet packet);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associate a given class with the given packet ID. Internal method.
|
||||||
|
* @param clazz - class to associate.
|
||||||
|
* @param packetID - the packet ID.
|
||||||
|
*/
|
||||||
|
public abstract void unregisterPacketClass(Class<?> clazz);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a given class from the packet registry. Internal method.
|
||||||
|
* @param clazz - class to remove.
|
||||||
|
*/
|
||||||
|
public abstract void registerPacketClass(Class<?> clazz, int packetID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the correct packet class from a given packet ID.
|
||||||
|
* @param packetID - the packet ID.
|
||||||
|
* @param forceVanilla - whether or not to look for vanilla classes, not injected classes.
|
||||||
|
* @return The associated class.
|
||||||
|
*/
|
||||||
|
public abstract Class<?> getPacketClassFromID(int packetID, boolean forceVanilla);
|
||||||
}
|
}
|
@ -157,7 +157,7 @@ class MinecraftRegistry {
|
|||||||
/**
|
/**
|
||||||
* Retrieves the correct packet class from a given packet ID.
|
* Retrieves the correct packet class from a given packet ID.
|
||||||
* @param packetID - the packet ID.
|
* @param packetID - the packet ID.
|
||||||
* @param vanilla - whether or not to look for vanilla classes, not injected classes.
|
* @param forceVanilla - whether or not to look for vanilla classes, not injected classes.
|
||||||
* @return The associated class.
|
* @return The associated class.
|
||||||
*/
|
*/
|
||||||
public static Class getPacketClassFromID(int packetID, boolean forceVanilla) {
|
public static Class getPacketClassFromID(int packetID, boolean forceVanilla) {
|
||||||
@ -172,6 +172,8 @@ class MinecraftRegistry {
|
|||||||
// Will most likely not be used
|
// Will most likely not be used
|
||||||
for (Map.Entry<Class, Integer> entry : getPacketToID().entrySet()) {
|
for (Map.Entry<Class, Integer> entry : getPacketToID().entrySet()) {
|
||||||
if (Objects.equal(entry.getValue(), packetID)) {
|
if (Objects.equal(entry.getValue(), packetID)) {
|
||||||
|
// Attempt to get the vanilla class here too
|
||||||
|
if (!forceVanilla || entry.getKey().getName().startsWith("net.minecraft.server"))
|
||||||
return entry.getKey();
|
return entry.getKey();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ import com.comphenix.protocol.events.PacketContainer;
|
|||||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.primitives.Primitives;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A packet constructor that uses an internal Minecraft.
|
* A packet constructor that uses an internal Minecraft.
|
||||||
@ -40,7 +41,7 @@ public class PacketConstructor {
|
|||||||
* <p>
|
* <p>
|
||||||
* Remember to call withPacket().
|
* Remember to call withPacket().
|
||||||
*/
|
*/
|
||||||
public static final PacketConstructor DEFAULT = new PacketConstructor(null);
|
public static PacketConstructor DEFAULT = new PacketConstructor(null);
|
||||||
|
|
||||||
// The constructor method that's actually responsible for creating the packet
|
// The constructor method that's actually responsible for creating the packet
|
||||||
private Constructor<?> constructorMethod;
|
private Constructor<?> constructorMethod;
|
||||||
@ -115,7 +116,7 @@ public class PacketConstructor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Class<?> packetType = MinecraftRegistry.getPacketClassFromID(id);
|
Class<?> packetType = MinecraftRegistry.getPacketClassFromID(id, true);
|
||||||
|
|
||||||
if (packetType == null)
|
if (packetType == null)
|
||||||
throw new IllegalArgumentException("Could not find a packet by the id " + id);
|
throw new IllegalArgumentException("Could not find a packet by the id " + id);
|
||||||
@ -176,7 +177,17 @@ public class PacketConstructor {
|
|||||||
// Determine if the types are similar
|
// Determine if the types are similar
|
||||||
if (params.length == types.length) {
|
if (params.length == types.length) {
|
||||||
for (int i = 0; i < params.length; i++) {
|
for (int i = 0; i < params.length; i++) {
|
||||||
if (!params[i].isAssignableFrom(types[i])) {
|
Class<?> inputType = types[i];
|
||||||
|
Class<?> paramType = params[i];
|
||||||
|
|
||||||
|
// The input type is always wrapped
|
||||||
|
if (paramType.isPrimitive()) {
|
||||||
|
// Wrap it
|
||||||
|
paramType = Primitives.wrap(paramType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare assignability
|
||||||
|
if (!paramType.isAssignableFrom(inputType)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,9 +23,8 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
@ -50,6 +49,7 @@ import com.comphenix.protocol.AsynchronousManager;
|
|||||||
import com.comphenix.protocol.ProtocolManager;
|
import com.comphenix.protocol.ProtocolManager;
|
||||||
import com.comphenix.protocol.async.AsyncFilterManager;
|
import com.comphenix.protocol.async.AsyncFilterManager;
|
||||||
import com.comphenix.protocol.async.AsyncMarker;
|
import com.comphenix.protocol.async.AsyncMarker;
|
||||||
|
import com.comphenix.protocol.error.ErrorReporter;
|
||||||
import com.comphenix.protocol.events.*;
|
import com.comphenix.protocol.events.*;
|
||||||
import com.comphenix.protocol.injector.player.PlayerInjectionHandler;
|
import com.comphenix.protocol.injector.player.PlayerInjectionHandler;
|
||||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||||
@ -108,8 +108,8 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
|||||||
private PlayerInjectionHandler playerInjection;
|
private PlayerInjectionHandler playerInjection;
|
||||||
|
|
||||||
// The two listener containers
|
// The two listener containers
|
||||||
private SortedPacketListenerList recievedListeners = new SortedPacketListenerList();
|
private SortedPacketListenerList recievedListeners;
|
||||||
private SortedPacketListenerList sendingListeners = new SortedPacketListenerList();
|
private SortedPacketListenerList sendingListeners;
|
||||||
|
|
||||||
// Whether or not this class has been closed
|
// Whether or not this class has been closed
|
||||||
private volatile boolean hasClosed;
|
private volatile boolean hasClosed;
|
||||||
@ -117,8 +117,8 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
|||||||
// The default class loader
|
// The default class loader
|
||||||
private ClassLoader classLoader;
|
private ClassLoader classLoader;
|
||||||
|
|
||||||
// Error logger
|
// Error repoter
|
||||||
private Logger logger;
|
private ErrorReporter reporter;
|
||||||
|
|
||||||
// The current server
|
// The current server
|
||||||
private Server server;
|
private Server server;
|
||||||
@ -134,24 +134,31 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
|||||||
private AtomicInteger phaseLoginCount = new AtomicInteger(0);
|
private AtomicInteger phaseLoginCount = new AtomicInteger(0);
|
||||||
private AtomicInteger phasePlayingCount = new AtomicInteger(0);
|
private AtomicInteger phasePlayingCount = new AtomicInteger(0);
|
||||||
|
|
||||||
|
// Whether or not plugins are using the send/receive methods
|
||||||
|
private AtomicBoolean packetCreation = new AtomicBoolean();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only create instances of this class if protocol lib is disabled.
|
* Only create instances of this class if protocol lib is disabled.
|
||||||
* @param unhookTask
|
* @param unhookTask
|
||||||
*/
|
*/
|
||||||
public PacketFilterManager(ClassLoader classLoader, Server server, DelayedSingleTask unhookTask, Logger logger) {
|
public PacketFilterManager(ClassLoader classLoader, Server server, DelayedSingleTask unhookTask, ErrorReporter reporter) {
|
||||||
if (logger == null)
|
if (reporter == null)
|
||||||
throw new IllegalArgumentException("logger cannot be NULL.");
|
throw new IllegalArgumentException("reporter cannot be NULL.");
|
||||||
if (classLoader == null)
|
if (classLoader == null)
|
||||||
throw new IllegalArgumentException("classLoader cannot be NULL.");
|
throw new IllegalArgumentException("classLoader cannot be NULL.");
|
||||||
|
|
||||||
// Just boilerplate
|
// Just boilerplate
|
||||||
final DelayedSingleTask finalUnhookTask = unhookTask;
|
final DelayedSingleTask finalUnhookTask = unhookTask;
|
||||||
|
|
||||||
|
// Listener containers
|
||||||
|
this.recievedListeners = new SortedPacketListenerList();
|
||||||
|
this.sendingListeners = new SortedPacketListenerList();
|
||||||
|
|
||||||
// References
|
// References
|
||||||
this.unhookTask = unhookTask;
|
this.unhookTask = unhookTask;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.classLoader = classLoader;
|
this.classLoader = classLoader;
|
||||||
this.logger = logger;
|
this.reporter = reporter;
|
||||||
|
|
||||||
// Used to determine if injection is needed
|
// Used to determine if injection is needed
|
||||||
Predicate<GamePhase> isInjectionNecessary = new Predicate<GamePhase>() {
|
Predicate<GamePhase> isInjectionNecessary = new Predicate<GamePhase>() {
|
||||||
@ -170,20 +177,20 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Initialize injection mangers
|
// Initialize injection mangers
|
||||||
this.playerInjection = new PlayerInjectionHandler(classLoader, logger, isInjectionNecessary, this, server);
|
this.playerInjection = new PlayerInjectionHandler(classLoader, reporter, isInjectionNecessary, this, packetListeners, server);
|
||||||
this.packetInjector = new PacketInjector(classLoader, this, playerInjection);
|
this.packetInjector = new PacketInjector(classLoader, this, playerInjection, reporter);
|
||||||
this.asyncFilterManager = new AsyncFilterManager(logger, server.getScheduler(), this);
|
this.asyncFilterManager = new AsyncFilterManager(reporter, server.getScheduler(), this);
|
||||||
|
|
||||||
// Attempt to load the list of server and client packets
|
// Attempt to load the list of server and client packets
|
||||||
try {
|
try {
|
||||||
this.serverPackets = MinecraftRegistry.getServerPackets();
|
this.serverPackets = MinecraftRegistry.getServerPackets();
|
||||||
this.clientPackets = MinecraftRegistry.getClientPackets();
|
this.clientPackets = MinecraftRegistry.getClientPackets();
|
||||||
} catch (FieldAccessException e) {
|
} catch (FieldAccessException e) {
|
||||||
logger.log(Level.WARNING, "Cannot load server and client packet list.", e);
|
reporter.reportWarning(this, "Cannot load server and client packet list.", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (IllegalAccessException e) {
|
} catch (IllegalAccessException e) {
|
||||||
logger.log(Level.SEVERE, "Unable to initialize packet injector.", e);
|
reporter.reportWarning(this, "Unable to initialize packet injector.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,13 +213,6 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
|||||||
*/
|
*/
|
||||||
public void setPlayerHook(PlayerInjectHooks playerHook) {
|
public void setPlayerHook(PlayerInjectHooks playerHook) {
|
||||||
playerInjection.setPlayerHook(playerHook);
|
playerInjection.setPlayerHook(playerHook);
|
||||||
|
|
||||||
// Make sure the current listeners are compatible
|
|
||||||
playerInjection.checkListener(packetListeners);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Logger getLogger() {
|
|
||||||
return logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -370,13 +370,17 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void invokePacketRecieving(PacketEvent event) {
|
public void invokePacketRecieving(PacketEvent event) {
|
||||||
|
if (!hasClosed) {
|
||||||
handlePacket(recievedListeners, event, false);
|
handlePacket(recievedListeners, event, false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void invokePacketSending(PacketEvent event) {
|
public void invokePacketSending(PacketEvent event) {
|
||||||
|
if (!hasClosed) {
|
||||||
handlePacket(sendingListeners, event, true);
|
handlePacket(sendingListeners, event, true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a packet sending or receiving event.
|
* Handle a packet sending or receiving event.
|
||||||
@ -394,9 +398,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
|||||||
|
|
||||||
// Process synchronous events
|
// Process synchronous events
|
||||||
if (sending)
|
if (sending)
|
||||||
packetListeners.invokePacketSending(logger, event);
|
packetListeners.invokePacketSending(reporter, event);
|
||||||
else
|
else
|
||||||
packetListeners.invokePacketRecieving(logger, event);
|
packetListeners.invokePacketRecieving(reporter, event);
|
||||||
|
|
||||||
// To cancel asynchronous processing, use the async marker
|
// To cancel asynchronous processing, use the async marker
|
||||||
if (!event.isCancelled() && !hasAsyncCancelled(event.getAsyncMarker())) {
|
if (!event.isCancelled() && !hasAsyncCancelled(event.getAsyncMarker())) {
|
||||||
@ -434,7 +438,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
|||||||
if (serverPackets != null && serverPackets.contains(packetID))
|
if (serverPackets != null && serverPackets.contains(packetID))
|
||||||
playerInjection.addPacketHandler(packetID);
|
playerInjection.addPacketHandler(packetID);
|
||||||
else
|
else
|
||||||
logger.warning(String.format(
|
reporter.reportWarning(this, String.format(
|
||||||
"[%s] Unsupported server packet ID in current Minecraft version: %s",
|
"[%s] Unsupported server packet ID in current Minecraft version: %s",
|
||||||
PacketAdapter.getPluginName(listener), packetID
|
PacketAdapter.getPluginName(listener), packetID
|
||||||
));
|
));
|
||||||
@ -445,7 +449,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
|||||||
if (clientPackets != null && clientPackets.contains(packetID))
|
if (clientPackets != null && clientPackets.contains(packetID))
|
||||||
packetInjector.addPacketHandler(packetID);
|
packetInjector.addPacketHandler(packetID);
|
||||||
else
|
else
|
||||||
logger.warning(String.format(
|
reporter.reportWarning(this, String.format(
|
||||||
"[%s] Unsupported client packet ID in current Minecraft version: %s",
|
"[%s] Unsupported client packet ID in current Minecraft version: %s",
|
||||||
PacketAdapter.getPluginName(listener), packetID
|
PacketAdapter.getPluginName(listener), packetID
|
||||||
));
|
));
|
||||||
@ -481,6 +485,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
|||||||
throw new IllegalArgumentException("reciever cannot be NULL.");
|
throw new IllegalArgumentException("reciever cannot be NULL.");
|
||||||
if (packet == null)
|
if (packet == null)
|
||||||
throw new IllegalArgumentException("packet cannot be NULL.");
|
throw new IllegalArgumentException("packet cannot be NULL.");
|
||||||
|
// We may have to enable player injection indefinitely after this
|
||||||
|
if (packetCreation.compareAndSet(false, true))
|
||||||
|
incrementPhases(GamePhase.PLAYING);
|
||||||
|
|
||||||
playerInjection.sendServerPacket(reciever, packet, filters);
|
playerInjection.sendServerPacket(reciever, packet, filters);
|
||||||
}
|
}
|
||||||
@ -492,11 +499,13 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void recieveClientPacket(Player sender, PacketContainer packet, boolean filters) throws IllegalAccessException, InvocationTargetException {
|
public void recieveClientPacket(Player sender, PacketContainer packet, boolean filters) throws IllegalAccessException, InvocationTargetException {
|
||||||
|
|
||||||
if (sender == null)
|
if (sender == null)
|
||||||
throw new IllegalArgumentException("sender cannot be NULL.");
|
throw new IllegalArgumentException("sender cannot be NULL.");
|
||||||
if (packet == null)
|
if (packet == null)
|
||||||
throw new IllegalArgumentException("packet cannot be NULL.");
|
throw new IllegalArgumentException("packet cannot be NULL.");
|
||||||
|
// And here too
|
||||||
|
if (packetCreation.compareAndSet(false, true))
|
||||||
|
incrementPhases(GamePhase.PLAYING);
|
||||||
|
|
||||||
Packet mcPacket = packet.getHandle();
|
Packet mcPacket = packet.getHandle();
|
||||||
|
|
||||||
@ -585,27 +594,44 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
|||||||
manager.registerEvents(new Listener() {
|
manager.registerEvents(new Listener() {
|
||||||
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||||
public void onPrePlayerJoin(PlayerJoinEvent event) {
|
public void onPrePlayerJoin(PlayerJoinEvent event) {
|
||||||
|
try {
|
||||||
// Let's clean up the other injection first.
|
// Let's clean up the other injection first.
|
||||||
playerInjection.uninjectPlayer(event.getPlayer().getAddress());
|
playerInjection.uninjectPlayer(event.getPlayer().getAddress());
|
||||||
|
} catch (Exception e) {
|
||||||
|
reporter.reportDetailed(PacketFilterManager.this, "Unable to uninject net handler for player.", e, event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||||
|
try {
|
||||||
// This call will be ignored if no listeners are registered
|
// This call will be ignored if no listeners are registered
|
||||||
playerInjection.injectPlayer(event.getPlayer());
|
playerInjection.injectPlayer(event.getPlayer());
|
||||||
|
} catch (Exception e) {
|
||||||
|
reporter.reportDetailed(PacketFilterManager.this, "Unable to inject player.", e, event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||||
|
try {
|
||||||
|
playerInjection.handleDisconnect(event.getPlayer());
|
||||||
playerInjection.uninjectPlayer(event.getPlayer());
|
playerInjection.uninjectPlayer(event.getPlayer());
|
||||||
|
} catch (Exception e) {
|
||||||
|
reporter.reportDetailed(PacketFilterManager.this, "Unable to uninject logged off player.", e, event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
public void onPluginDisabled(PluginDisableEvent event) {
|
public void onPluginDisabled(PluginDisableEvent event) {
|
||||||
|
try {
|
||||||
// Clean up in case the plugin forgets
|
// Clean up in case the plugin forgets
|
||||||
if (event.getPlugin() != plugin) {
|
if (event.getPlugin() != plugin) {
|
||||||
removePacketListeners(event.getPlugin());
|
removePacketListeners(event.getPlugin());
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
reporter.reportDetailed(PacketFilterManager.this, "Unable handle disabled plugin.", e, event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}, plugin);
|
}, plugin);
|
||||||
@ -640,6 +666,21 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
|||||||
return MinecraftRegistry.getPacketToID().get(packet.getClass());
|
return MinecraftRegistry.getPacketToID().get(packet.getClass());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerPacketClass(Class<?> clazz, int packetID) {
|
||||||
|
MinecraftRegistry.getPacketToID().put(clazz, packetID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unregisterPacketClass(Class<?> clazz) {
|
||||||
|
MinecraftRegistry.getPacketToID().remove(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> getPacketClassFromID(int packetID, boolean forceVanilla) {
|
||||||
|
return MinecraftRegistry.getPacketClassFromID(packetID, forceVanilla);
|
||||||
|
}
|
||||||
|
|
||||||
// Yes, this is crazy.
|
// Yes, this is crazy.
|
||||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
private void registerOld(PluginManager manager, Plugin plugin) {
|
private void registerOld(PluginManager manager, Plugin plugin) {
|
||||||
@ -680,10 +721,14 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
|||||||
Object event = args[0];
|
Object event = args[0];
|
||||||
|
|
||||||
// Check for the correct event
|
// Check for the correct event
|
||||||
if (event instanceof PlayerJoinEvent)
|
if (event instanceof PlayerJoinEvent) {
|
||||||
playerInjection.injectPlayer(((PlayerJoinEvent) event).getPlayer());
|
Player player = ((PlayerJoinEvent) event).getPlayer();
|
||||||
else if (event instanceof PlayerQuitEvent)
|
playerInjection.injectPlayer(player);
|
||||||
playerInjection.uninjectPlayer(((PlayerQuitEvent) event).getPlayer());
|
} else if (event instanceof PlayerQuitEvent) {
|
||||||
|
Player player = ((PlayerQuitEvent) event).getPlayer();
|
||||||
|
playerInjection.handleDisconnect(player);
|
||||||
|
playerInjection.uninjectPlayer(player);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -775,6 +820,8 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
|||||||
|
|
||||||
// Remove listeners
|
// Remove listeners
|
||||||
packetListeners.clear();
|
packetListeners.clear();
|
||||||
|
recievedListeners = null;
|
||||||
|
sendingListeners = null;
|
||||||
|
|
||||||
// Clean up async handlers. We have to do this last.
|
// Clean up async handlers. We have to do this last.
|
||||||
asyncFilterManager.cleanupAll();
|
asyncFilterManager.cleanupAll();
|
||||||
|
@ -31,6 +31,7 @@ import net.minecraft.server.Packet;
|
|||||||
import net.sf.cglib.proxy.Callback;
|
import net.sf.cglib.proxy.Callback;
|
||||||
import net.sf.cglib.proxy.Enhancer;
|
import net.sf.cglib.proxy.Enhancer;
|
||||||
|
|
||||||
|
import com.comphenix.protocol.error.ErrorReporter;
|
||||||
import com.comphenix.protocol.events.PacketContainer;
|
import com.comphenix.protocol.events.PacketContainer;
|
||||||
import com.comphenix.protocol.events.PacketEvent;
|
import com.comphenix.protocol.events.PacketEvent;
|
||||||
import com.comphenix.protocol.injector.player.PlayerInjectionHandler;
|
import com.comphenix.protocol.injector.player.PlayerInjectionHandler;
|
||||||
@ -51,6 +52,9 @@ class PacketInjector {
|
|||||||
// The packet filter manager
|
// The packet filter manager
|
||||||
private ListenerInvoker manager;
|
private ListenerInvoker manager;
|
||||||
|
|
||||||
|
// Error reporter
|
||||||
|
private ErrorReporter reporter;
|
||||||
|
|
||||||
// Allows us to determine the sender
|
// Allows us to determine the sender
|
||||||
private PlayerInjectionHandler playerInjection;
|
private PlayerInjectionHandler playerInjection;
|
||||||
|
|
||||||
@ -61,11 +65,12 @@ class PacketInjector {
|
|||||||
private ClassLoader classLoader;
|
private ClassLoader classLoader;
|
||||||
|
|
||||||
public PacketInjector(ClassLoader classLoader, ListenerInvoker manager,
|
public PacketInjector(ClassLoader classLoader, ListenerInvoker manager,
|
||||||
PlayerInjectionHandler playerInjection) throws IllegalAccessException {
|
PlayerInjectionHandler playerInjection, ErrorReporter reporter) throws IllegalAccessException {
|
||||||
|
|
||||||
this.classLoader = classLoader;
|
this.classLoader = classLoader;
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
this.playerInjection = playerInjection;
|
this.playerInjection = playerInjection;
|
||||||
|
this.reporter = reporter;
|
||||||
this.readModifier = new ConcurrentHashMap<Integer, ReadPacketModifier>();
|
this.readModifier = new ConcurrentHashMap<Integer, ReadPacketModifier>();
|
||||||
initialize();
|
initialize();
|
||||||
}
|
}
|
||||||
@ -129,12 +134,11 @@ class PacketInjector {
|
|||||||
// Subclass the specific packet class
|
// Subclass the specific packet class
|
||||||
ex.setSuperclass(old);
|
ex.setSuperclass(old);
|
||||||
ex.setCallbackType(ReadPacketModifier.class);
|
ex.setCallbackType(ReadPacketModifier.class);
|
||||||
ex.setUseCache(false);
|
|
||||||
ex.setClassLoader(classLoader);
|
ex.setClassLoader(classLoader);
|
||||||
Class proxy = ex.createClass();
|
Class proxy = ex.createClass();
|
||||||
|
|
||||||
// Create the proxy handler
|
// Create the proxy handler
|
||||||
ReadPacketModifier modifier = new ReadPacketModifier(packetID, this);
|
ReadPacketModifier modifier = new ReadPacketModifier(packetID, this, reporter);
|
||||||
readModifier.put(packetID, modifier);
|
readModifier.put(packetID, modifier);
|
||||||
|
|
||||||
// Add a static reference
|
// Add a static reference
|
||||||
@ -142,10 +146,10 @@ class PacketInjector {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Override values
|
// Override values
|
||||||
putMethod.invoke(intHashMap, packetID, proxy);
|
|
||||||
previous.put(packetID, old);
|
previous.put(packetID, old);
|
||||||
registry.put(proxy, packetID);
|
registry.put(proxy, packetID);
|
||||||
overwritten.put(packetID, proxy);
|
overwritten.put(packetID, proxy);
|
||||||
|
putMethod.invoke(intHashMap, packetID, proxy);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
|
@ -20,9 +20,12 @@ package com.comphenix.protocol.injector;
|
|||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
|
|
||||||
import com.comphenix.protocol.Packets;
|
import com.comphenix.protocol.Packets;
|
||||||
|
import com.comphenix.protocol.error.ErrorReporter;
|
||||||
import com.comphenix.protocol.events.PacketContainer;
|
import com.comphenix.protocol.events.PacketContainer;
|
||||||
import com.comphenix.protocol.events.PacketEvent;
|
import com.comphenix.protocol.events.PacketEvent;
|
||||||
|
|
||||||
@ -35,16 +38,23 @@ class ReadPacketModifier implements MethodInterceptor {
|
|||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings("rawtypes")
|
||||||
private static Class[] parameters = { DataInputStream.class };
|
private static Class[] parameters = { DataInputStream.class };
|
||||||
|
|
||||||
|
// A cancel marker
|
||||||
|
private static final Object CANCEL_MARKER = new Object();
|
||||||
|
|
||||||
// Common for all packets of the same type
|
// Common for all packets of the same type
|
||||||
private PacketInjector packetInjector;
|
private PacketInjector packetInjector;
|
||||||
private int packetID;
|
private int packetID;
|
||||||
|
|
||||||
// Whether or not a packet has been cancelled
|
// Report errors
|
||||||
private static WeakHashMap<Object, Object> override = new WeakHashMap<Object, Object>();
|
private ErrorReporter reporter;
|
||||||
|
|
||||||
public ReadPacketModifier(int packetID, PacketInjector packetInjector) {
|
// Whether or not a packet has been cancelled
|
||||||
|
private static Map<Object, Object> override = Collections.synchronizedMap(new WeakHashMap<Object, Object>());
|
||||||
|
|
||||||
|
public ReadPacketModifier(int packetID, PacketInjector packetInjector, ErrorReporter reporter) {
|
||||||
this.packetID = packetID;
|
this.packetID = packetID;
|
||||||
this.packetInjector = packetInjector;
|
this.packetInjector = packetInjector;
|
||||||
|
this.reporter = reporter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -75,11 +85,12 @@ class ReadPacketModifier implements MethodInterceptor {
|
|||||||
return proxy.invokeSuper(thisObj, args);
|
return proxy.invokeSuper(thisObj, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (override.containsKey(thisObj)) {
|
// Atomic retrieval
|
||||||
Object overridenObject = override.get(thisObj);
|
Object overridenObject = override.get(thisObj);
|
||||||
|
|
||||||
|
if (overridenObject != null) {
|
||||||
// This packet has been cancelled
|
// This packet has been cancelled
|
||||||
if (overridenObject == null) {
|
if (overridenObject == CANCEL_MARKER) {
|
||||||
// So, cancel all void methods
|
// So, cancel all void methods
|
||||||
if (method.getReturnType().equals(Void.TYPE))
|
if (method.getReturnType().equals(Void.TYPE))
|
||||||
return null;
|
return null;
|
||||||
@ -96,6 +107,7 @@ class ReadPacketModifier implements MethodInterceptor {
|
|||||||
if (returnValue == null &&
|
if (returnValue == null &&
|
||||||
Arrays.equals(method.getParameterTypes(), parameters)) {
|
Arrays.equals(method.getParameterTypes(), parameters)) {
|
||||||
|
|
||||||
|
try {
|
||||||
// We need this in order to get the correct player
|
// We need this in order to get the correct player
|
||||||
DataInputStream input = (DataInputStream) args[0];
|
DataInputStream input = (DataInputStream) args[0];
|
||||||
|
|
||||||
@ -108,7 +120,7 @@ class ReadPacketModifier implements MethodInterceptor {
|
|||||||
Packet result = event.getPacket().getHandle();
|
Packet result = event.getPacket().getHandle();
|
||||||
|
|
||||||
if (event.isCancelled()) {
|
if (event.isCancelled()) {
|
||||||
override.put(thisObj, null);
|
override.put(thisObj, CANCEL_MARKER);
|
||||||
} else if (!objectEquals(thisObj, result)) {
|
} else if (!objectEquals(thisObj, result)) {
|
||||||
override.put(thisObj, result);
|
override.put(thisObj, result);
|
||||||
}
|
}
|
||||||
@ -118,6 +130,10 @@ class ReadPacketModifier implements MethodInterceptor {
|
|||||||
packetInjector.scheduleDataInputRefresh(event.getPlayer());
|
packetInjector.scheduleDataInputRefresh(event.getPlayer());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
// Minecraft cannot handle this error
|
||||||
|
reporter.reportDetailed(this, "Cannot handle clienet packet.", e, args[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return returnValue;
|
return returnValue;
|
||||||
|
@ -18,11 +18,9 @@
|
|||||||
package com.comphenix.protocol.injector;
|
package com.comphenix.protocol.injector;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import com.comphenix.protocol.concurrency.AbstractConcurrentListenerMultimap;
|
import com.comphenix.protocol.concurrency.AbstractConcurrentListenerMultimap;
|
||||||
import com.comphenix.protocol.events.PacketAdapter;
|
import com.comphenix.protocol.error.ErrorReporter;
|
||||||
import com.comphenix.protocol.events.PacketEvent;
|
import com.comphenix.protocol.events.PacketEvent;
|
||||||
import com.comphenix.protocol.events.PacketListener;
|
import com.comphenix.protocol.events.PacketListener;
|
||||||
|
|
||||||
@ -31,14 +29,14 @@ import com.comphenix.protocol.events.PacketListener;
|
|||||||
*
|
*
|
||||||
* @author Kristian
|
* @author Kristian
|
||||||
*/
|
*/
|
||||||
class SortedPacketListenerList extends AbstractConcurrentListenerMultimap<PacketListener> {
|
public final class SortedPacketListenerList extends AbstractConcurrentListenerMultimap<PacketListener> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invokes the given packet event for every registered listener.
|
* Invokes the given packet event for every registered listener.
|
||||||
* @param logger - the logger that will be used to inform about listener exceptions.
|
* @param reporter - the error reporter that will be used to inform about listener exceptions.
|
||||||
* @param event - the packet event to invoke.
|
* @param event - the packet event to invoke.
|
||||||
*/
|
*/
|
||||||
public void invokePacketRecieving(Logger logger, PacketEvent event) {
|
public void invokePacketRecieving(ErrorReporter reporter, PacketEvent event) {
|
||||||
Collection<PrioritizedListener<PacketListener>> list = getListener(event.getPacketID());
|
Collection<PrioritizedListener<PacketListener>> list = getListener(event.getPacketID());
|
||||||
|
|
||||||
if (list == null)
|
if (list == null)
|
||||||
@ -50,19 +48,17 @@ class SortedPacketListenerList extends AbstractConcurrentListenerMultimap<Packet
|
|||||||
element.getListener().onPacketReceiving(event);
|
element.getListener().onPacketReceiving(event);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
// Minecraft doesn't want your Exception.
|
// Minecraft doesn't want your Exception.
|
||||||
logger.log(Level.SEVERE,
|
reporter.reportMinimal(element.getListener().getPlugin(), "onPacketReceiving()", e);
|
||||||
"Exception occured in onPacketReceiving() for " +
|
|
||||||
PacketAdapter.getPluginName(element.getListener()), e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invokes the given packet event for every registered listener.
|
* Invokes the given packet event for every registered listener.
|
||||||
* @param logger - the logger that will be used to inform about listener exceptions.
|
* @param reporter - the error reporter that will be used to inform about listener exceptions.
|
||||||
* @param event - the packet event to invoke.
|
* @param event - the packet event to invoke.
|
||||||
*/
|
*/
|
||||||
public void invokePacketSending(Logger logger, PacketEvent event) {
|
public void invokePacketSending(ErrorReporter reporter, PacketEvent event) {
|
||||||
Collection<PrioritizedListener<PacketListener>> list = getListener(event.getPacketID());
|
Collection<PrioritizedListener<PacketListener>> list = getListener(event.getPacketID());
|
||||||
|
|
||||||
if (list == null)
|
if (list == null)
|
||||||
@ -73,9 +69,7 @@ class SortedPacketListenerList extends AbstractConcurrentListenerMultimap<Packet
|
|||||||
element.getListener().onPacketSending(event);
|
element.getListener().onPacketSending(event);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
// Minecraft doesn't want your Exception.
|
// Minecraft doesn't want your Exception.
|
||||||
logger.log(Level.SEVERE,
|
reporter.reportMinimal(element.getListener().getPlugin(), "onPacketSending()", e);
|
||||||
"Exception occured in onPacketSending() for " +
|
|
||||||
PacketAdapter.getPluginName(element.getListener()), e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import java.lang.reflect.Method;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import com.comphenix.protocol.injector.ListenerInvoker;
|
||||||
import com.comphenix.protocol.injector.player.NetworkFieldInjector.FakePacket;
|
import com.comphenix.protocol.injector.player.NetworkFieldInjector.FakePacket;
|
||||||
|
|
||||||
import net.minecraft.server.Packet;
|
import net.minecraft.server.Packet;
|
||||||
@ -90,8 +91,10 @@ class InjectedArrayList extends ArrayList<Packet> {
|
|||||||
* @return The inverted packet.
|
* @return The inverted packet.
|
||||||
*/
|
*/
|
||||||
Packet createNegativePacket(Packet source) {
|
Packet createNegativePacket(Packet source) {
|
||||||
Enhancer ex = new Enhancer();
|
ListenerInvoker invoker = injector.getInvoker();
|
||||||
Class<?> type = source.getClass();
|
|
||||||
|
int packetID = invoker.getPacketID(source);
|
||||||
|
Class<?> type = invoker.getPacketClassFromID(packetID, true);
|
||||||
|
|
||||||
// We want to subtract the byte amount that were added to the running
|
// We want to subtract the byte amount that were added to the running
|
||||||
// total of outstanding packets. Otherwise, cancelling too many packets
|
// total of outstanding packets. Otherwise, cancelling too many packets
|
||||||
@ -111,11 +114,30 @@ class InjectedArrayList extends ArrayList<Packet> {
|
|||||||
// }
|
// }
|
||||||
// ect.
|
// ect.
|
||||||
// }
|
// }
|
||||||
|
Enhancer ex = new Enhancer();
|
||||||
|
ex.setSuperclass(type);
|
||||||
ex.setInterfaces(new Class[] { FakePacket.class } );
|
ex.setInterfaces(new Class[] { FakePacket.class } );
|
||||||
ex.setUseCache(true);
|
ex.setUseCache(true);
|
||||||
ex.setClassLoader(classLoader);
|
ex.setClassLoader(classLoader);
|
||||||
ex.setSuperclass(type);
|
ex.setCallbackType(InvertedIntegerCallback.class);
|
||||||
ex.setCallback(new MethodInterceptor() {
|
|
||||||
|
Class<?> proxyClass = ex.createClass();
|
||||||
|
|
||||||
|
// Temporarily associate the fake packet class
|
||||||
|
invoker.registerPacketClass(proxyClass, packetID);
|
||||||
|
|
||||||
|
Packet fake = (Packet) Enhancer.create(proxyClass, new InvertedIntegerCallback());
|
||||||
|
|
||||||
|
// Remove this association
|
||||||
|
invoker.unregisterPacketClass(proxyClass);
|
||||||
|
return fake;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inverts the integer result of every integer method.
|
||||||
|
* @author Kristian
|
||||||
|
*/
|
||||||
|
private class InvertedIntegerCallback implements MethodInterceptor {
|
||||||
@Override
|
@Override
|
||||||
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
|
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
|
||||||
if (method.getReturnType().equals(int.class) && args.length == 0) {
|
if (method.getReturnType().equals(int.class) && args.length == 0) {
|
||||||
@ -125,8 +147,5 @@ class InjectedArrayList extends ArrayList<Packet> {
|
|||||||
return proxy.invokeSuper(obj, args);
|
return proxy.invokeSuper(obj, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return (Packet) ex.create();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,14 +21,13 @@ import java.lang.reflect.Field;
|
|||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import net.minecraft.server.NetLoginHandler;
|
import net.minecraft.server.NetLoginHandler;
|
||||||
import net.sf.cglib.proxy.Factory;
|
import net.sf.cglib.proxy.Factory;
|
||||||
|
|
||||||
import org.bukkit.Server;
|
import org.bukkit.Server;
|
||||||
|
|
||||||
|
import com.comphenix.protocol.error.ErrorReporter;
|
||||||
import com.comphenix.protocol.reflect.FieldUtils;
|
import com.comphenix.protocol.reflect.FieldUtils;
|
||||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||||
import com.comphenix.protocol.reflect.ObjectCloner;
|
import com.comphenix.protocol.reflect.ObjectCloner;
|
||||||
@ -55,16 +54,16 @@ class InjectedServerConnection {
|
|||||||
private NetLoginInjector netLoginInjector;
|
private NetLoginInjector netLoginInjector;
|
||||||
|
|
||||||
private Server server;
|
private Server server;
|
||||||
private Logger logger;
|
private ErrorReporter reporter;
|
||||||
private boolean hasAttempted;
|
private boolean hasAttempted;
|
||||||
private boolean hasSuccess;
|
private boolean hasSuccess;
|
||||||
|
|
||||||
private Object minecraftServer = null;
|
private Object minecraftServer = null;
|
||||||
|
|
||||||
public InjectedServerConnection(Logger logger, Server server, NetLoginInjector netLoginInjector) {
|
public InjectedServerConnection(ErrorReporter reporter, Server server, NetLoginInjector netLoginInjector) {
|
||||||
this.listFields = new ArrayList<VolatileField>();
|
this.listFields = new ArrayList<VolatileField>();
|
||||||
this.replacedLists = new ArrayList<ReplacedArrayList<Object>>();
|
this.replacedLists = new ArrayList<ReplacedArrayList<Object>>();
|
||||||
this.logger = logger;
|
this.reporter = reporter;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.netLoginInjector = netLoginInjector;
|
this.netLoginInjector = netLoginInjector;
|
||||||
}
|
}
|
||||||
@ -83,7 +82,7 @@ class InjectedServerConnection {
|
|||||||
try {
|
try {
|
||||||
minecraftServer = FieldUtils.readField(minecraftServerField, server, true);
|
minecraftServer = FieldUtils.readField(minecraftServerField, server, true);
|
||||||
} catch (IllegalAccessException e1) {
|
} catch (IllegalAccessException e1) {
|
||||||
logger.log(Level.WARNING, "Cannot extract minecraft server from Bukkit.");
|
reporter.reportWarning(this, "Cannot extract minecraft server from Bukkit.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,15 +94,13 @@ class InjectedServerConnection {
|
|||||||
injectServerConnection();
|
injectServerConnection();
|
||||||
|
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
// DEBUG
|
|
||||||
logger.log(Level.WARNING, "Reverting to old 1.2.5 server connection injection.", e);
|
|
||||||
|
|
||||||
// Minecraft 1.2.5 or lower
|
// Minecraft 1.2.5 or lower
|
||||||
injectListenerThread();
|
injectListenerThread();
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Oh damn - inform the player
|
// Oh damn - inform the player
|
||||||
logger.log(Level.SEVERE, "Cannot inject into server connection. Bad things will happen.", e);
|
reporter.reportDetailed(this, "Cannot inject into server connection. Bad things will happen.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +112,7 @@ class InjectedServerConnection {
|
|||||||
listenerThreadField = FuzzyReflection.fromObject(minecraftServer).
|
listenerThreadField = FuzzyReflection.fromObject(minecraftServer).
|
||||||
getFieldByType(".*NetworkListenThread");
|
getFieldByType(".*NetworkListenThread");
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
logger.log(Level.SEVERE, "Cannot find listener thread in MinecraftServer.", e);
|
reporter.reportDetailed(this, "Cannot find listener thread in MinecraftServer.", e, minecraftServer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +122,7 @@ class InjectedServerConnection {
|
|||||||
try {
|
try {
|
||||||
listenerThread = listenerThreadField.get(minecraftServer);
|
listenerThread = listenerThreadField.get(minecraftServer);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.log(Level.WARNING, "Unable to read the listener thread.", e);
|
reporter.reportWarning(this, "Unable to read the listener thread.", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,7 +139,7 @@ class InjectedServerConnection {
|
|||||||
try {
|
try {
|
||||||
serverConnection = serverConnectionMethod.invoke(minecraftServer);
|
serverConnection = serverConnectionMethod.invoke(minecraftServer);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
logger.log(Level.WARNING, "Unable to retrieve server connection", ex);
|
reporter.reportDetailed(this, "Unable to retrieve server connection", ex, minecraftServer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +151,7 @@ class InjectedServerConnection {
|
|||||||
|
|
||||||
// Verify the field count
|
// Verify the field count
|
||||||
if (matches.size() != 1)
|
if (matches.size() != 1)
|
||||||
logger.log(Level.WARNING, "Unexpected number of threads in " + serverConnection.getClass().getName());
|
reporter.reportWarning(this, "Unexpected number of threads in " + serverConnection.getClass().getName());
|
||||||
else
|
else
|
||||||
dedicatedThreadField = matches.get(0);
|
dedicatedThreadField = matches.get(0);
|
||||||
}
|
}
|
||||||
@ -164,7 +161,7 @@ class InjectedServerConnection {
|
|||||||
if (dedicatedThreadField != null)
|
if (dedicatedThreadField != null)
|
||||||
injectEveryListField(FieldUtils.readField(dedicatedThreadField, serverConnection, true), 1);
|
injectEveryListField(FieldUtils.readField(dedicatedThreadField, serverConnection, true), 1);
|
||||||
} catch (IllegalAccessException e) {
|
} catch (IllegalAccessException e) {
|
||||||
logger.log(Level.WARNING, "Unable to retrieve net handler thread.", e);
|
reporter.reportWarning(this, "Unable to retrieve net handler thread.", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
injectIntoList(serverConnection, listField);
|
injectIntoList(serverConnection, listField);
|
||||||
@ -186,7 +183,7 @@ class InjectedServerConnection {
|
|||||||
|
|
||||||
// Warn about unexpected errors
|
// Warn about unexpected errors
|
||||||
if (lists.size() < minimum) {
|
if (lists.size() < minimum) {
|
||||||
logger.log(Level.WARNING, "Unable to inject " + minimum + " lists in " + container.getClass().getName());
|
reporter.reportWarning(this, "Unable to inject " + minimum + " lists in " + container.getClass().getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,7 +217,12 @@ class InjectedServerConnection {
|
|||||||
// Is this a normal Minecraft object?
|
// Is this a normal Minecraft object?
|
||||||
if (!(inserting instanceof Factory)) {
|
if (!(inserting instanceof Factory)) {
|
||||||
// If so, copy the content of the old element to the new
|
// If so, copy the content of the old element to the new
|
||||||
|
try {
|
||||||
ObjectCloner.copyTo(inserting, replacement, inserting.getClass());
|
ObjectCloner.copyTo(inserting, replacement, inserting.getClass());
|
||||||
|
} catch (Throwable e) {
|
||||||
|
reporter.reportDetailed(InjectedServerConnection.this, "Cannot copy old " + inserting +
|
||||||
|
" to new.", e, inserting, replacement);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,12 +4,11 @@ import java.util.concurrent.ConcurrentMap;
|
|||||||
import java.util.concurrent.locks.Lock;
|
import java.util.concurrent.locks.Lock;
|
||||||
import java.util.concurrent.locks.ReadWriteLock;
|
import java.util.concurrent.locks.ReadWriteLock;
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import org.bukkit.Server;
|
import org.bukkit.Server;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import com.comphenix.protocol.error.ErrorReporter;
|
||||||
import com.comphenix.protocol.injector.GamePhase;
|
import com.comphenix.protocol.injector.GamePhase;
|
||||||
import com.comphenix.protocol.injector.player.TemporaryPlayerFactory.InjectContainer;
|
import com.comphenix.protocol.injector.player.TemporaryPlayerFactory.InjectContainer;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
@ -27,16 +26,16 @@ class NetLoginInjector {
|
|||||||
private PlayerInjectionHandler injectionHandler;
|
private PlayerInjectionHandler injectionHandler;
|
||||||
private Server server;
|
private Server server;
|
||||||
|
|
||||||
// The current logger
|
// The current error rerporter
|
||||||
private Logger logger;
|
private ErrorReporter reporter;
|
||||||
|
|
||||||
private ReadWriteLock injectionLock = new ReentrantReadWriteLock();
|
private ReadWriteLock injectionLock = new ReentrantReadWriteLock();
|
||||||
|
|
||||||
// Used to create fake players
|
// Used to create fake players
|
||||||
private TemporaryPlayerFactory tempPlayerFactory = new TemporaryPlayerFactory();
|
private TemporaryPlayerFactory tempPlayerFactory = new TemporaryPlayerFactory();
|
||||||
|
|
||||||
public NetLoginInjector(Logger logger, PlayerInjectionHandler injectionHandler, Server server) {
|
public NetLoginInjector(ErrorReporter reporter, PlayerInjectionHandler injectionHandler, Server server) {
|
||||||
this.logger = logger;
|
this.reporter = reporter;
|
||||||
this.injectionHandler = injectionHandler;
|
this.injectionHandler = injectionHandler;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
}
|
}
|
||||||
@ -63,12 +62,15 @@ class NetLoginInjector {
|
|||||||
InjectContainer container = (InjectContainer) fakePlayer;
|
InjectContainer container = (InjectContainer) fakePlayer;
|
||||||
container.setInjector(injector);
|
container.setInjector(injector);
|
||||||
|
|
||||||
|
// Save the login
|
||||||
|
injectedLogins.putIfAbsent(inserting, injector);
|
||||||
|
|
||||||
// NetServerInjector can never work (currently), so we don't need to replace the NetLoginHandler
|
// NetServerInjector can never work (currently), so we don't need to replace the NetLoginHandler
|
||||||
return inserting;
|
return inserting;
|
||||||
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
// Minecraft can't handle this, so we'll deal with it here
|
// Minecraft can't handle this, so we'll deal with it here
|
||||||
logger.log(Level.WARNING, "Unable to hook NetLoginHandler.", e);
|
reporter.reportDetailed(this, "Unable to hook NetLoginHandler.", e, inserting);
|
||||||
return inserting;
|
return inserting;
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
@ -93,8 +95,34 @@ class NetLoginInjector {
|
|||||||
PlayerInjector injected = injectedLogins.get(removing);
|
PlayerInjector injected = injectedLogins.get(removing);
|
||||||
|
|
||||||
if (injected != null) {
|
if (injected != null) {
|
||||||
injected.cleanupAll();
|
try {
|
||||||
|
PlayerInjector newInjector = null;
|
||||||
|
Player player = injected.getPlayer();
|
||||||
|
|
||||||
|
// Clean up list
|
||||||
injectedLogins.remove(removing);
|
injectedLogins.remove(removing);
|
||||||
|
|
||||||
|
// No need to clean up twice
|
||||||
|
if (injected.isClean())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Hack to clean up other references
|
||||||
|
newInjector = injectionHandler.getInjectorByNetworkHandler(injected.getNetworkManager());
|
||||||
|
|
||||||
|
// Update NetworkManager
|
||||||
|
if (newInjector == null) {
|
||||||
|
injectionHandler.uninjectPlayer(player);
|
||||||
|
} else {
|
||||||
|
injectionHandler.uninjectPlayer(player, false);
|
||||||
|
|
||||||
|
if (injected instanceof NetworkObjectInjector)
|
||||||
|
newInjector.setNetworkManager(injected.getNetworkManager(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Throwable e) {
|
||||||
|
// Don't leak this to Minecraft
|
||||||
|
reporter.reportDetailed(this, "Cannot cleanup NetLoginHandler.", e, removing);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,11 +24,11 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import com.comphenix.protocol.Packets;
|
import com.comphenix.protocol.Packets;
|
||||||
|
import com.comphenix.protocol.error.ErrorReporter;
|
||||||
import com.comphenix.protocol.events.ListeningWhitelist;
|
import com.comphenix.protocol.events.ListeningWhitelist;
|
||||||
import com.comphenix.protocol.events.PacketListener;
|
import com.comphenix.protocol.events.PacketListener;
|
||||||
import com.comphenix.protocol.injector.GamePhase;
|
import com.comphenix.protocol.injector.GamePhase;
|
||||||
@ -73,10 +73,10 @@ class NetworkFieldInjector extends PlayerInjector {
|
|||||||
// Used to construct proxy objects
|
// Used to construct proxy objects
|
||||||
private ClassLoader classLoader;
|
private ClassLoader classLoader;
|
||||||
|
|
||||||
public NetworkFieldInjector(ClassLoader classLoader, Logger logger, Player player,
|
public NetworkFieldInjector(ClassLoader classLoader, ErrorReporter reporter, Player player,
|
||||||
ListenerInvoker manager, IntegerSet sendingFilters) throws IllegalAccessException {
|
ListenerInvoker manager, IntegerSet sendingFilters) throws IllegalAccessException {
|
||||||
|
|
||||||
super(logger, player, manager);
|
super(reporter, player, manager);
|
||||||
this.classLoader = classLoader;
|
this.classLoader = classLoader;
|
||||||
this.sendingFilters = sendingFilters;
|
this.sendingFilters = sendingFilters;
|
||||||
}
|
}
|
||||||
@ -123,11 +123,14 @@ class NetworkFieldInjector extends PlayerInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void checkListener(PacketListener listener) {
|
public UnsupportedListener checkListener(PacketListener listener) {
|
||||||
|
int[] unsupported = { Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK };
|
||||||
|
|
||||||
// Unfortunately, we don't support chunk packets
|
// Unfortunately, we don't support chunk packets
|
||||||
if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(),
|
if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), unsupported)) {
|
||||||
Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK)) {
|
return new UnsupportedListener("The NETWORK_FIELD_INJECTOR hook doesn't support map chunk listeners.", unsupported);
|
||||||
throw new IllegalStateException("The NETWORK_FIELD_INJECTOR hook doesn't support map chunk listeners.");
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +169,7 @@ class NetworkFieldInjector extends PlayerInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void cleanupAll() {
|
protected void cleanHook() {
|
||||||
// Clean up
|
// Clean up
|
||||||
for (VolatileField overriden : overridenLists) {
|
for (VolatileField overriden : overridenLists) {
|
||||||
List<Packet> minecraftList = (List<Packet>) overriden.getOldValue();
|
List<Packet> minecraftList = (List<Packet>) overriden.getOldValue();
|
||||||
@ -191,6 +194,11 @@ class NetworkFieldInjector extends PlayerInjector {
|
|||||||
overridenLists.clear();
|
overridenLists.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleDisconnect() {
|
||||||
|
// No need to do anything
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canInject(GamePhase phase) {
|
public boolean canInject(GamePhase phase) {
|
||||||
// All phases should work
|
// All phases should work
|
||||||
|
@ -28,11 +28,11 @@ import net.sf.cglib.proxy.MethodInterceptor;
|
|||||||
import net.sf.cglib.proxy.MethodProxy;
|
import net.sf.cglib.proxy.MethodProxy;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import com.comphenix.protocol.Packets;
|
import com.comphenix.protocol.Packets;
|
||||||
|
import com.comphenix.protocol.error.ErrorReporter;
|
||||||
import com.comphenix.protocol.events.ListeningWhitelist;
|
import com.comphenix.protocol.events.ListeningWhitelist;
|
||||||
import com.comphenix.protocol.events.PacketListener;
|
import com.comphenix.protocol.events.PacketListener;
|
||||||
import com.comphenix.protocol.injector.GamePhase;
|
import com.comphenix.protocol.injector.GamePhase;
|
||||||
@ -51,9 +51,12 @@ class NetworkObjectInjector extends PlayerInjector {
|
|||||||
// Used to construct proxy objects
|
// Used to construct proxy objects
|
||||||
private ClassLoader classLoader;
|
private ClassLoader classLoader;
|
||||||
|
|
||||||
public NetworkObjectInjector(ClassLoader classLoader, Logger logger, Player player,
|
// Shared callback filter - avoid creating a new class every time
|
||||||
|
private static CallbackFilter callbackFilter;
|
||||||
|
|
||||||
|
public NetworkObjectInjector(ClassLoader classLoader, ErrorReporter reporter, Player player,
|
||||||
ListenerInvoker invoker, IntegerSet sendingFilters) throws IllegalAccessException {
|
ListenerInvoker invoker, IntegerSet sendingFilters) throws IllegalAccessException {
|
||||||
super(logger, player, invoker);
|
super(reporter, player, invoker);
|
||||||
this.sendingFilters = sendingFilters;
|
this.sendingFilters = sendingFilters;
|
||||||
this.classLoader = classLoader;
|
this.classLoader = classLoader;
|
||||||
}
|
}
|
||||||
@ -85,11 +88,14 @@ class NetworkObjectInjector extends PlayerInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void checkListener(PacketListener listener) {
|
public UnsupportedListener checkListener(PacketListener listener) {
|
||||||
|
int[] unsupported = { Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK };
|
||||||
|
|
||||||
// Unfortunately, we don't support chunk packets
|
// Unfortunately, we don't support chunk packets
|
||||||
if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(),
|
if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), unsupported)) {
|
||||||
Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK)) {
|
return new UnsupportedListener("The NETWORK_OBJECT_INJECTOR hook doesn't support map chunk listeners.", unsupported);
|
||||||
throw new IllegalStateException("The NETWORK_FIELD_INJECTOR hook doesn't support map chunk listeners.");
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,12 +137,9 @@ class NetworkObjectInjector extends PlayerInjector {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create our proxy object
|
// Share callback filter - that way, we avoid generating a new class every time.
|
||||||
Enhancer ex = new Enhancer();
|
if (callbackFilter == null) {
|
||||||
ex.setClassLoader(classLoader);
|
callbackFilter = new CallbackFilter() {
|
||||||
ex.setSuperclass(networkInterface);
|
|
||||||
ex.setCallbacks(new Callback[] { queueFilter, dispatch });
|
|
||||||
ex.setCallbackFilter(new CallbackFilter() {
|
|
||||||
@Override
|
@Override
|
||||||
public int accept(Method method) {
|
public int accept(Method method) {
|
||||||
if (method.equals(queueMethod))
|
if (method.equals(queueMethod))
|
||||||
@ -144,7 +147,15 @@ class NetworkObjectInjector extends PlayerInjector {
|
|||||||
else
|
else
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create our proxy object
|
||||||
|
Enhancer ex = new Enhancer();
|
||||||
|
ex.setClassLoader(classLoader);
|
||||||
|
ex.setSuperclass(networkInterface);
|
||||||
|
ex.setCallbacks(new Callback[] { queueFilter, dispatch });
|
||||||
|
ex.setCallbackFilter(callbackFilter);
|
||||||
|
|
||||||
// Inject it, if we can.
|
// Inject it, if we can.
|
||||||
networkManagerRef.setValue(ex.create());
|
networkManagerRef.setValue(ex.create());
|
||||||
@ -152,13 +163,18 @@ class NetworkObjectInjector extends PlayerInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cleanupAll() {
|
protected void cleanHook() {
|
||||||
// Clean up
|
// Clean up
|
||||||
if (networkManagerRef != null && networkManagerRef.isCurrentSet()) {
|
if (networkManagerRef != null && networkManagerRef.isCurrentSet()) {
|
||||||
networkManagerRef.revertValue();
|
networkManagerRef.revertValue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleDisconnect() {
|
||||||
|
// No need to do anything
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canInject(GamePhase phase) {
|
public boolean canInject(GamePhase phase) {
|
||||||
// Works for all phases
|
// Works for all phases
|
||||||
|
@ -17,9 +17,9 @@
|
|||||||
|
|
||||||
package com.comphenix.protocol.injector.player;
|
package com.comphenix.protocol.injector.player;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import net.minecraft.server.Packet;
|
import net.minecraft.server.Packet;
|
||||||
import net.sf.cglib.proxy.Callback;
|
import net.sf.cglib.proxy.Callback;
|
||||||
@ -32,6 +32,7 @@ import net.sf.cglib.proxy.NoOp;
|
|||||||
|
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import com.comphenix.protocol.error.ErrorReporter;
|
||||||
import com.comphenix.protocol.events.PacketListener;
|
import com.comphenix.protocol.events.PacketListener;
|
||||||
import com.comphenix.protocol.injector.GamePhase;
|
import com.comphenix.protocol.injector.GamePhase;
|
||||||
import com.comphenix.protocol.injector.ListenerInvoker;
|
import com.comphenix.protocol.injector.ListenerInvoker;
|
||||||
@ -50,6 +51,9 @@ import com.comphenix.protocol.reflect.instances.ExistingGenerator;
|
|||||||
*/
|
*/
|
||||||
public class NetworkServerInjector extends PlayerInjector {
|
public class NetworkServerInjector extends PlayerInjector {
|
||||||
|
|
||||||
|
private volatile static CallbackFilter callbackFilter;
|
||||||
|
|
||||||
|
private static Field disconnectField;
|
||||||
private static Method sendPacketMethod;
|
private static Method sendPacketMethod;
|
||||||
private InjectedServerConnection serverInjection;
|
private InjectedServerConnection serverInjection;
|
||||||
|
|
||||||
@ -59,12 +63,15 @@ public class NetworkServerInjector extends PlayerInjector {
|
|||||||
// Used to create proxy objects
|
// Used to create proxy objects
|
||||||
private ClassLoader classLoader;
|
private ClassLoader classLoader;
|
||||||
|
|
||||||
|
// Whether or not the player has disconnected
|
||||||
|
private boolean hasDisconnected;
|
||||||
|
|
||||||
public NetworkServerInjector(
|
public NetworkServerInjector(
|
||||||
ClassLoader classLoader, Logger logger, Player player,
|
ClassLoader classLoader, ErrorReporter reporter, Player player,
|
||||||
ListenerInvoker invoker, IntegerSet sendingFilters,
|
ListenerInvoker invoker, IntegerSet sendingFilters,
|
||||||
InjectedServerConnection serverInjection) throws IllegalAccessException {
|
InjectedServerConnection serverInjection) throws IllegalAccessException {
|
||||||
|
|
||||||
super(logger, player, invoker);
|
super(reporter, player, invoker);
|
||||||
this.classLoader = classLoader;
|
this.classLoader = classLoader;
|
||||||
this.sendingFilters = sendingFilters;
|
this.sendingFilters = sendingFilters;
|
||||||
this.serverInjection = serverInjection;
|
this.serverInjection = serverInjection;
|
||||||
@ -164,10 +171,10 @@ public class NetworkServerInjector extends PlayerInjector {
|
|||||||
};
|
};
|
||||||
Callback noOpCallback = NoOp.INSTANCE;
|
Callback noOpCallback = NoOp.INSTANCE;
|
||||||
|
|
||||||
ex.setClassLoader(classLoader);
|
// Share callback filter - that way, we avoid generating a new class for
|
||||||
ex.setSuperclass(serverClass);
|
// every logged in player.
|
||||||
ex.setCallbacks(new Callback[] { sendPacketCallback, noOpCallback });
|
if (callbackFilter == null) {
|
||||||
ex.setCallbackFilter(new CallbackFilter() {
|
callbackFilter = new CallbackFilter() {
|
||||||
@Override
|
@Override
|
||||||
public int accept(Method method) {
|
public int accept(Method method) {
|
||||||
if (method.equals(sendPacketMethod))
|
if (method.equals(sendPacketMethod))
|
||||||
@ -175,7 +182,13 @@ public class NetworkServerInjector extends PlayerInjector {
|
|||||||
else
|
else
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ex.setClassLoader(classLoader);
|
||||||
|
ex.setSuperclass(serverClass);
|
||||||
|
ex.setCallbacks(new Callback[] { sendPacketCallback, noOpCallback });
|
||||||
|
ex.setCallbackFilter(callbackFilter);
|
||||||
|
|
||||||
// Find the Minecraft NetServerHandler superclass
|
// Find the Minecraft NetServerHandler superclass
|
||||||
Class<?> minecraftSuperClass = getFirstMinecraftSuperClass(serverHandler.getClass());
|
Class<?> minecraftSuperClass = getFirstMinecraftSuperClass(serverHandler.getClass());
|
||||||
@ -202,6 +215,7 @@ public class NetworkServerInjector extends PlayerInjector {
|
|||||||
if (proxyObject != null) {
|
if (proxyObject != null) {
|
||||||
// This will be done by InjectedServerConnection instead
|
// This will be done by InjectedServerConnection instead
|
||||||
//copyTo(serverHandler, proxyObject);
|
//copyTo(serverHandler, proxyObject);
|
||||||
|
|
||||||
serverInjection.replaceServerHandler(serverHandler, proxyObject);
|
serverInjection.replaceServerHandler(serverHandler, proxyObject);
|
||||||
serverHandlerRef.setValue(proxyObject);
|
serverHandlerRef.setValue(proxyObject);
|
||||||
return true;
|
return true;
|
||||||
@ -232,7 +246,7 @@ public class NetworkServerInjector extends PlayerInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cleanupAll() {
|
protected void cleanHook() {
|
||||||
if (serverHandlerRef != null && serverHandlerRef.isCurrentSet()) {
|
if (serverHandlerRef != null && serverHandlerRef.isCurrentSet()) {
|
||||||
ObjectCloner.copyTo(serverHandlerRef.getValue(), serverHandlerRef.getOldValue(), serverHandler.getClass());
|
ObjectCloner.copyTo(serverHandlerRef.getValue(), serverHandlerRef.getOldValue(), serverHandler.getClass());
|
||||||
serverHandlerRef.revertValue();
|
serverHandlerRef.revertValue();
|
||||||
@ -250,14 +264,46 @@ public class NetworkServerInjector extends PlayerInjector {
|
|||||||
} catch (IllegalAccessException e) {
|
} catch (IllegalAccessException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent the PlayerQuitEvent from being sent twice
|
||||||
|
if (hasDisconnected) {
|
||||||
|
setDisconnect(serverHandlerRef.getValue(), true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serverInjection.revertServerHandler(serverHandler);
|
serverInjection.revertServerHandler(serverHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void checkListener(PacketListener listener) {
|
public void handleDisconnect() {
|
||||||
|
hasDisconnected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the disconnected field in a NetServerHandler.
|
||||||
|
* @param handler - the NetServerHandler.
|
||||||
|
* @param value - the new value.
|
||||||
|
*/
|
||||||
|
private void setDisconnect(Object handler, boolean value) {
|
||||||
|
// Set it
|
||||||
|
try {
|
||||||
|
// Load the field
|
||||||
|
if (disconnectField == null) {
|
||||||
|
disconnectField = FuzzyReflection.fromObject(handler).getFieldByName("disconnected.*");
|
||||||
|
}
|
||||||
|
FieldUtils.writeField(disconnectField, handler, value);
|
||||||
|
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
reporter.reportDetailed(this, "Unable to find disconnect field. Is ProtocolLib up to date?", e, handler);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
reporter.reportWarning(this, "Unable to update disconnected field. Player quit event may be sent twice.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UnsupportedListener checkListener(PacketListener listener) {
|
||||||
// We support everything
|
// We support everything
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -24,14 +24,13 @@ import java.net.Socket;
|
|||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import net.minecraft.server.Packet;
|
import net.minecraft.server.Packet;
|
||||||
|
|
||||||
import org.bukkit.Server;
|
import org.bukkit.Server;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import com.comphenix.protocol.error.ErrorReporter;
|
||||||
import com.comphenix.protocol.events.PacketAdapter;
|
import com.comphenix.protocol.events.PacketAdapter;
|
||||||
import com.comphenix.protocol.events.PacketContainer;
|
import com.comphenix.protocol.events.PacketContainer;
|
||||||
import com.comphenix.protocol.events.PacketListener;
|
import com.comphenix.protocol.events.PacketListener;
|
||||||
@ -72,8 +71,8 @@ public class PlayerInjectionHandler {
|
|||||||
private volatile PlayerInjectHooks loginPlayerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT;
|
private volatile PlayerInjectHooks loginPlayerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT;
|
||||||
private volatile PlayerInjectHooks playingPlayerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT;
|
private volatile PlayerInjectHooks playingPlayerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT;
|
||||||
|
|
||||||
// Error logger
|
// Error reporter
|
||||||
private Logger logger;
|
private ErrorReporter reporter;
|
||||||
|
|
||||||
// Whether or not we're closing
|
// Whether or not we're closing
|
||||||
private boolean hasClosed;
|
private boolean hasClosed;
|
||||||
@ -84,21 +83,25 @@ public class PlayerInjectionHandler {
|
|||||||
// Enabled packet filters
|
// Enabled packet filters
|
||||||
private IntegerSet sendingFilters = new IntegerSet(MAXIMUM_PACKET_ID + 1);
|
private IntegerSet sendingFilters = new IntegerSet(MAXIMUM_PACKET_ID + 1);
|
||||||
|
|
||||||
|
// List of packet listeners
|
||||||
|
private Set<PacketListener> packetListeners;
|
||||||
|
|
||||||
// The class loader we're using
|
// The class loader we're using
|
||||||
private ClassLoader classLoader;
|
private ClassLoader classLoader;
|
||||||
|
|
||||||
// Used to filter injection attempts
|
// Used to filter injection attempts
|
||||||
private Predicate<GamePhase> injectionFilter;
|
private Predicate<GamePhase> injectionFilter;
|
||||||
|
|
||||||
public PlayerInjectionHandler(ClassLoader classLoader, Logger logger, Predicate<GamePhase> injectionFilter,
|
public PlayerInjectionHandler(ClassLoader classLoader, ErrorReporter reporter, Predicate<GamePhase> injectionFilter,
|
||||||
ListenerInvoker invoker, Server server) {
|
ListenerInvoker invoker, Set<PacketListener> packetListeners, Server server) {
|
||||||
|
|
||||||
this.classLoader = classLoader;
|
this.classLoader = classLoader;
|
||||||
this.logger = logger;
|
this.reporter = reporter;
|
||||||
this.invoker = invoker;
|
this.invoker = invoker;
|
||||||
this.injectionFilter = injectionFilter;
|
this.injectionFilter = injectionFilter;
|
||||||
this.netLoginInjector = new NetLoginInjector(logger, this, server);
|
this.packetListeners = packetListeners;
|
||||||
this.serverInjection = new InjectedServerConnection(logger, server, netLoginInjector);
|
this.netLoginInjector = new NetLoginInjector(reporter, this, server);
|
||||||
|
this.serverInjection = new InjectedServerConnection(reporter, server, netLoginInjector);
|
||||||
serverInjection.injectList();
|
serverInjection.injectList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,6 +147,9 @@ public class PlayerInjectionHandler {
|
|||||||
loginPlayerHook = playerHook;
|
loginPlayerHook = playerHook;
|
||||||
if (phase.hasPlaying())
|
if (phase.hasPlaying())
|
||||||
playingPlayerHook = playerHook;
|
playingPlayerHook = playerHook;
|
||||||
|
|
||||||
|
// Make sure the current listeners are compatible
|
||||||
|
checkListener(packetListeners);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -173,11 +179,11 @@ public class PlayerInjectionHandler {
|
|||||||
// Construct the correct player hook
|
// Construct the correct player hook
|
||||||
switch (hook) {
|
switch (hook) {
|
||||||
case NETWORK_HANDLER_FIELDS:
|
case NETWORK_HANDLER_FIELDS:
|
||||||
return new NetworkFieldInjector(classLoader, logger, player, invoker, sendingFilters);
|
return new NetworkFieldInjector(classLoader, reporter, player, invoker, sendingFilters);
|
||||||
case NETWORK_MANAGER_OBJECT:
|
case NETWORK_MANAGER_OBJECT:
|
||||||
return new NetworkObjectInjector(classLoader, logger, player, invoker, sendingFilters);
|
return new NetworkObjectInjector(classLoader, reporter, player, invoker, sendingFilters);
|
||||||
case NETWORK_SERVER_OBJECT:
|
case NETWORK_SERVER_OBJECT:
|
||||||
return new NetworkServerInjector(classLoader, logger, player, invoker, sendingFilters, serverInjection);
|
return new NetworkServerInjector(classLoader, reporter, player, invoker, sendingFilters, serverInjection);
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Cannot construct a player injector.");
|
throw new IllegalArgumentException("Cannot construct a player injector.");
|
||||||
}
|
}
|
||||||
@ -198,7 +204,7 @@ public class PlayerInjectionHandler {
|
|||||||
if (injector != null) {
|
if (injector != null) {
|
||||||
return injector.getPlayer();
|
return injector.getPlayer();
|
||||||
} else {
|
} else {
|
||||||
System.out.println("Unable to find stream: " + inputStream);
|
reporter.reportWarning(this, "Unable to find stream: " + inputStream);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,27 +287,21 @@ public class PlayerInjectionHandler {
|
|||||||
DataInputStream inputStream = injector.getInputStream(false);
|
DataInputStream inputStream = injector.getInputStream(false);
|
||||||
|
|
||||||
Socket socket = injector.getSocket();
|
Socket socket = injector.getSocket();
|
||||||
SocketAddress address = socket.getRemoteSocketAddress();
|
SocketAddress address = socket != null ? socket.getRemoteSocketAddress() : null;
|
||||||
|
|
||||||
// Make sure the current player is not logged out
|
// Guard against NPE here too
|
||||||
if (socket.isClosed()) {
|
PlayerInjector previous = address != null ? addressLookup.get(address) : null;
|
||||||
throw new PlayerLoggedOutException();
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerInjector previous = addressLookup.get(address);
|
|
||||||
|
|
||||||
// Close any previously associated hooks before we proceed
|
// Close any previously associated hooks before we proceed
|
||||||
if (previous != null) {
|
if (previous != null) {
|
||||||
uninjectPlayer(previous.getPlayer());
|
uninjectPlayer(previous.getPlayer(), false, true);
|
||||||
|
|
||||||
// Remove the "hooked" network manager in our instance as well
|
|
||||||
if (previous instanceof NetworkObjectInjector) {
|
|
||||||
injector.setNetworkManager(previous.getNetworkManager(), true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
injector.injectManager();
|
injector.injectManager();
|
||||||
|
|
||||||
|
if (inputStream != null)
|
||||||
dataInputLookup.put(inputStream, injector);
|
dataInputLookup.put(inputStream, injector);
|
||||||
|
if (address != null)
|
||||||
addressLookup.put(address, injector);
|
addressLookup.put(address, injector);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -311,7 +311,8 @@ public class PlayerInjectionHandler {
|
|||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Mark this injection attempt as a failure
|
// Mark this injection attempt as a failure
|
||||||
logger.log(Level.SEVERE, "Player hook " + tempHook.toString() + " failed.", e);
|
reporter.reportDetailed(this, "Player hook " + tempHook.toString() + " failed.",
|
||||||
|
e, player, injectionPoint, phase);
|
||||||
hookFailed = true;
|
hookFailed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,7 +320,7 @@ public class PlayerInjectionHandler {
|
|||||||
tempHook = PlayerInjectHooks.values()[tempHook.ordinal() - 1];
|
tempHook = PlayerInjectHooks.values()[tempHook.ordinal() - 1];
|
||||||
|
|
||||||
if (hookFailed)
|
if (hookFailed)
|
||||||
logger.log(Level.INFO, "Switching to " + tempHook.toString() + " instead.");
|
reporter.reportWarning(this, "Switching to " + tempHook.toString() + " instead.");
|
||||||
|
|
||||||
// Check for UTTER FAILURE
|
// Check for UTTER FAILURE
|
||||||
if (tempHook == PlayerInjectHooks.NONE) {
|
if (tempHook == PlayerInjectHooks.NONE) {
|
||||||
@ -340,9 +341,11 @@ public class PlayerInjectionHandler {
|
|||||||
if (permanentHook != getPlayerHook(phase))
|
if (permanentHook != getPlayerHook(phase))
|
||||||
setPlayerHook(phase, tempHook);
|
setPlayerHook(phase, tempHook);
|
||||||
|
|
||||||
// Save last injector
|
// Save injector
|
||||||
|
if (injector != null) {
|
||||||
playerInjection.put(player, injector);
|
playerInjection.put(player, injector);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return injector;
|
return injector;
|
||||||
}
|
}
|
||||||
@ -352,16 +355,50 @@ public class PlayerInjectionHandler {
|
|||||||
try {
|
try {
|
||||||
if (injector != null)
|
if (injector != null)
|
||||||
injector.cleanupAll();
|
injector.cleanupAll();
|
||||||
} catch (Exception e2) {
|
} catch (Exception ex) {
|
||||||
logger.log(Level.WARNING, "Cleaing up after player hook failed.", e2);
|
reporter.reportDetailed(this, "Cleaing up after player hook failed.", ex, injector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoke special routines for handling disconnect before a player is uninjected.
|
||||||
|
* @param player - player to process.
|
||||||
|
*/
|
||||||
|
public void handleDisconnect(Player player) {
|
||||||
|
PlayerInjector injector = getInjector(player);
|
||||||
|
|
||||||
|
if (injector != null) {
|
||||||
|
injector.handleDisconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregisters the given player.
|
* Unregisters the given player.
|
||||||
* @param player - player to unregister.
|
* @param player - player to unregister.
|
||||||
|
* @return TRUE if a player has been uninjected, FALSE otherwise.
|
||||||
*/
|
*/
|
||||||
public void uninjectPlayer(Player player) {
|
public boolean uninjectPlayer(Player player) {
|
||||||
|
return uninjectPlayer(player, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters the given player.
|
||||||
|
* @param player - player to unregister.
|
||||||
|
* @param removeAuxiliary - TRUE to remove auxiliary information, such as input stream and address.
|
||||||
|
* @return TRUE if a player has been uninjected, FALSE otherwise.
|
||||||
|
*/
|
||||||
|
public boolean uninjectPlayer(Player player, boolean removeAuxiliary) {
|
||||||
|
return uninjectPlayer(player, removeAuxiliary, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters the given player.
|
||||||
|
* @param player - player to unregister.
|
||||||
|
* @param removeAuxiliary - TRUE to remove auxiliary information, such as input stream and address.
|
||||||
|
* @param prepareNextHook - whether or not we need to fix any lingering hooks.
|
||||||
|
* @return TRUE if a player has been uninjected, FALSE otherwise.
|
||||||
|
*/
|
||||||
|
private boolean uninjectPlayer(Player player, boolean removeAuxiliary, boolean prepareNextHook) {
|
||||||
if (!hasClosed && player != null) {
|
if (!hasClosed && player != null) {
|
||||||
|
|
||||||
PlayerInjector injector = playerInjection.remove(player);
|
PlayerInjector injector = playerInjection.remove(player);
|
||||||
@ -371,26 +408,54 @@ public class PlayerInjectionHandler {
|
|||||||
InetSocketAddress address = player.getAddress();
|
InetSocketAddress address = player.getAddress();
|
||||||
injector.cleanupAll();
|
injector.cleanupAll();
|
||||||
|
|
||||||
dataInputLookup.remove(input);
|
// Remove the "hooked" network manager in our instance as well
|
||||||
|
if (prepareNextHook && injector instanceof NetworkObjectInjector) {
|
||||||
|
try {
|
||||||
|
PlayerInjector dummyInjector = getHookInstance(player, PlayerInjectHooks.NETWORK_SERVER_OBJECT);
|
||||||
|
dummyInjector.initializePlayer(player);
|
||||||
|
dummyInjector.setNetworkManager(injector.getNetworkManager(), true);
|
||||||
|
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
// Let the user know
|
||||||
|
reporter.reportWarning(this, "Unable to fully revert old injector. May cause conflicts.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
if (removeAuxiliary) {
|
||||||
|
if (input != null)
|
||||||
|
dataInputLookup.remove(input);
|
||||||
if (address != null)
|
if (address != null)
|
||||||
addressLookup.remove(address);
|
addressLookup.remove(address);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregisters a player by the given address.
|
* Unregisters a player by the given address.
|
||||||
|
* <p>
|
||||||
|
* If the server handler has been created before we've gotten a chance to unject the player,
|
||||||
|
* the method will try a workaround to remove the injected hook in the NetServerHandler.
|
||||||
|
*
|
||||||
* @param address - address of the player to unregister.
|
* @param address - address of the player to unregister.
|
||||||
|
* @param serverHandler - whether or not the net server handler has already been created.
|
||||||
|
* @return TRUE if a player has been uninjected, FALSE otherwise.
|
||||||
*/
|
*/
|
||||||
public void uninjectPlayer(InetSocketAddress address) {
|
public boolean uninjectPlayer(InetSocketAddress address) {
|
||||||
if (!hasClosed && address != null) {
|
if (!hasClosed && address != null) {
|
||||||
PlayerInjector injector = addressLookup.get(address);
|
PlayerInjector injector = addressLookup.get(address);
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
if (injector != null)
|
if (injector != null)
|
||||||
uninjectPlayer(injector.getPlayer());
|
uninjectPlayer(injector.getPlayer(), false, true);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -407,7 +472,7 @@ public class PlayerInjectionHandler {
|
|||||||
if (injector != null)
|
if (injector != null)
|
||||||
injector.sendServerPacket(packet.getHandle(), filters);
|
injector.sendServerPacket(packet.getHandle(), filters);
|
||||||
else
|
else
|
||||||
logger.log(Level.WARNING, String.format(
|
reporter.reportWarning(this, String.format(
|
||||||
"Unable to send packet %s (%s): Player %s has logged out.",
|
"Unable to send packet %s (%s): Player %s has logged out.",
|
||||||
packet.getID(), packet, reciever.getName()
|
packet.getID(), packet, reciever.getName()
|
||||||
));
|
));
|
||||||
@ -428,7 +493,7 @@ public class PlayerInjectionHandler {
|
|||||||
if (injector != null)
|
if (injector != null)
|
||||||
injector.processPacket(mcPacket);
|
injector.processPacket(mcPacket);
|
||||||
else
|
else
|
||||||
logger.log(Level.WARNING, String.format(
|
reporter.reportWarning(this, String.format(
|
||||||
"Unable to receieve packet %s. Player %s has logged out.",
|
"Unable to receieve packet %s. Player %s has logged out.",
|
||||||
mcPacket, player.getName()
|
mcPacket, player.getName()
|
||||||
));
|
));
|
||||||
@ -443,6 +508,26 @@ public class PlayerInjectionHandler {
|
|||||||
return playerInjection.get(player);
|
return playerInjection.get(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a player injector by looking for its NetworkManager.
|
||||||
|
* @param networkManager - current network manager.
|
||||||
|
* @return Related player injector.
|
||||||
|
*/
|
||||||
|
PlayerInjector getInjectorByNetworkHandler(Object networkManager) {
|
||||||
|
// That's not legal
|
||||||
|
if (networkManager == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// O(n) is okay in this instance. This is only a backup solution.
|
||||||
|
for (PlayerInjector injector : playerInjection.values()) {
|
||||||
|
if (injector.getNetworkManager() == networkManager)
|
||||||
|
return injector;
|
||||||
|
}
|
||||||
|
|
||||||
|
// None found
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the given listeners are valid.
|
* Determine if the given listeners are valid.
|
||||||
* @param listeners - listeners to check.
|
* @param listeners - listeners to check.
|
||||||
@ -451,26 +536,30 @@ public class PlayerInjectionHandler {
|
|||||||
// Make sure the current listeners are compatible
|
// Make sure the current listeners are compatible
|
||||||
if (lastSuccessfulHook != null) {
|
if (lastSuccessfulHook != null) {
|
||||||
for (PacketListener listener : listeners) {
|
for (PacketListener listener : listeners) {
|
||||||
try {
|
|
||||||
checkListener(listener);
|
checkListener(listener);
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
logger.log(Level.WARNING, "Unsupported listener.", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if a listener is valid or not.
|
* Determine if a listener is valid or not.
|
||||||
|
* <p>
|
||||||
|
* If not, a warning will be printed to the console.
|
||||||
* @param listener - listener to check.
|
* @param listener - listener to check.
|
||||||
* @throws IllegalStateException If the given listener's whitelist cannot be fulfilled.
|
|
||||||
*/
|
*/
|
||||||
public void checkListener(PacketListener listener) {
|
public void checkListener(PacketListener listener) {
|
||||||
try {
|
if (lastSuccessfulHook != null) {
|
||||||
if (lastSuccessfulHook != null)
|
UnsupportedListener result = lastSuccessfulHook.checkListener(listener);
|
||||||
lastSuccessfulHook.checkListener(listener);
|
|
||||||
} catch (Exception e) {
|
// We won't prevent the listener, as it may still have valid packets
|
||||||
throw new IllegalStateException("Registering listener " + PacketAdapter.getPluginName(listener) + " failed", e);
|
if (result != null) {
|
||||||
|
reporter.reportWarning(this, "Cannot fully register listener for " +
|
||||||
|
PacketAdapter.getPluginName(listener) + ": " + result.toString());
|
||||||
|
|
||||||
|
// These are illegal
|
||||||
|
for (int packetID : result.getPackets())
|
||||||
|
removePacketHandler(packetID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -482,14 +571,6 @@ public class PlayerInjectionHandler {
|
|||||||
return sendingFilters.toSet();
|
return sendingFilters.toSet();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the current logger.
|
|
||||||
* @return Error logger.
|
|
||||||
*/
|
|
||||||
public Logger getLogger() {
|
|
||||||
return logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() {
|
public void close() {
|
||||||
|
|
||||||
// Guard
|
// Guard
|
||||||
|
@ -23,8 +23,7 @@ import java.lang.reflect.Field;
|
|||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.util.logging.Level;
|
import java.net.SocketAddress;
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import net.minecraft.server.EntityPlayer;
|
import net.minecraft.server.EntityPlayer;
|
||||||
import net.minecraft.server.NetLoginHandler;
|
import net.minecraft.server.NetLoginHandler;
|
||||||
@ -35,6 +34,7 @@ import org.bukkit.craftbukkit.entity.CraftPlayer;
|
|||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import com.comphenix.protocol.Packets;
|
import com.comphenix.protocol.Packets;
|
||||||
|
import com.comphenix.protocol.error.ErrorReporter;
|
||||||
import com.comphenix.protocol.events.PacketContainer;
|
import com.comphenix.protocol.events.PacketContainer;
|
||||||
import com.comphenix.protocol.events.PacketEvent;
|
import com.comphenix.protocol.events.PacketEvent;
|
||||||
import com.comphenix.protocol.events.PacketListener;
|
import com.comphenix.protocol.events.PacketListener;
|
||||||
@ -99,17 +99,20 @@ abstract class PlayerInjector {
|
|||||||
protected DataInputStream cachedInput;
|
protected DataInputStream cachedInput;
|
||||||
|
|
||||||
// Handle errors
|
// Handle errors
|
||||||
protected Logger logger;
|
protected ErrorReporter reporter;
|
||||||
|
|
||||||
// Scheduled action on the next packet event
|
// Scheduled action on the next packet event
|
||||||
protected Runnable scheduledAction;
|
protected Runnable scheduledAction;
|
||||||
|
|
||||||
|
// Whether or not the injector has been cleaned
|
||||||
|
private boolean clean;
|
||||||
|
|
||||||
// Whether or not to update the current player on the first Packet1Login
|
// Whether or not to update the current player on the first Packet1Login
|
||||||
boolean updateOnLogin;
|
boolean updateOnLogin;
|
||||||
Player updatedPlayer;
|
Player updatedPlayer;
|
||||||
|
|
||||||
public PlayerInjector(Logger logger, Player player, ListenerInvoker invoker) throws IllegalAccessException {
|
public PlayerInjector(ErrorReporter reporter, Player player, ListenerInvoker invoker) throws IllegalAccessException {
|
||||||
this.logger = logger;
|
this.reporter = reporter;
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.invoker = invoker;
|
this.invoker = invoker;
|
||||||
}
|
}
|
||||||
@ -256,6 +259,21 @@ abstract class PlayerInjector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the associated address of this player.
|
||||||
|
* @return The associated address.
|
||||||
|
* @throws IllegalAccessException If we're unable to read the socket field.
|
||||||
|
*/
|
||||||
|
public SocketAddress getAddress() throws IllegalAccessException {
|
||||||
|
Socket socket = getSocket();
|
||||||
|
|
||||||
|
// Guard against NULL
|
||||||
|
if (socket != null)
|
||||||
|
return socket.getRemoteSocketAddress();
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to disconnect the current client.
|
* Attempt to disconnect the current client.
|
||||||
* @param message - the message to display.
|
* @param message - the message to display.
|
||||||
@ -284,19 +302,24 @@ abstract class PlayerInjector {
|
|||||||
disconnect.invoke(handler, message);
|
disconnect.invoke(handler, message);
|
||||||
return;
|
return;
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
logger.log(Level.WARNING, "Invalid argument passed to disconnect method: " + message, e);
|
reporter.reportDetailed(this, "Invalid argument passed to disconnect method: " + message, e, handler);
|
||||||
} catch (IllegalAccessException e) {
|
} catch (IllegalAccessException e) {
|
||||||
logger.log(Level.SEVERE, "Unable to access disconnect method.", e);
|
reporter.reportWarning(this, "Unable to access disconnect method.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fuck it
|
// Fuck it
|
||||||
try {
|
try {
|
||||||
getSocket().close();
|
Socket socket = getSocket();
|
||||||
|
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.log(Level.SEVERE, "Unable to close socket.", e);
|
reporter.reportDetailed(this, "Unable to close socket.", e, socket);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (IllegalAccessException e) {
|
} catch (IllegalAccessException e) {
|
||||||
logger.log(Level.SEVERE, "Insufficient permissions. Cannot close socket.", e);
|
reporter.reportWarning(this, "Insufficient permissions. Cannot close socket.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,7 +336,7 @@ abstract class PlayerInjector {
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
hasProxyType = true;
|
hasProxyType = true;
|
||||||
logger.log(Level.WARNING, "Detected server handler proxy type by another plugin. Conflict may occur!");
|
reporter.reportWarning(this, "Detected server handler proxy type by another plugin. Conflict may occur!");
|
||||||
|
|
||||||
// No? Is it a Proxy type?
|
// No? Is it a Proxy type?
|
||||||
try {
|
try {
|
||||||
@ -328,7 +351,7 @@ abstract class PlayerInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (IllegalAccessException e) {
|
} catch (IllegalAccessException e) {
|
||||||
logger.warning("Unable to load server handler from proxy type.");
|
reporter.reportWarning(this, "Unable to load server handler from proxy type.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nope, just go with it
|
// Nope, just go with it
|
||||||
@ -427,7 +450,29 @@ abstract class PlayerInjector {
|
|||||||
/**
|
/**
|
||||||
* Remove all hooks and modifications.
|
* Remove all hooks and modifications.
|
||||||
*/
|
*/
|
||||||
public abstract void cleanupAll();
|
public final void cleanupAll() {
|
||||||
|
if (!clean)
|
||||||
|
cleanHook();
|
||||||
|
clean = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up after the player has disconnected.
|
||||||
|
*/
|
||||||
|
public abstract void handleDisconnect();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add custom cleanup behavior.
|
||||||
|
*/
|
||||||
|
protected abstract void cleanHook();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether or not this hook has already been cleaned.
|
||||||
|
* @return TRUE if it has, FALSE otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isClean() {
|
||||||
|
return clean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if this inject method can even be attempted.
|
* Determine if this inject method can even be attempted.
|
||||||
@ -444,10 +489,12 @@ abstract class PlayerInjector {
|
|||||||
/**
|
/**
|
||||||
* Invoked before a new listener is registered.
|
* Invoked before a new listener is registered.
|
||||||
* <p>
|
* <p>
|
||||||
* The player injector should throw an exception if this listener cannot be properly supplied with packet events.
|
* The player injector should only return a non-null value if some or all of the packet IDs are unsupported.
|
||||||
|
*
|
||||||
* @param listener - the listener that is about to be registered.
|
* @param listener - the listener that is about to be registered.
|
||||||
|
* @return A error message with the unsupported packet IDs, or NULL if this listener is valid.
|
||||||
*/
|
*/
|
||||||
public abstract void checkListener(PacketListener listener);
|
public abstract UnsupportedListener checkListener(PacketListener listener);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows a packet to be sent by the listeners.
|
* Allows a packet to be sent by the listeners.
|
||||||
@ -455,6 +502,7 @@ abstract class PlayerInjector {
|
|||||||
* @return The given packet, or the packet replaced by the listeners.
|
* @return The given packet, or the packet replaced by the listeners.
|
||||||
*/
|
*/
|
||||||
public Packet handlePacketSending(Packet packet) {
|
public Packet handlePacketSending(Packet packet) {
|
||||||
|
try {
|
||||||
// Get the packet ID too
|
// Get the packet ID too
|
||||||
Integer id = invoker.getPacketID(packet);
|
Integer id = invoker.getPacketID(packet);
|
||||||
Player currentPlayer = player;
|
Player currentPlayer = player;
|
||||||
@ -470,7 +518,7 @@ abstract class PlayerInjector {
|
|||||||
try {
|
try {
|
||||||
updatedPlayer = getEntityPlayer(getNetHandler()).getBukkitEntity();
|
updatedPlayer = getEntityPlayer(getNetHandler()).getBukkitEntity();
|
||||||
} catch (IllegalAccessException e) {
|
} catch (IllegalAccessException e) {
|
||||||
logger.log(Level.WARNING, "Cannot update player in PlayerEvent.", e);
|
reporter.reportDetailed(this, "Cannot update player in PlayerEvent.", e, packet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -494,6 +542,10 @@ abstract class PlayerInjector {
|
|||||||
return event.getPacket().getHandle();
|
return event.getPacket().getHandle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch (Throwable e) {
|
||||||
|
reporter.reportDetailed(this, "Cannot handle server packet.", e, packet);
|
||||||
|
}
|
||||||
|
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -544,6 +596,14 @@ abstract class PlayerInjector {
|
|||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object that can invoke the packet events.
|
||||||
|
* @return Packet event invoker.
|
||||||
|
*/
|
||||||
|
public ListenerInvoker getInvoker() {
|
||||||
|
return invoker;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the hooked player object OR the more up-to-date player instance.
|
* Retrieve the hooked player object OR the more up-to-date player instance.
|
||||||
* @return The hooked player, or a more up-to-date instance.
|
* @return The hooked player, or a more up-to-date instance.
|
||||||
|
@ -68,7 +68,7 @@ class ReplacedArrayList<TKey> extends ArrayList<TKey> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoksed when an element is being removed.
|
* Invoked when an element is being removed.
|
||||||
* @param removing - the element being removed.
|
* @param removing - the element being removed.
|
||||||
*/
|
*/
|
||||||
protected void onRemoved(TKey removing) {
|
protected void onRemoved(TKey removing) {
|
||||||
@ -264,6 +264,15 @@ class ReplacedArrayList<TKey> extends ArrayList<TKey> {
|
|||||||
addMapping(target, replacement, false);
|
addMapping(target, replacement, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the old value, if it exists.
|
||||||
|
* @param target - the key.
|
||||||
|
* @return The value that was replaced, or NULL.
|
||||||
|
*/
|
||||||
|
public TKey getMapping(TKey target) {
|
||||||
|
return replaceMap.get(target);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a replace rule.
|
* Add a replace rule.
|
||||||
* <p>
|
* <p>
|
||||||
@ -284,8 +293,9 @@ class ReplacedArrayList<TKey> extends ArrayList<TKey> {
|
|||||||
/**
|
/**
|
||||||
* Revert the given mapping.
|
* Revert the given mapping.
|
||||||
* @param target - the instance we replaced.
|
* @param target - the instance we replaced.
|
||||||
|
* @return The old mapped value, or NULL if nothing was replaced.
|
||||||
*/
|
*/
|
||||||
public synchronized void removeMapping(TKey target) {
|
public synchronized TKey removeMapping(TKey target) {
|
||||||
// Make sure the mapping exist
|
// Make sure the mapping exist
|
||||||
if (replaceMap.containsKey(target)) {
|
if (replaceMap.containsKey(target)) {
|
||||||
TKey replacement = replaceMap.get(target);
|
TKey replacement = replaceMap.get(target);
|
||||||
@ -293,7 +303,25 @@ class ReplacedArrayList<TKey> extends ArrayList<TKey> {
|
|||||||
|
|
||||||
// Revert existing elements
|
// Revert existing elements
|
||||||
replaceAll(replacement, target);
|
replaceAll(replacement, target);
|
||||||
|
return replacement;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swap the new replaced value with its old value.
|
||||||
|
* @param target - the instance we replaced.
|
||||||
|
* @param The old mapped value, or NULL if nothing was swapped.
|
||||||
|
*/
|
||||||
|
public synchronized TKey swapMapping(TKey target) {
|
||||||
|
// Make sure the mapping exist
|
||||||
|
TKey replacement = removeMapping(target);
|
||||||
|
|
||||||
|
// Add the reverse
|
||||||
|
if (replacement != null) {
|
||||||
|
replaceMap.put(replacement, target);
|
||||||
|
}
|
||||||
|
return replacement;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,6 +42,9 @@ class TemporaryPlayerFactory {
|
|||||||
// Helpful constructors
|
// Helpful constructors
|
||||||
private final PacketConstructor chatPacket;
|
private final PacketConstructor chatPacket;
|
||||||
|
|
||||||
|
// Prevent too many class creations
|
||||||
|
private static CallbackFilter callbackFilter;
|
||||||
|
|
||||||
public TemporaryPlayerFactory() {
|
public TemporaryPlayerFactory() {
|
||||||
chatPacket = PacketConstructor.DEFAULT.withPacket(3, new Object[] { "DEMO" });
|
chatPacket = PacketConstructor.DEFAULT.withPacket(3, new Object[] { "DEMO" });
|
||||||
}
|
}
|
||||||
@ -76,13 +79,16 @@ class TemporaryPlayerFactory {
|
|||||||
String methodName = method.getName();
|
String methodName = method.getName();
|
||||||
PlayerInjector injector = ((InjectContainer) obj).getInjector();
|
PlayerInjector injector = ((InjectContainer) obj).getInjector();
|
||||||
|
|
||||||
|
if (injector == null)
|
||||||
|
throw new IllegalStateException("Unable to find injector.");
|
||||||
|
|
||||||
// Use the socket to get the address
|
// Use the socket to get the address
|
||||||
if (methodName.equalsIgnoreCase("getName"))
|
if (methodName.equalsIgnoreCase("getName"))
|
||||||
return "UNKNOWN[" + injector.getSocket().getRemoteSocketAddress() + "]";
|
return "UNKNOWN[" + injector.getSocket().getRemoteSocketAddress() + "]";
|
||||||
if (methodName.equalsIgnoreCase("getPlayer"))
|
if (methodName.equalsIgnoreCase("getPlayer"))
|
||||||
return injector.getUpdatedPlayer();
|
return injector.getUpdatedPlayer();
|
||||||
if (methodName.equalsIgnoreCase("getAddress"))
|
if (methodName.equalsIgnoreCase("getAddress"))
|
||||||
return injector.getSocket().getRemoteSocketAddress();
|
return injector.getAddress();
|
||||||
if (methodName.equalsIgnoreCase("getServer"))
|
if (methodName.equalsIgnoreCase("getServer"))
|
||||||
return server;
|
return server;
|
||||||
|
|
||||||
@ -117,12 +123,9 @@ class TemporaryPlayerFactory {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// CGLib is amazing
|
// Shared callback filter
|
||||||
Enhancer ex = new Enhancer();
|
if (callbackFilter == null) {
|
||||||
ex.setSuperclass(InjectContainer.class);
|
callbackFilter = new CallbackFilter() {
|
||||||
ex.setInterfaces(new Class[] { Player.class });
|
|
||||||
ex.setCallbacks(new Callback[] { NoOp.INSTANCE, implementation });
|
|
||||||
ex.setCallbackFilter(new CallbackFilter() {
|
|
||||||
@Override
|
@Override
|
||||||
public int accept(Method method) {
|
public int accept(Method method) {
|
||||||
// Do not override the object method or the superclass methods
|
// Do not override the object method or the superclass methods
|
||||||
@ -132,7 +135,15 @@ class TemporaryPlayerFactory {
|
|||||||
else
|
else
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// CGLib is amazing
|
||||||
|
Enhancer ex = new Enhancer();
|
||||||
|
ex.setSuperclass(InjectContainer.class);
|
||||||
|
ex.setInterfaces(new Class[] { Player.class });
|
||||||
|
ex.setCallbacks(new Callback[] { NoOp.INSTANCE, implementation });
|
||||||
|
ex.setCallbackFilter(callbackFilter);
|
||||||
|
|
||||||
return (Player) ex.create();
|
return (Player) ex.create();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
package com.comphenix.protocol.injector.player;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import com.google.common.base.Joiner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an error message from a player injector.
|
||||||
|
*
|
||||||
|
* @author Kristian
|
||||||
|
*/
|
||||||
|
class UnsupportedListener {
|
||||||
|
private String message;
|
||||||
|
private int[] packets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new error message.
|
||||||
|
* @param message - the message.
|
||||||
|
* @param packets - unsupported packets.
|
||||||
|
*/
|
||||||
|
public UnsupportedListener(String message, int[] packets) {
|
||||||
|
super();
|
||||||
|
this.message = message;
|
||||||
|
this.packets = packets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the error message.
|
||||||
|
* @return Error message.
|
||||||
|
*/
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve all unsupported packets.
|
||||||
|
* @return Unsupported packets.
|
||||||
|
*/
|
||||||
|
public int[] getPackets() {
|
||||||
|
return packets;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("%s (%s)", message, Joiner.on(", ").join(Arrays.asList(packets)));
|
||||||
|
}
|
||||||
|
}
|
@ -36,10 +36,11 @@ public class Statistics {
|
|||||||
|
|
||||||
public Statistics(Plugin plugin) throws IOException {
|
public Statistics(Plugin plugin) throws IOException {
|
||||||
metrics = new Metrics(plugin);
|
metrics = new Metrics(plugin);
|
||||||
metrics.start();
|
|
||||||
|
|
||||||
// Determine who is using this library
|
// Determine who is using this library
|
||||||
addPluginUserGraph(metrics);
|
addPluginUserGraph(metrics);
|
||||||
|
|
||||||
|
metrics.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addPluginUserGraph(Metrics metrics) {
|
private void addPluginUserGraph(Metrics metrics) {
|
||||||
|
688
ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Updater.java
Normale Datei
688
ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Updater.java
Normale Datei
@ -0,0 +1,688 @@
|
|||||||
|
package com.comphenix.protocol.metrics;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Updater for Bukkit.
|
||||||
|
*
|
||||||
|
* This class provides the means to safetly and easily update a plugin, or check to see if it is updated using dev.bukkit.org
|
||||||
|
*/
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
import javax.xml.stream.XMLEventReader;
|
||||||
|
import javax.xml.stream.XMLInputFactory;
|
||||||
|
import javax.xml.stream.XMLStreamException;
|
||||||
|
import javax.xml.stream.events.XMLEvent;
|
||||||
|
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check dev.bukkit.org to find updates for a given plugin, and download the updates if needed.
|
||||||
|
* <p>
|
||||||
|
* <b>VERY, VERY IMPORTANT</b>: Because there are no standards for adding auto-update toggles in your plugin's config, this system provides NO CHECK WITH YOUR CONFIG to make sure the user has allowed auto-updating.
|
||||||
|
* <br>
|
||||||
|
* It is a <b>BUKKIT POLICY</b> that you include a boolean value in your config that prevents the auto-updater from running <b>AT ALL</b>.
|
||||||
|
* <br>
|
||||||
|
* If you fail to include this option in your config, your plugin will be <b>REJECTED</b> when you attempt to submit it to dev.bukkit.org.
|
||||||
|
* <p>
|
||||||
|
* An example of a good configuration option would be something similar to 'auto-update: true' - if this value is set to false you may NOT run the auto-updater.
|
||||||
|
* <br>
|
||||||
|
* If you are unsure about these rules, please read the plugin submission guidelines: http://goo.gl/8iU5l
|
||||||
|
*
|
||||||
|
* @author H31IX
|
||||||
|
*/
|
||||||
|
public class Updater
|
||||||
|
{
|
||||||
|
// If the version number contains one of these, don't update.
|
||||||
|
private static final String[] noUpdateTag = {"-DEV", "-PRE", "-SNAPSHOT"};
|
||||||
|
|
||||||
|
// Slugs will be appended to this to get to the project's RSS feed
|
||||||
|
private static final String DBOUrl = "http://dev.bukkit.org/server-mods/";
|
||||||
|
private static final int BYTE_SIZE = 1024; // Used for downloading files
|
||||||
|
|
||||||
|
private final Plugin plugin;
|
||||||
|
private final String slug;
|
||||||
|
|
||||||
|
private volatile long totalSize; // Holds the total size of the file
|
||||||
|
private volatile int sizeLine; // Used for detecting file size
|
||||||
|
private volatile int multiplier; // Used for determining when to broadcast download updates
|
||||||
|
|
||||||
|
private volatile URL url; // Connecting to RSS
|
||||||
|
|
||||||
|
private volatile String updateFolder = YamlConfiguration.loadConfiguration(new File("bukkit.yml")).getString("settings.update-folder"); // The folder that downloads will be placed in
|
||||||
|
|
||||||
|
// Used for determining the outcome of the update process
|
||||||
|
private volatile Updater.UpdateResult result = Updater.UpdateResult.SUCCESS;
|
||||||
|
|
||||||
|
// Whether to announce file downloads
|
||||||
|
private volatile boolean announce;
|
||||||
|
|
||||||
|
private volatile UpdateType type;
|
||||||
|
private volatile String downloadedVersion;
|
||||||
|
private volatile String versionTitle;
|
||||||
|
private volatile String versionLink;
|
||||||
|
private volatile File file;
|
||||||
|
|
||||||
|
// Used to announce progress
|
||||||
|
private volatile Logger logger;
|
||||||
|
|
||||||
|
// Strings for reading RSS
|
||||||
|
private static final String TITLE = "title";
|
||||||
|
private static final String LINK = "link";
|
||||||
|
private static final String ITEM = "item";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gives the dev the result of the update process. Can be obtained by called getResult().
|
||||||
|
*/
|
||||||
|
public enum UpdateResult
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The updater found an update, and has readied it to be loaded the next time the server restarts/reloads.
|
||||||
|
*/
|
||||||
|
SUCCESS(1, "The updater found an update, and has readied it to be loaded the next time the server restarts/reloads."),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updater did not find an update, and nothing was downloaded.
|
||||||
|
*/
|
||||||
|
NO_UPDATE(2, "The updater did not find an update, and nothing was downloaded."),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updater found an update, but was unable to download it.
|
||||||
|
*/
|
||||||
|
FAIL_DOWNLOAD(3, "The updater found an update, but was unable to download it."),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For some reason, the updater was unable to contact dev.bukkit.org to download the file.
|
||||||
|
*/
|
||||||
|
FAIL_DBO(4, "For some reason, the updater was unable to contact dev.bukkit.org to download the file."),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When running the version check, the file on DBO did not contain the a version in the format 'vVersion' such as 'v1.0'.
|
||||||
|
*/
|
||||||
|
FAIL_NOVERSION(5, "When running the version check, the file on DBO did not contain the a version in the format 'vVersion' such as 'v1.0'."),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The slug provided by the plugin running the updater was invalid and doesn't exist on DBO.
|
||||||
|
*/
|
||||||
|
FAIL_BADSLUG(6, "The slug provided by the plugin running the updater was invalid and doesn't exist on DBO."),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updater found an update, but because of the UpdateType being set to NO_DOWNLOAD, it wasn't downloaded.
|
||||||
|
*/
|
||||||
|
UPDATE_AVAILABLE(7, "The updater found an update, but because of the UpdateType being set to NO_DOWNLOAD, it wasn't downloaded.");
|
||||||
|
|
||||||
|
private static final Map<Integer, Updater.UpdateResult> valueList = new HashMap<Integer, Updater.UpdateResult>();
|
||||||
|
private final int value;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
private UpdateResult(int value, String description)
|
||||||
|
{
|
||||||
|
this.value = value;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getValue()
|
||||||
|
{
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Updater.UpdateResult getResult(int value)
|
||||||
|
{
|
||||||
|
return valueList.get(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
for(Updater.UpdateResult result : Updater.UpdateResult.values())
|
||||||
|
{
|
||||||
|
valueList.put(result.value, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the dev to specify the type of update that will be run.
|
||||||
|
*/
|
||||||
|
public enum UpdateType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run a version check, and then if the file is out of date, download the newest version.
|
||||||
|
*/
|
||||||
|
DEFAULT(1),
|
||||||
|
/**
|
||||||
|
* Don't run a version check, just find the latest update and download it.
|
||||||
|
*/
|
||||||
|
NO_VERSION_CHECK(2),
|
||||||
|
/**
|
||||||
|
* Get information about the version and the download size, but don't actually download anything.
|
||||||
|
*/
|
||||||
|
NO_DOWNLOAD(3);
|
||||||
|
|
||||||
|
private static final Map<Integer, Updater.UpdateType> valueList = new HashMap<Integer, Updater.UpdateType>();
|
||||||
|
private final int value;
|
||||||
|
|
||||||
|
private UpdateType(int value)
|
||||||
|
{
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getValue()
|
||||||
|
{
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Updater.UpdateType getResult(int value)
|
||||||
|
{
|
||||||
|
return valueList.get(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
for(Updater.UpdateType result : Updater.UpdateType.values())
|
||||||
|
{
|
||||||
|
valueList.put(result.value, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the updater
|
||||||
|
*
|
||||||
|
* @param plugin
|
||||||
|
* The plugin that is checking for an update.
|
||||||
|
* @param slug
|
||||||
|
* The dev.bukkit.org slug of the project (http://dev.bukkit.org/server-mods/SLUG_IS_HERE)
|
||||||
|
* @param file
|
||||||
|
* The file that the plugin is running from, get this by doing this.getFile() from within your main class.
|
||||||
|
* @param permission
|
||||||
|
* Permission needed to read the output of the update process.
|
||||||
|
*/
|
||||||
|
public Updater(Plugin plugin, Logger logger, String slug, File file, String permission)
|
||||||
|
{
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.file = file;
|
||||||
|
this.slug = slug;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the plugin.
|
||||||
|
*
|
||||||
|
* @param type
|
||||||
|
* Specify the type of update this will be. See {@link UpdateType}
|
||||||
|
* @param announce
|
||||||
|
* True if the program should announce the progress of new updates in console
|
||||||
|
* @return The result of the update process.
|
||||||
|
*/
|
||||||
|
public synchronized UpdateResult update(UpdateType type, boolean announce)
|
||||||
|
{
|
||||||
|
this.type = type;
|
||||||
|
this.announce = announce;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Obtain the results of the project's file feed
|
||||||
|
url = null;
|
||||||
|
url = new URL(DBOUrl + slug + "/files.rss");
|
||||||
|
}
|
||||||
|
catch (MalformedURLException ex)
|
||||||
|
{
|
||||||
|
// The slug doesn't exist
|
||||||
|
logger.warning("The author of this plugin has misconfigured their Auto Update system");
|
||||||
|
logger.warning("The project slug added ('" + slug + "') is invalid, and does not exist on dev.bukkit.org");
|
||||||
|
result = Updater.UpdateResult.FAIL_BADSLUG; // Bad slug! Bad!
|
||||||
|
}
|
||||||
|
if (url != null)
|
||||||
|
{
|
||||||
|
// Obtain the results of the project's file feed
|
||||||
|
readFeed();
|
||||||
|
if(versionCheck(versionTitle))
|
||||||
|
{
|
||||||
|
String fileLink = getFile(versionLink);
|
||||||
|
if(fileLink != null && type != UpdateType.NO_DOWNLOAD)
|
||||||
|
{
|
||||||
|
String name = file.getName();
|
||||||
|
// If it's a zip file, it shouldn't be downloaded as the plugin's name
|
||||||
|
if(fileLink.endsWith(".zip"))
|
||||||
|
{
|
||||||
|
String [] split = fileLink.split("/");
|
||||||
|
name = split[split.length-1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Never download the same file twice
|
||||||
|
if (!downloadedVersion.equalsIgnoreCase(versionLink)) {
|
||||||
|
saveFile(new File("plugins/" + updateFolder), name, fileLink);
|
||||||
|
downloadedVersion = versionLink;
|
||||||
|
result = UpdateResult.SUCCESS;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
result = UpdateResult.UPDATE_AVAILABLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = UpdateResult.UPDATE_AVAILABLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the result of the update process.
|
||||||
|
*/
|
||||||
|
public Updater.UpdateResult getResult()
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the total bytes of the file (can only be used after running a version check or a normal run).
|
||||||
|
*/
|
||||||
|
public long getFileSize()
|
||||||
|
{
|
||||||
|
return totalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the version string latest file avaliable online.
|
||||||
|
*/
|
||||||
|
public String getLatestVersionString()
|
||||||
|
{
|
||||||
|
return versionTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save an update from dev.bukkit.org into the server's update folder.
|
||||||
|
*/
|
||||||
|
private void saveFile(File folder, String file, String u)
|
||||||
|
{
|
||||||
|
if(!folder.exists())
|
||||||
|
{
|
||||||
|
folder.mkdir();
|
||||||
|
}
|
||||||
|
BufferedInputStream in = null;
|
||||||
|
FileOutputStream fout = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Download the file
|
||||||
|
URL url = new URL(u);
|
||||||
|
int fileLength = url.openConnection().getContentLength();
|
||||||
|
in = new BufferedInputStream(url.openStream());
|
||||||
|
fout = new FileOutputStream(folder.getAbsolutePath() + "/" + file);
|
||||||
|
|
||||||
|
byte[] data = new byte[BYTE_SIZE];
|
||||||
|
int count;
|
||||||
|
if(announce) logger.info("About to download a new update: " + versionTitle);
|
||||||
|
long downloaded = 0;
|
||||||
|
while ((count = in.read(data, 0, BYTE_SIZE)) != -1)
|
||||||
|
{
|
||||||
|
downloaded += count;
|
||||||
|
fout.write(data, 0, count);
|
||||||
|
int percent = (int) (downloaded * 100 / fileLength);
|
||||||
|
if(announce && (percent % 10 == 0))
|
||||||
|
{
|
||||||
|
logger.info("Downloading update: " + percent + "% of " + fileLength + " bytes.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Just a quick check to make sure we didn't leave any files from last time...
|
||||||
|
for(File xFile : new File("plugins/" + updateFolder).listFiles())
|
||||||
|
{
|
||||||
|
if(xFile.getName().endsWith(".zip"))
|
||||||
|
{
|
||||||
|
xFile.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check to see if it's a zip file, if it is, unzip it.
|
||||||
|
File dFile = new File(folder.getAbsolutePath() + "/" + file);
|
||||||
|
if(dFile.getName().endsWith(".zip"))
|
||||||
|
{
|
||||||
|
// Unzip
|
||||||
|
unzip(dFile.getCanonicalPath());
|
||||||
|
}
|
||||||
|
if(announce) logger.info("Finished updating.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.warning("The auto-updater tried to download a new update, but was unsuccessful.");
|
||||||
|
result = Updater.UpdateResult.FAIL_DOWNLOAD;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (in != null)
|
||||||
|
{
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
|
if (fout != null)
|
||||||
|
{
|
||||||
|
fout.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Part of Zip-File-Extractor, modified by H31IX for use with Bukkit
|
||||||
|
*/
|
||||||
|
private void unzip(String file)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File fSourceZip = new File(file);
|
||||||
|
String zipPath = file.substring(0, file.length()-4);
|
||||||
|
ZipFile zipFile = new ZipFile(fSourceZip);
|
||||||
|
Enumeration<? extends ZipEntry> e = zipFile.entries();
|
||||||
|
while(e.hasMoreElements())
|
||||||
|
{
|
||||||
|
ZipEntry entry = (ZipEntry)e.nextElement();
|
||||||
|
File destinationFilePath = new File(zipPath,entry.getName());
|
||||||
|
destinationFilePath.getParentFile().mkdirs();
|
||||||
|
if(entry.isDirectory())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry));
|
||||||
|
int b;
|
||||||
|
byte buffer[] = new byte[BYTE_SIZE];
|
||||||
|
FileOutputStream fos = new FileOutputStream(destinationFilePath);
|
||||||
|
BufferedOutputStream bos = new BufferedOutputStream(fos, BYTE_SIZE);
|
||||||
|
while((b = bis.read(buffer, 0, BYTE_SIZE)) != -1)
|
||||||
|
{
|
||||||
|
bos.write(buffer, 0, b);
|
||||||
|
}
|
||||||
|
bos.flush();
|
||||||
|
bos.close();
|
||||||
|
bis.close();
|
||||||
|
String name = destinationFilePath.getName();
|
||||||
|
if(name.endsWith(".jar") && pluginFile(name))
|
||||||
|
{
|
||||||
|
destinationFilePath.renameTo(new File("plugins/" + updateFolder + "/" + name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry = null;
|
||||||
|
destinationFilePath = null;
|
||||||
|
}
|
||||||
|
e = null;
|
||||||
|
zipFile.close();
|
||||||
|
zipFile = null;
|
||||||
|
// Move any plugin data folders that were included to the right place, Bukkit won't do this for us.
|
||||||
|
for(File dFile : new File(zipPath).listFiles())
|
||||||
|
{
|
||||||
|
if(dFile.isDirectory())
|
||||||
|
{
|
||||||
|
if(pluginFile(dFile.getName()))
|
||||||
|
{
|
||||||
|
File oFile = new File("plugins/" + dFile.getName()); // Get current dir
|
||||||
|
File [] contents = oFile.listFiles(); // List of existing files in the current dir
|
||||||
|
for(File cFile : dFile.listFiles()) // Loop through all the files in the new dir
|
||||||
|
{
|
||||||
|
boolean found = false;
|
||||||
|
for(File xFile : contents) // Loop through contents to see if it exists
|
||||||
|
{
|
||||||
|
if(xFile.getName().equals(cFile.getName()))
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!found)
|
||||||
|
{
|
||||||
|
// Move the new file into the current dir
|
||||||
|
cFile.renameTo(new File(oFile.getCanonicalFile() + "/" + cFile.getName()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This file already exists, so we don't need it anymore.
|
||||||
|
cFile.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dFile.delete();
|
||||||
|
}
|
||||||
|
new File(zipPath).delete();
|
||||||
|
fSourceZip.delete();
|
||||||
|
}
|
||||||
|
catch(IOException ex)
|
||||||
|
{
|
||||||
|
ex.printStackTrace();
|
||||||
|
logger.warning("The auto-updater tried to unzip a new update file, but was unsuccessful.");
|
||||||
|
result = Updater.UpdateResult.FAIL_DOWNLOAD;
|
||||||
|
}
|
||||||
|
new File(file).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the name of a jar is one of the plugins currently installed, used for extracting the correct files out of a zip.
|
||||||
|
*/
|
||||||
|
public boolean pluginFile(String name)
|
||||||
|
{
|
||||||
|
for(File file : new File("plugins").listFiles())
|
||||||
|
{
|
||||||
|
if(file.getName().equals(name))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtain the direct download file url from the file's page.
|
||||||
|
*/
|
||||||
|
private String getFile(String link)
|
||||||
|
{
|
||||||
|
String download = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Open a connection to the page
|
||||||
|
URL url = new URL(link);
|
||||||
|
URLConnection urlConn = url.openConnection();
|
||||||
|
InputStreamReader inStream = new InputStreamReader(urlConn.getInputStream());
|
||||||
|
BufferedReader buff = new BufferedReader(inStream);
|
||||||
|
|
||||||
|
int counter = 0;
|
||||||
|
String line;
|
||||||
|
while((line = buff.readLine()) != null)
|
||||||
|
{
|
||||||
|
counter++;
|
||||||
|
// Search for the download link
|
||||||
|
if(line.contains("<li class=\"user-action user-action-download\">"))
|
||||||
|
{
|
||||||
|
// Get the raw link
|
||||||
|
download = line.split("<a href=\"")[1].split("\">Download</a>")[0];
|
||||||
|
}
|
||||||
|
// Search for size
|
||||||
|
else if (line.contains("<dt>Size</dt>"))
|
||||||
|
{
|
||||||
|
sizeLine = counter+1;
|
||||||
|
}
|
||||||
|
else if(counter == sizeLine)
|
||||||
|
{
|
||||||
|
String size = line.replaceAll("<dd>", "").replaceAll("</dd>", "");
|
||||||
|
multiplier = size.contains("MiB") ? 1048576 : 1024;
|
||||||
|
size = size.replace(" KiB", "").replace(" MiB", "");
|
||||||
|
totalSize = (long)(Double.parseDouble(size)*multiplier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
urlConn = null;
|
||||||
|
inStream = null;
|
||||||
|
buff.close();
|
||||||
|
buff = null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ex.printStackTrace();
|
||||||
|
logger.warning("The auto-updater tried to contact dev.bukkit.org, but was unsuccessful.");
|
||||||
|
result = Updater.UpdateResult.FAIL_DBO;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return download;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to see if the program should continue by evaluation whether the plugin is already updated, or shouldn't be updated
|
||||||
|
*/
|
||||||
|
private boolean versionCheck(String title)
|
||||||
|
{
|
||||||
|
if (type != UpdateType.NO_VERSION_CHECK)
|
||||||
|
{
|
||||||
|
String[] parts = title.split(" ");
|
||||||
|
String version = plugin.getDescription().getVersion();
|
||||||
|
|
||||||
|
if(parts.length == 2)
|
||||||
|
{
|
||||||
|
String remoteVersion = parts[1].split(" ")[0]; // Get the newest file's version number
|
||||||
|
int remVer = -1, curVer=0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
remVer = calVer(remoteVersion);
|
||||||
|
curVer = calVer(version);
|
||||||
|
}
|
||||||
|
catch(NumberFormatException nfe)
|
||||||
|
{
|
||||||
|
remVer=-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hasTag(version)||version.equalsIgnoreCase(remoteVersion)||curVer>=remVer)
|
||||||
|
{
|
||||||
|
// We already have the latest version, or this build is tagged for no-update
|
||||||
|
result = Updater.UpdateResult.NO_UPDATE;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// The file's name did not contain the string 'vVersion'
|
||||||
|
logger.warning("The author of this plugin has misconfigured their Auto Update system");
|
||||||
|
logger.warning("Files uploaded to BukkitDev should contain the version number, seperated from the name by a 'v', such as PluginName v1.0");
|
||||||
|
logger.warning("Please notify the author (" + plugin.getDescription().getAuthors().get(0) + ") of this error.");
|
||||||
|
result = Updater.UpdateResult.FAIL_NOVERSION;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Used to calculate the version string as an Integer
|
||||||
|
*/
|
||||||
|
private Integer calVer(String s) throws NumberFormatException
|
||||||
|
{
|
||||||
|
if(s.contains("."))
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i <s.length(); i++)
|
||||||
|
{
|
||||||
|
Character c = s.charAt(i);
|
||||||
|
if (Character.isLetterOrDigit(c))
|
||||||
|
{
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Integer.parseInt(sb.toString());
|
||||||
|
}
|
||||||
|
return Integer.parseInt(s);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Evaluate whether the version number is marked showing that it should not be updated by this program
|
||||||
|
*/
|
||||||
|
private boolean hasTag(String version)
|
||||||
|
{
|
||||||
|
for(String string : noUpdateTag)
|
||||||
|
{
|
||||||
|
if(version.contains(string))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Part of RSS Reader by Vogella, modified by H31IX for use with Bukkit
|
||||||
|
*/
|
||||||
|
private void readFeed()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Set header values intial to the empty string
|
||||||
|
String title = "";
|
||||||
|
String link = "";
|
||||||
|
// First create a new XMLInputFactory
|
||||||
|
XMLInputFactory inputFactory = XMLInputFactory.newInstance();
|
||||||
|
// Setup a new eventReader
|
||||||
|
InputStream in = read();
|
||||||
|
XMLEventReader eventReader = inputFactory.createXMLEventReader(in);
|
||||||
|
// Read the XML document
|
||||||
|
while (eventReader.hasNext())
|
||||||
|
{
|
||||||
|
XMLEvent event = eventReader.nextEvent();
|
||||||
|
if (event.isStartElement())
|
||||||
|
{
|
||||||
|
if (event.asStartElement().getName().getLocalPart().equals(TITLE))
|
||||||
|
{
|
||||||
|
event = eventReader.nextEvent();
|
||||||
|
title = event.asCharacters().getData();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (event.asStartElement().getName().getLocalPart().equals(LINK))
|
||||||
|
{
|
||||||
|
event = eventReader.nextEvent();
|
||||||
|
link = event.asCharacters().getData();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (event.isEndElement())
|
||||||
|
{
|
||||||
|
if (event.asEndElement().getName().getLocalPart().equals(ITEM))
|
||||||
|
{
|
||||||
|
// Store the title and link of the first entry we get - the first file on the list is all we need
|
||||||
|
versionTitle = title;
|
||||||
|
versionLink = link;
|
||||||
|
// All done, we don't need to know about older files.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (XMLStreamException e)
|
||||||
|
{
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the RSS feed
|
||||||
|
*/
|
||||||
|
private InputStream read()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return url.openStream();
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,7 @@
|
|||||||
package com.comphenix.protocol.reflect;
|
package com.comphenix.protocol.reflect;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import com.google.common.collect.BiMap;
|
import com.google.common.collect.BiMap;
|
||||||
@ -100,6 +101,6 @@ public class IntEnum {
|
|||||||
* @return Enumeration of every value.
|
* @return Enumeration of every value.
|
||||||
*/
|
*/
|
||||||
public Set<Integer> values() {
|
public Set<Integer> values() {
|
||||||
return members.keySet();
|
return new HashSet<Integer>(members.keySet());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,174 @@
|
|||||||
|
package com.comphenix.protocol.reflect;
|
||||||
|
|
||||||
|
import java.lang.reflect.Array;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import com.google.common.primitives.Primitives;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to print the content of an arbitrary class.
|
||||||
|
*
|
||||||
|
* @author Kristian
|
||||||
|
*/
|
||||||
|
public class PrettyPrinter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How far we will recurse.
|
||||||
|
*/
|
||||||
|
public final static int RECURSE_DEPTH = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print the content of an object.
|
||||||
|
* @param object - the object to serialize.
|
||||||
|
* @param stop - superclass that will stop the process.
|
||||||
|
* @return String representation of the class.
|
||||||
|
* @throws IllegalAccessException
|
||||||
|
*/
|
||||||
|
public static String printObject(Object object, Class<?> start, Class<?> stop) throws IllegalAccessException {
|
||||||
|
return printObject(object, start, stop, RECURSE_DEPTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print the content of an object.
|
||||||
|
* @param object - the object to serialize.
|
||||||
|
* @param stop - superclass that will stop the process.
|
||||||
|
* @param depth - how far in the hierachy until we stop.
|
||||||
|
* @return String representation of the class.
|
||||||
|
* @throws IllegalAccessException
|
||||||
|
*/
|
||||||
|
public static String printObject(Object object, Class<?> start, Class<?> stop, int hierachyDepth) throws IllegalAccessException {
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
Set<Object> previous = new HashSet<Object>();
|
||||||
|
|
||||||
|
// Start and stop
|
||||||
|
output.append("{ ");
|
||||||
|
printObject(output, object, start, stop, previous, hierachyDepth);
|
||||||
|
output.append(" }");
|
||||||
|
|
||||||
|
return output.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
private static void printIterables(StringBuilder output, Iterable iterable, Class<?> current, Class<?> stop,
|
||||||
|
Set<Object> previous, int hierachyIndex) throws IllegalAccessException {
|
||||||
|
|
||||||
|
boolean first = true;
|
||||||
|
output.append("(");
|
||||||
|
|
||||||
|
for (Object value : iterable) {
|
||||||
|
if (first)
|
||||||
|
first = false;
|
||||||
|
else
|
||||||
|
output.append(", ");
|
||||||
|
|
||||||
|
// Handle exceptions
|
||||||
|
if (value != null)
|
||||||
|
printValue(output, value, value.getClass(), stop, previous, hierachyIndex - 1);
|
||||||
|
else
|
||||||
|
output.append("NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
output.append(")");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void printArray(StringBuilder output, Object array, Class<?> current, Class<?> stop,
|
||||||
|
Set<Object> previous, int hierachyIndex) throws IllegalAccessException {
|
||||||
|
|
||||||
|
Class<?> component = current.getComponentType();
|
||||||
|
boolean first = true;
|
||||||
|
|
||||||
|
if (!component.isArray())
|
||||||
|
output.append(component.getName());
|
||||||
|
output.append("[");
|
||||||
|
|
||||||
|
for (int i = 0; i < Array.getLength(array); i++) {
|
||||||
|
if (first)
|
||||||
|
first = false;
|
||||||
|
else
|
||||||
|
output.append(", ");
|
||||||
|
|
||||||
|
// Handle exceptions
|
||||||
|
try {
|
||||||
|
printValue(output, Array.get(array, i), component, stop, previous, hierachyIndex - 1);
|
||||||
|
} catch (ArrayIndexOutOfBoundsException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
break;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.append("]");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal recursion method
|
||||||
|
private static void printObject(StringBuilder output, Object object, Class<?> current, Class<?> stop,
|
||||||
|
Set<Object> previous, int hierachyIndex) throws IllegalAccessException {
|
||||||
|
// Trickery
|
||||||
|
boolean first = true;
|
||||||
|
|
||||||
|
// See if we're supposed to skip this class
|
||||||
|
if (current == Object.class || (stop != null && current.equals(stop))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't iterate twice
|
||||||
|
previous.add(object);
|
||||||
|
|
||||||
|
// Hard coded limit
|
||||||
|
if (hierachyIndex < 0) {
|
||||||
|
output.append("...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Field field : current.getDeclaredFields()) {
|
||||||
|
int mod = field.getModifiers();
|
||||||
|
|
||||||
|
// Skip a good number of the fields
|
||||||
|
if (!Modifier.isTransient(mod) && !Modifier.isStatic(mod)) {
|
||||||
|
Class<?> type = field.getType();
|
||||||
|
Object value = FieldUtils.readField(field, object, true);
|
||||||
|
|
||||||
|
if (first)
|
||||||
|
first = false;
|
||||||
|
else
|
||||||
|
output.append(", ");
|
||||||
|
|
||||||
|
output.append(field.getName());
|
||||||
|
output.append(" = ");
|
||||||
|
printValue(output, value, type, stop, previous, hierachyIndex - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse
|
||||||
|
printObject(output, object, current.getSuperclass(), stop, previous, hierachyIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
private static void printValue(StringBuilder output, Object value, Class<?> type,
|
||||||
|
Class<?> stop, Set<Object> previous, int hierachyIndex) throws IllegalAccessException {
|
||||||
|
// Just print primitive types
|
||||||
|
if (value == null) {
|
||||||
|
output.append("NULL");
|
||||||
|
} else if (type.isPrimitive() || Primitives.isWrapperType(type)) {
|
||||||
|
output.append(value);
|
||||||
|
} else if (type == String.class || hierachyIndex <= 0) {
|
||||||
|
output.append("\"" + value + "\"");
|
||||||
|
} else if (type.isArray()) {
|
||||||
|
printArray(output, value, type, stop, previous, hierachyIndex);
|
||||||
|
} else if (Iterable.class.isAssignableFrom(type)) {
|
||||||
|
printIterables(output, (Iterable) value, type, stop, previous, hierachyIndex);
|
||||||
|
} else if (ClassLoader.class.isAssignableFrom(type) || previous.contains(value)) {
|
||||||
|
// Don't print previous objects
|
||||||
|
output.append("\"" + value + "\"");
|
||||||
|
} else {
|
||||||
|
output.append("{ ");
|
||||||
|
printObject(output, value, value.getClass(), stop, previous, hierachyIndex);
|
||||||
|
output.append(" }");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,124 +0,0 @@
|
|||||||
package com.comphenix.protocol.reflect;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) 2008 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import java.lang.reflect.Type;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains static utility methods pertaining to primitive types and their
|
|
||||||
* corresponding wrapper types.
|
|
||||||
*
|
|
||||||
* @author Kevin Bourrillion
|
|
||||||
*/
|
|
||||||
public final class PrimitiveUtils {
|
|
||||||
private PrimitiveUtils() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A map from primitive types to their corresponding wrapper types. */
|
|
||||||
private static final Map<Class<?>, Class<?>> PRIMITIVE_TO_WRAPPER_TYPE;
|
|
||||||
|
|
||||||
/** A map from wrapper types to their corresponding primitive types. */
|
|
||||||
private static final Map<Class<?>, Class<?>> WRAPPER_TO_PRIMITIVE_TYPE;
|
|
||||||
|
|
||||||
// Sad that we can't use a BiMap. :(
|
|
||||||
static {
|
|
||||||
Map<Class<?>, Class<?>> primToWrap = new HashMap<Class<?>, Class<?>>(16);
|
|
||||||
Map<Class<?>, Class<?>> wrapToPrim = new HashMap<Class<?>, Class<?>>(16);
|
|
||||||
|
|
||||||
add(primToWrap, wrapToPrim, boolean.class, Boolean.class);
|
|
||||||
add(primToWrap, wrapToPrim, byte.class, Byte.class);
|
|
||||||
add(primToWrap, wrapToPrim, char.class, Character.class);
|
|
||||||
add(primToWrap, wrapToPrim, double.class, Double.class);
|
|
||||||
add(primToWrap, wrapToPrim, float.class, Float.class);
|
|
||||||
add(primToWrap, wrapToPrim, int.class, Integer.class);
|
|
||||||
add(primToWrap, wrapToPrim, long.class, Long.class);
|
|
||||||
add(primToWrap, wrapToPrim, short.class, Short.class);
|
|
||||||
add(primToWrap, wrapToPrim, void.class, Void.class);
|
|
||||||
|
|
||||||
PRIMITIVE_TO_WRAPPER_TYPE = Collections.unmodifiableMap(primToWrap);
|
|
||||||
WRAPPER_TO_PRIMITIVE_TYPE = Collections.unmodifiableMap(wrapToPrim);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void add(Map<Class<?>, Class<?>> forward,
|
|
||||||
Map<Class<?>, Class<?>> backward, Class<?> key, Class<?> value) {
|
|
||||||
forward.put(key, value);
|
|
||||||
backward.put(value, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if this type is a primitive.
|
|
||||||
*/
|
|
||||||
public static boolean isPrimitive(Type type) {
|
|
||||||
return PRIMITIVE_TO_WRAPPER_TYPE.containsKey(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns {@code true} if {@code type} is one of the nine primitive-wrapper
|
|
||||||
* types, such as {@link Integer}.
|
|
||||||
*
|
|
||||||
* @see Class#isPrimitive
|
|
||||||
*/
|
|
||||||
public static boolean isWrapperType(Type type) {
|
|
||||||
return WRAPPER_TO_PRIMITIVE_TYPE.containsKey(checkNotNull(type));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the corresponding wrapper type of {@code type} if it is a
|
|
||||||
* primitive type; otherwise returns {@code type} itself. Idempotent.
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* wrap(int.class) == Integer.class
|
|
||||||
* wrap(Integer.class) == Integer.class
|
|
||||||
* wrap(String.class) == String.class
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public static <T> Class<T> wrap(Class<T> type) {
|
|
||||||
// cast is safe: long.class and Long.class are both of type Class<Long>
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Class<T> wrapped = (Class<T>) PRIMITIVE_TO_WRAPPER_TYPE
|
|
||||||
.get(checkNotNull(type));
|
|
||||||
return (wrapped == null) ? type : wrapped;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the corresponding primitive type of {@code type} if it is a
|
|
||||||
* wrapper type; otherwise returns {@code type} itself. Idempotent.
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* unwrap(Integer.class) == int.class
|
|
||||||
* unwrap(int.class) == int.class
|
|
||||||
* unwrap(String.class) == String.class
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public static <T> Class<T> unwrap(Class<T> type) {
|
|
||||||
// cast is safe: long.class and Long.class are both of type Class<Long>
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Class<T> unwrapped = (Class<T>) WRAPPER_TO_PRIMITIVE_TYPE
|
|
||||||
.get(checkNotNull(type));
|
|
||||||
return (unwrapped == null) ? type : unwrapped;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> T checkNotNull(T obj) {
|
|
||||||
if (obj == null) {
|
|
||||||
throw new NullPointerException();
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
}
|
|
@ -426,7 +426,7 @@ public class StructureModifier<TField> {
|
|||||||
Class<?> type = field.getType();
|
Class<?> type = field.getType();
|
||||||
|
|
||||||
// First, ignore primitive fields
|
// First, ignore primitive fields
|
||||||
if (!PrimitiveUtils.isPrimitive(type)) {
|
if (!type.isPrimitive()) {
|
||||||
// Next, see if we actually can generate a default value
|
// Next, see if we actually can generate a default value
|
||||||
if (generator.getDefault(type) != null) {
|
if (generator.getDefault(type) != null) {
|
||||||
// If so, require it
|
// If so, require it
|
||||||
|
@ -68,6 +68,16 @@ public abstract class CompiledStructureModifier<TField> extends StructureModifie
|
|||||||
return (TField) result;
|
return (TField) result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the given field index using reflection.
|
||||||
|
* @param index - index of field.
|
||||||
|
* @return Resulting value.
|
||||||
|
* @throws FieldAccessException The field doesn't exist, or it cannot be accessed under the current security contraints.
|
||||||
|
*/
|
||||||
|
protected Object readReflected(int index) throws FieldAccessException {
|
||||||
|
return super.read(index);
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract Object readGenerated(int fieldIndex) throws FieldAccessException;
|
protected abstract Object readGenerated(int fieldIndex) throws FieldAccessException;
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@ -78,6 +88,17 @@ public abstract class CompiledStructureModifier<TField> extends StructureModifie
|
|||||||
return writeGenerated(index, value);
|
return writeGenerated(index, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the given field using reflection.
|
||||||
|
* @param index - index of field.
|
||||||
|
* @param value - new value.
|
||||||
|
* @throws FieldAccessException The field doesn't exist, or it cannot be accessed under the current security contraints.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected void writeReflected(int index, Object value) throws FieldAccessException {
|
||||||
|
super.write(index, (TField) value);
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract StructureModifier<TField> writeGenerated(int index, Object value) throws FieldAccessException;
|
protected abstract StructureModifier<TField> writeGenerated(int index, Object value) throws FieldAccessException;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -25,9 +25,9 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.comphenix.protocol.reflect.PrimitiveUtils;
|
|
||||||
import com.comphenix.protocol.reflect.StructureModifier;
|
import com.comphenix.protocol.reflect.StructureModifier;
|
||||||
import com.google.common.base.Objects;
|
import com.google.common.base.Objects;
|
||||||
|
import com.google.common.primitives.Primitives;
|
||||||
|
|
||||||
import net.sf.cglib.asm.*;
|
import net.sf.cglib.asm.*;
|
||||||
|
|
||||||
@ -306,7 +306,7 @@ public final class StructureCompiler {
|
|||||||
for (int i = 0; i < fields.size(); i++) {
|
for (int i = 0; i < fields.size(); i++) {
|
||||||
|
|
||||||
Class<?> outputType = fields.get(i).getType();
|
Class<?> outputType = fields.get(i).getType();
|
||||||
Class<?> inputType = PrimitiveUtils.wrap(outputType);
|
Class<?> inputType = Primitives.wrap(outputType);
|
||||||
String typeDescriptor = Type.getDescriptor(outputType);
|
String typeDescriptor = Type.getDescriptor(outputType);
|
||||||
String inputPath = inputType.getName().replace('.', '/');
|
String inputPath = inputType.getName().replace('.', '/');
|
||||||
|
|
||||||
@ -323,7 +323,7 @@ public final class StructureCompiler {
|
|||||||
mv.visitVarInsn(Opcodes.ALOAD, 3);
|
mv.visitVarInsn(Opcodes.ALOAD, 3);
|
||||||
mv.visitVarInsn(Opcodes.ALOAD, 2);
|
mv.visitVarInsn(Opcodes.ALOAD, 2);
|
||||||
|
|
||||||
if (!PrimitiveUtils.isPrimitive(outputType))
|
if (!outputType.isPrimitive())
|
||||||
mv.visitTypeInsn(Opcodes.CHECKCAST, inputPath);
|
mv.visitTypeInsn(Opcodes.CHECKCAST, inputPath);
|
||||||
else
|
else
|
||||||
boxingHelper.unbox(Type.getType(outputType));
|
boxingHelper.unbox(Type.getType(outputType));
|
||||||
@ -335,8 +335,7 @@ public final class StructureCompiler {
|
|||||||
mv.visitVarInsn(Opcodes.ALOAD, 0);
|
mv.visitVarInsn(Opcodes.ALOAD, 0);
|
||||||
mv.visitVarInsn(Opcodes.ILOAD, 1);
|
mv.visitVarInsn(Opcodes.ILOAD, 1);
|
||||||
mv.visitVarInsn(Opcodes.ALOAD, 2);
|
mv.visitVarInsn(Opcodes.ALOAD, 2);
|
||||||
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, COMPILED_CLASS, "write", "(ILjava/lang/Object;)L" + SUPER_CLASS + ";");
|
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, COMPILED_CLASS, "writeReflected", "(ILjava/lang/Object;)V;");
|
||||||
mv.visitInsn(Opcodes.POP);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mv.visitJumpInsn(Opcodes.GOTO, returnLabel);
|
mv.visitJumpInsn(Opcodes.GOTO, returnLabel);
|
||||||
@ -408,7 +407,7 @@ public final class StructureCompiler {
|
|||||||
// We have to use reflection for private and protected fields.
|
// We have to use reflection for private and protected fields.
|
||||||
mv.visitVarInsn(Opcodes.ALOAD, 0);
|
mv.visitVarInsn(Opcodes.ALOAD, 0);
|
||||||
mv.visitVarInsn(Opcodes.ILOAD, 1);
|
mv.visitVarInsn(Opcodes.ILOAD, 1);
|
||||||
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, COMPILED_CLASS, "read", "(I)Ljava/lang/Object;");
|
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, COMPILED_CLASS, "readReflected", "(I)Ljava/lang/Object;");
|
||||||
}
|
}
|
||||||
|
|
||||||
mv.visitInsn(Opcodes.ARETURN);
|
mv.visitInsn(Opcodes.ARETURN);
|
||||||
|
@ -206,7 +206,6 @@ public class DefaultInstances {
|
|||||||
// Just check if any of them are NULL
|
// Just check if any of them are NULL
|
||||||
for (Class<?> type : types) {
|
for (Class<?> type : types) {
|
||||||
if (getDefaultInternal(type, providers, recursionLevel) == null) {
|
if (getDefaultInternal(type, providers, recursionLevel) == null) {
|
||||||
System.out.println(type.getName() + " is NULL!");
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,8 +21,8 @@ import java.lang.reflect.Array;
|
|||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import com.comphenix.protocol.reflect.PrimitiveUtils;
|
|
||||||
import com.google.common.base.Defaults;
|
import com.google.common.base.Defaults;
|
||||||
|
import com.google.common.primitives.Primitives;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides constructors for primtive types, wrappers, arrays and strings.
|
* Provides constructors for primtive types, wrappers, arrays and strings.
|
||||||
@ -58,10 +58,10 @@ public class PrimitiveGenerator implements InstanceProvider {
|
|||||||
@Override
|
@Override
|
||||||
public Object create(@Nullable Class<?> type) {
|
public Object create(@Nullable Class<?> type) {
|
||||||
|
|
||||||
if (PrimitiveUtils.isPrimitive(type)) {
|
if (type.isPrimitive()) {
|
||||||
return Defaults.defaultValue(type);
|
return Defaults.defaultValue(type);
|
||||||
} else if (PrimitiveUtils.isWrapperType(type)) {
|
} else if (Primitives.isWrapperType(type)) {
|
||||||
return Defaults.defaultValue(PrimitiveUtils.unwrap(type));
|
return Defaults.defaultValue(Primitives.unwrap(type));
|
||||||
} else if (type.isArray()) {
|
} else if (type.isArray()) {
|
||||||
Class<?> arrayType = type.getComponentType();
|
Class<?> arrayType = type.getComponentType();
|
||||||
return Array.newInstance(arrayType, 0);
|
return Array.newInstance(arrayType, 0);
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
package com.comphenix.protocol.utility;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import com.comphenix.protocol.Packets;
|
||||||
|
import com.comphenix.protocol.ProtocolManager;
|
||||||
|
import com.comphenix.protocol.injector.PacketConstructor;
|
||||||
|
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility methods for sending chat messages.
|
||||||
|
*
|
||||||
|
* @author Kristian
|
||||||
|
*/
|
||||||
|
public class ChatExtensions {
|
||||||
|
|
||||||
|
// Used to sent chat messages
|
||||||
|
private PacketConstructor chatConstructor;
|
||||||
|
private ProtocolManager manager;
|
||||||
|
|
||||||
|
public ChatExtensions(ProtocolManager manager) {
|
||||||
|
this.manager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message without invoking the packet listeners.
|
||||||
|
* @param receiver - the receiver.
|
||||||
|
* @param message - the message to send.
|
||||||
|
* @return TRUE if the message was sent successfully, FALSE otherwise.
|
||||||
|
* @throws InvocationTargetException If we were unable to send the message.
|
||||||
|
*/
|
||||||
|
public void sendMessageSilently(CommandSender receiver, String message) throws InvocationTargetException {
|
||||||
|
if (receiver == null)
|
||||||
|
throw new IllegalArgumentException("receiver cannot be NULL.");
|
||||||
|
if (message == null)
|
||||||
|
throw new IllegalArgumentException("message cannot be NULL.");
|
||||||
|
|
||||||
|
// Handle the player case by manually sending packets
|
||||||
|
if (receiver instanceof Player) {
|
||||||
|
sendMessageSilently((Player) receiver, message);
|
||||||
|
} else {
|
||||||
|
receiver.sendMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message without invoking the packet listeners.
|
||||||
|
* @param player - the player to send it to.
|
||||||
|
* @param message - the message to send.
|
||||||
|
* @return TRUE if the message was sent successfully, FALSE otherwise.
|
||||||
|
* @throws InvocationTargetException If we were unable to send the message.
|
||||||
|
*/
|
||||||
|
private void sendMessageSilently(Player player, String message) throws InvocationTargetException {
|
||||||
|
if (chatConstructor == null)
|
||||||
|
chatConstructor = manager.createPacketConstructor(Packets.Server.CHAT, message);
|
||||||
|
|
||||||
|
try {
|
||||||
|
manager.sendServerPacket(player, chatConstructor.createPacket(message), false);
|
||||||
|
} catch (FieldAccessException e) {
|
||||||
|
throw new InvocationTargetException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast a message without invoking any packet listeners.
|
||||||
|
* @param message - message to send.
|
||||||
|
* @param permission - permission required to receieve the message. NULL to target everyone.
|
||||||
|
* @throws InvocationTargetException If we were unable to send the message.
|
||||||
|
*/
|
||||||
|
public void broadcastMessageSilently(String message, String permission) throws InvocationTargetException {
|
||||||
|
if (message == null)
|
||||||
|
throw new IllegalArgumentException("message cannot be NULL.");
|
||||||
|
|
||||||
|
// Send this message to every online player
|
||||||
|
for (Player player : Bukkit.getServer().getOnlinePlayers()) {
|
||||||
|
if (permission == null || player.hasPermission(permission)) {
|
||||||
|
sendMessageSilently(player, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +0,0 @@
|
|||||||
name: ProtocolLib
|
|
||||||
version: 1.4.0
|
|
||||||
description: Provides read/write access to the Minecraft protocol.
|
|
||||||
author: Comphenix
|
|
||||||
website: http://www.comphenix.net/ProtocolLib
|
|
||||||
|
|
||||||
main: com.comphenix.protocol.ProtocolLibrary
|
|
||||||
database: false
|
|
12
ProtocolLib/src/main/resources/config.yml
Normale Datei
12
ProtocolLib/src/main/resources/config.yml
Normale Datei
@ -0,0 +1,12 @@
|
|||||||
|
global:
|
||||||
|
# Settings for the automatic version updater
|
||||||
|
auto updater:
|
||||||
|
notify: true
|
||||||
|
download: true
|
||||||
|
|
||||||
|
# Number of seconds to wait until a new update is downloaded
|
||||||
|
delay: 43200 # 12 hours
|
||||||
|
# Last update time
|
||||||
|
last: 0
|
||||||
|
|
||||||
|
metrics: true
|
33
ProtocolLib/src/main/resources/plugin.yml
Normale Datei
33
ProtocolLib/src/main/resources/plugin.yml
Normale Datei
@ -0,0 +1,33 @@
|
|||||||
|
name: ProtocolLib
|
||||||
|
version: 1.5.1
|
||||||
|
description: Provides read/write access to the Minecraft protocol.
|
||||||
|
author: Comphenix
|
||||||
|
website: http://www.comphenix.net/ProtocolLib
|
||||||
|
|
||||||
|
main: com.comphenix.protocol.ProtocolLibrary
|
||||||
|
database: false
|
||||||
|
|
||||||
|
commands:
|
||||||
|
protocol:
|
||||||
|
description: Performs administrative tasks regarding ProtocolLib.
|
||||||
|
usage: /<command> config|check|update
|
||||||
|
permission: experiencemod.admin
|
||||||
|
permission-message: You don't have <permission>
|
||||||
|
packet:
|
||||||
|
description: Add or remove a simple packet listener.
|
||||||
|
usage: /<command> add|remove|names client|server [ID start]-[ID stop] [detailed]
|
||||||
|
permission: experiencemod.admin
|
||||||
|
permission-message: You don't have <permission>
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
protocol.*:
|
||||||
|
description: Gives access to everything.
|
||||||
|
children:
|
||||||
|
protocol.admin: true
|
||||||
|
protocol.info: true
|
||||||
|
protocol.admin:
|
||||||
|
description: Able to initiate the update process, and can configure debug mode.
|
||||||
|
default: op
|
||||||
|
protocol.info:
|
||||||
|
description: Can read update notifications and error reports.
|
||||||
|
default: op
|
In neuem Issue referenzieren
Einen Benutzer sperren