Merge branch 'master' into gh-pages
Conflicts: ItemDisguise/.settings/org.eclipse.core.resources.prefs
Dieser Commit ist enthalten in:
Commit
7e20abdd37
@ -7,6 +7,12 @@
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry combineaccessrules="false" kind="src" path="/ProtocolLib"/>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<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">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.comphenix.protocol</groupId>
|
||||
<artifactId>ProtocolLib</artifactId>
|
||||
<version>2.3.0</version>
|
||||
<version>2.4.3</version>
|
||||
<packaging>jar</packaging>
|
||||
<description>Provides read/write access to the Minecraft protocol.</description>
|
||||
|
||||
@ -57,7 +57,7 @@
|
||||
</goals>
|
||||
<configuration>
|
||||
<shadedArtifactAttached>false</shadedArtifactAttached>
|
||||
<createDependencyReducedPom>true</createDependencyReducedPom>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
|
||||
<relocations>
|
||||
<relocation>
|
||||
@ -203,7 +203,7 @@
|
||||
<dependency>
|
||||
<groupId>org.bukkit</groupId>
|
||||
<artifactId>craftbukkit</artifactId>
|
||||
<version>1.4.7-R0.1</version>
|
||||
<version>1.5.1-R0.2-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@ -219,16 +219,16 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.powermock</groupId>
|
||||
<artifactId>powermock-module-junit4</artifactId>
|
||||
<version>${powermock.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.powermock</groupId>
|
||||
<artifactId>powermock-api-mockito</artifactId>
|
||||
<version>${powermock.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<groupId>org.powermock</groupId>
|
||||
<artifactId>powermock-module-junit4</artifactId>
|
||||
<version>${powermock.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.powermock</groupId>
|
||||
<artifactId>powermock-api-mockito</artifactId>
|
||||
<version>${powermock.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@ -1,160 +1,167 @@
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.comphenix.protocol.async.AsyncListenerHandler;
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.events.ListeningWhitelist;
|
||||
import com.comphenix.protocol.events.PacketContainer;
|
||||
import com.comphenix.protocol.injector.BukkitUnwrapper;
|
||||
import com.comphenix.protocol.injector.server.AbstractInputStreamLookup;
|
||||
import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
|
||||
import com.comphenix.protocol.injector.spigot.SpigotPacketInjector;
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.reflect.MethodUtils;
|
||||
import com.comphenix.protocol.reflect.ObjectWriter;
|
||||
import com.comphenix.protocol.reflect.compiler.BackgroundCompiler;
|
||||
import com.comphenix.protocol.reflect.compiler.StructureCompiler;
|
||||
import com.comphenix.protocol.reflect.instances.CollectionGenerator;
|
||||
import com.comphenix.protocol.reflect.instances.DefaultInstances;
|
||||
import com.comphenix.protocol.reflect.instances.PrimitiveGenerator;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.wrappers.ChunkPosition;
|
||||
import com.comphenix.protocol.wrappers.WrappedDataWatcher;
|
||||
import com.comphenix.protocol.wrappers.WrappedWatchableObject;
|
||||
import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer;
|
||||
|
||||
/**
|
||||
* Used to fix ClassLoader leaks that may lead to filling up the permanent generation.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
class CleanupStaticMembers {
|
||||
|
||||
private ClassLoader loader;
|
||||
private ErrorReporter reporter;
|
||||
|
||||
public CleanupStaticMembers(ClassLoader loader, ErrorReporter reporter) {
|
||||
this.loader = loader;
|
||||
this.reporter = reporter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the previous ClassLoader is not leaking.
|
||||
*/
|
||||
public void resetAll() {
|
||||
// This list must always be updated
|
||||
Class<?>[] publicClasses = {
|
||||
AsyncListenerHandler.class, ListeningWhitelist.class, PacketContainer.class,
|
||||
BukkitUnwrapper.class, DefaultInstances.class, CollectionGenerator.class,
|
||||
PrimitiveGenerator.class, FuzzyReflection.class, MethodUtils.class,
|
||||
BackgroundCompiler.class, StructureCompiler.class,
|
||||
ObjectWriter.class, Packets.Server.class, Packets.Client.class,
|
||||
ChunkPosition.class, WrappedDataWatcher.class, WrappedWatchableObject.class,
|
||||
AbstractInputStreamLookup.class, TemporaryPlayerFactory.class, SpigotPacketInjector.class,
|
||||
MinecraftReflection.class, NbtBinarySerializer.class
|
||||
};
|
||||
|
||||
String[] internalClasses = {
|
||||
"com.comphenix.protocol.events.SerializedOfflinePlayer",
|
||||
"com.comphenix.protocol.injector.player.InjectedServerConnection",
|
||||
"com.comphenix.protocol.injector.player.NetworkFieldInjector",
|
||||
"com.comphenix.protocol.injector.player.NetworkObjectInjector",
|
||||
"com.comphenix.protocol.injector.player.NetworkServerInjector",
|
||||
"com.comphenix.protocol.injector.player.PlayerInjector",
|
||||
"com.comphenix.protocol.injector.EntityUtilities",
|
||||
"com.comphenix.protocol.injector.packet.PacketRegistry",
|
||||
"com.comphenix.protocol.injector.packet.PacketInjector",
|
||||
"com.comphenix.protocol.injector.packet.ReadPacketModifier",
|
||||
"com.comphenix.protocol.injector.StructureCache",
|
||||
"com.comphenix.protocol.reflect.compiler.BoxingHelper",
|
||||
"com.comphenix.protocol.reflect.compiler.MethodDescriptor",
|
||||
"com.comphenix.protocol.wrappers.nbt.WrappedElement",
|
||||
};
|
||||
|
||||
resetClasses(publicClasses);
|
||||
resetClasses(getClasses(loader, internalClasses));
|
||||
}
|
||||
|
||||
private void resetClasses(Class<?>[] classes) {
|
||||
// Reset each class one by one
|
||||
for (Class<?> clazz : classes) {
|
||||
resetClass(clazz);
|
||||
}
|
||||
}
|
||||
|
||||
private void resetClass(Class<?> clazz) {
|
||||
for (Field field : clazz.getFields()) {
|
||||
Class<?> type = field.getType();
|
||||
|
||||
// Only check static non-primitive fields. We also skip strings.
|
||||
if (Modifier.isStatic(field.getModifiers()) &&
|
||||
!type.isPrimitive() && !type.equals(String.class)) {
|
||||
|
||||
try {
|
||||
setFinalStatic(field, null);
|
||||
} catch (IllegalAccessException e) {
|
||||
// Just inform the player
|
||||
reporter.reportWarning(this, "Unable to reset field " + field.getName() + ": " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HACK! HAACK!
|
||||
private static void setFinalStatic(Field field, Object newValue) throws IllegalAccessException {
|
||||
int modifier = field.getModifiers();
|
||||
boolean isFinal = Modifier.isFinal(modifier);
|
||||
|
||||
Field modifiersField = isFinal ? FieldUtils.getField(Field.class, "modifiers", true) : null;
|
||||
|
||||
// We have to remove the final field first
|
||||
if (isFinal) {
|
||||
FieldUtils.writeField(modifiersField, field, modifier & ~Modifier.FINAL, true);
|
||||
}
|
||||
|
||||
// Now we can safely modify the field
|
||||
FieldUtils.writeStaticField(field, newValue, true);
|
||||
|
||||
// Revert modifier
|
||||
if (isFinal) {
|
||||
FieldUtils.writeField(modifiersField, field, modifier, true);
|
||||
}
|
||||
}
|
||||
|
||||
private Class<?>[] getClasses(ClassLoader loader, String[] names) {
|
||||
List<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]);
|
||||
}
|
||||
}
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.comphenix.protocol.async.AsyncListenerHandler;
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.error.Report;
|
||||
import com.comphenix.protocol.error.ReportType;
|
||||
import com.comphenix.protocol.events.ListeningWhitelist;
|
||||
import com.comphenix.protocol.events.PacketContainer;
|
||||
import com.comphenix.protocol.injector.BukkitUnwrapper;
|
||||
import com.comphenix.protocol.injector.server.AbstractInputStreamLookup;
|
||||
import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
|
||||
import com.comphenix.protocol.injector.spigot.SpigotPacketInjector;
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.reflect.MethodUtils;
|
||||
import com.comphenix.protocol.reflect.ObjectWriter;
|
||||
import com.comphenix.protocol.reflect.compiler.BackgroundCompiler;
|
||||
import com.comphenix.protocol.reflect.compiler.StructureCompiler;
|
||||
import com.comphenix.protocol.reflect.instances.CollectionGenerator;
|
||||
import com.comphenix.protocol.reflect.instances.DefaultInstances;
|
||||
import com.comphenix.protocol.reflect.instances.PrimitiveGenerator;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.wrappers.ChunkPosition;
|
||||
import com.comphenix.protocol.wrappers.WrappedDataWatcher;
|
||||
import com.comphenix.protocol.wrappers.WrappedWatchableObject;
|
||||
import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer;
|
||||
|
||||
/**
|
||||
* Used to fix ClassLoader leaks that may lead to filling up the permanent generation.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
class CleanupStaticMembers {
|
||||
// Reports
|
||||
public final static ReportType REPORT_CANNOT_RESET_FIELD = new ReportType("Unable to reset field %s: %s");
|
||||
public final static ReportType REPORT_CANNOT_UNLOAD_CLASS = new ReportType("Unable to unload class %s.");
|
||||
|
||||
private ClassLoader loader;
|
||||
private ErrorReporter reporter;
|
||||
|
||||
public CleanupStaticMembers(ClassLoader loader, ErrorReporter reporter) {
|
||||
this.loader = loader;
|
||||
this.reporter = reporter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the previous ClassLoader is not leaking.
|
||||
*/
|
||||
public void resetAll() {
|
||||
// This list must always be updated
|
||||
Class<?>[] publicClasses = {
|
||||
AsyncListenerHandler.class, ListeningWhitelist.class, PacketContainer.class,
|
||||
BukkitUnwrapper.class, DefaultInstances.class, CollectionGenerator.class,
|
||||
PrimitiveGenerator.class, FuzzyReflection.class, MethodUtils.class,
|
||||
BackgroundCompiler.class, StructureCompiler.class,
|
||||
ObjectWriter.class, Packets.Server.class, Packets.Client.class,
|
||||
ChunkPosition.class, WrappedDataWatcher.class, WrappedWatchableObject.class,
|
||||
AbstractInputStreamLookup.class, TemporaryPlayerFactory.class, SpigotPacketInjector.class,
|
||||
MinecraftReflection.class, NbtBinarySerializer.class
|
||||
};
|
||||
|
||||
String[] internalClasses = {
|
||||
"com.comphenix.protocol.events.SerializedOfflinePlayer",
|
||||
"com.comphenix.protocol.injector.player.InjectedServerConnection",
|
||||
"com.comphenix.protocol.injector.player.NetworkFieldInjector",
|
||||
"com.comphenix.protocol.injector.player.NetworkObjectInjector",
|
||||
"com.comphenix.protocol.injector.player.NetworkServerInjector",
|
||||
"com.comphenix.protocol.injector.player.PlayerInjector",
|
||||
"com.comphenix.protocol.injector.EntityUtilities",
|
||||
"com.comphenix.protocol.injector.packet.PacketRegistry",
|
||||
"com.comphenix.protocol.injector.packet.PacketInjector",
|
||||
"com.comphenix.protocol.injector.packet.ReadPacketModifier",
|
||||
"com.comphenix.protocol.injector.StructureCache",
|
||||
"com.comphenix.protocol.reflect.compiler.BoxingHelper",
|
||||
"com.comphenix.protocol.reflect.compiler.MethodDescriptor",
|
||||
"com.comphenix.protocol.wrappers.nbt.WrappedElement",
|
||||
};
|
||||
|
||||
resetClasses(publicClasses);
|
||||
resetClasses(getClasses(loader, internalClasses));
|
||||
}
|
||||
|
||||
private void resetClasses(Class<?>[] classes) {
|
||||
// Reset each class one by one
|
||||
for (Class<?> clazz : classes) {
|
||||
resetClass(clazz);
|
||||
}
|
||||
}
|
||||
|
||||
private void resetClass(Class<?> clazz) {
|
||||
for (Field field : clazz.getFields()) {
|
||||
Class<?> type = field.getType();
|
||||
|
||||
// Only check static non-primitive fields. We also skip strings.
|
||||
if (Modifier.isStatic(field.getModifiers()) &&
|
||||
!type.isPrimitive() && !type.equals(String.class)) {
|
||||
|
||||
try {
|
||||
setFinalStatic(field, null);
|
||||
} catch (IllegalAccessException e) {
|
||||
// Just inform the player
|
||||
reporter.reportWarning(this,
|
||||
Report.newBuilder(REPORT_CANNOT_RESET_FIELD).error(e).messageParam(field.getName(), e.getMessage())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HACK! HAACK!
|
||||
private static void setFinalStatic(Field field, Object newValue) throws IllegalAccessException {
|
||||
int modifier = field.getModifiers();
|
||||
boolean isFinal = Modifier.isFinal(modifier);
|
||||
|
||||
Field modifiersField = isFinal ? FieldUtils.getField(Field.class, "modifiers", true) : null;
|
||||
|
||||
// We have to remove the final field first
|
||||
if (isFinal) {
|
||||
FieldUtils.writeField(modifiersField, field, modifier & ~Modifier.FINAL, true);
|
||||
}
|
||||
|
||||
// Now we can safely modify the field
|
||||
FieldUtils.writeStaticField(field, newValue, true);
|
||||
|
||||
// Revert modifier
|
||||
if (isFinal) {
|
||||
FieldUtils.writeField(modifiersField, field, modifier, true);
|
||||
}
|
||||
}
|
||||
|
||||
private Class<?>[] getClasses(ClassLoader loader, String[] names) {
|
||||
List<Class<?>> output = new ArrayList<Class<?>>();
|
||||
|
||||
for (String name : names) {
|
||||
try {
|
||||
output.add(loader.loadClass(name));
|
||||
} catch (ClassNotFoundException e) {
|
||||
// Warn the user
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_UNLOAD_CLASS).error(e).messageParam(name));
|
||||
}
|
||||
}
|
||||
|
||||
return output.toArray(new Class<?>[0]);
|
||||
}
|
||||
}
|
||||
|
@ -1,109 +1,117 @@
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
|
||||
/**
|
||||
* Base class for all our commands.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
abstract class CommandBase implements CommandExecutor {
|
||||
|
||||
public static final String PERMISSION_ADMIN = "protocol.admin";
|
||||
|
||||
private String permission;
|
||||
private String name;
|
||||
private int minimumArgumentCount;
|
||||
|
||||
protected ErrorReporter reporter;
|
||||
|
||||
public CommandBase(ErrorReporter reporter, String permission, String name) {
|
||||
this(reporter, permission, name, 0);
|
||||
}
|
||||
|
||||
public CommandBase(ErrorReporter reporter, String permission, String name, int minimumArgumentCount) {
|
||||
this.reporter = reporter;
|
||||
this.name = name;
|
||||
this.permission = permission;
|
||||
this.minimumArgumentCount = minimumArgumentCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
try {
|
||||
// Make sure we're dealing with the correct command
|
||||
if (!command.getName().equalsIgnoreCase(name)) {
|
||||
return false;
|
||||
}
|
||||
if (permission != null && !sender.hasPermission(permission)) {
|
||||
sender.sendMessage(ChatColor.RED + "You haven't got permission to run this command.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check argument length
|
||||
if (args != null && args.length >= minimumArgumentCount) {
|
||||
return handleCommand(sender, args);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
reporter.reportDetailed(this, "Cannot execute command " + name, e, sender, label, args);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the permission necessary to execute this command.
|
||||
* @return The permission, or NULL if not needed.
|
||||
*/
|
||||
public String getPermission() {
|
||||
return permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the primary name of this command.
|
||||
* @return Primary name.
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the error reporter.
|
||||
* @return Error reporter.
|
||||
*/
|
||||
protected ErrorReporter getReporter() {
|
||||
return reporter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main implementation of this command.
|
||||
* @param sender - command sender.
|
||||
* @param args
|
||||
* @return
|
||||
*/
|
||||
protected abstract boolean handleCommand(CommandSender sender, String[] args);
|
||||
}
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.error.Report;
|
||||
import com.comphenix.protocol.error.ReportType;
|
||||
|
||||
/**
|
||||
* Base class for all our commands.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
abstract class CommandBase implements CommandExecutor {
|
||||
public static final ReportType REPORT_COMMAND_ERROR = new ReportType("Cannot execute command %s.");
|
||||
public static final ReportType REPORT_UNEXPECTED_COMMAND = new ReportType("Incorrect command assigned to %s.");
|
||||
|
||||
public static final String PERMISSION_ADMIN = "protocol.admin";
|
||||
|
||||
private String permission;
|
||||
private String name;
|
||||
private int minimumArgumentCount;
|
||||
|
||||
protected ErrorReporter reporter;
|
||||
|
||||
public CommandBase(ErrorReporter reporter, String permission, String name) {
|
||||
this(reporter, permission, name, 0);
|
||||
}
|
||||
|
||||
public CommandBase(ErrorReporter reporter, String permission, String name, int minimumArgumentCount) {
|
||||
this.reporter = reporter;
|
||||
this.name = name;
|
||||
this.permission = permission;
|
||||
this.minimumArgumentCount = minimumArgumentCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
try {
|
||||
// Make sure we're dealing with the correct command
|
||||
if (!command.getName().equalsIgnoreCase(name)) {
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_UNEXPECTED_COMMAND).messageParam(this));
|
||||
return false;
|
||||
}
|
||||
if (permission != null && !sender.hasPermission(permission)) {
|
||||
sender.sendMessage(ChatColor.RED + "You haven't got permission to run this command.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check argument length
|
||||
if (args != null && args.length >= minimumArgumentCount) {
|
||||
return handleCommand(sender, args);
|
||||
} else {
|
||||
sender.sendMessage(ChatColor.RED + "Insufficient commands. You need at least " + minimumArgumentCount);
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
reporter.reportDetailed(this,
|
||||
Report.newBuilder(REPORT_COMMAND_ERROR).error(e).messageParam(name).callerParam(sender, label, args)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the permission necessary to execute this command.
|
||||
* @return The permission, or NULL if not needed.
|
||||
*/
|
||||
public String getPermission() {
|
||||
return permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the primary name of this command.
|
||||
* @return Primary name.
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the error reporter.
|
||||
* @return Error reporter.
|
||||
*/
|
||||
protected ErrorReporter getReporter() {
|
||||
return reporter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main implementation of this command.
|
||||
* @param sender - command sender.
|
||||
* @param args
|
||||
* @return
|
||||
*/
|
||||
protected abstract boolean handleCommand(CommandSender sender, String[] args);
|
||||
}
|
||||
|
482
ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java
Normale Datei
482
ProtocolLib/src/main/java/com/comphenix/protocol/CommandFilter.java
Normale Datei
@ -0,0 +1,482 @@
|
||||
package com.comphenix.protocol;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.script.Invocable;
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptEngineManager;
|
||||
import javax.script.ScriptException;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.conversations.Conversable;
|
||||
import org.bukkit.conversations.Conversation;
|
||||
import org.bukkit.conversations.ConversationAbandonedEvent;
|
||||
import org.bukkit.conversations.ConversationAbandonedListener;
|
||||
import org.bukkit.conversations.ConversationContext;
|
||||
import org.bukkit.conversations.ConversationFactory;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import com.comphenix.protocol.MultipleLinesPrompt.MultipleConversationCanceller;
|
||||
import com.comphenix.protocol.concurrency.IntegerSet;
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.error.Report;
|
||||
import com.comphenix.protocol.error.ReportType;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.google.common.collect.DiscreteDomains;
|
||||
import com.google.common.collect.Range;
|
||||
import com.google.common.collect.Ranges;
|
||||
|
||||
/**
|
||||
* A command to apply JavaScript filtering to the packet command.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class CommandFilter extends CommandBase {
|
||||
public static final ReportType REPORT_FALLBACK_ENGINE = new ReportType("Falling back to the Rhino engine.");
|
||||
public static final ReportType REPORT_CANNOT_LOAD_FALLBACK_ENGINE = new ReportType("Could not load Rhino either. Please upgrade your JVM or OS.");
|
||||
public static final ReportType REPORT_PACKAGES_UNSUPPORTED_IN_ENGINE = new ReportType("Unable to initialize packages for JavaScript engine.");
|
||||
public static final ReportType REPORT_FILTER_REMOVED_FOR_ERROR = new ReportType("Removing filter %s for causing %s.");
|
||||
public static final ReportType REPORT_CANNOT_HANDLE_CONVERSATION = new ReportType("Cannot handle conversation.");
|
||||
|
||||
public interface FilterFailedHandler{
|
||||
/**
|
||||
* Invoked when a given filter has failed.
|
||||
* @param event - the packet event.
|
||||
* @param filter - the filter that failed.
|
||||
* @param ex - the failure.
|
||||
* @returns TRUE to keep processing this filter, FALSE to remove it.
|
||||
*/
|
||||
public boolean handle(PacketEvent event, Filter filter, Exception ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Possible sub commands.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
private enum SubCommand {
|
||||
ADD, REMOVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* A filter that will be used to process a packet event.
|
||||
* @author Kristian
|
||||
*/
|
||||
public static class Filter {
|
||||
private final String name;
|
||||
private final String predicate;
|
||||
|
||||
private final IntegerSet ranges;
|
||||
|
||||
/**
|
||||
* Construct a new immutable filter.
|
||||
* @param name - the unique name of the filter.
|
||||
* @param predicate - the JavaScript predicate that will be used to filter packet events.
|
||||
* @param ranges - a list of valid packet ID ranges that this filter applies to.
|
||||
*/
|
||||
public Filter(String name, String predicate, Set<Integer> packets) {
|
||||
this.name = name;
|
||||
this.predicate = predicate;
|
||||
this.ranges = new IntegerSet(Packets.MAXIMUM_PACKET_ID + 1);
|
||||
this.ranges.addAll(packets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the unique name of the filter.
|
||||
* @return Unique name of the filter.
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the JavaScript predicate that will be used to filter packet events.
|
||||
* @return Predicate itself.
|
||||
*/
|
||||
public String getPredicate() {
|
||||
return predicate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a copy of the set of packets this filter applies to.
|
||||
* @return Set of packets this filter applies to.
|
||||
*/
|
||||
public Set<Integer> getRanges() {
|
||||
return ranges.toSet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether or not a packet event needs to be passed to this filter.
|
||||
* @param event - the event to test.
|
||||
* @return TRUE if it does, FALSE otherwise.
|
||||
*/
|
||||
private boolean isApplicable(PacketEvent event) {
|
||||
return ranges.contains(event.getPacketID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate the current filter using the provided ScriptEngine as context.
|
||||
* <p>
|
||||
* This context may be modified with additional code.
|
||||
* @param context - the current script context.
|
||||
* @param event - the packet event to evaluate.
|
||||
* @return TRUE to pass this packet event on to the debug listeners, FALSE otherwise.
|
||||
* @throws ScriptException If the compilation failed or the filter is not valid.
|
||||
*/
|
||||
public boolean evaluate(ScriptEngine context, PacketEvent event) throws ScriptException {
|
||||
if (!isApplicable(event))
|
||||
return true;
|
||||
// Ensure that the predicate has been compiled
|
||||
compile(context);
|
||||
|
||||
try {
|
||||
Object result = ((Invocable) context).invokeFunction(name, event, event.getPacket().getHandle());
|
||||
|
||||
if (result instanceof Boolean)
|
||||
return (Boolean) result;
|
||||
else
|
||||
throw new ScriptException("Filter result wasn't a boolean: " + result);
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
// Must be a fault with the script engine itself
|
||||
throw new IllegalStateException("Unable to compile " + name + " into current script engine.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force the compilation of a specific filter.
|
||||
* @param context - the current script context.
|
||||
* @throws ScriptException If the compilation failed.
|
||||
*/
|
||||
public void compile(ScriptEngine context) throws ScriptException {
|
||||
if (context.get(name) == null) {
|
||||
context.eval("var " + name + " = function(event, packet) {\n" + predicate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all associated code from this filter in the provided script engine.
|
||||
* @param context - the current script context.
|
||||
*/
|
||||
public void close(ScriptEngine context) {
|
||||
context.put(name, null);
|
||||
}
|
||||
}
|
||||
|
||||
private class CompilationSuccessCanceller implements MultipleConversationCanceller {
|
||||
@Override
|
||||
public boolean cancelBasedOnInput(ConversationContext context, String in) {
|
||||
throw new UnsupportedOperationException("Cannot cancel on the last line alone.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setConversation(Conversation conversation) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancelBasedOnInput(ConversationContext context, String currentLine, StringBuilder lines, int lineCount) {
|
||||
try {
|
||||
engine.eval("function(event, packet) {\n" + lines.toString());
|
||||
|
||||
// It compiles - accept the filter!
|
||||
return true;
|
||||
} catch (ScriptException e) {
|
||||
// We also have the function() line
|
||||
int realLineCount = lineCount + 1;
|
||||
|
||||
// Only possible to recover from an error on the last line.
|
||||
return e.getLineNumber() < realLineCount;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompilationSuccessCanceller clone() {
|
||||
return new CompilationSuccessCanceller();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of this command.
|
||||
*/
|
||||
public static final String NAME = "filter";
|
||||
|
||||
// Default error handler
|
||||
private FilterFailedHandler defaultFailedHandler;
|
||||
|
||||
// Currently registered filters
|
||||
private List<Filter> filters = new ArrayList<Filter>();
|
||||
|
||||
// Owner plugin
|
||||
private final Plugin plugin;
|
||||
|
||||
// Whether or not the command is enabled
|
||||
private ProtocolConfig config;
|
||||
|
||||
// Script engine
|
||||
private ScriptEngine engine;
|
||||
|
||||
public CommandFilter(ErrorReporter reporter, Plugin plugin, ProtocolConfig config) {
|
||||
super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 2);
|
||||
this.plugin = plugin;
|
||||
this.config = config;
|
||||
|
||||
// Start the engine
|
||||
initalizeScript();
|
||||
}
|
||||
|
||||
private void initalizeScript() {
|
||||
try {
|
||||
// First attempt
|
||||
initializeEngine();
|
||||
|
||||
// Oh for ..
|
||||
if (!isInitialized()) {
|
||||
throw new ScriptException("A JavaScript engine could not be found.");
|
||||
}
|
||||
} catch (ScriptException e1) {
|
||||
// It's not a huge deal
|
||||
printPackageWarning(e1);
|
||||
|
||||
if (!config.getScriptEngineName().equals("rhino")) {
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_FALLBACK_ENGINE));
|
||||
config.setScriptEngineName("rhino");
|
||||
config.saveAll();
|
||||
|
||||
try {
|
||||
initializeEngine();
|
||||
|
||||
if (!isInitialized()) {
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_LOAD_FALLBACK_ENGINE));
|
||||
}
|
||||
} catch (ScriptException e2) {
|
||||
// And again ..
|
||||
printPackageWarning(e2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void printPackageWarning(ScriptException e) {
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_PACKAGES_UNSUPPORTED_IN_ENGINE).error(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the current configured engine.
|
||||
* @throws ScriptException If we are unable to import packages.
|
||||
*/
|
||||
private void initializeEngine() throws ScriptException {
|
||||
ScriptEngineManager manager = new ScriptEngineManager();
|
||||
engine = manager.getEngineByName(config.getScriptEngineName());
|
||||
|
||||
// Import useful packages
|
||||
if (engine != null) {
|
||||
engine.eval("importPackage(org.bukkit);");
|
||||
engine.eval("importPackage(com.comphenix.protocol.reflect);");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the filter engine has been successfully initialized.
|
||||
* @return TRUE if it has, FALSE otherwise.
|
||||
*/
|
||||
public boolean isInitialized() {
|
||||
return engine != null;
|
||||
}
|
||||
|
||||
private FilterFailedHandler getDefaultErrorHandler() {
|
||||
// No need to create a new object every time
|
||||
if (defaultFailedHandler == null) {
|
||||
defaultFailedHandler = new FilterFailedHandler() {
|
||||
@Override
|
||||
public boolean handle(PacketEvent event, Filter filter, Exception ex) {
|
||||
reporter.reportMinimal(plugin, "filterEvent(PacketEvent)", ex, event);
|
||||
reporter.reportWarning(this,
|
||||
Report.newBuilder(REPORT_FILTER_REMOVED_FOR_ERROR).messageParam(filter.getName(), ex.getClass().getSimpleName())
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
return defaultFailedHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether or not to pass the given packet event to the packet listeners.
|
||||
* <p>
|
||||
* Uses a default filter failure handler that simply prints the error message and removes the filter.
|
||||
* @param event - the event.
|
||||
* @return TRUE if we should, FALSE otherwise.
|
||||
*/
|
||||
public boolean filterEvent(PacketEvent event) {
|
||||
return filterEvent(event, getDefaultErrorHandler());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether or not to pass the given packet event to the packet listeners.
|
||||
* @param event - the event.
|
||||
* @param handler - failure handler.
|
||||
* @return TRUE if we should, FALSE otherwise.
|
||||
* @throws FilterFailedException If one of the filters failed.
|
||||
*/
|
||||
public boolean filterEvent(PacketEvent event, FilterFailedHandler handler) {
|
||||
for (Iterator<Filter> it = filters.iterator(); it.hasNext(); ) {
|
||||
Filter filter = it.next();
|
||||
|
||||
try {
|
||||
if (!filter.evaluate(engine, event)) {
|
||||
return false;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
if (!handler.handle(event, filter, ex)) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Pass!
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Description: Adds or removes a simple packet filter.
|
||||
Usage: /<command> add|remove name [packet IDs]
|
||||
*/
|
||||
@Override
|
||||
protected boolean handleCommand(CommandSender sender, String[] args) {
|
||||
if (!config.isDebug()) {
|
||||
sender.sendMessage(ChatColor.RED + "Debug mode must be enabled in the configuration first!");
|
||||
return true;
|
||||
}
|
||||
if (!isInitialized()) {
|
||||
sender.sendMessage(ChatColor.RED + "JavaScript engine was not present. Filter system is disabled.");
|
||||
return true;
|
||||
}
|
||||
|
||||
final SubCommand command = parseCommand(args, 0);
|
||||
final String name = args[1];
|
||||
|
||||
switch (command) {
|
||||
case ADD:
|
||||
// Never overwrite an existing filter
|
||||
if (findFilter(name) != null) {
|
||||
sender.sendMessage(ChatColor.RED + "Filter " + name + " already exists. Remove it first.");
|
||||
return true;
|
||||
}
|
||||
|
||||
final Set<Integer> packets = parseRanges(args, 2);
|
||||
sender.sendMessage("Enter filter program ('}' to complete or CANCEL):");
|
||||
|
||||
// Make sure we can use the conversable interface
|
||||
if (sender instanceof Conversable) {
|
||||
final MultipleLinesPrompt prompt =
|
||||
new MultipleLinesPrompt(new CompilationSuccessCanceller(), "function(event, packet) {");
|
||||
|
||||
new ConversationFactory(plugin).
|
||||
withFirstPrompt(prompt).
|
||||
withEscapeSequence("CANCEL").
|
||||
withLocalEcho(false).
|
||||
addConversationAbandonedListener(new ConversationAbandonedListener() {
|
||||
@Override
|
||||
public void conversationAbandoned(ConversationAbandonedEvent event) {
|
||||
try {
|
||||
final Conversable whom = event.getContext().getForWhom();
|
||||
|
||||
if (event.gracefulExit()) {
|
||||
String predicate = prompt.removeAccumulatedInput(event.getContext());
|
||||
Filter filter = new Filter(name, predicate, packets);
|
||||
|
||||
// Print the last line as well
|
||||
whom.sendRawMessage(prompt.getPromptText(event.getContext()));
|
||||
|
||||
try {
|
||||
// Force early compilation
|
||||
filter.compile(engine);
|
||||
|
||||
filters.add(filter);
|
||||
whom.sendRawMessage(ChatColor.GOLD + "Added filter " + name);
|
||||
} catch (ScriptException e) {
|
||||
e.printStackTrace();
|
||||
whom.sendRawMessage(ChatColor.GOLD + "Compilation error: " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// Too bad
|
||||
whom.sendRawMessage(ChatColor.RED + "Cancelled filter.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
reporter.reportDetailed(this,
|
||||
Report.newBuilder(REPORT_CANNOT_HANDLE_CONVERSATION).error(e).callerParam(event)
|
||||
);
|
||||
}
|
||||
}
|
||||
}).
|
||||
buildConversation((Conversable) sender).
|
||||
begin();
|
||||
} else {
|
||||
sender.sendMessage(ChatColor.RED + "Only console and players are supported!");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case REMOVE:
|
||||
Filter filter = findFilter(name);
|
||||
|
||||
// See if it exists before we remove it
|
||||
if (filter != null) {
|
||||
filter.close(engine);
|
||||
filters.remove(filter);
|
||||
sender.sendMessage(ChatColor.GOLD + "Removed filter " + name);
|
||||
} else {
|
||||
sender.sendMessage(ChatColor.RED + "Unable to find a filter by the name " + name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Set<Integer> parseRanges(String[] args, int start) {
|
||||
List<Range<Integer>> ranges = RangeParser.getRanges(args, 2, args.length - 1, Ranges.closed(0, 255));
|
||||
Set<Integer> flatten = new HashSet<Integer>();
|
||||
|
||||
if (ranges.isEmpty()) {
|
||||
// Use every packet ID
|
||||
ranges.add(Ranges.closed(0, 255));
|
||||
}
|
||||
|
||||
// Finally, flatten it all
|
||||
for (Range<Integer> range : ranges) {
|
||||
flatten.addAll(range.asSet(DiscreteDomains.integers()));
|
||||
}
|
||||
return flatten;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a filter by its name.
|
||||
* @param name - the filter name.
|
||||
* @return The filter, or NULL if not found.
|
||||
*/
|
||||
private Filter findFilter(String name) {
|
||||
// We'll just use a linear scan for now - we don't expect that many filters
|
||||
for (Filter filter : filters) {
|
||||
if (filter.getName().equalsIgnoreCase(name)) {
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private SubCommand parseCommand(String[] args, int index) {
|
||||
String text = args[index].toUpperCase();
|
||||
|
||||
try {
|
||||
return SubCommand.valueOf(text);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException(text + " is not a valid sub command. Must be add or remove.", e);
|
||||
}
|
||||
}
|
||||
}
|
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
@ -1,137 +1,147 @@
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.metrics.Updater;
|
||||
import com.comphenix.protocol.metrics.Updater.UpdateResult;
|
||||
import com.comphenix.protocol.metrics.Updater.UpdateType;
|
||||
import com.comphenix.protocol.utility.WrappedScheduler;
|
||||
|
||||
/**
|
||||
* Handles the "protocol" administration command.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
class CommandProtocol extends CommandBase {
|
||||
/**
|
||||
* Name of this command.
|
||||
*/
|
||||
public static final String NAME = "protocol";
|
||||
|
||||
private Plugin plugin;
|
||||
private Updater updater;
|
||||
private ProtocolConfig config;
|
||||
|
||||
public CommandProtocol(ErrorReporter reporter, Plugin plugin, Updater updater, ProtocolConfig config) {
|
||||
super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 1);
|
||||
this.plugin = plugin;
|
||||
this.updater = updater;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean handleCommand(CommandSender sender, String[] args) {
|
||||
String subCommand = args[0];
|
||||
|
||||
// Only return TRUE if we executed the correct command
|
||||
if (subCommand.equalsIgnoreCase("config") || subCommand.equalsIgnoreCase("reload"))
|
||||
reloadConfiguration(sender);
|
||||
else if (subCommand.equalsIgnoreCase("check"))
|
||||
checkVersion(sender);
|
||||
else if (subCommand.equalsIgnoreCase("update"))
|
||||
updateVersion(sender);
|
||||
else
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void checkVersion(final CommandSender sender) {
|
||||
// Perform on an async thread
|
||||
WrappedScheduler.runAsynchronouslyOnce(plugin, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
UpdateResult result = updater.update(UpdateType.NO_DOWNLOAD, true);
|
||||
sender.sendMessage(ChatColor.BLUE + "[ProtocolLib] " + result.toString());
|
||||
} catch (Exception e) {
|
||||
if (isHttpError(e)) {
|
||||
getReporter().reportWarning(this, "Http error: " + e.getCause().getMessage());
|
||||
} else {
|
||||
getReporter().reportDetailed(this, "Cannot check updates for ProtocolLib.", e, sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0L);
|
||||
|
||||
updateFinished();
|
||||
}
|
||||
|
||||
public void updateVersion(final CommandSender sender) {
|
||||
// Perform on an async thread
|
||||
WrappedScheduler.runAsynchronouslyOnce(plugin, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
UpdateResult result = updater.update(UpdateType.DEFAULT, true);
|
||||
sender.sendMessage(ChatColor.BLUE + "[ProtocolLib] " + result.toString());
|
||||
} catch (Exception e) {
|
||||
if (isHttpError(e)) {
|
||||
getReporter().reportWarning(this, "Http error: " + e.getCause().getMessage());
|
||||
} else {
|
||||
getReporter().reportDetailed(this, "Cannot update ProtocolLib.", e, sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0L);
|
||||
|
||||
updateFinished();
|
||||
}
|
||||
|
||||
private boolean isHttpError(Exception e) {
|
||||
Throwable cause = e.getCause();
|
||||
|
||||
if (cause instanceof IOException) {
|
||||
// Thanks for making the message a part of the API ...
|
||||
return cause.getMessage().contains("HTTP response");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent further automatic updates until the next delay.
|
||||
*/
|
||||
public void updateFinished() {
|
||||
long currentTime = System.currentTimeMillis() / ProtocolLibrary.MILLI_PER_SECOND;
|
||||
|
||||
config.setAutoLastTime(currentTime);
|
||||
config.saveAll();
|
||||
}
|
||||
|
||||
public void reloadConfiguration(CommandSender sender) {
|
||||
plugin.reloadConfig();
|
||||
sender.sendMessage(ChatColor.BLUE + "Reloaded configuration!");
|
||||
}
|
||||
}
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.error.Report;
|
||||
import com.comphenix.protocol.error.ReportType;
|
||||
import com.comphenix.protocol.metrics.Updater;
|
||||
import com.comphenix.protocol.metrics.Updater.UpdateResult;
|
||||
import com.comphenix.protocol.metrics.Updater.UpdateType;
|
||||
import com.comphenix.protocol.utility.WrappedScheduler;
|
||||
|
||||
/**
|
||||
* Handles the "protocol" administration command.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
class CommandProtocol extends CommandBase {
|
||||
/**
|
||||
* Name of this command.
|
||||
*/
|
||||
public static final String NAME = "protocol";
|
||||
|
||||
public static final ReportType REPORT_HTTP_ERROR = new ReportType("Http error: %s");
|
||||
public static final ReportType REPORT_CANNOT_CHECK_FOR_UPDATES = new ReportType("Cannot check updates for ProtocolLib.");
|
||||
public static final ReportType REPORT_CANNOT_UPDATE_PLUGIN = new ReportType("Cannot update ProtocolLib.");
|
||||
|
||||
private Plugin plugin;
|
||||
private Updater updater;
|
||||
private ProtocolConfig config;
|
||||
|
||||
public CommandProtocol(ErrorReporter reporter, Plugin plugin, Updater updater, ProtocolConfig config) {
|
||||
super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 1);
|
||||
this.plugin = plugin;
|
||||
this.updater = updater;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean handleCommand(CommandSender sender, String[] args) {
|
||||
String subCommand = args[0];
|
||||
|
||||
// Only return TRUE if we executed the correct command
|
||||
if (subCommand.equalsIgnoreCase("config") || subCommand.equalsIgnoreCase("reload"))
|
||||
reloadConfiguration(sender);
|
||||
else if (subCommand.equalsIgnoreCase("check"))
|
||||
checkVersion(sender);
|
||||
else if (subCommand.equalsIgnoreCase("update"))
|
||||
updateVersion(sender);
|
||||
else
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void checkVersion(final CommandSender sender) {
|
||||
// Perform on an async thread
|
||||
WrappedScheduler.runAsynchronouslyOnce(plugin, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
UpdateResult result = updater.update(UpdateType.NO_DOWNLOAD, true);
|
||||
sender.sendMessage(ChatColor.BLUE + "[ProtocolLib] " + result.toString());
|
||||
} catch (Exception e) {
|
||||
if (isHttpError(e)) {
|
||||
getReporter().reportWarning(this,
|
||||
Report.newBuilder(REPORT_HTTP_ERROR).messageParam(e.getCause().getMessage())
|
||||
);
|
||||
} else {
|
||||
getReporter().reportDetailed(this, Report.newBuilder(REPORT_CANNOT_CHECK_FOR_UPDATES).error(e).callerParam(sender));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0L);
|
||||
|
||||
updateFinished();
|
||||
}
|
||||
|
||||
public void updateVersion(final CommandSender sender) {
|
||||
// Perform on an async thread
|
||||
WrappedScheduler.runAsynchronouslyOnce(plugin, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
UpdateResult result = updater.update(UpdateType.DEFAULT, true);
|
||||
sender.sendMessage(ChatColor.BLUE + "[ProtocolLib] " + result.toString());
|
||||
} catch (Exception e) {
|
||||
if (isHttpError(e)) {
|
||||
getReporter().reportWarning(this,
|
||||
Report.newBuilder(REPORT_HTTP_ERROR).messageParam(e.getCause().getMessage())
|
||||
);
|
||||
} else {
|
||||
getReporter().reportDetailed(this,Report.newBuilder(REPORT_CANNOT_UPDATE_PLUGIN).error(e).callerParam(sender));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0L);
|
||||
|
||||
updateFinished();
|
||||
}
|
||||
|
||||
private boolean isHttpError(Exception e) {
|
||||
Throwable cause = e.getCause();
|
||||
|
||||
if (cause instanceof IOException) {
|
||||
// Thanks for making the message a part of the API ...
|
||||
return cause.getMessage().contains("HTTP response");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent further automatic updates until the next delay.
|
||||
*/
|
||||
public void updateFinished() {
|
||||
long currentTime = System.currentTimeMillis() / ProtocolLibrary.MILLI_PER_SECOND;
|
||||
|
||||
config.setAutoLastTime(currentTime);
|
||||
config.saveAll();
|
||||
}
|
||||
|
||||
public void reloadConfiguration(CommandSender sender) {
|
||||
plugin.reloadConfig();
|
||||
sender.sendMessage(ChatColor.BLUE + "Reloaded configuration!");
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,163 @@
|
||||
package com.comphenix.protocol;
|
||||
|
||||
import org.bukkit.conversations.Conversation;
|
||||
import org.bukkit.conversations.ConversationCanceller;
|
||||
import org.bukkit.conversations.ConversationContext;
|
||||
import org.bukkit.conversations.ExactMatchConversationCanceller;
|
||||
import org.bukkit.conversations.Prompt;
|
||||
import org.bukkit.conversations.StringPrompt;
|
||||
|
||||
/**
|
||||
* Represents a conversation prompt that accepts a list of lines.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
class MultipleLinesPrompt extends StringPrompt {
|
||||
/**
|
||||
* Represents a canceller that determines if the multiple lines prompt is finished.
|
||||
* @author Kristian
|
||||
*/
|
||||
public static interface MultipleConversationCanceller extends ConversationCanceller {
|
||||
@Override
|
||||
public boolean cancelBasedOnInput(ConversationContext context, String currentLine);
|
||||
|
||||
/**
|
||||
* Determine if the current prompt is done based on the context, last
|
||||
* line and collected lines.
|
||||
*
|
||||
* @param context - current context.
|
||||
* @param currentLine - current (last) line.
|
||||
* @param lines - collected lines.
|
||||
* @param lineCount - number of lines.
|
||||
* @return TRUE if we are done, FALSE otherwise.
|
||||
*/
|
||||
public boolean cancelBasedOnInput(ConversationContext context, String currentLine,
|
||||
StringBuilder lines, int lineCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper class for turning a ConversationCanceller into a MultipleConversationCanceller.
|
||||
* @author Kristian
|
||||
*/
|
||||
private static class MultipleWrapper implements MultipleConversationCanceller {
|
||||
private ConversationCanceller canceller;
|
||||
|
||||
public MultipleWrapper(ConversationCanceller canceller) {
|
||||
this.canceller = canceller;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancelBasedOnInput(ConversationContext context, String currentLine) {
|
||||
return canceller.cancelBasedOnInput(context, currentLine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancelBasedOnInput(ConversationContext context, String currentLine,
|
||||
StringBuilder lines, int lineCount) {
|
||||
return cancelBasedOnInput(context, currentLine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setConversation(Conversation conversation) {
|
||||
canceller.setConversation(conversation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultipleWrapper clone() {
|
||||
return new MultipleWrapper(canceller.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Feels a bit like Android
|
||||
private static final String KEY = "multiple_lines_prompt";
|
||||
private static final String KEY_LAST = KEY + ".last_line";
|
||||
private static final String KEY_LINES = KEY + ".linecount";
|
||||
|
||||
private final MultipleConversationCanceller endMarker;
|
||||
private final String initialPrompt;
|
||||
|
||||
/**
|
||||
* Retrieve and remove the current accumulated input.
|
||||
*
|
||||
* @param context
|
||||
* - conversation context.
|
||||
* @return The accumulated input, or NULL if not found.
|
||||
*/
|
||||
public String removeAccumulatedInput(ConversationContext context) {
|
||||
Object result = context.getSessionData(KEY);
|
||||
|
||||
if (result instanceof StringBuilder) {
|
||||
context.setSessionData(KEY, null);
|
||||
context.setSessionData(KEY_LINES, null);
|
||||
return ((StringBuilder) result).toString();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a multiple lines input prompt with a specific end marker.
|
||||
* <p>
|
||||
* This is usually an empty string.
|
||||
*
|
||||
* @param endMarker - the end marker.
|
||||
*/
|
||||
public MultipleLinesPrompt(String endMarker, String initialPrompt) {
|
||||
this(new ExactMatchConversationCanceller(endMarker), initialPrompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a multiple lines input prompt with a specific end marker implementation.
|
||||
* <p>
|
||||
* Note: Use {@link #MultipleLinesPrompt(MultipleConversationCanceller, String)} if implementing a custom canceller.
|
||||
* @param endMarker - the end marker.
|
||||
* @param initialPrompt - the initial prompt text.
|
||||
*/
|
||||
public MultipleLinesPrompt(ConversationCanceller endMarker, String initialPrompt) {
|
||||
this.endMarker = new MultipleWrapper(endMarker);
|
||||
this.initialPrompt = initialPrompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a multiple lines input prompt with a specific end marker implementation.
|
||||
* @param endMarker - the end marker.
|
||||
* @param initialPrompt - the initial prompt text.
|
||||
*/
|
||||
public MultipleLinesPrompt(MultipleConversationCanceller endMarker, String initialPrompt) {
|
||||
this.endMarker = endMarker;
|
||||
this.initialPrompt = initialPrompt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Prompt acceptInput(ConversationContext context, String in) {
|
||||
StringBuilder result = (StringBuilder) context.getSessionData(KEY);
|
||||
Integer count = (Integer) context.getSessionData(KEY_LINES);
|
||||
|
||||
// Handle first run
|
||||
if (result == null)
|
||||
context.setSessionData(KEY, result = new StringBuilder());
|
||||
if (count == null)
|
||||
count = 0;
|
||||
|
||||
// Save the last line as well
|
||||
context.setSessionData(KEY_LAST, in);
|
||||
context.setSessionData(KEY_LINES, ++count);
|
||||
result.append(in + "\n");
|
||||
|
||||
// And we're done
|
||||
if (endMarker.cancelBasedOnInput(context, in, result, count))
|
||||
return Prompt.END_OF_CONVERSATION;
|
||||
else
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPromptText(ConversationContext context) {
|
||||
Object last = context.getSessionData(KEY_LAST);
|
||||
|
||||
if (last instanceof String)
|
||||
return (String) last;
|
||||
else
|
||||
return initialPrompt;
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import com.comphenix.protocol.events.ListenerPriority;
|
||||
import com.comphenix.protocol.events.PacketContainer;
|
||||
|
||||
/**
|
||||
@ -43,7 +44,7 @@ public interface PacketStream {
|
||||
* Send a packet to the given player.
|
||||
* @param reciever - the reciever.
|
||||
* @param packet - packet to send.
|
||||
* @param filters - whether or not to invoke any packet filters.
|
||||
* @param filters - whether or not to invoke any packet filters below {@link ListenerPriority#MONITOR}.
|
||||
* @throws InvocationTargetException - if an error occured when sending the packet.
|
||||
*/
|
||||
public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters)
|
||||
@ -63,7 +64,7 @@ public interface PacketStream {
|
||||
* Simulate recieving a certain packet from a given player.
|
||||
* @param sender - the sender.
|
||||
* @param packet - the packet that was sent.
|
||||
* @param filters - whether or not to invoke any packet filters.
|
||||
* @param filters - whether or not to invoke any packet filters below {@link ListenerPriority#MONITOR}.
|
||||
* @throws InvocationTargetException If the reflection machinery failed.
|
||||
* @throws IllegalAccessException If the underlying method caused an error.
|
||||
*/
|
||||
|
@ -114,6 +114,10 @@ public final class Packets {
|
||||
public static final int PLAYER_INFO = 201;
|
||||
public static final int ABILITIES = 202;
|
||||
public static final int TAB_COMPLETE = 203;
|
||||
public static final int SCOREBOARD_OBJECTIVE = 206;
|
||||
public static final int UPDATE_SCORE = 207;
|
||||
public static final int DISPLAY_SCOREBOARD = 208;
|
||||
public static final int TEAMS = 209;
|
||||
public static final int CUSTOM_PAYLOAD = 250;
|
||||
public static final int KEY_RESPONSE = 252;
|
||||
public static final int KEY_REQUEST = 253;
|
||||
|
@ -18,12 +18,15 @@
|
||||
package com.comphenix.protocol;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.bukkit.configuration.Configuration;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.io.Files;
|
||||
|
||||
/**
|
||||
* Represents the configuration of ProtocolLib.
|
||||
@ -31,6 +34,7 @@ import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks;
|
||||
* @author Kristian
|
||||
*/
|
||||
class ProtocolConfig {
|
||||
private static final String LAST_UPDATE_FILE = "lastupdate";
|
||||
|
||||
private static final String SECTION_GLOBAL = "global";
|
||||
private static final String SECTION_AUTOUPDATER = "auto updater";
|
||||
@ -40,12 +44,14 @@ class ProtocolConfig {
|
||||
private static final String IGNORE_VERSION_CHECK = "ignore version check";
|
||||
private static final String BACKGROUND_COMPILER_ENABLED = "background compiler";
|
||||
|
||||
private static final String DEBUG_MODE_ENABLED = "debug";
|
||||
private static final String INJECTION_METHOD = "injection method";
|
||||
|
||||
private static final String SCRIPT_ENGINE_NAME = "script engine";
|
||||
|
||||
private static final String UPDATER_NOTIFY = "notify";
|
||||
private static final String UPDATER_DOWNLAD = "download";
|
||||
private static final String UPDATER_DELAY = "delay";
|
||||
private static final String UPDATER_LAST_TIME = "last";
|
||||
|
||||
// Defaults
|
||||
private static final long DEFAULT_UPDATER_DELAY = 43200;
|
||||
@ -57,6 +63,11 @@ class ProtocolConfig {
|
||||
private ConfigurationSection global;
|
||||
private ConfigurationSection updater;
|
||||
|
||||
// Last update time
|
||||
private long lastUpdateTime;
|
||||
private boolean configChanged;
|
||||
private boolean valuesChanged;
|
||||
|
||||
public ProtocolConfig(Plugin plugin) {
|
||||
this(plugin, plugin.getConfig());
|
||||
}
|
||||
@ -70,10 +81,64 @@ class ProtocolConfig {
|
||||
* Reload configuration file.
|
||||
*/
|
||||
public void reloadConfig() {
|
||||
// Reset
|
||||
configChanged = false;
|
||||
valuesChanged = false;
|
||||
|
||||
this.config = plugin.getConfig();
|
||||
this.lastUpdateTime = loadLastUpdate();
|
||||
loadSections(!loadingSections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the last update time stamp from the file system.
|
||||
* @return Last update time stamp.
|
||||
*/
|
||||
private long loadLastUpdate() {
|
||||
File dataFile = getLastUpdateFile();
|
||||
|
||||
if (dataFile.exists()) {
|
||||
try {
|
||||
return Long.parseLong(Files.toString(dataFile, Charsets.UTF_8));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new RuntimeException("Cannot parse " + dataFile + " as a number.", e);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Cannot read " + dataFile, e);
|
||||
}
|
||||
} else {
|
||||
// Default last update
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the given time stamp.
|
||||
* @param value - time stamp to store.
|
||||
*/
|
||||
private void saveLastUpdate(long value) {
|
||||
File dataFile = getLastUpdateFile();
|
||||
|
||||
// The data folder must exist
|
||||
dataFile.getParentFile().mkdirs();
|
||||
|
||||
if (dataFile.exists())
|
||||
dataFile.delete();
|
||||
|
||||
try {
|
||||
Files.write(Long.toString(value), dataFile, Charsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Cannot write " + dataFile, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the file that is used to store the update time stamp.
|
||||
* @return File storing the update time stamp.
|
||||
*/
|
||||
private File getLastUpdateFile() {
|
||||
return new File(plugin.getDataFolder(), LAST_UPDATE_FILE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load data sections.
|
||||
* @param copyDefaults - whether or not to copy configuration defaults.
|
||||
@ -100,6 +165,17 @@ class ProtocolConfig {
|
||||
System.out.println("[ProtocolLib] Created default configuration.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a particular configuration key value pair.
|
||||
* @param section - the configuration root.
|
||||
* @param path - the path to the key.
|
||||
* @param value - the value to set.
|
||||
*/
|
||||
private void setConfig(ConfigurationSection section, String path, Object value) {
|
||||
configChanged = true;
|
||||
section.set(path, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a reference to the configuration file.
|
||||
@ -122,7 +198,7 @@ class ProtocolConfig {
|
||||
* @param value - TRUE to do this automatically, FALSE otherwise.
|
||||
*/
|
||||
public void setAutoNotify(boolean value) {
|
||||
updater.set(UPDATER_NOTIFY, value);
|
||||
setConfig(updater, UPDATER_NOTIFY, value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -138,7 +214,25 @@ class ProtocolConfig {
|
||||
* @param value - TRUE if it should. FALSE otherwise.
|
||||
*/
|
||||
public void setAutoDownload(boolean value) {
|
||||
updater.set(UPDATER_DOWNLAD, value);
|
||||
setConfig(updater, UPDATER_DOWNLAD, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether or not debug mode is enabled.
|
||||
* <p>
|
||||
* This grants access to the filter command.
|
||||
* @return TRUE if it is, FALSE otherwise.
|
||||
*/
|
||||
public boolean isDebug() {
|
||||
return global.getBoolean(DEBUG_MODE_ENABLED, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether or not debug mode is enabled.
|
||||
* @param value - TRUE if it is enabled, FALSE otherwise.
|
||||
*/
|
||||
public void setDebug(boolean value) {
|
||||
setConfig(global, DEBUG_MODE_ENABLED, value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -160,17 +254,9 @@ class ProtocolConfig {
|
||||
// Silently fix the delay
|
||||
if (delaySeconds < DEFAULT_UPDATER_DELAY)
|
||||
delaySeconds = DEFAULT_UPDATER_DELAY;
|
||||
updater.set(UPDATER_DELAY, delaySeconds);
|
||||
setConfig(updater, UPDATER_DELAY, delaySeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the last time we updated, in seconds since 1970.01.01 00:00.
|
||||
* @return Last update time.
|
||||
*/
|
||||
public long getAutoLastTime() {
|
||||
return updater.getLong(UPDATER_LAST_TIME, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* The version of Minecraft to ignore the built-in safety feature.
|
||||
* @return The version to ignore ProtocolLib's satefy.
|
||||
@ -188,7 +274,7 @@ class ProtocolConfig {
|
||||
* @param ignoreVersion - the version of Minecraft where the satefy will be disabled.
|
||||
*/
|
||||
public void setIgnoreVersionCheck(String ignoreVersion) {
|
||||
global.set(IGNORE_VERSION_CHECK, ignoreVersion);
|
||||
setConfig(global, IGNORE_VERSION_CHECK, ignoreVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -207,7 +293,7 @@ class ProtocolConfig {
|
||||
* @param enabled - whether or not metrics is enabled.
|
||||
*/
|
||||
public void setMetricsEnabled(boolean enabled) {
|
||||
global.set(METRICS_ENABLED, enabled);
|
||||
setConfig(global, METRICS_ENABLED, enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -226,7 +312,15 @@ class ProtocolConfig {
|
||||
* @param enabled - TRUE if is enabled/running, FALSE otherwise.
|
||||
*/
|
||||
public void setBackgroundCompilerEnabled(boolean enabled) {
|
||||
global.set(BACKGROUND_COMPILER_ENABLED, enabled);
|
||||
setConfig(global, BACKGROUND_COMPILER_ENABLED, enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the last time we updated, in seconds since 1970.01.01 00:00.
|
||||
* @return Last update time.
|
||||
*/
|
||||
public long getAutoLastTime() {
|
||||
return lastUpdateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -234,7 +328,26 @@ class ProtocolConfig {
|
||||
* @param lastTimeSeconds - new last update time.
|
||||
*/
|
||||
public void setAutoLastTime(long lastTimeSeconds) {
|
||||
updater.set(UPDATER_LAST_TIME, lastTimeSeconds);
|
||||
this.valuesChanged = true;
|
||||
this.lastUpdateTime = lastTimeSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the unique name of the script engine to use for filtering.
|
||||
* @return Script engine to use.
|
||||
*/
|
||||
public String getScriptEngineName() {
|
||||
return global.getString(SCRIPT_ENGINE_NAME, "JavaScript");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the unique name of the script engine to use for filtering.
|
||||
* <p>
|
||||
* This setting will take effect next time ProtocolLib is started.
|
||||
* @param name - name of the script engine to use.
|
||||
*/
|
||||
public void setScriptEngineName(String name) {
|
||||
setConfig(global, SCRIPT_ENGINE_NAME, name);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -266,13 +379,20 @@ class ProtocolConfig {
|
||||
* @return Injection method.
|
||||
*/
|
||||
public void setInjectionMethod(PlayerInjectHooks hook) {
|
||||
global.set(INJECTION_METHOD, hook.name());
|
||||
setConfig(global, INJECTION_METHOD, hook.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current configuration file.
|
||||
*/
|
||||
public void saveAll() {
|
||||
plugin.saveConfig();
|
||||
if (valuesChanged)
|
||||
saveLastUpdate(lastUpdateTime);
|
||||
if (configChanged)
|
||||
plugin.saveConfig();
|
||||
|
||||
// And we're done
|
||||
valuesChanged = false;
|
||||
configChanged = false;
|
||||
}
|
||||
}
|
||||
|
@ -33,8 +33,11 @@ import org.bukkit.plugin.PluginManager;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import com.comphenix.protocol.async.AsyncFilterManager;
|
||||
import com.comphenix.protocol.error.BasicErrorReporter;
|
||||
import com.comphenix.protocol.error.DetailedErrorReporter;
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.error.Report;
|
||||
import com.comphenix.protocol.error.ReportType;
|
||||
import com.comphenix.protocol.injector.DelayedSingleTask;
|
||||
import com.comphenix.protocol.injector.PacketFilterManager;
|
||||
import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks;
|
||||
@ -43,6 +46,7 @@ import com.comphenix.protocol.metrics.Updater;
|
||||
import com.comphenix.protocol.metrics.Updater.UpdateResult;
|
||||
import com.comphenix.protocol.reflect.compiler.BackgroundCompiler;
|
||||
import com.comphenix.protocol.utility.ChatExtensions;
|
||||
import com.comphenix.protocol.utility.MinecraftVersion;
|
||||
|
||||
/**
|
||||
* The main entry point for ProtocolLib.
|
||||
@ -50,6 +54,24 @@ import com.comphenix.protocol.utility.ChatExtensions;
|
||||
* @author Kristian
|
||||
*/
|
||||
public class ProtocolLibrary extends JavaPlugin {
|
||||
// Every possible error or warning report type
|
||||
public static final ReportType REPORT_CANNOT_LOAD_CONFIG = new ReportType("Cannot load configuration");
|
||||
public static final ReportType REPORT_CANNOT_DELETE_CONFIG = new ReportType("Cannot delete old ProtocolLib configuration.");
|
||||
public static final ReportType REPORT_CANNOT_PARSE_INJECTION_METHOD = new ReportType("Cannot parse injection method. Using default.");
|
||||
|
||||
public static final ReportType REPORT_PLUGIN_LOAD_ERROR = new ReportType("Cannot load ProtocolLib.");
|
||||
public static final ReportType REPORT_PLUGIN_ENABLE_ERROR = new ReportType("Cannot enable ProtocolLib.");
|
||||
|
||||
public static final ReportType REPORT_METRICS_IO_ERROR = new ReportType("Unable to enable metrics due to network problems.");
|
||||
public static final ReportType REPORT_METRICS_GENERIC_ERROR = new ReportType("Unable to enable metrics due to network problems.");
|
||||
|
||||
public static final ReportType REPORT_CANNOT_PARSE_MINECRAFT_VERSION = new ReportType("Unable to retrieve current Minecraft version.");
|
||||
public static final ReportType REPORT_CANNOT_DETECT_CONFLICTING_PLUGINS = new ReportType("Unable to detect conflicting plugin versions.");
|
||||
public static final ReportType REPORT_CANNOT_REGISTER_COMMAND = new ReportType("Cannot register command %s: %s");
|
||||
|
||||
public static final ReportType REPORT_CANNOT_CREATE_TIMEOUT_TASK = new ReportType("Unable to create packet timeout task.");
|
||||
public static final ReportType REPORT_CANNOT_UPDATE_PLUGIN = new ReportType("Cannot perform automatic updates.");
|
||||
|
||||
/**
|
||||
* The minimum version ProtocolLib has been tested with.
|
||||
*/
|
||||
@ -58,7 +80,7 @@ public class ProtocolLibrary extends JavaPlugin {
|
||||
/**
|
||||
* The maximum version ProtocolLib has been tested with,
|
||||
*/
|
||||
private static final String MAXIMUM_MINECRAFT_VERSION = "1.4.7";
|
||||
private static final String MAXIMUM_MINECRAFT_VERSION = "1.5.2";
|
||||
|
||||
/**
|
||||
* The number of milliseconds per second.
|
||||
@ -71,7 +93,7 @@ public class ProtocolLibrary extends JavaPlugin {
|
||||
private static PacketFilterManager protocolManager;
|
||||
|
||||
// Error reporter
|
||||
private static ErrorReporter reporter;
|
||||
private static ErrorReporter reporter = new BasicErrorReporter();
|
||||
|
||||
// Metrics and statistisc
|
||||
private Statistics statistisc;
|
||||
@ -102,6 +124,7 @@ public class ProtocolLibrary extends JavaPlugin {
|
||||
// Commands
|
||||
private CommandProtocol commandProtocol;
|
||||
private CommandPacket commandPacket;
|
||||
private CommandFilter commandFilter;
|
||||
|
||||
// Whether or not disable is not needed
|
||||
private boolean skipDisable;
|
||||
@ -118,26 +141,34 @@ public class ProtocolLibrary extends JavaPlugin {
|
||||
try {
|
||||
config = new ProtocolConfig(this);
|
||||
} catch (Exception e) {
|
||||
detailedReporter.reportWarning(this, "Cannot load configuration", e);
|
||||
detailedReporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_LOAD_CONFIG).error(e));
|
||||
|
||||
// Load it again
|
||||
if (deleteConfig()) {
|
||||
config = new ProtocolConfig(this);
|
||||
} else {
|
||||
reporter.reportWarning(this, "Cannot delete old ProtocolLib configuration.");
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_DELETE_CONFIG));
|
||||
}
|
||||
}
|
||||
|
||||
// Print the state of the debug mode
|
||||
if (config.isDebug()) {
|
||||
logger.warning("Debug mode is enabled!");
|
||||
}
|
||||
|
||||
try {
|
||||
// Check for other versions
|
||||
checkConflictingVersions();
|
||||
|
||||
// Handle unexpected Minecraft versions
|
||||
MinecraftVersion version = verifyMinecraftVersion();
|
||||
|
||||
// Set updater
|
||||
updater = new Updater(this, logger, "protocollib", getFile(), "protocol.info");
|
||||
|
||||
unhookTask = new DelayedSingleTask(this);
|
||||
protocolManager = new PacketFilterManager(
|
||||
getClassLoader(), getServer(), unhookTask, detailedReporter);
|
||||
getClassLoader(), getServer(), this, version, unhookTask, detailedReporter);
|
||||
|
||||
// Setup error reporter
|
||||
detailedReporter.addGlobalParameter("manager", protocolManager);
|
||||
@ -152,18 +183,19 @@ public class ProtocolLibrary extends JavaPlugin {
|
||||
protocolManager.setPlayerHook(hook);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
detailedReporter.reportWarning(config, "Cannot parse injection method. Using default.", e);
|
||||
detailedReporter.reportWarning(config, Report.newBuilder(REPORT_CANNOT_PARSE_INJECTION_METHOD).error(e));
|
||||
}
|
||||
|
||||
// Initialize command handlers
|
||||
commandProtocol = new CommandProtocol(detailedReporter, this, updater, config);
|
||||
commandPacket = new CommandPacket(detailedReporter, this, logger, protocolManager);
|
||||
commandFilter = new CommandFilter(detailedReporter, this, config);
|
||||
commandPacket = new CommandPacket(detailedReporter, this, logger, commandFilter, protocolManager);
|
||||
|
||||
// Send logging information to player listeners too
|
||||
setupBroadcastUsers(PERMISSION_INFO);
|
||||
|
||||
} catch (Throwable e) {
|
||||
detailedReporter.reportDetailed(this, "Cannot load ProtocolLib.", e, protocolManager);
|
||||
detailedReporter.reportDetailed(this, Report.newBuilder(REPORT_PLUGIN_LOAD_ERROR).error(e).callerParam(protocolManager));
|
||||
disablePlugin();
|
||||
}
|
||||
}
|
||||
@ -249,12 +281,10 @@ public class ProtocolLibrary extends JavaPlugin {
|
||||
logger.info("Structure compiler thread has been disabled.");
|
||||
}
|
||||
|
||||
// Handle unexpected Minecraft versions
|
||||
verifyMinecraftVersion();
|
||||
|
||||
// Set up command handlers
|
||||
registerCommand(CommandProtocol.NAME, commandProtocol);
|
||||
registerCommand(CommandPacket.NAME, commandPacket);
|
||||
registerCommand(CommandFilter.NAME, commandFilter);
|
||||
|
||||
// Player login and logout events
|
||||
protocolManager.registerEvents(manager, this);
|
||||
@ -264,7 +294,7 @@ public class ProtocolLibrary extends JavaPlugin {
|
||||
createAsyncTask(server);
|
||||
|
||||
} catch (Throwable e) {
|
||||
reporter.reportDetailed(this, "Cannot enable ProtocolLib.", e);
|
||||
reporter.reportDetailed(this, Report.newBuilder(REPORT_PLUGIN_ENABLE_ERROR).error(e));
|
||||
disablePlugin();
|
||||
return;
|
||||
}
|
||||
@ -275,14 +305,14 @@ public class ProtocolLibrary extends JavaPlugin {
|
||||
statistisc = new Statistics(this);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
reporter.reportDetailed(this, "Unable to enable metrics.", e, statistisc);
|
||||
reporter.reportDetailed(this, Report.newBuilder(REPORT_METRICS_IO_ERROR).error(e).callerParam(statistisc));
|
||||
} catch (Throwable e) {
|
||||
reporter.reportDetailed(this, "Metrics cannot be enabled. Incompatible Bukkit version.", e, statistisc);
|
||||
reporter.reportDetailed(this, Report.newBuilder(REPORT_METRICS_GENERIC_ERROR).error(e).callerParam(statistisc));
|
||||
}
|
||||
}
|
||||
|
||||
// Used to check Minecraft version
|
||||
private void verifyMinecraftVersion() {
|
||||
private MinecraftVersion verifyMinecraftVersion() {
|
||||
try {
|
||||
MinecraftVersion minimum = new MinecraftVersion(MINIMUM_MINECRAFT_VERSION);
|
||||
MinecraftVersion maximum = new MinecraftVersion(MAXIMUM_MINECRAFT_VERSION);
|
||||
@ -296,9 +326,14 @@ public class ProtocolLibrary extends JavaPlugin {
|
||||
if (current.compareTo(maximum) > 0)
|
||||
logger.warning("Version " + current + " has not yet been tested! Proceed with caution.");
|
||||
}
|
||||
return current;
|
||||
|
||||
} catch (Exception e) {
|
||||
reporter.reportWarning(this, "Unable to retrieve current Minecraft version.", e);
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_PARSE_MINECRAFT_VERSION).error(e));
|
||||
}
|
||||
|
||||
// Unknown version
|
||||
return null;
|
||||
}
|
||||
|
||||
private void checkConflictingVersions() {
|
||||
@ -331,7 +366,7 @@ public class ProtocolLibrary extends JavaPlugin {
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
reporter.reportWarning(this, "Unable to detect conflicting plugin versions.", e);
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_DETECT_CONFLICTING_PLUGINS).error(e));
|
||||
}
|
||||
|
||||
// See if the newest version is actually higher
|
||||
@ -360,7 +395,9 @@ public class ProtocolLibrary extends JavaPlugin {
|
||||
throw new RuntimeException("plugin.yml might be corrupt.");
|
||||
|
||||
} catch (RuntimeException e) {
|
||||
reporter.reportWarning(this, "Cannot register command " + name + ": " + e.getMessage());
|
||||
reporter.reportWarning(this,
|
||||
Report.newBuilder(REPORT_CANNOT_REGISTER_COMMAND).messageParam(name, e.getMessage()).error(e)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -394,7 +431,7 @@ public class ProtocolLibrary extends JavaPlugin {
|
||||
|
||||
} catch (Throwable e) {
|
||||
if (asyncPacketTask == -1) {
|
||||
reporter.reportDetailed(this, "Unable to create packet timeout task.", e);
|
||||
reporter.reportDetailed(this, Report.newBuilder(REPORT_CANNOT_CREATE_TIMEOUT_TASK).error(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -417,7 +454,7 @@ public class ProtocolLibrary extends JavaPlugin {
|
||||
commandProtocol.updateFinished();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
reporter.reportDetailed(this, "Cannot perform automatic updates.", e);
|
||||
reporter.reportDetailed(this, Report.newBuilder(REPORT_CANNOT_UPDATE_PLUGIN).error(e));
|
||||
updateDisabled = true;
|
||||
}
|
||||
}
|
||||
@ -454,7 +491,9 @@ public class ProtocolLibrary extends JavaPlugin {
|
||||
unhookTask.close();
|
||||
protocolManager = null;
|
||||
statistisc = null;
|
||||
reporter = null;
|
||||
|
||||
// To clean up global parameters
|
||||
reporter = new BasicErrorReporter();
|
||||
|
||||
// Leaky ClassLoader begone!
|
||||
if (updater == null || updater.getResult() != UpdateResult.SUCCESS) {
|
||||
@ -479,6 +518,8 @@ public class ProtocolLibrary extends JavaPlugin {
|
||||
|
||||
/**
|
||||
* Retrieve the current error reporter.
|
||||
* <p>
|
||||
* This is guaranteed to not be NULL in 2.5.0 and later.
|
||||
* @return Current error reporter.
|
||||
*/
|
||||
public static ErrorReporter getErrorReporter() {
|
||||
|
@ -27,6 +27,7 @@ import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import com.comphenix.protocol.async.AsyncMarker;
|
||||
import com.comphenix.protocol.events.ListenerPriority;
|
||||
import com.comphenix.protocol.events.PacketContainer;
|
||||
import com.comphenix.protocol.events.PacketListener;
|
||||
import com.comphenix.protocol.injector.PacketConstructor;
|
||||
@ -47,7 +48,7 @@ public interface ProtocolManager extends PacketStream {
|
||||
*
|
||||
* @param reciever - the reciever.
|
||||
* @param packet - packet to send.
|
||||
* @param filters - whether or not to invoke any packet filters.
|
||||
* @param filters - whether or not to invoke any packet filters below {@link ListenerPriority#MONITOR}.
|
||||
* @throws InvocationTargetException - if an error occured when sending the packet.
|
||||
*/
|
||||
@Override
|
||||
@ -62,7 +63,7 @@ public interface ProtocolManager extends PacketStream {
|
||||
*
|
||||
* @param sender - the sender.
|
||||
* @param packet - the packet that was sent.
|
||||
* @param filters - whether or not to invoke any packet filters.
|
||||
* @param filters - whether or not to invoke any packet filters below {@link ListenerPriority#MONITOR}.
|
||||
* @throws InvocationTargetException If the reflection machinery failed.
|
||||
* @throws IllegalAccessException If the underlying method caused an error.
|
||||
*/
|
||||
|
@ -23,6 +23,7 @@ import java.util.PriorityQueue;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
import com.comphenix.protocol.Packets;
|
||||
import com.comphenix.protocol.concurrency.AbstractConcurrentListenerMultimap;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.comphenix.protocol.injector.PrioritizedListener;
|
||||
@ -66,7 +67,7 @@ class PacketProcessingQueue extends AbstractConcurrentListenerMultimap<AsyncList
|
||||
}
|
||||
|
||||
public PacketProcessingQueue(PlayerSendingHandler sendingHandler, int initialSize, int maximumSize, int maximumConcurrency) {
|
||||
super();
|
||||
super(Packets.MAXIMUM_PACKET_ID);
|
||||
|
||||
try {
|
||||
this.processingQueue = Synchronization.queue(MinMaxPriorityQueue.
|
||||
|
@ -1,304 +1,305 @@
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.async;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.PriorityBlockingQueue;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.comphenix.protocol.injector.PlayerLoggedOutException;
|
||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||
|
||||
/**
|
||||
* Represents packets ready to be transmitted to a client.
|
||||
* @author Kristian
|
||||
*/
|
||||
abstract class PacketSendingQueue {
|
||||
|
||||
public static final int INITIAL_CAPACITY = 10;
|
||||
|
||||
private PriorityBlockingQueue<PacketEventHolder> sendingQueue;
|
||||
|
||||
// Asynchronous packet sending
|
||||
private Executor asynchronousSender;
|
||||
|
||||
// Whether or not packet transmission must occur on a specific thread
|
||||
private final boolean notThreadSafe;
|
||||
|
||||
// Whether or not we've run the cleanup procedure
|
||||
private boolean cleanedUp = false;
|
||||
|
||||
/**
|
||||
* Create a packet sending queue.
|
||||
* @param notThreadSafe - whether or not to synchronize with the main thread or a background thread.
|
||||
*/
|
||||
public PacketSendingQueue(boolean notThreadSafe, Executor asynchronousSender) {
|
||||
this.sendingQueue = new PriorityBlockingQueue<PacketEventHolder>(INITIAL_CAPACITY);
|
||||
this.notThreadSafe = notThreadSafe;
|
||||
this.asynchronousSender = asynchronousSender;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of packet events in the queue.
|
||||
* @return The number of packet events in the queue.
|
||||
*/
|
||||
public int size() {
|
||||
return sendingQueue.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue a packet for sending.
|
||||
* @param packet - packet to queue.
|
||||
*/
|
||||
public void enqueue(PacketEvent packet) {
|
||||
sendingQueue.add(new PacketEventHolder(packet));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when one of the packets have finished processing.
|
||||
* @param packetUpdated - the packet that has now been updated.
|
||||
* @param onMainThread - whether or not this is occuring on the main thread.
|
||||
*/
|
||||
public synchronized void signalPacketUpdate(PacketEvent packetUpdated, boolean onMainThread) {
|
||||
|
||||
AsyncMarker marker = packetUpdated.getAsyncMarker();
|
||||
|
||||
// Should we reorder the event?
|
||||
if (marker.getQueuedSendingIndex() != marker.getNewSendingIndex() && !marker.hasExpired()) {
|
||||
PacketEvent copy = PacketEvent.fromSynchronous(packetUpdated, marker);
|
||||
|
||||
// "Cancel" the original event
|
||||
packetUpdated.setCancelled(true);
|
||||
|
||||
// Enqueue the copy with the new sending index
|
||||
enqueue(copy);
|
||||
}
|
||||
|
||||
// Mark this packet as finished
|
||||
marker.setProcessed(true);
|
||||
trySendPackets(onMainThread);
|
||||
}
|
||||
|
||||
/***
|
||||
* Invoked when a list of packet IDs are no longer associated with any listeners.
|
||||
* @param packetsRemoved - packets that no longer have any listeners.
|
||||
* @param onMainThread - whether or not this is occuring on the main thread.
|
||||
*/
|
||||
public synchronized void signalPacketUpdate(List<Integer> packetsRemoved, boolean onMainThread) {
|
||||
|
||||
Set<Integer> lookup = new HashSet<Integer>(packetsRemoved);
|
||||
|
||||
// Note that this is O(n), so it might be expensive
|
||||
for (PacketEventHolder holder : sendingQueue) {
|
||||
PacketEvent event = holder.getEvent();
|
||||
|
||||
if (lookup.contains(event.getPacketID())) {
|
||||
event.getAsyncMarker().setProcessed(true);
|
||||
}
|
||||
}
|
||||
|
||||
// This is likely to have changed the situation a bit
|
||||
trySendPackets(onMainThread);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to send any remaining packets.
|
||||
* @param onMainThread - whether or not this is occuring on the main thread.
|
||||
*/
|
||||
public void trySendPackets(boolean onMainThread) {
|
||||
// Whether or not to continue sending packets
|
||||
boolean sending = true;
|
||||
|
||||
// Transmit as many packets as we can
|
||||
while (sending) {
|
||||
PacketEventHolder holder = sendingQueue.poll();
|
||||
|
||||
if (holder != null) {
|
||||
sending = processPacketHolder(onMainThread, holder);
|
||||
|
||||
if (!sending) {
|
||||
// Add it back again
|
||||
sendingQueue.add(holder);
|
||||
}
|
||||
|
||||
} else {
|
||||
// No more packets to send
|
||||
sending = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a packet might be ready for transmission.
|
||||
* @param onMainThread - TRUE if we're on the main thread, FALSE otherwise.
|
||||
* @param holder - packet container.
|
||||
* @return TRUE to continue sending packets, FALSE otherwise.
|
||||
*/
|
||||
private boolean processPacketHolder(boolean onMainThread, final PacketEventHolder holder) {
|
||||
PacketEvent current = holder.getEvent();
|
||||
AsyncMarker marker = current.getAsyncMarker();
|
||||
boolean hasExpired = marker.hasExpired();
|
||||
|
||||
// Guard in cause the queue is closed
|
||||
if (cleanedUp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// End condition?
|
||||
if (marker.isProcessed() || hasExpired) {
|
||||
if (hasExpired) {
|
||||
// Notify timeout listeners
|
||||
onPacketTimeout(current);
|
||||
|
||||
// Recompute
|
||||
marker = current.getAsyncMarker();
|
||||
hasExpired = marker.hasExpired();
|
||||
|
||||
// Could happen due to the timeout listeners
|
||||
if (!marker.isProcessed() && !hasExpired) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Is it okay to send the packet?
|
||||
if (!current.isCancelled() && !hasExpired) {
|
||||
// Make sure we're on the main thread
|
||||
if (notThreadSafe) {
|
||||
try {
|
||||
boolean wantAsync = marker.isMinecraftAsync(current);
|
||||
boolean wantSync = !wantAsync;
|
||||
|
||||
// Wait for the next main thread heartbeat if we haven't fulfilled our promise
|
||||
if (!onMainThread && wantSync) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Let's give it what it wants
|
||||
if (onMainThread && wantAsync) {
|
||||
asynchronousSender.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// We know this isn't on the main thread
|
||||
processPacketHolder(false, holder);
|
||||
}
|
||||
});
|
||||
|
||||
// Scheduler will do the rest
|
||||
return true;
|
||||
}
|
||||
|
||||
} catch (FieldAccessException e) {
|
||||
e.printStackTrace();
|
||||
|
||||
// Just drop the packet
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Silently skip players that have logged out
|
||||
if (isOnline(current.getPlayer())) {
|
||||
sendPacket(current);
|
||||
}
|
||||
}
|
||||
|
||||
// Drop the packet
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add it back and stop sending
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a packet has timed out.
|
||||
* @param event - the timed out packet.
|
||||
*/
|
||||
protected abstract void onPacketTimeout(PacketEvent event);
|
||||
|
||||
private boolean isOnline(Player player) {
|
||||
return player != null && player.isOnline();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send every packet, regardless of the processing state.
|
||||
*/
|
||||
private void forceSend() {
|
||||
while (true) {
|
||||
PacketEventHolder holder = sendingQueue.poll();
|
||||
|
||||
if (holder != null) {
|
||||
sendPacket(holder.getEvent());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the packet transmission must synchronize with the main thread.
|
||||
* @return TRUE if it must, FALSE otherwise.
|
||||
*/
|
||||
public boolean isSynchronizeMain() {
|
||||
return notThreadSafe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmit a packet, if it hasn't already.
|
||||
* @param event - the packet to transmit.
|
||||
*/
|
||||
private void sendPacket(PacketEvent event) {
|
||||
|
||||
AsyncMarker marker = event.getAsyncMarker();
|
||||
|
||||
try {
|
||||
// Don't send a packet twice
|
||||
if (marker != null && !marker.isTransmitted()) {
|
||||
marker.sendPacket(event);
|
||||
}
|
||||
|
||||
} catch (PlayerLoggedOutException e) {
|
||||
System.out.println(String.format(
|
||||
"[ProtocolLib] Warning: Dropped packet index %s of ID %s",
|
||||
marker.getOriginalSendingIndex(), event.getPacketID()
|
||||
));
|
||||
|
||||
} catch (IOException e) {
|
||||
// Just print the error
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically transmits every delayed packet.
|
||||
*/
|
||||
public void cleanupAll() {
|
||||
if (!cleanedUp) {
|
||||
// Note that the cleanup itself will always occur on the main thread
|
||||
forceSend();
|
||||
|
||||
// And we're done
|
||||
cleanedUp = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.async;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.PriorityBlockingQueue;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.comphenix.protocol.injector.PlayerLoggedOutException;
|
||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||
|
||||
/**
|
||||
* Represents packets ready to be transmitted to a client.
|
||||
* @author Kristian
|
||||
*/
|
||||
abstract class PacketSendingQueue {
|
||||
|
||||
public static final int INITIAL_CAPACITY = 10;
|
||||
|
||||
private PriorityBlockingQueue<PacketEventHolder> sendingQueue;
|
||||
|
||||
// Asynchronous packet sending
|
||||
private Executor asynchronousSender;
|
||||
|
||||
// Whether or not packet transmission must occur on a specific thread
|
||||
private final boolean notThreadSafe;
|
||||
|
||||
// Whether or not we've run the cleanup procedure
|
||||
private boolean cleanedUp = false;
|
||||
|
||||
/**
|
||||
* Create a packet sending queue.
|
||||
* @param notThreadSafe - whether or not to synchronize with the main thread or a background thread.
|
||||
*/
|
||||
public PacketSendingQueue(boolean notThreadSafe, Executor asynchronousSender) {
|
||||
this.sendingQueue = new PriorityBlockingQueue<PacketEventHolder>(INITIAL_CAPACITY);
|
||||
this.notThreadSafe = notThreadSafe;
|
||||
this.asynchronousSender = asynchronousSender;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of packet events in the queue.
|
||||
* @return The number of packet events in the queue.
|
||||
*/
|
||||
public int size() {
|
||||
return sendingQueue.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue a packet for sending.
|
||||
* @param packet - packet to queue.
|
||||
*/
|
||||
public void enqueue(PacketEvent packet) {
|
||||
sendingQueue.add(new PacketEventHolder(packet));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when one of the packets have finished processing.
|
||||
* @param packetUpdated - the packet that has now been updated.
|
||||
* @param onMainThread - whether or not this is occuring on the main thread.
|
||||
*/
|
||||
public synchronized void signalPacketUpdate(PacketEvent packetUpdated, boolean onMainThread) {
|
||||
|
||||
AsyncMarker marker = packetUpdated.getAsyncMarker();
|
||||
|
||||
// Should we reorder the event?
|
||||
if (marker.getQueuedSendingIndex() != marker.getNewSendingIndex() && !marker.hasExpired()) {
|
||||
PacketEvent copy = PacketEvent.fromSynchronous(packetUpdated, marker);
|
||||
|
||||
// "Cancel" the original event
|
||||
packetUpdated.setReadOnly(false);
|
||||
packetUpdated.setCancelled(true);
|
||||
|
||||
// Enqueue the copy with the new sending index
|
||||
enqueue(copy);
|
||||
}
|
||||
|
||||
// Mark this packet as finished
|
||||
marker.setProcessed(true);
|
||||
trySendPackets(onMainThread);
|
||||
}
|
||||
|
||||
/***
|
||||
* Invoked when a list of packet IDs are no longer associated with any listeners.
|
||||
* @param packetsRemoved - packets that no longer have any listeners.
|
||||
* @param onMainThread - whether or not this is occuring on the main thread.
|
||||
*/
|
||||
public synchronized void signalPacketUpdate(List<Integer> packetsRemoved, boolean onMainThread) {
|
||||
|
||||
Set<Integer> lookup = new HashSet<Integer>(packetsRemoved);
|
||||
|
||||
// Note that this is O(n), so it might be expensive
|
||||
for (PacketEventHolder holder : sendingQueue) {
|
||||
PacketEvent event = holder.getEvent();
|
||||
|
||||
if (lookup.contains(event.getPacketID())) {
|
||||
event.getAsyncMarker().setProcessed(true);
|
||||
}
|
||||
}
|
||||
|
||||
// This is likely to have changed the situation a bit
|
||||
trySendPackets(onMainThread);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to send any remaining packets.
|
||||
* @param onMainThread - whether or not this is occuring on the main thread.
|
||||
*/
|
||||
public void trySendPackets(boolean onMainThread) {
|
||||
// Whether or not to continue sending packets
|
||||
boolean sending = true;
|
||||
|
||||
// Transmit as many packets as we can
|
||||
while (sending) {
|
||||
PacketEventHolder holder = sendingQueue.poll();
|
||||
|
||||
if (holder != null) {
|
||||
sending = processPacketHolder(onMainThread, holder);
|
||||
|
||||
if (!sending) {
|
||||
// Add it back again
|
||||
sendingQueue.add(holder);
|
||||
}
|
||||
|
||||
} else {
|
||||
// No more packets to send
|
||||
sending = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a packet might be ready for transmission.
|
||||
* @param onMainThread - TRUE if we're on the main thread, FALSE otherwise.
|
||||
* @param holder - packet container.
|
||||
* @return TRUE to continue sending packets, FALSE otherwise.
|
||||
*/
|
||||
private boolean processPacketHolder(boolean onMainThread, final PacketEventHolder holder) {
|
||||
PacketEvent current = holder.getEvent();
|
||||
AsyncMarker marker = current.getAsyncMarker();
|
||||
boolean hasExpired = marker.hasExpired();
|
||||
|
||||
// Guard in cause the queue is closed
|
||||
if (cleanedUp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// End condition?
|
||||
if (marker.isProcessed() || hasExpired) {
|
||||
if (hasExpired) {
|
||||
// Notify timeout listeners
|
||||
onPacketTimeout(current);
|
||||
|
||||
// Recompute
|
||||
marker = current.getAsyncMarker();
|
||||
hasExpired = marker.hasExpired();
|
||||
|
||||
// Could happen due to the timeout listeners
|
||||
if (!marker.isProcessed() && !hasExpired) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Is it okay to send the packet?
|
||||
if (!current.isCancelled() && !hasExpired) {
|
||||
// Make sure we're on the main thread
|
||||
if (notThreadSafe) {
|
||||
try {
|
||||
boolean wantAsync = marker.isMinecraftAsync(current);
|
||||
boolean wantSync = !wantAsync;
|
||||
|
||||
// Wait for the next main thread heartbeat if we haven't fulfilled our promise
|
||||
if (!onMainThread && wantSync) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Let's give it what it wants
|
||||
if (onMainThread && wantAsync) {
|
||||
asynchronousSender.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// We know this isn't on the main thread
|
||||
processPacketHolder(false, holder);
|
||||
}
|
||||
});
|
||||
|
||||
// Scheduler will do the rest
|
||||
return true;
|
||||
}
|
||||
|
||||
} catch (FieldAccessException e) {
|
||||
e.printStackTrace();
|
||||
|
||||
// Just drop the packet
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Silently skip players that have logged out
|
||||
if (isOnline(current.getPlayer())) {
|
||||
sendPacket(current);
|
||||
}
|
||||
}
|
||||
|
||||
// Drop the packet
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add it back and stop sending
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a packet has timed out.
|
||||
* @param event - the timed out packet.
|
||||
*/
|
||||
protected abstract void onPacketTimeout(PacketEvent event);
|
||||
|
||||
private boolean isOnline(Player player) {
|
||||
return player != null && player.isOnline();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send every packet, regardless of the processing state.
|
||||
*/
|
||||
private void forceSend() {
|
||||
while (true) {
|
||||
PacketEventHolder holder = sendingQueue.poll();
|
||||
|
||||
if (holder != null) {
|
||||
sendPacket(holder.getEvent());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the packet transmission must synchronize with the main thread.
|
||||
* @return TRUE if it must, FALSE otherwise.
|
||||
*/
|
||||
public boolean isSynchronizeMain() {
|
||||
return notThreadSafe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmit a packet, if it hasn't already.
|
||||
* @param event - the packet to transmit.
|
||||
*/
|
||||
private void sendPacket(PacketEvent event) {
|
||||
|
||||
AsyncMarker marker = event.getAsyncMarker();
|
||||
|
||||
try {
|
||||
// Don't send a packet twice
|
||||
if (marker != null && !marker.isTransmitted()) {
|
||||
marker.sendPacket(event);
|
||||
}
|
||||
|
||||
} catch (PlayerLoggedOutException e) {
|
||||
System.out.println(String.format(
|
||||
"[ProtocolLib] Warning: Dropped packet index %s of ID %s",
|
||||
marker.getOriginalSendingIndex(), event.getPacketID()
|
||||
));
|
||||
|
||||
} catch (IOException e) {
|
||||
// Just print the error
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically transmits every delayed packet.
|
||||
*/
|
||||
public void cleanupAll() {
|
||||
if (!cleanedUp) {
|
||||
// Note that the cleanup itself will always occur on the main thread
|
||||
forceSend();
|
||||
|
||||
// And we're done
|
||||
cleanedUp = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
|
||||
import com.comphenix.protocol.events.ListeningWhitelist;
|
||||
import com.comphenix.protocol.injector.PrioritizedListener;
|
||||
@ -34,10 +35,14 @@ import com.google.common.collect.Iterables;
|
||||
* @author Kristian
|
||||
*/
|
||||
public abstract class AbstractConcurrentListenerMultimap<TListener> {
|
||||
|
||||
// The core of our map
|
||||
private ConcurrentMap<Integer, SortedCopyOnWriteArray<PrioritizedListener<TListener>>> listeners =
|
||||
new ConcurrentHashMap<Integer, SortedCopyOnWriteArray<PrioritizedListener<TListener>>>();
|
||||
private AtomicReferenceArray<SortedCopyOnWriteArray<PrioritizedListener<TListener>>> arrayListeners;
|
||||
private ConcurrentMap<Integer, SortedCopyOnWriteArray<PrioritizedListener<TListener>>> mapListeners;
|
||||
|
||||
public AbstractConcurrentListenerMultimap(int maximumPacketID) {
|
||||
arrayListeners = new AtomicReferenceArray<SortedCopyOnWriteArray<PrioritizedListener<TListener>>>(maximumPacketID + 1);
|
||||
mapListeners = new ConcurrentHashMap<Integer, SortedCopyOnWriteArray<PrioritizedListener<TListener>>>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to its requested list of packet recievers.
|
||||
@ -45,7 +50,6 @@ public abstract class AbstractConcurrentListenerMultimap<TListener> {
|
||||
* @param whitelist - the packet whitelist to use.
|
||||
*/
|
||||
public void addListener(TListener listener, ListeningWhitelist whitelist) {
|
||||
|
||||
PrioritizedListener<TListener> prioritized = new PrioritizedListener<TListener>(listener, whitelist.getPriority());
|
||||
|
||||
for (Integer packetID : whitelist.getWhitelist()) {
|
||||
@ -55,21 +59,20 @@ public abstract class AbstractConcurrentListenerMultimap<TListener> {
|
||||
|
||||
// Add the listener to a specific packet notifcation list
|
||||
private void addListener(Integer packetID, PrioritizedListener<TListener> listener) {
|
||||
|
||||
SortedCopyOnWriteArray<PrioritizedListener<TListener>> list = listeners.get(packetID);
|
||||
SortedCopyOnWriteArray<PrioritizedListener<TListener>> list = arrayListeners.get(packetID);
|
||||
|
||||
// We don't want to create this for every lookup
|
||||
if (list == null) {
|
||||
// It would be nice if we could use a PriorityBlockingQueue, but it doesn't preseve iterator order,
|
||||
// which is a essential feature for our purposes.
|
||||
final SortedCopyOnWriteArray<PrioritizedListener<TListener>> value = new SortedCopyOnWriteArray<PrioritizedListener<TListener>>();
|
||||
|
||||
list = listeners.putIfAbsent(packetID, value);
|
||||
|
||||
// We may end up creating multiple multisets, but we'll agree
|
||||
// on the one to use.
|
||||
if (list == null) {
|
||||
|
||||
// We may end up creating multiple multisets, but we'll agree on which to use
|
||||
if (arrayListeners.compareAndSet(packetID, null, value)) {
|
||||
mapListeners.put(packetID, value);
|
||||
list = value;
|
||||
} else {
|
||||
list = arrayListeners.get(packetID);
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,13 +87,11 @@ public abstract class AbstractConcurrentListenerMultimap<TListener> {
|
||||
* @return Every packet ID that was removed due to no listeners.
|
||||
*/
|
||||
public List<Integer> removeListener(TListener listener, ListeningWhitelist whitelist) {
|
||||
|
||||
List<Integer> removedPackets = new ArrayList<Integer>();
|
||||
|
||||
// Again, not terribly efficient. But adding or removing listeners should be a rare event.
|
||||
for (Integer packetID : whitelist.getWhitelist()) {
|
||||
|
||||
SortedCopyOnWriteArray<PrioritizedListener<TListener>> list = listeners.get(packetID);
|
||||
SortedCopyOnWriteArray<PrioritizedListener<TListener>> list = arrayListeners.get(packetID);
|
||||
|
||||
// Remove any listeners
|
||||
if (list != null) {
|
||||
@ -100,7 +101,8 @@ public abstract class AbstractConcurrentListenerMultimap<TListener> {
|
||||
list.remove(new PrioritizedListener<TListener>(listener, whitelist.getPriority()));
|
||||
|
||||
if (list.size() == 0) {
|
||||
listeners.remove(packetID);
|
||||
arrayListeners.set(packetID, null);
|
||||
mapListeners.remove(packetID);
|
||||
removedPackets.add(packetID);
|
||||
}
|
||||
}
|
||||
@ -120,7 +122,7 @@ public abstract class AbstractConcurrentListenerMultimap<TListener> {
|
||||
* @return Registered listeners.
|
||||
*/
|
||||
public Collection<PrioritizedListener<TListener>> getListener(int packetID) {
|
||||
return listeners.get(packetID);
|
||||
return arrayListeners.get(packetID);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -128,7 +130,7 @@ public abstract class AbstractConcurrentListenerMultimap<TListener> {
|
||||
* @return Every listener.
|
||||
*/
|
||||
public Iterable<PrioritizedListener<TListener>> values() {
|
||||
return Iterables.concat(listeners.values());
|
||||
return Iterables.concat(mapListeners.values());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -136,13 +138,15 @@ public abstract class AbstractConcurrentListenerMultimap<TListener> {
|
||||
* @return Registered packet ID.
|
||||
*/
|
||||
public Set<Integer> keySet() {
|
||||
return listeners.keySet();
|
||||
return mapListeners.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all packet listeners.
|
||||
*/
|
||||
protected void clearListeners() {
|
||||
listeners.clear();
|
||||
arrayListeners = new AtomicReferenceArray<
|
||||
SortedCopyOnWriteArray<PrioritizedListener<TListener>>>(arrayListeners.length());
|
||||
mapListeners.clear();
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
package com.comphenix.protocol.concurrency;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@ -60,6 +61,16 @@ public class IntegerSet {
|
||||
array[element] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given collection of elements to the set.
|
||||
* @param packets - elements to add.
|
||||
*/
|
||||
public void addAll(Collection<Integer> packets) {
|
||||
for (Integer id : packets) {
|
||||
add(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given element from the set, or do nothing if it's already removed.
|
||||
* @param element - element to remove.
|
||||
|
@ -0,0 +1,96 @@
|
||||
package com.comphenix.protocol.error;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import com.comphenix.protocol.error.Report.ReportBuilder;
|
||||
import com.comphenix.protocol.reflect.PrettyPrinter;
|
||||
|
||||
/**
|
||||
* Represents a basic error reporter that prints error reports to the standard error stream.
|
||||
* <p>
|
||||
* Note that this implementation doesn't distinguish between {@link #reportWarning(Object, Report)}
|
||||
* and {@link #reportDetailed(Object, Report)} - they both have the exact same behavior.
|
||||
* @author Kristian
|
||||
*/
|
||||
public class BasicErrorReporter implements ErrorReporter {
|
||||
private final PrintStream output;
|
||||
|
||||
/**
|
||||
* Construct a new basic error reporter that prints directly the standard error stream.
|
||||
*/
|
||||
public BasicErrorReporter() {
|
||||
this(System.err);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a error reporter that prints to the given output stream.
|
||||
* @param output - the output stream.
|
||||
*/
|
||||
public BasicErrorReporter(PrintStream output) {
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportMinimal(Plugin sender, String methodName, Throwable error) {
|
||||
output.println("Unhandled exception occured in " + methodName + " for " + sender.getName());
|
||||
error.printStackTrace(output);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters) {
|
||||
reportMinimal(sender, methodName, error);
|
||||
|
||||
// Also print parameters
|
||||
printParameters(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportWarning(Object sender, Report report) {
|
||||
// Basic warning
|
||||
output.println("[" + sender.getClass().getSimpleName() + "] " + report.getReportMessage());
|
||||
|
||||
if (report.getException() != null) {
|
||||
report.getException().printStackTrace(output);
|
||||
}
|
||||
printParameters(report.getCallerParameters());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportWarning(Object sender, ReportBuilder reportBuilder) {
|
||||
reportWarning(sender, reportBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportDetailed(Object sender, Report report) {
|
||||
// No difference from warning
|
||||
reportWarning(sender, report);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportDetailed(Object sender, ReportBuilder reportBuilder) {
|
||||
reportWarning(sender, reportBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the given parameters to the standard error stream.
|
||||
* @param parameters - the output parameters.
|
||||
*/
|
||||
private void printParameters(Object[] parameters) {
|
||||
if (parameters != null && parameters.length > 0) {
|
||||
output.println("Parameters: ");
|
||||
|
||||
try {
|
||||
for (Object parameter : parameters) {
|
||||
if (parameter == null)
|
||||
output.println("[NULL]");
|
||||
else
|
||||
output.println(PrettyPrinter.printObject(parameter));
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
// Damn it
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package com.comphenix.protocol.error;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import com.comphenix.protocol.error.Report.ReportBuilder;
|
||||
|
||||
/**
|
||||
* Construct an error reporter that delegates to another error reporter.
|
||||
* @author Kristian
|
||||
*/
|
||||
public class DelegatedErrorReporter implements ErrorReporter {
|
||||
private final ErrorReporter delegated;
|
||||
|
||||
/**
|
||||
* Construct a new error reporter that forwards all reports to a given reporter.
|
||||
* @param delegated - the delegated reporter.
|
||||
*/
|
||||
public DelegatedErrorReporter(ErrorReporter delegated) {
|
||||
this.delegated = delegated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the underlying error reporter.
|
||||
* @return Underlying error reporter.
|
||||
*/
|
||||
public ErrorReporter getDelegated() {
|
||||
return delegated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportMinimal(Plugin sender, String methodName, Throwable error) {
|
||||
delegated.reportMinimal(sender, methodName, error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters) {
|
||||
delegated.reportMinimal(sender, methodName, error, parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportWarning(Object sender, Report report) {
|
||||
Report transformed = filterReport(sender, report, false);
|
||||
|
||||
if (transformed != null) {
|
||||
delegated.reportWarning(sender, transformed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportDetailed(Object sender, Report report) {
|
||||
Report transformed = filterReport(sender, report, true);
|
||||
|
||||
if (transformed != null) {
|
||||
delegated.reportDetailed(sender, transformed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked before an error report is passed on to the underlying error reporter.
|
||||
* <p>
|
||||
* To cancel a report, return NULL.
|
||||
* @param sender - the sender component.
|
||||
* @param report - the error report.
|
||||
* @param detailed - whether or not the report will be displayed in detail.
|
||||
* @return The report to pass on, or NULL to cancel it.
|
||||
*/
|
||||
protected Report filterReport(Object sender, Report report, boolean detailed) {
|
||||
return report;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportWarning(Object sender, ReportBuilder reportBuilder) {
|
||||
reportWarning(sender, reportBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportDetailed(Object sender, ReportBuilder reportBuilder) {
|
||||
reportDetailed(sender, reportBuilder.build());
|
||||
}
|
||||
}
|
@ -1,387 +1,499 @@
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.error;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.apache.commons.lang.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang.builder.ToStringStyle;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import com.comphenix.protocol.events.PacketAdapter;
|
||||
import com.comphenix.protocol.reflect.PrettyPrinter;
|
||||
import com.google.common.primitives.Primitives;
|
||||
|
||||
/**
|
||||
* Internal class used to handle exceptions.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class DetailedErrorReporter implements ErrorReporter {
|
||||
|
||||
public static final String SECOND_LEVEL_PREFIX = " ";
|
||||
public static final String DEFAULT_PREFIX = " ";
|
||||
public static final String DEFAULT_SUPPORT_URL = "http://dev.bukkit.org/server-mods/protocollib/";
|
||||
public static final String PLUGIN_NAME = "ProtocolLib";
|
||||
|
||||
// Users that are informed about errors in the chat
|
||||
public static final String ERROR_PERMISSION = "protocol.info";
|
||||
|
||||
// We don't want to spam the server
|
||||
public static final int DEFAULT_MAX_ERROR_COUNT = 20;
|
||||
|
||||
// Prevent spam per plugin too
|
||||
private ConcurrentMap<String, AtomicInteger> warningCount = new ConcurrentHashMap<String, AtomicInteger>();
|
||||
|
||||
protected String prefix;
|
||||
protected String supportURL;
|
||||
|
||||
protected AtomicInteger internalErrorCount = new AtomicInteger();
|
||||
|
||||
protected int maxErrorCount;
|
||||
protected Logger logger;
|
||||
|
||||
protected WeakReference<Plugin> pluginReference;
|
||||
|
||||
// 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(Plugin plugin) {
|
||||
this(plugin, DEFAULT_PREFIX, DEFAULT_SUPPORT_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a central error reporting system.
|
||||
* @param plugin - the plugin owner.
|
||||
* @param prefix - default line prefix.
|
||||
* @param supportURL - URL to report the error.
|
||||
*/
|
||||
public DetailedErrorReporter(Plugin plugin, String prefix, String supportURL) {
|
||||
this(plugin, prefix, supportURL, DEFAULT_MAX_ERROR_COUNT, getBukkitLogger());
|
||||
}
|
||||
|
||||
// Attempt to get the logger.
|
||||
private static Logger getBukkitLogger() {
|
||||
try {
|
||||
return Bukkit.getLogger();
|
||||
} catch (Throwable e) {
|
||||
return Logger.getLogger("Minecraft");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a central error reporting system.
|
||||
* @param plugin - the plugin owner.
|
||||
* @param prefix - default line prefix.
|
||||
* @param supportURL - URL to report the error.
|
||||
* @param maxErrorCount - number of errors to print before giving up.
|
||||
* @param logger - current logger.
|
||||
*/
|
||||
public DetailedErrorReporter(Plugin plugin, String prefix, String supportURL, int maxErrorCount, Logger logger) {
|
||||
if (plugin == null)
|
||||
throw new IllegalArgumentException("Plugin cannot be NULL.");
|
||||
|
||||
this.pluginReference = new WeakReference<Plugin>(plugin);
|
||||
this.prefix = prefix;
|
||||
this.supportURL = supportURL;
|
||||
this.maxErrorCount = maxErrorCount;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters) {
|
||||
if (reportMinimalNoSpam(sender, methodName, error)) {
|
||||
// Print parameters, if they are given
|
||||
if (parameters != null && parameters.length > 0) {
|
||||
logger.log(Level.SEVERE, " Parameters:");
|
||||
|
||||
// Print each parameter
|
||||
for (Object parameter : parameters) {
|
||||
logger.log(Level.SEVERE, " " + getStringDescription(parameter));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportMinimal(Plugin sender, String methodName, Throwable error) {
|
||||
reportMinimalNoSpam(sender, methodName, error);
|
||||
}
|
||||
|
||||
public boolean reportMinimalNoSpam(Plugin sender, String methodName, Throwable error) {
|
||||
String pluginName = PacketAdapter.getPluginName(sender);
|
||||
AtomicInteger counter = warningCount.get(pluginName);
|
||||
|
||||
// Thread safe pattern
|
||||
if (counter == null) {
|
||||
AtomicInteger created = new AtomicInteger();
|
||||
counter = warningCount.putIfAbsent(pluginName, created);
|
||||
|
||||
if (counter == null) {
|
||||
counter = created;
|
||||
}
|
||||
}
|
||||
|
||||
final int errorCount = counter.incrementAndGet();
|
||||
|
||||
// See if we should print the full error
|
||||
if (errorCount < getMaxErrorCount()) {
|
||||
logger.log(Level.SEVERE, "[" + PLUGIN_NAME + "] Unhandled exception occured in " +
|
||||
methodName + " for " + pluginName, error);
|
||||
return true;
|
||||
|
||||
} else {
|
||||
// Nope - only print the error count occationally
|
||||
if (isPowerOfTwo(errorCount)) {
|
||||
logger.log(Level.SEVERE, "[" + PLUGIN_NAME + "] Unhandled exception number " + errorCount + " occured in " +
|
||||
methodName + " for " + pluginName, error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a given number is a power of two.
|
||||
* <p>
|
||||
* That is, if there exists an N such that 2^N = number.
|
||||
* @param number - the number to check.
|
||||
* @return TRUE if the given number is a power of two, FALSE otherwise.
|
||||
*/
|
||||
private boolean isPowerOfTwo(int number) {
|
||||
return (number & (number - 1)) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportWarning(Object sender, String message) {
|
||||
logger.log(Level.WARNING, "[" + PLUGIN_NAME + "] [" + getSenderName(sender) + "] " + message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportWarning(Object sender, String message, Throwable error) {
|
||||
logger.log(Level.WARNING, "[" + PLUGIN_NAME + "] [" + getSenderName(sender) + "] " + message, error);
|
||||
}
|
||||
|
||||
private String getSenderName(Object sender) {
|
||||
if (sender != null)
|
||||
return sender.getClass().getSimpleName();
|
||||
else
|
||||
return "NULL";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportDetailed(Object sender, String message, Throwable error, Object... parameters) {
|
||||
|
||||
final Plugin plugin = pluginReference.get();
|
||||
final int errorCount = internalErrorCount.incrementAndGet();
|
||||
|
||||
// Do not overtly spam the server!
|
||||
if (errorCount > getMaxErrorCount()) {
|
||||
// Only allow the error count at rare occations
|
||||
if (isPowerOfTwo(errorCount)) {
|
||||
// Permit it - but print the number of exceptions first
|
||||
reportWarning(this, "Internal exception count: " + errorCount + "!");
|
||||
} else {
|
||||
// NEVER SPAM THE CONSOLE
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
StringWriter text = new StringWriter();
|
||||
PrintWriter writer = new PrintWriter(text);
|
||||
|
||||
// Helpful message
|
||||
writer.println("[ProtocolLib] INTERNAL ERROR: " + message);
|
||||
writer.println("If this problem hasn't already been reported, please open a ticket");
|
||||
writer.println("at " + supportURL + " with the following data:");
|
||||
|
||||
// Now, let us print important exception information
|
||||
writer.println(" ===== STACK TRACE =====");
|
||||
|
||||
if (error != null)
|
||||
error.printStackTrace(writer);
|
||||
|
||||
// Data dump!
|
||||
writer.println(" ===== DUMP =====");
|
||||
|
||||
// Relevant parameters
|
||||
if (parameters != null && parameters.length > 0) {
|
||||
writer.println("Parameters:");
|
||||
|
||||
// We *really* want to get as much information as possible
|
||||
for (Object param : parameters) {
|
||||
writer.println(addPrefix(getStringDescription(param), SECOND_LEVEL_PREFIX));
|
||||
}
|
||||
}
|
||||
|
||||
// Global parameters
|
||||
for (String param : globalParameters()) {
|
||||
writer.println(SECOND_LEVEL_PREFIX + param + ":");
|
||||
writer.println(addPrefix(getStringDescription(getGlobalParameter(param)),
|
||||
SECOND_LEVEL_PREFIX + SECOND_LEVEL_PREFIX));
|
||||
}
|
||||
|
||||
// Now, for the sender itself
|
||||
writer.println("Sender:");
|
||||
writer.println(addPrefix(getStringDescription(sender), SECOND_LEVEL_PREFIX));
|
||||
|
||||
// And plugin
|
||||
if (plugin != null) {
|
||||
writer.println("Version:");
|
||||
writer.println(addPrefix(plugin.toString(), SECOND_LEVEL_PREFIX));
|
||||
}
|
||||
|
||||
// Add the server version too
|
||||
if (Bukkit.getServer() != null) {
|
||||
writer.println("Server:");
|
||||
writer.println(addPrefix(Bukkit.getServer().getVersion(), SECOND_LEVEL_PREFIX));
|
||||
|
||||
// Inform of this occurrence
|
||||
if (ERROR_PERMISSION != null) {
|
||||
Bukkit.getServer().broadcast(
|
||||
String.format("Error %s (%s) occured in %s.", message, error, sender),
|
||||
ERROR_PERMISSION
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure it is reported
|
||||
logger.severe(addPrefix(text.toString(), prefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given prefix to every line in the text.
|
||||
* @param text - text to modify.
|
||||
* @param prefix - prefix added to every line in the text.
|
||||
* @return The modified text.
|
||||
*/
|
||||
protected String addPrefix(String text, String prefix) {
|
||||
return text.replaceAll("(?m)^", prefix);
|
||||
}
|
||||
|
||||
protected String getStringDescription(Object value) {
|
||||
|
||||
// We can't only rely on toString.
|
||||
if (value == null) {
|
||||
return "[NULL]";
|
||||
} if (isSimpleType(value)) {
|
||||
return value.toString();
|
||||
} else {
|
||||
try {
|
||||
if (!apacheCommonsMissing)
|
||||
return (ToStringBuilder.reflectionToString(value, ToStringStyle.MULTI_LINE_STYLE, false, null));
|
||||
} catch (Throwable ex) {
|
||||
// Apache is probably missing
|
||||
apacheCommonsMissing = true;
|
||||
}
|
||||
|
||||
// Use our custom object printer instead
|
||||
try {
|
||||
return PrettyPrinter.printObject(value, value.getClass(), Object.class);
|
||||
} catch (IllegalAccessException e) {
|
||||
return "[Error: " + e.getMessage() + "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given object is a wrapper for a primitive/simple type or not.
|
||||
* @param test - the object to test.
|
||||
* @return TRUE if this object is simple enough to simply be printed, FALSE othewise.
|
||||
*/
|
||||
protected boolean isSimpleType(Object test) {
|
||||
return test instanceof String || Primitives.isWrapperType(test.getClass());
|
||||
}
|
||||
|
||||
public int getErrorCount() {
|
||||
return internalErrorCount.get();
|
||||
}
|
||||
|
||||
public void setErrorCount(int errorCount) {
|
||||
internalErrorCount.set(errorCount);
|
||||
}
|
||||
|
||||
public int getMaxErrorCount() {
|
||||
return maxErrorCount;
|
||||
}
|
||||
|
||||
public void setMaxErrorCount(int maxErrorCount) {
|
||||
this.maxErrorCount = maxErrorCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given global parameter. It will be included in every error report.
|
||||
* @param key - name of parameter.
|
||||
* @param value - the global parameter itself.
|
||||
*/
|
||||
public void addGlobalParameter(String key, Object value) {
|
||||
globalParameters.put(key, value);
|
||||
}
|
||||
|
||||
public Object getGlobalParameter(String key) {
|
||||
return globalParameters.get(key);
|
||||
}
|
||||
|
||||
public void clearGlobalParameters() {
|
||||
globalParameters.clear();
|
||||
}
|
||||
|
||||
public Set<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;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.error;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.apache.commons.lang.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang.builder.ToStringStyle;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import com.comphenix.protocol.error.Report.ReportBuilder;
|
||||
import com.comphenix.protocol.events.PacketAdapter;
|
||||
import com.comphenix.protocol.reflect.PrettyPrinter;
|
||||
import com.google.common.primitives.Primitives;
|
||||
|
||||
/**
|
||||
* Internal class used to handle exceptions.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class DetailedErrorReporter implements ErrorReporter {
|
||||
/**
|
||||
* Report format for printing the current exception count.
|
||||
*/
|
||||
public static final ReportType REPORT_EXCEPTION_COUNT = new ReportType("Internal exception count: %s!");
|
||||
|
||||
public static final String SECOND_LEVEL_PREFIX = " ";
|
||||
public static final String DEFAULT_PREFIX = " ";
|
||||
public static final String DEFAULT_SUPPORT_URL = "http://dev.bukkit.org/server-mods/protocollib/";
|
||||
|
||||
// Users that are informed about errors in the chat
|
||||
public static final String ERROR_PERMISSION = "protocol.info";
|
||||
|
||||
// We don't want to spam the server
|
||||
public static final int DEFAULT_MAX_ERROR_COUNT = 20;
|
||||
|
||||
// Prevent spam per plugin too
|
||||
private ConcurrentMap<String, AtomicInteger> warningCount = new ConcurrentHashMap<String, AtomicInteger>();
|
||||
|
||||
protected String prefix;
|
||||
protected String supportURL;
|
||||
|
||||
protected AtomicInteger internalErrorCount = new AtomicInteger();
|
||||
|
||||
protected int maxErrorCount;
|
||||
protected Logger logger;
|
||||
|
||||
protected WeakReference<Plugin> pluginReference;
|
||||
protected String pluginName;
|
||||
|
||||
// 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(Plugin plugin) {
|
||||
this(plugin, DEFAULT_PREFIX, DEFAULT_SUPPORT_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a central error reporting system.
|
||||
* @param plugin - the plugin owner.
|
||||
* @param prefix - default line prefix.
|
||||
* @param supportURL - URL to report the error.
|
||||
*/
|
||||
public DetailedErrorReporter(Plugin plugin, String prefix, String supportURL) {
|
||||
this(plugin, prefix, supportURL, DEFAULT_MAX_ERROR_COUNT, getBukkitLogger());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a central error reporting system.
|
||||
* @param plugin - the plugin owner.
|
||||
* @param prefix - default line prefix.
|
||||
* @param supportURL - URL to report the error.
|
||||
* @param maxErrorCount - number of errors to print before giving up.
|
||||
* @param logger - current logger.
|
||||
*/
|
||||
public DetailedErrorReporter(Plugin plugin, String prefix, String supportURL, int maxErrorCount, Logger logger) {
|
||||
if (plugin == null)
|
||||
throw new IllegalArgumentException("Plugin cannot be NULL.");
|
||||
|
||||
this.pluginReference = new WeakReference<Plugin>(plugin);
|
||||
this.pluginName = plugin.getName();
|
||||
this.prefix = prefix;
|
||||
this.supportURL = supportURL;
|
||||
this.maxErrorCount = maxErrorCount;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
// Attempt to get the logger.
|
||||
private static Logger getBukkitLogger() {
|
||||
try {
|
||||
return Bukkit.getLogger();
|
||||
} catch (Throwable e) {
|
||||
return Logger.getLogger("Minecraft");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters) {
|
||||
if (reportMinimalNoSpam(sender, methodName, error)) {
|
||||
// Print parameters, if they are given
|
||||
if (parameters != null && parameters.length > 0) {
|
||||
logger.log(Level.SEVERE, printParameters(parameters));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportMinimal(Plugin sender, String methodName, Throwable error) {
|
||||
reportMinimalNoSpam(sender, methodName, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a problem with a given method and plugin, ensuring that we don't exceed the maximum number of error reports.
|
||||
* @param sender - the component that observed this exception.
|
||||
* @param methodName - the method name.
|
||||
* @param error - the error itself.
|
||||
* @return TRUE if the error was printed, FALSE if it was suppressed.
|
||||
*/
|
||||
public boolean reportMinimalNoSpam(Plugin sender, String methodName, Throwable error) {
|
||||
String pluginName = PacketAdapter.getPluginName(sender);
|
||||
AtomicInteger counter = warningCount.get(pluginName);
|
||||
|
||||
// Thread safe pattern
|
||||
if (counter == null) {
|
||||
AtomicInteger created = new AtomicInteger();
|
||||
counter = warningCount.putIfAbsent(pluginName, created);
|
||||
|
||||
if (counter == null) {
|
||||
counter = created;
|
||||
}
|
||||
}
|
||||
|
||||
final int errorCount = counter.incrementAndGet();
|
||||
|
||||
// See if we should print the full error
|
||||
if (errorCount < getMaxErrorCount()) {
|
||||
logger.log(Level.SEVERE, "[" + pluginName + "] Unhandled exception occured in " +
|
||||
methodName + " for " + pluginName, error);
|
||||
return true;
|
||||
|
||||
} else {
|
||||
// Nope - only print the error count occationally
|
||||
if (isPowerOfTwo(errorCount)) {
|
||||
logger.log(Level.SEVERE, "[" + pluginName + "] Unhandled exception number " + errorCount + " occured in " +
|
||||
methodName + " for " + pluginName, error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a given number is a power of two.
|
||||
* <p>
|
||||
* That is, if there exists an N such that 2^N = number.
|
||||
* @param number - the number to check.
|
||||
* @return TRUE if the given number is a power of two, FALSE otherwise.
|
||||
*/
|
||||
private boolean isPowerOfTwo(int number) {
|
||||
return (number & (number - 1)) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportWarning(Object sender, ReportBuilder reportBuilder) {
|
||||
if (reportBuilder == null)
|
||||
throw new IllegalArgumentException("reportBuilder cannot be NULL.");
|
||||
|
||||
reportWarning(sender, reportBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportWarning(Object sender, Report report) {
|
||||
String message = "[" + pluginName + "] [" + getSenderName(sender) + "] " + report.getReportMessage();
|
||||
|
||||
// Print the main warning
|
||||
if (report.getException() != null) {
|
||||
logger.log(Level.WARNING, message, report.getException());
|
||||
} else {
|
||||
logger.log(Level.WARNING, message);
|
||||
}
|
||||
|
||||
// Parameters?
|
||||
if (report.hasCallerParameters()) {
|
||||
// Write it
|
||||
logger.log(Level.WARNING, printParameters(report.getCallerParameters()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the name of a sender class.
|
||||
* @param sender - sender object.
|
||||
* @return The name of the sender's class.
|
||||
*/
|
||||
private String getSenderName(Object sender) {
|
||||
if (sender != null)
|
||||
return sender.getClass().getSimpleName();
|
||||
else
|
||||
return "NULL";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportDetailed(Object sender, ReportBuilder reportBuilder) {
|
||||
reportDetailed(sender, reportBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportDetailed(Object sender, Report report) {
|
||||
final Plugin plugin = pluginReference.get();
|
||||
final int errorCount = internalErrorCount.incrementAndGet();
|
||||
|
||||
// Do not overtly spam the server!
|
||||
if (errorCount > getMaxErrorCount()) {
|
||||
// Only allow the error count at rare occations
|
||||
if (isPowerOfTwo(errorCount)) {
|
||||
// Permit it - but print the number of exceptions first
|
||||
reportWarning(this, Report.newBuilder(REPORT_EXCEPTION_COUNT).messageParam(errorCount).build());
|
||||
} else {
|
||||
// NEVER SPAM THE CONSOLE
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
StringWriter text = new StringWriter();
|
||||
PrintWriter writer = new PrintWriter(text);
|
||||
|
||||
// Helpful message
|
||||
writer.println("[" + pluginName + "] INTERNAL ERROR: " + report.getReportMessage());
|
||||
writer.println("If this problem hasn't already been reported, please open a ticket");
|
||||
writer.println("at " + supportURL + " with the following data:");
|
||||
|
||||
// Now, let us print important exception information
|
||||
writer.println(" ===== STACK TRACE =====");
|
||||
|
||||
if (report.getException() != null) {
|
||||
report.getException().printStackTrace(writer);
|
||||
}
|
||||
|
||||
// Data dump!
|
||||
writer.println(" ===== DUMP =====");
|
||||
|
||||
// Relevant parameters
|
||||
if (report.hasCallerParameters()) {
|
||||
printParameters(writer, report.getCallerParameters());
|
||||
}
|
||||
|
||||
// Global parameters
|
||||
for (String param : globalParameters()) {
|
||||
writer.println(SECOND_LEVEL_PREFIX + param + ":");
|
||||
writer.println(addPrefix(getStringDescription(getGlobalParameter(param)),
|
||||
SECOND_LEVEL_PREFIX + SECOND_LEVEL_PREFIX));
|
||||
}
|
||||
|
||||
// Now, for the sender itself
|
||||
writer.println("Sender:");
|
||||
writer.println(addPrefix(getStringDescription(sender), SECOND_LEVEL_PREFIX));
|
||||
|
||||
// And plugin
|
||||
if (plugin != null) {
|
||||
writer.println("Version:");
|
||||
writer.println(addPrefix(plugin.toString(), SECOND_LEVEL_PREFIX));
|
||||
}
|
||||
|
||||
// Add the server version too
|
||||
if (Bukkit.getServer() != null) {
|
||||
writer.println("Server:");
|
||||
writer.println(addPrefix(Bukkit.getServer().getVersion(), SECOND_LEVEL_PREFIX));
|
||||
|
||||
// Inform of this occurrence
|
||||
if (ERROR_PERMISSION != null) {
|
||||
Bukkit.getServer().broadcast(
|
||||
String.format("Error %s (%s) occured in %s.", report.getReportMessage(), report.getException(), sender),
|
||||
ERROR_PERMISSION
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure it is reported
|
||||
logger.severe(addPrefix(text.toString(), prefix));
|
||||
}
|
||||
|
||||
private String printParameters(Object... parameters) {
|
||||
StringWriter writer = new StringWriter();
|
||||
|
||||
// Print and retrieve the string buffer
|
||||
printParameters(new PrintWriter(writer), parameters);
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
private void printParameters(PrintWriter writer, Object[] parameters) {
|
||||
writer.println("Parameters: ");
|
||||
|
||||
// We *really* want to get as much information as possible
|
||||
for (Object param : parameters) {
|
||||
writer.println(addPrefix(getStringDescription(param), SECOND_LEVEL_PREFIX));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given prefix to every line in the text.
|
||||
* @param text - text to modify.
|
||||
* @param prefix - prefix added to every line in the text.
|
||||
* @return The modified text.
|
||||
*/
|
||||
protected String addPrefix(String text, String prefix) {
|
||||
return text.replaceAll("(?m)^", prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a string representation of the given object.
|
||||
* @param value - object to convert.
|
||||
* @return String representation.
|
||||
*/
|
||||
protected String getStringDescription(Object value) {
|
||||
// We can't only rely on toString.
|
||||
if (value == null) {
|
||||
return "[NULL]";
|
||||
} if (isSimpleType(value)) {
|
||||
return value.toString();
|
||||
} else {
|
||||
try {
|
||||
if (!apacheCommonsMissing)
|
||||
return (ToStringBuilder.reflectionToString(value, ToStringStyle.MULTI_LINE_STYLE, false, null));
|
||||
} catch (Throwable ex) {
|
||||
// Apache is probably missing
|
||||
apacheCommonsMissing = true;
|
||||
}
|
||||
|
||||
// Use our custom object printer instead
|
||||
try {
|
||||
return PrettyPrinter.printObject(value, value.getClass(), Object.class);
|
||||
} catch (IllegalAccessException e) {
|
||||
return "[Error: " + e.getMessage() + "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given object is a wrapper for a primitive/simple type or not.
|
||||
* @param test - the object to test.
|
||||
* @return TRUE if this object is simple enough to simply be printed, FALSE othewise.
|
||||
*/
|
||||
protected boolean isSimpleType(Object test) {
|
||||
return test instanceof String || Primitives.isWrapperType(test.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current number of errors printed through {@link #reportDetailed(Object, Report)}.
|
||||
* @return Number of errors printed.
|
||||
*/
|
||||
public int getErrorCount() {
|
||||
return internalErrorCount.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the number of errors printed.
|
||||
* @param errorCount - new number of errors printed.
|
||||
*/
|
||||
public void setErrorCount(int errorCount) {
|
||||
internalErrorCount.set(errorCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the maximum number of errors we can print before we begin suppressing errors.
|
||||
* @return Maximum number of errors.
|
||||
*/
|
||||
public int getMaxErrorCount() {
|
||||
return maxErrorCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum number of errors we can print before we begin suppressing errors.
|
||||
* @param maxErrorCount - new max count.
|
||||
*/
|
||||
public void setMaxErrorCount(int maxErrorCount) {
|
||||
this.maxErrorCount = maxErrorCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given global parameter. It will be included in every error report.
|
||||
* <p>
|
||||
* Both key and value must be non-null.
|
||||
* @param key - name of parameter.
|
||||
* @param value - the global parameter itself.
|
||||
*/
|
||||
public void addGlobalParameter(String key, Object value) {
|
||||
if (key == null)
|
||||
throw new IllegalArgumentException("key cannot be NULL.");
|
||||
if (value == null)
|
||||
throw new IllegalArgumentException("value cannot be NULL.");
|
||||
|
||||
globalParameters.put(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a global parameter by its key.
|
||||
* @param key - key of the parameter to retrieve.
|
||||
* @return The value of the global parameter, or NULL if not found.
|
||||
*/
|
||||
public Object getGlobalParameter(String key) {
|
||||
if (key == null)
|
||||
throw new IllegalArgumentException("key cannot be NULL.");
|
||||
|
||||
return globalParameters.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all global parameters.
|
||||
*/
|
||||
public void clearGlobalParameters() {
|
||||
globalParameters.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a set of every registered global parameter.
|
||||
* @return Set of all registered global parameters.
|
||||
*/
|
||||
public Set<String> globalParameters() {
|
||||
return globalParameters.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the support URL that will be added to all detailed reports.
|
||||
* @return Support URL.
|
||||
*/
|
||||
public String getSupportURL() {
|
||||
return supportURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the support URL that will be added to all detailed reports.
|
||||
* @param supportURL - the new support URL.
|
||||
*/
|
||||
public void setSupportURL(String supportURL) {
|
||||
this.supportURL = supportURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the prefix to apply to every line in the error reports.
|
||||
* @return Error report prefix.
|
||||
*/
|
||||
public String getPrefix() {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the prefix to apply to every line in the error reports.
|
||||
* @param prefix - new prefix.
|
||||
*/
|
||||
public void setPrefix(String prefix) {
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current logger that is used to print all reports.
|
||||
* @return The current logger.
|
||||
*/
|
||||
public Logger getLogger() {
|
||||
return logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current logger that is used to print all reports.
|
||||
* @param logger - new logger.
|
||||
*/
|
||||
public void setLogger(Logger logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
}
|
||||
|
@ -1,65 +1,74 @@
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.error;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
public interface ErrorReporter {
|
||||
|
||||
/**
|
||||
* Prints a small minimal error report about an exception from another plugin.
|
||||
* @param sender - the other plugin.
|
||||
* @param methodName - name of the caller method.
|
||||
* @param error - the exception itself.
|
||||
*/
|
||||
public abstract void reportMinimal(Plugin sender, String methodName, Throwable error);
|
||||
|
||||
/**
|
||||
* Prints a small minimal error report about an exception from another plugin.
|
||||
* @param sender - the other plugin.
|
||||
* @param methodName - name of the caller method.
|
||||
* @param error - the exception itself.
|
||||
* @param parameters - any relevant parameters to print.
|
||||
*/
|
||||
public abstract void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters);
|
||||
|
||||
/**
|
||||
* Prints a warning message from the current plugin.
|
||||
* @param sender - the object containing the caller method.
|
||||
* @param message - error message.
|
||||
*/
|
||||
public abstract void reportWarning(Object sender, String message);
|
||||
|
||||
/**
|
||||
* Prints a warning message from the current plugin.
|
||||
* @param sender - the object containing the caller method.
|
||||
* @param message - error message.
|
||||
* @param error - the exception that was thrown.
|
||||
*/
|
||||
public abstract void reportWarning(Object sender, String message, Throwable error);
|
||||
|
||||
/**
|
||||
* Prints a detailed error report about an unhandled exception.
|
||||
* @param sender - the object containing the caller method.
|
||||
* @param message - an error message to include.
|
||||
* @param error - the exception that was thrown in the caller method.
|
||||
* @param parameters - parameters from the caller method.
|
||||
*/
|
||||
public abstract void reportDetailed(Object sender, String message, Throwable error, Object... parameters);
|
||||
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.error;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import com.comphenix.protocol.error.Report.ReportBuilder;
|
||||
|
||||
/**
|
||||
* Represents an object that can forward an error {@link Report} to the display and permanent storage.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public interface ErrorReporter {
|
||||
/**
|
||||
* Prints a small minimal error report regarding an exception from another plugin.
|
||||
* @param sender - the other plugin.
|
||||
* @param methodName - name of the caller method.
|
||||
* @param error - the exception itself.
|
||||
*/
|
||||
public abstract void reportMinimal(Plugin sender, String methodName, Throwable error);
|
||||
|
||||
/**
|
||||
* Prints a small minimal error report regarding an exception from another plugin.
|
||||
* @param sender - the other plugin.
|
||||
* @param methodName - name of the caller method.
|
||||
* @param error - the exception itself.
|
||||
* @param parameters - any relevant parameters to print.
|
||||
*/
|
||||
public abstract void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters);
|
||||
|
||||
/**
|
||||
* Prints a warning message from the current plugin.
|
||||
* @param sender - the object containing the caller method.
|
||||
* @param report - an error report to include.
|
||||
*/
|
||||
public abstract void reportWarning(Object sender, Report report);
|
||||
|
||||
/**
|
||||
* Prints a warning message from the current plugin.
|
||||
* @param sender - the object containing the caller method.
|
||||
* @param reportBuilder - an error report builder that will be used to get the report.
|
||||
*/
|
||||
public abstract void reportWarning(Object sender, ReportBuilder reportBuilder);
|
||||
|
||||
/**
|
||||
* Prints a detailed error report about an unhandled exception.
|
||||
* @param sender - the object containing the caller method.
|
||||
* @param report - an error report to include.
|
||||
*/
|
||||
public abstract void reportDetailed(Object sender, Report report);
|
||||
|
||||
/**
|
||||
* Prints a detailed error report about an unhandled exception.
|
||||
* @param sender - the object containing the caller method.
|
||||
* @param reportBuilder - an error report builder that will be used to get the report.
|
||||
*/
|
||||
public abstract void reportDetailed(Object sender, ReportBuilder reportBuilder);
|
||||
}
|
162
ProtocolLib/src/main/java/com/comphenix/protocol/error/Report.java
Normale Datei
162
ProtocolLib/src/main/java/com/comphenix/protocol/error/Report.java
Normale Datei
@ -0,0 +1,162 @@
|
||||
package com.comphenix.protocol.error;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Represents a error or warning report.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class Report {
|
||||
private final ReportType type;
|
||||
private final Throwable exception;
|
||||
private final Object[] messageParameters;
|
||||
private final Object[] callerParameters;
|
||||
|
||||
/**
|
||||
* Must be constructed through the factory method in Report.
|
||||
*/
|
||||
public static class ReportBuilder {
|
||||
private ReportType type;
|
||||
private Throwable exception;
|
||||
private Object[] messageParameters;
|
||||
private Object[] callerParameters;
|
||||
|
||||
private ReportBuilder() {
|
||||
// Don't allow
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current report type. Cannot be NULL.
|
||||
* @param type - report type.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public ReportBuilder type(ReportType type) {
|
||||
if (type == null)
|
||||
throw new IllegalArgumentException("Report type cannot be set to NULL.");
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current exception that occured.
|
||||
* @param exception - exception that occured.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public ReportBuilder error(@Nullable Throwable exception) {
|
||||
this.exception = exception;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the message parameters that are used to construct a message text.
|
||||
* @param messageParameters - parameters for the report type.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public ReportBuilder messageParam(@Nullable Object... messageParameters) {
|
||||
this.messageParameters = messageParameters;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the parameters in the caller method. This is optional.
|
||||
* @param callerParameters - parameters of the caller method.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public ReportBuilder callerParam(@Nullable Object... callerParameters) {
|
||||
this.callerParameters = callerParameters;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new report with the provided input.
|
||||
* @return A new report.
|
||||
*/
|
||||
public Report build() {
|
||||
return new Report(type, exception, messageParameters, callerParameters);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new report builder.
|
||||
* @param type - the initial report type.
|
||||
* @return Report builder.
|
||||
*/
|
||||
public static ReportBuilder newBuilder(ReportType type) {
|
||||
return new ReportBuilder().type(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new report with the given type and parameters.
|
||||
* @param exception - exception that occured in the caller method.
|
||||
* @param type - the report type that will be used to construct the message.
|
||||
* @param messageParameters - parameters used to construct the report message.
|
||||
* @param callerParameters - parameters from the caller method.
|
||||
*/
|
||||
protected Report(ReportType type, @Nullable Throwable exception, @Nullable Object[] messageParameters, @Nullable Object[] callerParameters) {
|
||||
if (type == null)
|
||||
throw new IllegalArgumentException("type cannot be NULL.");
|
||||
this.type = type;
|
||||
this.exception = exception;
|
||||
this.messageParameters = messageParameters;
|
||||
this.callerParameters = callerParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the current report type with the provided message parameters.
|
||||
* @return The formated report message.
|
||||
*/
|
||||
public String getReportMessage() {
|
||||
return type.getMessage(messageParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the message parameters that will be used to construc the report message.
|
||||
* <p<
|
||||
* This should not be confused with the method parameters of the caller method.
|
||||
* @return Message parameters.
|
||||
*/
|
||||
public Object[] getMessageParameters() {
|
||||
return messageParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the parameters of the caller method. Optional - may be NULL.
|
||||
* @return Parameters or the caller method.
|
||||
*/
|
||||
public Object[] getCallerParameters() {
|
||||
return callerParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the report type.
|
||||
* @return Report type.
|
||||
*/
|
||||
public ReportType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the associated exception, or NULL if not found.
|
||||
* @return Associated exception, or NULL.
|
||||
*/
|
||||
public Throwable getException() {
|
||||
return exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we have any message parameters.
|
||||
* @return TRUE if there are any message parameters, FALSE otherwise.
|
||||
*/
|
||||
public boolean hasMessageParameters() {
|
||||
return messageParameters != null && messageParameters.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we have any caller parameters.
|
||||
* @return TRUE if there are any caller parameters, FALSE otherwise.
|
||||
*/
|
||||
public boolean hasCallerParameters() {
|
||||
return callerParameters != null && callerParameters.length > 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package com.comphenix.protocol.error;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||
|
||||
/**
|
||||
* Represents a strongly-typed report. Subclasses should be immutable.
|
||||
* <p>
|
||||
* By convention, a report must be declared as a static field publicly accessible from the sender class.
|
||||
* @author Kristian
|
||||
*/
|
||||
public class ReportType {
|
||||
private final String errorFormat;
|
||||
|
||||
/**
|
||||
* Construct a new report type.
|
||||
* @param errorFormat - string used to format the underlying report.
|
||||
*/
|
||||
public ReportType(String errorFormat) {
|
||||
this.errorFormat = errorFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given report to a string, using the provided parameters.
|
||||
* @param parameters - parameters to insert, or NULL to insert nothing.
|
||||
* @return The full report in string format.
|
||||
*/
|
||||
public String getMessage(Object[] parameters) {
|
||||
if (parameters == null || parameters.length == 0)
|
||||
return toString();
|
||||
else
|
||||
return String.format(errorFormat, parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return errorFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all publicly associated reports.
|
||||
* @param clazz - sender class.
|
||||
* @return All associated reports.
|
||||
*/
|
||||
public static ReportType[] getReports(Class<?> clazz) {
|
||||
if (clazz == null)
|
||||
throw new IllegalArgumentException("clazz cannot be NULL.");
|
||||
List<ReportType> result = new ArrayList<ReportType>();
|
||||
|
||||
for (Field field : clazz.getFields()) {
|
||||
if (Modifier.isStatic(field.getModifiers()) &&
|
||||
ReportType.class.isAssignableFrom(field.getDeclaringClass())) {
|
||||
try {
|
||||
result.add((ReportType) field.get(null));
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new FieldAccessException("Unable to access field.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toArray(new ReportType[0]);
|
||||
}
|
||||
}
|
@ -44,6 +44,9 @@ public class PacketEvent extends EventObject implements Cancellable {
|
||||
private AsyncMarker asyncMarker;
|
||||
private boolean asynchronous;
|
||||
|
||||
// Whether or not a packet event is read only
|
||||
private boolean readOnly;
|
||||
|
||||
/**
|
||||
* Use the static constructors to create instances of this event.
|
||||
* @param source - the event source.
|
||||
@ -114,6 +117,8 @@ public class PacketEvent extends EventObject implements Cancellable {
|
||||
* @param packet - the packet that will be sent instead.
|
||||
*/
|
||||
public void setPacket(PacketContainer packet) {
|
||||
if (readOnly)
|
||||
throw new IllegalStateException("The packet event is read-only.");
|
||||
this.packet = packet;
|
||||
}
|
||||
|
||||
@ -147,6 +152,8 @@ public class PacketEvent extends EventObject implements Cancellable {
|
||||
* @param cancel - TRUE if it should be cancelled, FALSE otherwise.
|
||||
*/
|
||||
public void setCancelled(boolean cancel) {
|
||||
if (readOnly)
|
||||
throw new IllegalStateException("The packet event is read-only.");
|
||||
this.cancel = cancel;
|
||||
}
|
||||
|
||||
@ -193,9 +200,34 @@ public class PacketEvent extends EventObject implements Cancellable {
|
||||
public void setAsyncMarker(AsyncMarker asyncMarker) {
|
||||
if (isAsynchronous())
|
||||
throw new IllegalStateException("The marker is immutable for asynchronous events");
|
||||
if (readOnly)
|
||||
throw new IllegalStateException("The packet event is read-only.");
|
||||
this.asyncMarker = asyncMarker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current packet event is read only.
|
||||
* <p>
|
||||
* This is used to ensure that a monitor listener doesn't accidentally alter the state of the event. However,
|
||||
* it is still possible to modify the packet itself, as it would require too many resources to verify its integrity.
|
||||
* <p>
|
||||
* Thus, the packet is considered immutable if the packet event is read only.
|
||||
* @return TRUE if it is, FALSE otherwise.
|
||||
*/
|
||||
public boolean isReadOnly() {
|
||||
return readOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the read-only state of this packet event.
|
||||
* <p>
|
||||
* This will be reset for every packet listener.
|
||||
* @param readOnly - TRUE if it is read-only, FALSE otherwise.
|
||||
*/
|
||||
public void setReadOnly(boolean readOnly) {
|
||||
this.readOnly = readOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the packet event has been executed asynchronously or not.
|
||||
* @return TRUE if this packet event is asynchronous, FALSE otherwise.
|
||||
@ -203,7 +235,7 @@ public class PacketEvent extends EventObject implements Cancellable {
|
||||
public boolean isAsynchronous() {
|
||||
return asynchronous;
|
||||
}
|
||||
|
||||
|
||||
private void writeObject(ObjectOutputStream output) throws IOException {
|
||||
// Default serialization
|
||||
output.defaultWriteObject();
|
||||
|
@ -1,181 +1,213 @@
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.injector;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.comphenix.protocol.ProtocolLibrary;
|
||||
import com.comphenix.protocol.injector.PacketConstructor.Unwrapper;
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.instances.DefaultInstances;
|
||||
import com.google.common.primitives.Primitives;
|
||||
|
||||
/**
|
||||
* Represents an object capable of converting wrapped Bukkit objects into NMS objects.
|
||||
* <p>
|
||||
* Typical conversions include:
|
||||
* <ul>
|
||||
* <li>org.bukkit.entity.Player -> net.minecraft.server.EntityPlayer</li>
|
||||
* <li>org.bukkit.World -> net.minecraft.server.WorldServer</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class BukkitUnwrapper implements Unwrapper {
|
||||
private static Map<Class<?>, Unwrapper> unwrapperCache = new ConcurrentHashMap<Class<?>, Unwrapper>();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object unwrapItem(Object wrappedObject) {
|
||||
// Special case
|
||||
if (wrappedObject == null)
|
||||
return null;
|
||||
Class<?> currentClass = wrappedObject.getClass();
|
||||
|
||||
// Next, check for types that doesn't have a getHandle()
|
||||
if (wrappedObject instanceof Collection) {
|
||||
return handleCollection((Collection<Object>) wrappedObject);
|
||||
} else if (Primitives.isWrapperType(currentClass) || wrappedObject instanceof String) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Unwrapper specificUnwrapper = getSpecificUnwrapper(currentClass);
|
||||
|
||||
// Retrieve the handle
|
||||
if (specificUnwrapper != null)
|
||||
return specificUnwrapper.unwrapItem(wrappedObject);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle a collection of items
|
||||
private Object handleCollection(Collection<Object> wrappedObject) {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Collection<Object> copy = DefaultInstances.DEFAULT.getDefault(wrappedObject.getClass());
|
||||
|
||||
if (copy != null) {
|
||||
// Unwrap every element
|
||||
for (Object element : wrappedObject) {
|
||||
copy.add(unwrapItem(element));
|
||||
}
|
||||
return copy;
|
||||
|
||||
} else {
|
||||
// Impossible
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a cached class unwrapper for the given class.
|
||||
* @param type - the type of the class.
|
||||
* @return An unwrapper for the given class.
|
||||
*/
|
||||
private Unwrapper getSpecificUnwrapper(Class<?> type) {
|
||||
// See if we're already determined this
|
||||
if (unwrapperCache.containsKey(type)) {
|
||||
// We will never remove from the cache, so this ought to be thread safe
|
||||
return unwrapperCache.get(type);
|
||||
}
|
||||
|
||||
try {
|
||||
final Method find = type.getMethod("getHandle");
|
||||
|
||||
// It's thread safe, as getMethod should return the same handle
|
||||
Unwrapper methodUnwrapper = new Unwrapper() {
|
||||
@Override
|
||||
public Object unwrapItem(Object wrappedObject) {
|
||||
|
||||
try {
|
||||
return find.invoke(wrappedObject);
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
ProtocolLibrary.getErrorReporter().reportDetailed(
|
||||
this, "Illegal argument.", e, wrappedObject, find);
|
||||
} catch (IllegalAccessException e) {
|
||||
// Should not occur either
|
||||
return null;
|
||||
} catch (InvocationTargetException e) {
|
||||
// This is really bad
|
||||
throw new RuntimeException("Minecraft error.", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
unwrapperCache.put(type, methodUnwrapper);
|
||||
return methodUnwrapper;
|
||||
|
||||
} catch (SecurityException e) {
|
||||
ProtocolLibrary.getErrorReporter().reportDetailed(this, "Security limitation.", e, type.getName());
|
||||
} catch (NoSuchMethodException e) {
|
||||
// Try getting the field unwrapper too
|
||||
Unwrapper fieldUnwrapper = getFieldUnwrapper(type);
|
||||
|
||||
if (fieldUnwrapper != null)
|
||||
return fieldUnwrapper;
|
||||
else
|
||||
ProtocolLibrary.getErrorReporter().reportDetailed(this, "Cannot find method.", e, type.getName());
|
||||
}
|
||||
|
||||
// Default method
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a cached unwrapper using the handle field.
|
||||
* @param type - a cached field unwrapper.
|
||||
* @return The cached field unwrapper.
|
||||
*/
|
||||
private Unwrapper getFieldUnwrapper(Class<?> type) {
|
||||
final Field find = FieldUtils.getField(type, "handle", true);
|
||||
|
||||
// See if we succeeded
|
||||
if (find != null) {
|
||||
Unwrapper fieldUnwrapper = new Unwrapper() {
|
||||
@Override
|
||||
public Object unwrapItem(Object wrappedObject) {
|
||||
try {
|
||||
return FieldUtils.readField(find, wrappedObject, true);
|
||||
} catch (IllegalAccessException e) {
|
||||
ProtocolLibrary.getErrorReporter().reportDetailed(
|
||||
this, "Cannot read field 'handle'.", e, wrappedObject, find.getName());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
unwrapperCache.put(type, fieldUnwrapper);
|
||||
return fieldUnwrapper;
|
||||
|
||||
} else {
|
||||
// Inform about this too
|
||||
ProtocolLibrary.getErrorReporter().reportDetailed(
|
||||
this, "Could not find field 'handle'.",
|
||||
new Exception("Unable to find 'handle'"), type.getName());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.injector;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.comphenix.protocol.ProtocolLibrary;
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.error.Report;
|
||||
import com.comphenix.protocol.error.ReportType;
|
||||
import com.comphenix.protocol.injector.PacketConstructor.Unwrapper;
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.instances.DefaultInstances;
|
||||
import com.google.common.primitives.Primitives;
|
||||
|
||||
/**
|
||||
* Represents an object capable of converting wrapped Bukkit objects into NMS objects.
|
||||
* <p>
|
||||
* Typical conversions include:
|
||||
* <ul>
|
||||
* <li>org.bukkit.entity.Player -> net.minecraft.server.EntityPlayer</li>
|
||||
* <li>org.bukkit.World -> net.minecraft.server.WorldServer</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class BukkitUnwrapper implements Unwrapper {
|
||||
public static final ReportType REPORT_ILLEGAL_ARGUMENT = new ReportType("Illegal argument.");
|
||||
public static final ReportType REPORT_SECURITY_LIMITATION = new ReportType("Security limitation.");
|
||||
public static final ReportType REPORT_CANNOT_FIND_UNWRAP_METHOD = new ReportType("Cannot find method.");
|
||||
|
||||
public static final ReportType REPORT_CANNOT_READ_FIELD_HANDLE = new ReportType("Cannot read field 'handle'.");
|
||||
|
||||
private static Map<Class<?>, Unwrapper> unwrapperCache = new ConcurrentHashMap<Class<?>, Unwrapper>();
|
||||
|
||||
// The current error reporter
|
||||
private final ErrorReporter reporter;
|
||||
|
||||
/**
|
||||
* Construct a new Bukkit unwrapper with ProtocolLib's default error reporter.
|
||||
*/
|
||||
public BukkitUnwrapper() {
|
||||
this(ProtocolLibrary.getErrorReporter());
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new Bukkit unwrapper with the given error reporter.
|
||||
* @param reporter - the error reporter to use.
|
||||
*/
|
||||
public BukkitUnwrapper(ErrorReporter reporter) {
|
||||
this.reporter = reporter;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object unwrapItem(Object wrappedObject) {
|
||||
// Special case
|
||||
if (wrappedObject == null)
|
||||
return null;
|
||||
Class<?> currentClass = wrappedObject.getClass();
|
||||
|
||||
// Next, check for types that doesn't have a getHandle()
|
||||
if (wrappedObject instanceof Collection) {
|
||||
return handleCollection((Collection<Object>) wrappedObject);
|
||||
} else if (Primitives.isWrapperType(currentClass) || wrappedObject instanceof String) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Unwrapper specificUnwrapper = getSpecificUnwrapper(currentClass);
|
||||
|
||||
// Retrieve the handle
|
||||
if (specificUnwrapper != null)
|
||||
return specificUnwrapper.unwrapItem(wrappedObject);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle a collection of items
|
||||
private Object handleCollection(Collection<Object> wrappedObject) {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Collection<Object> copy = DefaultInstances.DEFAULT.getDefault(wrappedObject.getClass());
|
||||
|
||||
if (copy != null) {
|
||||
// Unwrap every element
|
||||
for (Object element : wrappedObject) {
|
||||
copy.add(unwrapItem(element));
|
||||
}
|
||||
return copy;
|
||||
|
||||
} else {
|
||||
// Impossible
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a cached class unwrapper for the given class.
|
||||
* @param type - the type of the class.
|
||||
* @return An unwrapper for the given class.
|
||||
*/
|
||||
private Unwrapper getSpecificUnwrapper(Class<?> type) {
|
||||
// See if we're already determined this
|
||||
if (unwrapperCache.containsKey(type)) {
|
||||
// We will never remove from the cache, so this ought to be thread safe
|
||||
return unwrapperCache.get(type);
|
||||
}
|
||||
|
||||
try {
|
||||
final Method find = type.getMethod("getHandle");
|
||||
|
||||
// It's thread safe, as getMethod should return the same handle
|
||||
Unwrapper methodUnwrapper = new Unwrapper() {
|
||||
@Override
|
||||
public Object unwrapItem(Object wrappedObject) {
|
||||
|
||||
try {
|
||||
return find.invoke(wrappedObject);
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
reporter.reportDetailed(this,
|
||||
Report.newBuilder(REPORT_ILLEGAL_ARGUMENT).error(e).callerParam(wrappedObject, find)
|
||||
);
|
||||
} catch (IllegalAccessException e) {
|
||||
// Should not occur either
|
||||
return null;
|
||||
} catch (InvocationTargetException e) {
|
||||
// This is really bad
|
||||
throw new RuntimeException("Minecraft error.", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
unwrapperCache.put(type, methodUnwrapper);
|
||||
return methodUnwrapper;
|
||||
|
||||
} catch (SecurityException e) {
|
||||
reporter.reportDetailed(this,
|
||||
Report.newBuilder(REPORT_SECURITY_LIMITATION).error(e).callerParam(type)
|
||||
);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// Try getting the field unwrapper too
|
||||
Unwrapper fieldUnwrapper = getFieldUnwrapper(type);
|
||||
|
||||
if (fieldUnwrapper != null)
|
||||
return fieldUnwrapper;
|
||||
else
|
||||
reporter.reportDetailed(this,
|
||||
Report.newBuilder(REPORT_CANNOT_FIND_UNWRAP_METHOD).error(e).callerParam(type));
|
||||
}
|
||||
|
||||
// Default method
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a cached unwrapper using the handle field.
|
||||
* @param type - a cached field unwrapper.
|
||||
* @return The cached field unwrapper.
|
||||
*/
|
||||
private Unwrapper getFieldUnwrapper(Class<?> type) {
|
||||
final Field find = FieldUtils.getField(type, "handle", true);
|
||||
|
||||
// See if we succeeded
|
||||
if (find != null) {
|
||||
Unwrapper fieldUnwrapper = new Unwrapper() {
|
||||
@Override
|
||||
public Object unwrapItem(Object wrappedObject) {
|
||||
try {
|
||||
return FieldUtils.readField(find, wrappedObject, true);
|
||||
} catch (IllegalAccessException e) {
|
||||
reporter.reportDetailed(this,
|
||||
Report.newBuilder(REPORT_CANNOT_READ_FIELD_HANDLE).error(e).callerParam(wrappedObject, find)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
unwrapperCache.put(type, fieldUnwrapper);
|
||||
return fieldUnwrapper;
|
||||
|
||||
} else {
|
||||
// Inform about this too
|
||||
reporter.reportDetailed(this,
|
||||
Report.newBuilder(REPORT_CANNOT_READ_FIELD_HANDLE).callerParam(find)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,67 +1,68 @@
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.injector;
|
||||
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
|
||||
/**
|
||||
* Represents an object that initiate the packet listeners.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public interface ListenerInvoker {
|
||||
|
||||
/**
|
||||
* Invokes the given packet event for every registered listener.
|
||||
* @param event - the packet event to invoke.
|
||||
*/
|
||||
public abstract void invokePacketRecieving(PacketEvent event);
|
||||
|
||||
/**
|
||||
* Invokes the given packet event for every registered listener.
|
||||
* @param event - the packet event to invoke.
|
||||
*/
|
||||
public abstract void invokePacketSending(PacketEvent event);
|
||||
|
||||
/**
|
||||
* Retrieve the associated ID of a packet.
|
||||
* @param packet - the packet.
|
||||
* @return The packet ID.
|
||||
*/
|
||||
public abstract int getPacketID(Object packet);
|
||||
|
||||
/**
|
||||
* Associate a given class with the given packet ID. Internal method.
|
||||
* @param clazz - class to associate.
|
||||
*/
|
||||
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);
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.injector;
|
||||
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
|
||||
/**
|
||||
* Represents an object that initiate the packet listeners.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public interface ListenerInvoker {
|
||||
|
||||
/**
|
||||
* Invokes the given packet event for every registered listener.
|
||||
* @param event - the packet event to invoke.
|
||||
*/
|
||||
public abstract void invokePacketRecieving(PacketEvent event);
|
||||
|
||||
/**
|
||||
* Invokes the given packet event for every registered listener.
|
||||
* @param event - the packet event to invoke.
|
||||
*/
|
||||
public abstract void invokePacketSending(PacketEvent event);
|
||||
|
||||
/**
|
||||
* Retrieve the associated ID of a packet.
|
||||
* @param packet - the packet.
|
||||
* @return The packet ID.
|
||||
*/
|
||||
public abstract int getPacketID(Object packet);
|
||||
|
||||
/**
|
||||
* Associate a given class with the given packet ID. Internal method.
|
||||
* @param clazz - class to associate.
|
||||
*/
|
||||
public abstract void unregisterPacketClass(Class<?> clazz);
|
||||
|
||||
/**
|
||||
* Register a given class in the packet registry. Internal method.
|
||||
* @param clazz - class to register.
|
||||
* @param packetID - the the new associated packet ID.
|
||||
*/
|
||||
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);
|
||||
}
|
@ -50,6 +50,8 @@ import com.comphenix.protocol.ProtocolManager;
|
||||
import com.comphenix.protocol.async.AsyncFilterManager;
|
||||
import com.comphenix.protocol.async.AsyncMarker;
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.error.Report;
|
||||
import com.comphenix.protocol.error.ReportType;
|
||||
import com.comphenix.protocol.events.*;
|
||||
import com.comphenix.protocol.injector.packet.PacketInjector;
|
||||
import com.comphenix.protocol.injector.packet.PacketInjectorBuilder;
|
||||
@ -61,12 +63,29 @@ import com.comphenix.protocol.injector.spigot.SpigotPacketInjector;
|
||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.utility.MinecraftVersion;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
public final class PacketFilterManager implements ProtocolManager, ListenerInvoker {
|
||||
public static final ReportType REPORT_CANNOT_LOAD_PACKET_LIST = new ReportType("Cannot load server and client packet list.");
|
||||
public static final ReportType REPORT_CANNOT_INITIALIZE_PACKET_INJECTOR = new ReportType("Unable to initialize packet injector");
|
||||
|
||||
public static final ReportType REPORT_PLUGIN_DEPEND_MISSING =
|
||||
new ReportType("%s doesn't depend on ProtocolLib. Check that its plugin.yml has a 'depend' directive.");
|
||||
|
||||
// Registering packet IDs that are not supported
|
||||
public static final ReportType REPORT_UNSUPPORTED_SERVER_PACKET_ID = new ReportType("[%s] Unsupported server packet ID in current Minecraft version: %s");
|
||||
public static final ReportType REPORT_UNSUPPORTED_CLIENT_PACKET_ID = new ReportType("[%s] Unsupported client packet ID in current Minecraft version: %s");
|
||||
|
||||
// Problems injecting and uninjecting players
|
||||
public static final ReportType REPORT_CANNOT_UNINJECT_PLAYER = new ReportType("Unable to uninject net handler for player.");
|
||||
public static final ReportType REPORT_CANNOT_UNINJECT_OFFLINE_PLAYER = new ReportType("Unable to uninject logged off player.");
|
||||
public static final ReportType REPORT_CANNOT_INJECT_PLAYER = new ReportType("Unable to inject player.");
|
||||
|
||||
public static final ReportType REPORT_CANNOT_UNREGISTER_PLUGIN = new ReportType("Unable to handle disabled plugin.");
|
||||
|
||||
/**
|
||||
* Sets the inject hook type. Different types allow for maximum compatibility.
|
||||
* @author Kristian
|
||||
@ -147,11 +166,22 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
||||
// Spigot listener, if in use
|
||||
private SpigotPacketInjector spigotInjector;
|
||||
|
||||
// Plugin verifier
|
||||
private PluginVerifier pluginVerifier;
|
||||
|
||||
/**
|
||||
* Only create instances of this class if protocol lib is disabled.
|
||||
* @param unhookTask
|
||||
*/
|
||||
public PacketFilterManager(ClassLoader classLoader, Server server, DelayedSingleTask unhookTask, ErrorReporter reporter) {
|
||||
public PacketFilterManager(ClassLoader classLoader, Server server, Plugin library, DelayedSingleTask unhookTask, ErrorReporter reporter) {
|
||||
this(classLoader, server, library, new MinecraftVersion(server), unhookTask, reporter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only create instances of this class if protocol lib is disabled.
|
||||
*/
|
||||
public PacketFilterManager(ClassLoader classLoader, Server server, Plugin library,
|
||||
MinecraftVersion mcVersion, DelayedSingleTask unhookTask, ErrorReporter reporter) {
|
||||
|
||||
if (reporter == null)
|
||||
throw new IllegalArgumentException("reporter cannot be NULL.");
|
||||
if (classLoader == null)
|
||||
@ -170,6 +200,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
||||
this.classLoader = classLoader;
|
||||
this.reporter = reporter;
|
||||
|
||||
// The plugin verifier
|
||||
this.pluginVerifier = new PluginVerifier(library);
|
||||
|
||||
// Used to determine if injection is needed
|
||||
Predicate<GamePhase> isInjectionNecessary = new Predicate<GamePhase>() {
|
||||
@Override
|
||||
@ -201,6 +234,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
||||
classLoader(classLoader).
|
||||
packetListeners(packetListeners).
|
||||
injectionFilter(isInjectionNecessary).
|
||||
version(mcVersion).
|
||||
buildHandler();
|
||||
|
||||
this.packetInjector = PacketInjectorBuilder.newBuilder().
|
||||
@ -218,11 +252,11 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
||||
knowsServerPackets = PacketRegistry.getServerPackets() != null;
|
||||
knowsClientPackets = PacketRegistry.getClientPackets() != null;
|
||||
} catch (FieldAccessException e) {
|
||||
reporter.reportWarning(this, "Cannot load server and client packet list.", e);
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_LOAD_PACKET_LIST).error(e));
|
||||
}
|
||||
|
||||
} catch (IllegalAccessException e) {
|
||||
reporter.reportWarning(this, "Unable to initialize packet injector.", e);
|
||||
} catch (FieldAccessException e) {
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_INITIALIZE_PACKET_INJECTOR).error(e));
|
||||
}
|
||||
}
|
||||
|
||||
@ -259,6 +293,20 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
||||
return ImmutableSet.copyOf(packetListeners);
|
||||
}
|
||||
|
||||
/**
|
||||
* Warn of common programming mistakes.
|
||||
* @param plugin - plugin to check.
|
||||
*/
|
||||
private void printPluginWarnings(Plugin plugin) {
|
||||
switch (pluginVerifier.verify(plugin)) {
|
||||
case NO_DEPEND:
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_PLUGIN_DEPEND_MISSING).messageParam(plugin.getName()));
|
||||
case VALID:
|
||||
// Do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPacketListener(PacketListener listener) {
|
||||
if (listener == null)
|
||||
@ -267,6 +315,8 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
||||
// A listener can only be added once
|
||||
if (packetListeners.contains(listener))
|
||||
return;
|
||||
// Check plugin
|
||||
printPluginWarnings(listener.getPlugin());
|
||||
|
||||
ListeningWhitelist sending = listener.getSendingWhitelist();
|
||||
ListeningWhitelist receiving = listener.getReceivingWhitelist();
|
||||
@ -446,6 +496,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
||||
asyncFilterManager.enqueueSyncPacket(event, event.getAsyncMarker());
|
||||
|
||||
// The above makes a copy of the event, so it's safe to cancel it
|
||||
event.setReadOnly(false);
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
@ -478,10 +529,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
||||
if (!knowsServerPackets || PacketRegistry.getServerPackets().contains(packetID))
|
||||
playerInjection.addPacketHandler(packetID);
|
||||
else
|
||||
reporter.reportWarning(this, String.format(
|
||||
"[%s] Unsupported server packet ID in current Minecraft version: %s",
|
||||
PacketAdapter.getPluginName(listener), packetID
|
||||
));
|
||||
reporter.reportWarning(this,
|
||||
Report.newBuilder(REPORT_UNSUPPORTED_SERVER_PACKET_ID).messageParam(PacketAdapter.getPluginName(listener), packetID)
|
||||
);
|
||||
}
|
||||
|
||||
// As above, only for client packets
|
||||
@ -489,10 +539,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
||||
if (!knowsClientPackets || PacketRegistry.getClientPackets().contains(packetID))
|
||||
packetInjector.addPacketHandler(packetID);
|
||||
else
|
||||
reporter.reportWarning(this, String.format(
|
||||
"[%s] Unsupported client packet ID in current Minecraft version: %s",
|
||||
PacketAdapter.getPluginName(listener), packetID
|
||||
));
|
||||
reporter.reportWarning(this,
|
||||
Report.newBuilder(REPORT_UNSUPPORTED_CLIENT_PACKET_ID).messageParam(PacketAdapter.getPluginName(listener), packetID)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -529,6 +578,14 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
||||
if (packetCreation.compareAndSet(false, true))
|
||||
incrementPhases(GamePhase.PLAYING);
|
||||
|
||||
// Inform the MONITOR packets
|
||||
if (!filters) {
|
||||
sendingListeners.invokePacketSending(
|
||||
reporter,
|
||||
PacketEvent.fromServer(this, packet, reciever),
|
||||
ListenerPriority.MONITOR);
|
||||
}
|
||||
|
||||
playerInjection.sendServerPacket(reciever, packet, filters);
|
||||
}
|
||||
|
||||
@ -559,9 +616,16 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
||||
mcPacket = event.getPacket().getHandle();
|
||||
else
|
||||
return;
|
||||
|
||||
} else {
|
||||
// Let the monitors know though
|
||||
recievedListeners.invokePacketSending(
|
||||
reporter,
|
||||
PacketEvent.fromClient(this, packet, sender),
|
||||
ListenerPriority.MONITOR);
|
||||
}
|
||||
|
||||
playerInjection.processPacket(sender, mcPacket);
|
||||
playerInjection.recieveClientPacket(sender, mcPacket);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -673,8 +737,11 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
||||
try {
|
||||
// Let's clean up the other injection first.
|
||||
playerInjection.uninjectPlayer(event.getPlayer().getAddress());
|
||||
playerInjection.updatePlayer(event.getPlayer());
|
||||
} catch (Exception e) {
|
||||
reporter.reportDetailed(PacketFilterManager.this, "Unable to uninject net handler for player.", e, event);
|
||||
reporter.reportDetailed(PacketFilterManager.this,
|
||||
Report.newBuilder(REPORT_CANNOT_UNINJECT_PLAYER).callerParam(event).error(e)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -683,7 +750,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
||||
// This call will be ignored if no listeners are registered
|
||||
playerInjection.injectPlayer(event.getPlayer(), ConflictStrategy.OVERRIDE);
|
||||
} catch (Exception e) {
|
||||
reporter.reportDetailed(PacketFilterManager.this, "Unable to inject player.", e, event);
|
||||
reporter.reportDetailed(PacketFilterManager.this,
|
||||
Report.newBuilder(REPORT_CANNOT_INJECT_PLAYER).callerParam(event).error(e)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -695,7 +764,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
||||
playerInjection.handleDisconnect(player);
|
||||
playerInjection.uninjectPlayer(player);
|
||||
} catch (Exception e) {
|
||||
reporter.reportDetailed(PacketFilterManager.this, "Unable to uninject logged off player.", e, event);
|
||||
reporter.reportDetailed(PacketFilterManager.this,
|
||||
Report.newBuilder(REPORT_CANNOT_UNINJECT_OFFLINE_PLAYER).callerParam(event).error(e)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -706,7 +777,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
||||
removePacketListeners(event.getPlugin());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
reporter.reportDetailed(PacketFilterManager.this, "Unable handle disabled plugin.", e, event);
|
||||
reporter.reportDetailed(PacketFilterManager.this,
|
||||
Report.newBuilder(REPORT_CANNOT_UNREGISTER_PLUGIN).callerParam(event).error(e)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -733,7 +806,14 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
|
||||
if (!MinecraftReflection.isPacketClass(packet))
|
||||
throw new IllegalArgumentException("The given object " + packet + " is not a packet.");
|
||||
|
||||
return PacketRegistry.getPacketToID().get(packet.getClass());
|
||||
Integer id = PacketRegistry.getPacketToID().get(packet.getClass());
|
||||
|
||||
if (id != null) {
|
||||
return id;
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Unable to find associated packet of " + packet + ": Lookup returned NULL.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,226 @@
|
||||
package com.comphenix.protocol.injector;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.PluginLoadOrder;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
/**
|
||||
* Determine if a plugin using ProtocolLib is correct.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
class PluginVerifier {
|
||||
/**
|
||||
* A named plugin cannot be found.
|
||||
* @author Kristian
|
||||
*/
|
||||
public static class PluginNotFoundException extends RuntimeException {
|
||||
/**
|
||||
* Generated by Eclipse.
|
||||
*/
|
||||
private static final long serialVersionUID = 8956699101336877611L;
|
||||
|
||||
public PluginNotFoundException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public PluginNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
public enum VerificationResult {
|
||||
VALID,
|
||||
|
||||
/**
|
||||
* The plugin doesn't depend on ProtocolLib directly or indirectly.
|
||||
*/
|
||||
NO_DEPEND;
|
||||
|
||||
/**
|
||||
* Determine if the verification was valid.
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return this == VALID;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of plugins that have been loaded after ProtocolLib.
|
||||
*/
|
||||
private final Set<String> loadedAfter = new HashSet<String>();
|
||||
|
||||
/**
|
||||
* Reference to ProtocolLib.
|
||||
*/
|
||||
private final Plugin dependency;
|
||||
|
||||
/**
|
||||
* Construct a new plugin verifier.
|
||||
* @param dependency - reference to ProtocolLib, a dependency we require of plugins.
|
||||
*/
|
||||
public PluginVerifier(Plugin dependency) {
|
||||
if (dependency == null)
|
||||
throw new IllegalArgumentException("dependency cannot be NULL.");
|
||||
// This would screw with the assumption in hasDependency(Plugin, Plugin)
|
||||
if (safeConversion(dependency.getDescription().getLoadBefore()).size() > 0)
|
||||
throw new IllegalArgumentException("dependency cannot have a load directives.");
|
||||
|
||||
this.dependency = dependency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a plugin by name.
|
||||
* @param pluginName - the non-null name of the plugin to retrieve.
|
||||
* @return The retrieved plugin.
|
||||
* @throws PluginNotFoundException If a plugin with the given name cannot be found.
|
||||
*/
|
||||
private Plugin getPlugin(String pluginName) {
|
||||
Plugin plugin = getPluginOrDefault(pluginName);
|
||||
|
||||
// Ensure that the plugin exists
|
||||
if (plugin != null)
|
||||
return plugin;
|
||||
else
|
||||
throw new PluginNotFoundException("Cannot find plugin " + pluginName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a plugin by name.
|
||||
* @param pluginName - the non-null name of the plugin to retrieve.
|
||||
* @return The retrieved plugin, or NULL if not found.
|
||||
*/
|
||||
private Plugin getPluginOrDefault(String pluginName) {
|
||||
return Bukkit.getPluginManager().getPlugin(pluginName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs simple verifications on the given plugin.
|
||||
* <p>
|
||||
* Results may be cached.
|
||||
* @param pluginName - the plugin to verify.
|
||||
* @return A verification result.
|
||||
* @throws IllegalArgumentException If plugin name is NULL.
|
||||
* @throws PluginNotFoundException If a plugin with the given name cannot be found.
|
||||
*/
|
||||
public VerificationResult verify(String pluginName) {
|
||||
if (pluginName == null)
|
||||
throw new IllegalArgumentException("pluginName cannot be NULL.");
|
||||
return verify(getPlugin(pluginName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs simple verifications on the given plugin.
|
||||
* <p>
|
||||
* Results may be cached.
|
||||
* @param plugin - the plugin to verify.
|
||||
* @return A verification result.
|
||||
* @throws IllegalArgumentException If plugin name is NULL.
|
||||
* @throws PluginNotFoundException If a plugin with the given name cannot be found.
|
||||
*/
|
||||
public VerificationResult verify(Plugin plugin) {
|
||||
if (plugin == null)
|
||||
throw new IllegalArgumentException("plugin cannot be NULL.");
|
||||
|
||||
// Skip the load order check for ProtocolLib itself
|
||||
if (!dependency.equals(plugin)) {
|
||||
if (!loadedAfter.contains(plugin.getName())) {
|
||||
if (verifyLoadOrder(dependency, plugin)) {
|
||||
// Memorize
|
||||
loadedAfter.add(plugin.getName());
|
||||
} else {
|
||||
return VerificationResult.NO_DEPEND;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Everything seems to be in order
|
||||
return VerificationResult.VALID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a given plugin is guarenteed to be loaded before the other.
|
||||
* <p>
|
||||
* Note that the before plugin is assumed to have no "load" directives - that is, plugins to be
|
||||
* loaded after itself. The after plugin may have "load" directives, but it is irrelevant for our purposes.
|
||||
* @param beforePlugin - the plugin that is loaded first.
|
||||
* @param afterPlugin - the plugin that is loaded last.
|
||||
* @return TRUE if it will, FALSE if it may or must load in the opposite other.
|
||||
*/
|
||||
private boolean verifyLoadOrder(Plugin beforePlugin, Plugin afterPlugin) {
|
||||
// If a plugin has a dependency, it will be loaded after its dependency
|
||||
if (hasDependency(afterPlugin, beforePlugin)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// No dependency - check the load order
|
||||
if (beforePlugin.getDescription().getLoad() == PluginLoadOrder.STARTUP &&
|
||||
afterPlugin.getDescription().getLoad() == PluginLoadOrder.POSTWORLD) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a plugin has a given dependency, either directly or indirectly.
|
||||
* @param plugin - the plugin to check.
|
||||
* @param dependency - the dependency to find.
|
||||
* @return TRUE if the plugin has the given dependency, FALSE otherwise.
|
||||
*/
|
||||
private boolean hasDependency(Plugin plugin, Plugin dependency) {
|
||||
return hasDependency(plugin, dependency, Sets.<String>newHashSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a list to a set.
|
||||
* <p>
|
||||
* A null list will be converted to an empty set.
|
||||
* @param list - the list to convert.
|
||||
* @return The converted list.
|
||||
*/
|
||||
private Set<String> safeConversion(List<String> list) {
|
||||
if (list == null)
|
||||
return Collections.emptySet();
|
||||
else
|
||||
return Sets.newHashSet(list);
|
||||
}
|
||||
|
||||
// Avoid cycles. DFS.
|
||||
private boolean hasDependency(Plugin plugin, Plugin dependency, Set<String> checking) {
|
||||
Set<String> childNames = Sets.union(
|
||||
safeConversion(plugin.getDescription().getDepend()),
|
||||
safeConversion(plugin.getDescription().getSoftDepend())
|
||||
);
|
||||
|
||||
// Ensure that the same plugin isn't processed twice
|
||||
if (!checking.add(plugin.getName())) {
|
||||
throw new IllegalStateException("Cycle detected in dependency graph: " + plugin);
|
||||
}
|
||||
// Look for the dependency in the immediate children
|
||||
if (childNames.contains(dependency.getName())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Recurse through their dependencies
|
||||
for (String childName : childNames) {
|
||||
Plugin childPlugin = getPluginOrDefault(childName);
|
||||
|
||||
if (childPlugin != null && hasDependency(childPlugin, dependency, checking)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Cross edges are permitted
|
||||
checking.remove(plugin.getName());
|
||||
|
||||
// No dependency found!
|
||||
return false;
|
||||
}
|
||||
}
|
@ -19,8 +19,10 @@ package com.comphenix.protocol.injector;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import com.comphenix.protocol.Packets;
|
||||
import com.comphenix.protocol.concurrency.AbstractConcurrentListenerMultimap;
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.events.ListenerPriority;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.comphenix.protocol.events.PacketListener;
|
||||
|
||||
@ -30,6 +32,9 @@ import com.comphenix.protocol.events.PacketListener;
|
||||
* @author Kristian
|
||||
*/
|
||||
public final class SortedPacketListenerList extends AbstractConcurrentListenerMultimap<PacketListener> {
|
||||
public SortedPacketListenerList() {
|
||||
super(Packets.MAXIMUM_PACKET_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the given packet event for every registered listener.
|
||||
@ -45,7 +50,44 @@ public final class SortedPacketListenerList extends AbstractConcurrentListenerMu
|
||||
// The returned list is thread-safe
|
||||
for (PrioritizedListener<PacketListener> element : list) {
|
||||
try {
|
||||
event.setReadOnly(element.getPriority() == ListenerPriority.MONITOR);
|
||||
element.getListener().onPacketReceiving(event);
|
||||
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw e;
|
||||
} catch (ThreadDeath e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
// Minecraft doesn't want your Exception.
|
||||
reporter.reportMinimal(element.getListener().getPlugin(), "onPacketReceiving(PacketEvent)", e,
|
||||
event.getPacket().getHandle());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the given packet event for every registered listener of the given priority.
|
||||
* @param reporter - the error reporter that will be used to inform about listener exceptions.
|
||||
* @param event - the packet event to invoke.
|
||||
* @param priorityFilter - the required priority for a listener to be invoked.
|
||||
*/
|
||||
public void invokePacketRecieving(ErrorReporter reporter, PacketEvent event, ListenerPriority priorityFilter) {
|
||||
Collection<PrioritizedListener<PacketListener>> list = getListener(event.getPacketID());
|
||||
|
||||
if (list == null)
|
||||
return;
|
||||
|
||||
for (PrioritizedListener<PacketListener> element : list) {
|
||||
try {
|
||||
if (element.getPriority() == priorityFilter) {
|
||||
event.setReadOnly(element.getPriority() == ListenerPriority.MONITOR);
|
||||
element.getListener().onPacketReceiving(event);
|
||||
}
|
||||
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw e;
|
||||
} catch (ThreadDeath e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
// Minecraft doesn't want your Exception.
|
||||
reporter.reportMinimal(element.getListener().getPlugin(), "onPacketReceiving(PacketEvent)", e,
|
||||
@ -67,7 +109,13 @@ public final class SortedPacketListenerList extends AbstractConcurrentListenerMu
|
||||
|
||||
for (PrioritizedListener<PacketListener> element : list) {
|
||||
try {
|
||||
event.setReadOnly(element.getPriority() == ListenerPriority.MONITOR);
|
||||
element.getListener().onPacketSending(event);
|
||||
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw e;
|
||||
} catch (ThreadDeath e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
// Minecraft doesn't want your Exception.
|
||||
reporter.reportMinimal(element.getListener().getPlugin(), "onPacketSending(PacketEvent)", e,
|
||||
@ -76,4 +124,34 @@ public final class SortedPacketListenerList extends AbstractConcurrentListenerMu
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the given packet event for every registered listener of the given priority.
|
||||
* @param reporter - the error reporter that will be used to inform about listener exceptions.
|
||||
* @param event - the packet event to invoke.
|
||||
* @param priorityFilter - the required priority for a listener to be invoked.
|
||||
*/
|
||||
public void invokePacketSending(ErrorReporter reporter, PacketEvent event, ListenerPriority priorityFilter) {
|
||||
Collection<PrioritizedListener<PacketListener>> list = getListener(event.getPacketID());
|
||||
|
||||
if (list == null)
|
||||
return;
|
||||
|
||||
for (PrioritizedListener<PacketListener> element : list) {
|
||||
try {
|
||||
if (element.getPriority() == priorityFilter) {
|
||||
event.setReadOnly(element.getPriority() == ListenerPriority.MONITOR);
|
||||
element.getListener().onPacketSending(event);
|
||||
}
|
||||
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw e;
|
||||
} catch (ThreadDeath e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
// Minecraft doesn't want your Exception.
|
||||
reporter.reportMinimal(element.getListener().getPlugin(), "onPacketSending(PacketEvent)", e,
|
||||
event.getPacket().getHandle());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.injector.ListenerInvoker;
|
||||
import com.comphenix.protocol.injector.PacketFilterManager;
|
||||
import com.comphenix.protocol.injector.player.PlayerInjectionHandler;
|
||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
/**
|
||||
@ -100,9 +101,9 @@ public class PacketInjectorBuilder {
|
||||
* <p>
|
||||
* Note that any non-null builder parameters must be set.
|
||||
* @return The created injector.
|
||||
* @throws IllegalAccessException If anything goes wrong in terms of reflection.
|
||||
* @throws FieldAccessException If anything goes wrong in terms of reflection.
|
||||
*/
|
||||
public PacketInjector buildInjector() throws IllegalAccessException {
|
||||
public PacketInjector buildInjector() throws FieldAccessException {
|
||||
initializeDefaults();
|
||||
return new ProxyPacketInjector(classLoader, invoker, playerInjection, reporter);
|
||||
}
|
||||
|
@ -1,228 +1,303 @@
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.injector.packet;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import net.sf.cglib.proxy.Factory;
|
||||
|
||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
/**
|
||||
* Static packet registry in Minecraft.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class PacketRegistry {
|
||||
|
||||
// Fuzzy reflection
|
||||
private static FuzzyReflection packetRegistry;
|
||||
|
||||
// The packet class to packet ID translator
|
||||
private static Map<Class, Integer> packetToID;
|
||||
|
||||
// Whether or not certain packets are sent by the client or the server
|
||||
private static ImmutableSet<Integer> serverPackets;
|
||||
private static ImmutableSet<Integer> clientPackets;
|
||||
|
||||
// The underlying sets
|
||||
private static Set<Integer> serverPacketsRef;
|
||||
private static Set<Integer> clientPacketsRef;
|
||||
|
||||
// New proxy values
|
||||
private static Map<Integer, Class> overwrittenPackets = new HashMap<Integer, Class>();
|
||||
|
||||
// Vanilla packets
|
||||
private static Map<Integer, Class> previousValues = new HashMap<Integer, Class>();
|
||||
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
public static Map<Class, Integer> getPacketToID() {
|
||||
// Initialize it, if we haven't already
|
||||
if (packetToID == null) {
|
||||
try {
|
||||
Field packetsField = getPacketRegistry().getFieldByType("packetsField", Map.class);
|
||||
packetToID = (Map<Class, Integer>) FieldUtils.readStaticField(packetsField, true);
|
||||
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException("Unable to retrieve the packetClassToIdMap", e);
|
||||
}
|
||||
}
|
||||
|
||||
return packetToID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cached fuzzy reflection instance allowing access to the packet registry.
|
||||
* @return Reflected packet registry.
|
||||
*/
|
||||
private static FuzzyReflection getPacketRegistry() {
|
||||
if (packetRegistry == null)
|
||||
packetRegistry = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass(), true);
|
||||
return packetRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the injected proxy classes handlig each packet ID.
|
||||
* @return Injected classes.
|
||||
*/
|
||||
public static Map<Integer, Class> getOverwrittenPackets() {
|
||||
return overwrittenPackets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the vanilla classes handling each packet ID.
|
||||
* @return Vanilla classes.
|
||||
*/
|
||||
public static Map<Integer, Class> getPreviousPackets() {
|
||||
return previousValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve every known and supported server packet.
|
||||
* @return An immutable set of every known server packet.
|
||||
* @throws FieldAccessException If we're unable to retrieve the server packet data from Minecraft.
|
||||
*/
|
||||
public static Set<Integer> getServerPackets() throws FieldAccessException {
|
||||
initializeSets();
|
||||
return serverPackets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve every known and supported client packet.
|
||||
* @return An immutable set of every known client packet.
|
||||
* @throws FieldAccessException If we're unable to retrieve the client packet data from Minecraft.
|
||||
*/
|
||||
public static Set<Integer> getClientPackets() throws FieldAccessException {
|
||||
initializeSets();
|
||||
return clientPackets;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void initializeSets() throws FieldAccessException {
|
||||
if (serverPacketsRef == null || clientPacketsRef == null) {
|
||||
List<Field> sets = getPacketRegistry().getFieldListByType(Set.class);
|
||||
|
||||
try {
|
||||
if (sets.size() > 1) {
|
||||
serverPacketsRef = (Set<Integer>) FieldUtils.readStaticField(sets.get(0), true);
|
||||
clientPacketsRef = (Set<Integer>) FieldUtils.readStaticField(sets.get(1), true);
|
||||
|
||||
// Impossible
|
||||
if (serverPacketsRef == null || clientPacketsRef == null)
|
||||
throw new FieldAccessException("Packet sets are in an illegal state.");
|
||||
|
||||
// NEVER allow callers to modify the underlying sets
|
||||
serverPackets = ImmutableSet.copyOf(serverPacketsRef);
|
||||
clientPackets = ImmutableSet.copyOf(clientPacketsRef);
|
||||
|
||||
} else {
|
||||
throw new FieldAccessException("Cannot retrieve packet client/server sets.");
|
||||
}
|
||||
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new FieldAccessException("Cannot access field.", e);
|
||||
}
|
||||
|
||||
} else {
|
||||
// Copy over again if it has changed
|
||||
if (serverPacketsRef != null && serverPacketsRef.size() != serverPackets.size())
|
||||
serverPackets = ImmutableSet.copyOf(serverPacketsRef);
|
||||
if (clientPacketsRef != null && clientPacketsRef.size() != clientPackets.size())
|
||||
clientPackets = ImmutableSet.copyOf(clientPacketsRef);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the correct packet class from a given packet ID.
|
||||
* @param packetID - the packet ID.
|
||||
* @return The associated class.
|
||||
*/
|
||||
public static Class getPacketClassFromID(int packetID) {
|
||||
return getPacketClassFromID(packetID, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 static Class getPacketClassFromID(int packetID, boolean forceVanilla) {
|
||||
|
||||
Map<Integer, Class> lookup = forceVanilla ? previousValues : overwrittenPackets;
|
||||
|
||||
// Optimized lookup
|
||||
if (lookup.containsKey(packetID)) {
|
||||
return removeEnhancer(lookup.get(packetID), forceVanilla);
|
||||
}
|
||||
|
||||
// Will most likely not be used
|
||||
for (Map.Entry<Class, Integer> entry : getPacketToID().entrySet()) {
|
||||
if (Objects.equal(entry.getValue(), packetID)) {
|
||||
// Attempt to get the vanilla class here too
|
||||
if (!forceVanilla || MinecraftReflection.isMinecraftClass(entry.getKey()))
|
||||
return removeEnhancer(entry.getKey(), forceVanilla);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("The packet ID " + packetID + " is not registered.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the packet ID of a given packet.
|
||||
* @param packet - the type of packet to check.
|
||||
* @return The ID of the given packet.
|
||||
* @throws IllegalArgumentException If this is not a valid packet.
|
||||
*/
|
||||
public static int getPacketID(Class<?> packet) {
|
||||
if (packet == null)
|
||||
throw new IllegalArgumentException("Packet type class cannot be NULL.");
|
||||
if (!MinecraftReflection.getPacketClass().isAssignableFrom(packet))
|
||||
throw new IllegalArgumentException("Type must be a packet.");
|
||||
|
||||
// The registry contains both the overridden and original packets
|
||||
return getPacketToID().get(packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first superclass that is not a CBLib proxy object.
|
||||
* @param clazz - the class whose hierachy we're going to search through.
|
||||
* @param remove - whether or not to skip enhanced (proxy) classes.
|
||||
* @return If remove is TRUE, the first superclass that is not a proxy.
|
||||
*/
|
||||
private static Class removeEnhancer(Class clazz, boolean remove) {
|
||||
if (remove) {
|
||||
// Get the underlying vanilla class
|
||||
while (Factory.class.isAssignableFrom(clazz) && !clazz.equals(Object.class)) {
|
||||
clazz = clazz.getSuperclass();
|
||||
}
|
||||
}
|
||||
|
||||
return clazz;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.injector.packet;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import net.sf.cglib.proxy.Factory;
|
||||
|
||||
import com.comphenix.protocol.ProtocolLibrary;
|
||||
import com.comphenix.protocol.error.Report;
|
||||
import com.comphenix.protocol.error.ReportType;
|
||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.reflect.fuzzy.FuzzyClassContract;
|
||||
import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract;
|
||||
import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.wrappers.TroveWrapper;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
/**
|
||||
* Static packet registry in Minecraft.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class PacketRegistry {
|
||||
public static final ReportType REPORT_CANNOT_CORRECT_TROVE_MAP = new ReportType("Unable to correct no entry value.");
|
||||
|
||||
public static final ReportType REPORT_INSUFFICIENT_SERVER_PACKETS = new ReportType("Too few server packets detected: %s");
|
||||
public static final ReportType REPORT_INSUFFICIENT_CLIENT_PACKETS = new ReportType("Too few client packets detected: %s");
|
||||
|
||||
private static final int MIN_SERVER_PACKETS = 5;
|
||||
private static final int MIN_CLIENT_PACKETS = 5;
|
||||
|
||||
// Fuzzy reflection
|
||||
private static FuzzyReflection packetRegistry;
|
||||
|
||||
// The packet class to packet ID translator
|
||||
private static Map<Class, Integer> packetToID;
|
||||
|
||||
// Whether or not certain packets are sent by the client or the server
|
||||
private static ImmutableSet<Integer> serverPackets;
|
||||
private static ImmutableSet<Integer> clientPackets;
|
||||
|
||||
// The underlying sets
|
||||
private static Set<Integer> serverPacketsRef;
|
||||
private static Set<Integer> clientPacketsRef;
|
||||
|
||||
// New proxy values
|
||||
private static Map<Integer, Class> overwrittenPackets = new HashMap<Integer, Class>();
|
||||
|
||||
// Vanilla packets
|
||||
private static Map<Integer, Class> previousValues = new HashMap<Integer, Class>();
|
||||
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
public static Map<Class, Integer> getPacketToID() {
|
||||
// Initialize it, if we haven't already
|
||||
if (packetToID == null) {
|
||||
try {
|
||||
Field packetsField = getPacketRegistry().getFieldByType("packetsField", Map.class);
|
||||
packetToID = (Map<Class, Integer>) FieldUtils.readStaticField(packetsField, true);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Spigot 1.2.5 MCPC workaround
|
||||
try {
|
||||
packetToID = getSpigotWrapper();
|
||||
} catch (Exception e2) {
|
||||
// Very bad indeed
|
||||
throw new IllegalArgumentException(e.getMessage() + "; Spigot workaround failed.", e2);
|
||||
}
|
||||
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException("Unable to retrieve the packetClassToIdMap", e);
|
||||
}
|
||||
}
|
||||
|
||||
return packetToID;
|
||||
}
|
||||
|
||||
private static Map<Class, Integer> getSpigotWrapper() throws IllegalAccessException {
|
||||
// If it talks like a duck, etc.
|
||||
// Perhaps it would be nice to have a proper duck typing library as well
|
||||
FuzzyClassContract mapLike = FuzzyClassContract.newBuilder().
|
||||
method(FuzzyMethodContract.newBuilder().
|
||||
nameExact("size").returnTypeExact(int.class)).
|
||||
method(FuzzyMethodContract.newBuilder().
|
||||
nameExact("put").parameterCount(2)).
|
||||
method(FuzzyMethodContract.newBuilder().
|
||||
nameExact("get").parameterCount(1)).
|
||||
build();
|
||||
|
||||
Field packetsField = getPacketRegistry().getField(
|
||||
FuzzyFieldContract.newBuilder().typeMatches(mapLike).build());
|
||||
Object troveMap = FieldUtils.readStaticField(packetsField, true);
|
||||
|
||||
// Check for stupid no_entry_values
|
||||
try {
|
||||
Field field = FieldUtils.getField(troveMap.getClass(), "no_entry_value", true);
|
||||
Integer value = (Integer) FieldUtils.readField(field, troveMap, true);
|
||||
|
||||
if (value >= 0 && value < 256) {
|
||||
// Someone forgot to set the no entry value. Let's help them.
|
||||
FieldUtils.writeField(field, troveMap, -1);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Whatever
|
||||
ProtocolLibrary.getErrorReporter().reportWarning(PacketRegistry.class,
|
||||
Report.newBuilder(REPORT_CANNOT_CORRECT_TROVE_MAP).error(e));
|
||||
}
|
||||
|
||||
// We'll assume this a Trove map
|
||||
return TroveWrapper.getDecoratedMap(troveMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cached fuzzy reflection instance allowing access to the packet registry.
|
||||
* @return Reflected packet registry.
|
||||
*/
|
||||
private static FuzzyReflection getPacketRegistry() {
|
||||
if (packetRegistry == null)
|
||||
packetRegistry = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass(), true);
|
||||
return packetRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the injected proxy classes handlig each packet ID.
|
||||
* @return Injected classes.
|
||||
*/
|
||||
public static Map<Integer, Class> getOverwrittenPackets() {
|
||||
return overwrittenPackets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the vanilla classes handling each packet ID.
|
||||
* @return Vanilla classes.
|
||||
*/
|
||||
public static Map<Integer, Class> getPreviousPackets() {
|
||||
return previousValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve every known and supported server packet.
|
||||
* @return An immutable set of every known server packet.
|
||||
* @throws FieldAccessException If we're unable to retrieve the server packet data from Minecraft.
|
||||
*/
|
||||
public static Set<Integer> getServerPackets() throws FieldAccessException {
|
||||
initializeSets();
|
||||
|
||||
// Sanity check. This is impossible!
|
||||
if (serverPackets != null && serverPackets.size() < MIN_SERVER_PACKETS)
|
||||
throw new FieldAccessException("Server packet list is empty. Seems to be unsupported");
|
||||
return serverPackets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve every known and supported client packet.
|
||||
* @return An immutable set of every known client packet.
|
||||
* @throws FieldAccessException If we're unable to retrieve the client packet data from Minecraft.
|
||||
*/
|
||||
public static Set<Integer> getClientPackets() throws FieldAccessException {
|
||||
initializeSets();
|
||||
|
||||
// As above
|
||||
if (clientPackets != null && clientPackets.size() < MIN_CLIENT_PACKETS)
|
||||
throw new FieldAccessException("Client packet list is empty. Seems to be unsupported");
|
||||
return clientPackets;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void initializeSets() throws FieldAccessException {
|
||||
if (serverPacketsRef == null || clientPacketsRef == null) {
|
||||
List<Field> sets = getPacketRegistry().getFieldListByType(Set.class);
|
||||
|
||||
try {
|
||||
if (sets.size() > 1) {
|
||||
serverPacketsRef = (Set<Integer>) FieldUtils.readStaticField(sets.get(0), true);
|
||||
clientPacketsRef = (Set<Integer>) FieldUtils.readStaticField(sets.get(1), true);
|
||||
|
||||
// Impossible
|
||||
if (serverPacketsRef == null || clientPacketsRef == null)
|
||||
throw new FieldAccessException("Packet sets are in an illegal state.");
|
||||
|
||||
// NEVER allow callers to modify the underlying sets
|
||||
serverPackets = ImmutableSet.copyOf(serverPacketsRef);
|
||||
clientPackets = ImmutableSet.copyOf(clientPacketsRef);
|
||||
|
||||
// Check sizes
|
||||
if (serverPackets.size() < MIN_SERVER_PACKETS)
|
||||
ProtocolLibrary.getErrorReporter().reportWarning(
|
||||
PacketRegistry.class, Report.newBuilder(REPORT_INSUFFICIENT_SERVER_PACKETS).messageParam(serverPackets.size())
|
||||
);
|
||||
if (clientPackets.size() < MIN_CLIENT_PACKETS)
|
||||
ProtocolLibrary.getErrorReporter().reportWarning(
|
||||
PacketRegistry.class, Report.newBuilder(REPORT_INSUFFICIENT_CLIENT_PACKETS).messageParam(clientPackets.size())
|
||||
);
|
||||
|
||||
} else {
|
||||
throw new FieldAccessException("Cannot retrieve packet client/server sets.");
|
||||
}
|
||||
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new FieldAccessException("Cannot access field.", e);
|
||||
}
|
||||
|
||||
} else {
|
||||
// Copy over again if it has changed
|
||||
if (serverPacketsRef != null && serverPacketsRef.size() != serverPackets.size())
|
||||
serverPackets = ImmutableSet.copyOf(serverPacketsRef);
|
||||
if (clientPacketsRef != null && clientPacketsRef.size() != clientPackets.size())
|
||||
clientPackets = ImmutableSet.copyOf(clientPacketsRef);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the correct packet class from a given packet ID.
|
||||
* @param packetID - the packet ID.
|
||||
* @return The associated class.
|
||||
*/
|
||||
public static Class getPacketClassFromID(int packetID) {
|
||||
return getPacketClassFromID(packetID, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 static Class getPacketClassFromID(int packetID, boolean forceVanilla) {
|
||||
|
||||
Map<Integer, Class> lookup = forceVanilla ? previousValues : overwrittenPackets;
|
||||
|
||||
// Optimized lookup
|
||||
if (lookup.containsKey(packetID)) {
|
||||
return removeEnhancer(lookup.get(packetID), forceVanilla);
|
||||
}
|
||||
|
||||
// Will most likely not be used
|
||||
for (Map.Entry<Class, Integer> entry : getPacketToID().entrySet()) {
|
||||
if (Objects.equal(entry.getValue(), packetID)) {
|
||||
// Attempt to get the vanilla class here too
|
||||
if (!forceVanilla || MinecraftReflection.isMinecraftClass(entry.getKey()))
|
||||
return removeEnhancer(entry.getKey(), forceVanilla);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("The packet ID " + packetID + " is not registered.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the packet ID of a given packet.
|
||||
* @param packet - the type of packet to check.
|
||||
* @return The ID of the given packet.
|
||||
* @throws IllegalArgumentException If this is not a valid packet.
|
||||
*/
|
||||
public static int getPacketID(Class<?> packet) {
|
||||
if (packet == null)
|
||||
throw new IllegalArgumentException("Packet type class cannot be NULL.");
|
||||
if (!MinecraftReflection.getPacketClass().isAssignableFrom(packet))
|
||||
throw new IllegalArgumentException("Type must be a packet.");
|
||||
|
||||
// The registry contains both the overridden and original packets
|
||||
return getPacketToID().get(packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first superclass that is not a CBLib proxy object.
|
||||
* @param clazz - the class whose hierachy we're going to search through.
|
||||
* @param remove - whether or not to skip enhanced (proxy) classes.
|
||||
* @return If remove is TRUE, the first superclass that is not a proxy.
|
||||
*/
|
||||
private static Class removeEnhancer(Class clazz, boolean remove) {
|
||||
if (remove) {
|
||||
// Get the underlying vanilla class
|
||||
while (Factory.class.isAssignableFrom(clazz) && !clazz.equals(Object.class)) {
|
||||
clazz = clazz.getSuperclass();
|
||||
}
|
||||
}
|
||||
|
||||
return clazz;
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ import com.comphenix.protocol.events.PacketContainer;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.comphenix.protocol.injector.ListenerInvoker;
|
||||
import com.comphenix.protocol.injector.player.PlayerInjectionHandler;
|
||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.reflect.MethodInfo;
|
||||
@ -49,6 +50,85 @@ import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
* @author Kristian
|
||||
*/
|
||||
class ProxyPacketInjector implements PacketInjector {
|
||||
/**
|
||||
* Represents a way to update the packet ID to class lookup table.
|
||||
* @author Kristian
|
||||
*/
|
||||
private static interface PacketClassLookup {
|
||||
public void setLookup(int packetID, Class<?> clazz);
|
||||
}
|
||||
|
||||
private static class IntHashMapLookup implements PacketClassLookup {
|
||||
// The "put" method that associates a packet ID with a packet class
|
||||
private Method putMethod;
|
||||
private Object intHashMap;
|
||||
|
||||
public IntHashMapLookup() throws IllegalAccessException {
|
||||
initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLookup(int packetID, Class<?> clazz) {
|
||||
try {
|
||||
putMethod.invoke(intHashMap, packetID, clazz);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new RuntimeException("Illegal argument.", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException("Cannot access method.", e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException("Exception occured in IntHashMap.put.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void initialize() throws IllegalAccessException {
|
||||
if (intHashMap == null) {
|
||||
// We're looking for the first static field with a Minecraft-object. This should be a IntHashMap.
|
||||
Field intHashMapField = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass(), true).
|
||||
getFieldByType(MinecraftReflection.getMinecraftObjectRegex());
|
||||
|
||||
try {
|
||||
intHashMap = FieldUtils.readField(intHashMapField, (Object) null, true);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new RuntimeException("Minecraft is incompatible.", e);
|
||||
}
|
||||
|
||||
// Now, get the "put" method.
|
||||
putMethod = FuzzyReflection.fromObject(intHashMap).
|
||||
getMethodByParameters("put", int.class, Object.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ArrayLookup implements PacketClassLookup {
|
||||
private Class<?>[] array;
|
||||
|
||||
public ArrayLookup() throws IllegalAccessException {
|
||||
initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLookup(int packetID, Class<?> clazz) {
|
||||
array[packetID] = clazz;
|
||||
}
|
||||
|
||||
private void initialize() throws IllegalAccessException {
|
||||
FuzzyReflection reflection = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass());
|
||||
|
||||
// Is there a Class array with 256 elements instead?
|
||||
for (Field field : reflection.getFieldListByType(Class[].class)) {
|
||||
Class<?>[] test = (Class<?>[]) FieldUtils.readField(field, (Object)null);
|
||||
|
||||
if (test.length == 256) {
|
||||
array = test;
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
"Unable to find an array with the type " + Class[].class +
|
||||
" in " + MinecraftReflection.getPacketClass());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the readPacketData(DataInputStream) method in Packet.
|
||||
*/
|
||||
@ -58,9 +138,7 @@ class ProxyPacketInjector implements PacketInjector {
|
||||
parameterCount(1).
|
||||
build();
|
||||
|
||||
// The "put" method that associates a packet ID with a packet class
|
||||
private static Method putMethod;
|
||||
private static Object intHashMap;
|
||||
private static PacketClassLookup lookup;
|
||||
|
||||
// The packet filter manager
|
||||
private ListenerInvoker manager;
|
||||
@ -78,7 +156,7 @@ class ProxyPacketInjector implements PacketInjector {
|
||||
private CallbackFilter filter;
|
||||
|
||||
public ProxyPacketInjector(ClassLoader classLoader, ListenerInvoker manager,
|
||||
PlayerInjectionHandler playerInjection, ErrorReporter reporter) throws IllegalAccessException {
|
||||
PlayerInjectionHandler playerInjection, ErrorReporter reporter) throws FieldAccessException {
|
||||
|
||||
this.classLoader = classLoader;
|
||||
this.manager = manager;
|
||||
@ -100,20 +178,21 @@ class ProxyPacketInjector implements PacketInjector {
|
||||
}
|
||||
}
|
||||
|
||||
private void initialize() throws IllegalAccessException {
|
||||
if (intHashMap == null) {
|
||||
// We're looking for the first static field with a Minecraft-object. This should be a IntHashMap.
|
||||
Field intHashMapField = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass(), true).
|
||||
getFieldByType(MinecraftReflection.getMinecraftObjectRegex());
|
||||
|
||||
private void initialize() throws FieldAccessException {
|
||||
if (lookup == null) {
|
||||
try {
|
||||
intHashMap = FieldUtils.readField(intHashMapField, (Object) null, true);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new RuntimeException("Minecraft is incompatible.", e);
|
||||
lookup = new IntHashMapLookup();
|
||||
} catch (Exception e1) {
|
||||
|
||||
try {
|
||||
lookup = new ArrayLookup();
|
||||
} catch (Exception e2) {
|
||||
// Wow
|
||||
throw new FieldAccessException(e1.getMessage() + ". Workaround failed too.", e2);
|
||||
}
|
||||
}
|
||||
|
||||
// Now, get the "put" method.
|
||||
putMethod = FuzzyReflection.fromObject(intHashMap).getMethodByParameters("put", int.class, Object.class);
|
||||
// Should work fine now
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,21 +252,12 @@ class ProxyPacketInjector implements PacketInjector {
|
||||
// Add a static reference
|
||||
Enhancer.registerStaticCallbacks(proxy, new Callback[] { NoOp.INSTANCE, modifierReadPacket, modifierRest });
|
||||
|
||||
try {
|
||||
// Override values
|
||||
previous.put(packetID, old);
|
||||
registry.put(proxy, packetID);
|
||||
overwritten.put(packetID, proxy);
|
||||
putMethod.invoke(intHashMap, packetID, proxy);
|
||||
return true;
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new RuntimeException("Illegal argument.", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException("Cannot access method.", e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException("Exception occured in IntHashMap.put.", e);
|
||||
}
|
||||
// Override values
|
||||
previous.put(packetID, old);
|
||||
registry.put(proxy, packetID);
|
||||
overwritten.put(packetID, proxy);
|
||||
lookup.setLookup(packetID, proxy);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -200,25 +270,14 @@ class ProxyPacketInjector implements PacketInjector {
|
||||
Map<Integer, Class> previous = PacketRegistry.getPreviousPackets();
|
||||
Map<Integer, Class> overwritten = PacketRegistry.getOverwrittenPackets();
|
||||
|
||||
// Use the old class definition
|
||||
try {
|
||||
Class old = previous.get(packetID);
|
||||
Class proxy = PacketRegistry.getPacketClassFromID(packetID);
|
||||
|
||||
putMethod.invoke(intHashMap, packetID, old);
|
||||
previous.remove(packetID);
|
||||
registry.remove(proxy);
|
||||
overwritten.remove(packetID);
|
||||
return true;
|
||||
|
||||
// Handle some problems
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException("Cannot access method.", e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException("Exception occured in IntHashMap.put.", e);
|
||||
}
|
||||
Class old = previous.get(packetID);
|
||||
Class proxy = PacketRegistry.getPacketClassFromID(packetID);
|
||||
|
||||
lookup.setLookup(packetID, old);
|
||||
previous.remove(packetID);
|
||||
registry.remove(proxy);
|
||||
overwritten.remove(packetID);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,134 +1,140 @@
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.injector.packet;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.events.PacketContainer;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.google.common.collect.MapMaker;
|
||||
|
||||
import net.sf.cglib.proxy.MethodInterceptor;
|
||||
import net.sf.cglib.proxy.MethodProxy;
|
||||
|
||||
class ReadPacketModifier implements MethodInterceptor {
|
||||
// A cancel marker
|
||||
private static final Object CANCEL_MARKER = new Object();
|
||||
|
||||
// Common for all packets of the same type
|
||||
private ProxyPacketInjector packetInjector;
|
||||
private int packetID;
|
||||
|
||||
// Report errors
|
||||
private ErrorReporter reporter;
|
||||
|
||||
// If this is a read packet data method
|
||||
private boolean isReadPacketDataMethod;
|
||||
|
||||
// Whether or not a packet has been cancelled
|
||||
private static Map<Object, Object> override = new MapMaker().weakKeys().makeMap();
|
||||
|
||||
public ReadPacketModifier(int packetID, ProxyPacketInjector packetInjector, ErrorReporter reporter, boolean isReadPacketDataMethod) {
|
||||
this.packetID = packetID;
|
||||
this.packetInjector = packetInjector;
|
||||
this.reporter = reporter;
|
||||
this.isReadPacketDataMethod = isReadPacketDataMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any packet overrides.
|
||||
* @param packet - the packet to rever
|
||||
*/
|
||||
public static void removeOverride(Object packet) {
|
||||
override.remove(packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the packet that overrides the methods of the given packet.
|
||||
* @param packet - the given packet.
|
||||
* @return Overriden object.
|
||||
*/
|
||||
public static Object getOverride(Object packet) {
|
||||
return override.get(packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given packet has been cancelled before.
|
||||
* @param packet - the packet to check.
|
||||
* @return TRUE if it has been cancelled, FALSE otherwise.
|
||||
*/
|
||||
public static boolean hasCancelled(Object packet) {
|
||||
return getOverride(packet) == CANCEL_MARKER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object intercept(Object thisObj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
|
||||
// Atomic retrieval
|
||||
Object overridenObject = override.get(thisObj);
|
||||
Object returnValue = null;
|
||||
|
||||
if (overridenObject != null) {
|
||||
// This packet has been cancelled
|
||||
if (overridenObject == CANCEL_MARKER) {
|
||||
// So, cancel all void methods
|
||||
if (method.getReturnType().equals(Void.TYPE))
|
||||
return null;
|
||||
else // Revert to normal for everything else
|
||||
overridenObject = thisObj;
|
||||
}
|
||||
|
||||
returnValue = proxy.invokeSuper(overridenObject, args);
|
||||
} else {
|
||||
returnValue = proxy.invokeSuper(thisObj, args);
|
||||
}
|
||||
|
||||
// Is this a readPacketData method?
|
||||
if (isReadPacketDataMethod) {
|
||||
try {
|
||||
// We need this in order to get the correct player
|
||||
DataInputStream input = (DataInputStream) args[0];
|
||||
|
||||
// Let the people know
|
||||
PacketContainer container = new PacketContainer(packetID, thisObj);
|
||||
PacketEvent event = packetInjector.packetRecieved(container, input);
|
||||
|
||||
// Handle override
|
||||
if (event != null) {
|
||||
Object result = event.getPacket().getHandle();
|
||||
|
||||
if (event.isCancelled()) {
|
||||
override.put(thisObj, CANCEL_MARKER);
|
||||
} else if (!objectEquals(thisObj, result)) {
|
||||
override.put(thisObj, result);
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
// Minecraft cannot handle this error
|
||||
reporter.reportDetailed(this, "Cannot handle client packet.", e, args[0]);
|
||||
}
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
private boolean objectEquals(Object a, Object b) {
|
||||
return System.identityHashCode(a) != System.identityHashCode(b);
|
||||
}
|
||||
}
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.injector.packet;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.error.Report;
|
||||
import com.comphenix.protocol.error.ReportType;
|
||||
import com.comphenix.protocol.events.PacketContainer;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.google.common.collect.MapMaker;
|
||||
|
||||
import net.sf.cglib.proxy.MethodInterceptor;
|
||||
import net.sf.cglib.proxy.MethodProxy;
|
||||
|
||||
class ReadPacketModifier implements MethodInterceptor {
|
||||
public static final ReportType REPORT_CANNOT_HANDLE_CLIENT_PACKET = new ReportType("Cannot handle client packet.");
|
||||
|
||||
// A cancel marker
|
||||
private static final Object CANCEL_MARKER = new Object();
|
||||
|
||||
// Common for all packets of the same type
|
||||
private ProxyPacketInjector packetInjector;
|
||||
private int packetID;
|
||||
|
||||
// Report errors
|
||||
private ErrorReporter reporter;
|
||||
|
||||
// If this is a read packet data method
|
||||
private boolean isReadPacketDataMethod;
|
||||
|
||||
// Whether or not a packet has been cancelled
|
||||
private static Map<Object, Object> override = new MapMaker().weakKeys().makeMap();
|
||||
|
||||
public ReadPacketModifier(int packetID, ProxyPacketInjector packetInjector, ErrorReporter reporter, boolean isReadPacketDataMethod) {
|
||||
this.packetID = packetID;
|
||||
this.packetInjector = packetInjector;
|
||||
this.reporter = reporter;
|
||||
this.isReadPacketDataMethod = isReadPacketDataMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any packet overrides.
|
||||
* @param packet - the packet to rever
|
||||
*/
|
||||
public static void removeOverride(Object packet) {
|
||||
override.remove(packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the packet that overrides the methods of the given packet.
|
||||
* @param packet - the given packet.
|
||||
* @return Overriden object.
|
||||
*/
|
||||
public static Object getOverride(Object packet) {
|
||||
return override.get(packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given packet has been cancelled before.
|
||||
* @param packet - the packet to check.
|
||||
* @return TRUE if it has been cancelled, FALSE otherwise.
|
||||
*/
|
||||
public static boolean hasCancelled(Object packet) {
|
||||
return getOverride(packet) == CANCEL_MARKER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object intercept(Object thisObj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
|
||||
// Atomic retrieval
|
||||
Object overridenObject = override.get(thisObj);
|
||||
Object returnValue = null;
|
||||
|
||||
if (overridenObject != null) {
|
||||
// This packet has been cancelled
|
||||
if (overridenObject == CANCEL_MARKER) {
|
||||
// So, cancel all void methods
|
||||
if (method.getReturnType().equals(Void.TYPE))
|
||||
return null;
|
||||
else // Revert to normal for everything else
|
||||
overridenObject = thisObj;
|
||||
}
|
||||
|
||||
returnValue = proxy.invokeSuper(overridenObject, args);
|
||||
} else {
|
||||
returnValue = proxy.invokeSuper(thisObj, args);
|
||||
}
|
||||
|
||||
// Is this a readPacketData method?
|
||||
if (isReadPacketDataMethod) {
|
||||
try {
|
||||
// We need this in order to get the correct player
|
||||
DataInputStream input = (DataInputStream) args[0];
|
||||
|
||||
// Let the people know
|
||||
PacketContainer container = new PacketContainer(packetID, thisObj);
|
||||
PacketEvent event = packetInjector.packetRecieved(container, input);
|
||||
|
||||
// Handle override
|
||||
if (event != null) {
|
||||
Object result = event.getPacket().getHandle();
|
||||
|
||||
if (event.isCancelled()) {
|
||||
override.put(thisObj, CANCEL_MARKER);
|
||||
} else if (!objectEquals(thisObj, result)) {
|
||||
override.put(thisObj, result);
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
// Minecraft cannot handle this error
|
||||
reporter.reportDetailed(this,
|
||||
Report.newBuilder(REPORT_CANNOT_HANDLE_CLIENT_PACKET).callerParam(args[0]).error(e)
|
||||
);
|
||||
}
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
private boolean objectEquals(Object a, Object b) {
|
||||
return System.identityHashCode(a) != System.identityHashCode(b);
|
||||
}
|
||||
}
|
||||
|
@ -21,11 +21,15 @@ import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import com.comphenix.protocol.ProtocolLibrary;
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.error.Report;
|
||||
import com.comphenix.protocol.error.ReportType;
|
||||
import com.comphenix.protocol.injector.ListenerInvoker;
|
||||
import com.comphenix.protocol.injector.player.NetworkFieldInjector.FakePacket;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.google.common.collect.MapMaker;
|
||||
|
||||
import net.sf.cglib.proxy.Callback;
|
||||
import net.sf.cglib.proxy.Enhancer;
|
||||
@ -38,12 +42,16 @@ import net.sf.cglib.proxy.MethodProxy;
|
||||
* @author Kristian
|
||||
*/
|
||||
class InjectedArrayList extends ArrayList<Object> {
|
||||
|
||||
public static final ReportType REPORT_CANNOT_REVERT_CANCELLED_PACKET = new ReportType("Reverting cancelled packet failed.");
|
||||
|
||||
/**
|
||||
* Silly Eclipse.
|
||||
*/
|
||||
private static final long serialVersionUID = -1173865905404280990L;
|
||||
|
||||
// Fake inverted proxy objects
|
||||
private static ConcurrentMap<Object, Object> delegateLookup = new MapMaker().weakKeys().makeMap();
|
||||
|
||||
private transient PlayerInjector injector;
|
||||
private transient Set<Object> ignoredPackets;
|
||||
private transient ClassLoader classLoader;
|
||||
@ -85,15 +93,10 @@ class InjectedArrayList extends ArrayList<Object> {
|
||||
return true;
|
||||
|
||||
} catch (InvocationTargetException e) {
|
||||
ErrorReporter reporter = ProtocolLibrary.getErrorReporter();
|
||||
|
||||
// Prefer to report this to the user, instead of risking sending it to Minecraft
|
||||
if (reporter != null) {
|
||||
reporter.reportDetailed(this, "Reverting cancelled packet failed.", e, packet);
|
||||
} else {
|
||||
System.out.println("[ProtocolLib] Reverting cancelled packet failed.");
|
||||
e.printStackTrace();
|
||||
}
|
||||
ProtocolLibrary.getErrorReporter().reportDetailed(this,
|
||||
Report.newBuilder(REPORT_CANNOT_REVERT_CANCELLED_PACKET).error(e).callerParam(packet)
|
||||
);
|
||||
|
||||
// Failure
|
||||
return false;
|
||||
@ -111,10 +114,7 @@ class InjectedArrayList extends ArrayList<Object> {
|
||||
ListenerInvoker invoker = injector.getInvoker();
|
||||
|
||||
int packetID = invoker.getPacketID(source);
|
||||
Class<?> type = invoker.getPacketClassFromID(packetID, true);
|
||||
|
||||
System.out.println(type.getName());
|
||||
|
||||
// We want to subtract the byte amount that were added to the running
|
||||
// total of outstanding packets. Otherwise, cancelling too many packets
|
||||
// might cause a "disconnect.overflow" error.
|
||||
@ -134,7 +134,7 @@ class InjectedArrayList extends ArrayList<Object> {
|
||||
// ect.
|
||||
// }
|
||||
Enhancer ex = new Enhancer();
|
||||
ex.setSuperclass(type);
|
||||
ex.setSuperclass(MinecraftReflection.getPacketClass());
|
||||
ex.setInterfaces(new Class[] { FakePacket.class } );
|
||||
ex.setUseCache(true);
|
||||
ex.setClassLoader(classLoader);
|
||||
@ -146,7 +146,10 @@ class InjectedArrayList extends ArrayList<Object> {
|
||||
try {
|
||||
// Temporarily associate the fake packet class
|
||||
invoker.registerPacketClass(proxyClass, packetID);
|
||||
return proxyClass.newInstance();
|
||||
Object proxy = proxyClass.newInstance();
|
||||
|
||||
InjectedArrayList.registerDelegate(proxy, source);
|
||||
return proxy;
|
||||
|
||||
} catch (Exception e) {
|
||||
// Don't pollute the throws tree
|
||||
@ -157,18 +160,33 @@ class InjectedArrayList extends ArrayList<Object> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the inverted integer proxy uses the given object as source.
|
||||
* @param proxy - inverted integer proxy.
|
||||
* @param source - source object.
|
||||
*/
|
||||
private static void registerDelegate(Object proxy, Object source) {
|
||||
delegateLookup.put(proxy, source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverts the integer result of every integer method.
|
||||
* @author Kristian
|
||||
*/
|
||||
private class InvertedIntegerCallback implements MethodInterceptor {
|
||||
private class InvertedIntegerCallback implements MethodInterceptor {
|
||||
@Override
|
||||
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
|
||||
final Object delegate = delegateLookup.get(obj);
|
||||
|
||||
if (delegate == null) {
|
||||
throw new IllegalStateException("Unable to find delegate source for " + obj);
|
||||
}
|
||||
|
||||
if (method.getReturnType().equals(int.class) && args.length == 0) {
|
||||
Integer result = (Integer) proxy.invokeSuper(obj, args);
|
||||
Integer result = (Integer) proxy.invoke(delegate, args);
|
||||
return -result;
|
||||
} else {
|
||||
return proxy.invokeSuper(obj, args);
|
||||
return proxy.invoke(delegate, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,318 +1,338 @@
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.injector.player;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import net.sf.cglib.proxy.Factory;
|
||||
|
||||
import org.bukkit.Server;
|
||||
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.injector.server.AbstractInputStreamLookup;
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.reflect.ObjectWriter;
|
||||
import com.comphenix.protocol.reflect.VolatileField;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
|
||||
/**
|
||||
* Used to ensure that the 1.3 server is referencing the correct server handler.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
class InjectedServerConnection {
|
||||
|
||||
private static Field listenerThreadField;
|
||||
private static Field minecraftServerField;
|
||||
private static Field listField;
|
||||
private static Field dedicatedThreadField;
|
||||
|
||||
private static Method serverConnectionMethod;
|
||||
|
||||
private List<VolatileField> listFields;
|
||||
private List<ReplacedArrayList<Object>> replacedLists;
|
||||
|
||||
// Used to inject net handlers
|
||||
private NetLoginInjector netLoginInjector;
|
||||
|
||||
// Inject server connections
|
||||
private AbstractInputStreamLookup socketInjector;
|
||||
|
||||
private Server server;
|
||||
private ErrorReporter reporter;
|
||||
private boolean hasAttempted;
|
||||
private boolean hasSuccess;
|
||||
|
||||
private Object minecraftServer = null;
|
||||
|
||||
public InjectedServerConnection(ErrorReporter reporter, AbstractInputStreamLookup socketInjector, Server server, NetLoginInjector netLoginInjector) {
|
||||
this.listFields = new ArrayList<VolatileField>();
|
||||
this.replacedLists = new ArrayList<ReplacedArrayList<Object>>();
|
||||
this.reporter = reporter;
|
||||
this.server = server;
|
||||
this.socketInjector = socketInjector;
|
||||
this.netLoginInjector = netLoginInjector;
|
||||
}
|
||||
|
||||
public void injectList() {
|
||||
|
||||
// Only execute this method once
|
||||
if (!hasAttempted)
|
||||
hasAttempted = true;
|
||||
else
|
||||
return;
|
||||
|
||||
if (minecraftServerField == null)
|
||||
minecraftServerField = FuzzyReflection.fromObject(server, true).
|
||||
getFieldByType("MinecraftServer", MinecraftReflection.getMinecraftServerClass());
|
||||
|
||||
try {
|
||||
minecraftServer = FieldUtils.readField(minecraftServerField, server, true);
|
||||
} catch (IllegalAccessException e1) {
|
||||
reporter.reportWarning(this, "Cannot extract minecraft server from Bukkit.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (serverConnectionMethod == null)
|
||||
serverConnectionMethod = FuzzyReflection.fromClass(minecraftServerField.getType()).
|
||||
getMethodByParameters("getServerConnection",
|
||||
MinecraftReflection.getServerConnectionClass(), new Class[] {});
|
||||
// We're using Minecraft 1.3.1
|
||||
injectServerConnection();
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
|
||||
// Minecraft 1.2.5 or lower
|
||||
injectListenerThread();
|
||||
|
||||
} catch (Exception e) {
|
||||
// Oh damn - inform the player
|
||||
reporter.reportDetailed(this, "Cannot inject into server connection. Bad things will happen.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void injectListenerThread() {
|
||||
try {
|
||||
if (listenerThreadField == null)
|
||||
listenerThreadField = FuzzyReflection.fromObject(minecraftServer).
|
||||
getFieldByType("networkListenThread", MinecraftReflection.getNetworkListenThreadClass());
|
||||
} catch (RuntimeException e) {
|
||||
reporter.reportDetailed(this, "Cannot find listener thread in MinecraftServer.", e, minecraftServer);
|
||||
return;
|
||||
}
|
||||
|
||||
Object listenerThread = null;
|
||||
|
||||
// Attempt to get the thread
|
||||
try {
|
||||
listenerThread = listenerThreadField.get(minecraftServer);
|
||||
} catch (Exception e) {
|
||||
reporter.reportWarning(this, "Unable to read the listener thread.", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Inject the server socket too
|
||||
injectServerSocket(listenerThread);
|
||||
|
||||
// Just inject every list field we can get
|
||||
injectEveryListField(listenerThread, 1);
|
||||
hasSuccess = true;
|
||||
}
|
||||
|
||||
private void injectServerConnection() {
|
||||
|
||||
Object serverConnection = null;
|
||||
|
||||
// Careful - we might fail
|
||||
try {
|
||||
serverConnection = serverConnectionMethod.invoke(minecraftServer);
|
||||
} catch (Exception ex) {
|
||||
reporter.reportDetailed(this, "Unable to retrieve server connection", ex, minecraftServer);
|
||||
return;
|
||||
}
|
||||
|
||||
if (listField == null)
|
||||
listField = FuzzyReflection.fromClass(serverConnectionMethod.getReturnType(), true).
|
||||
getFieldByType("netServerHandlerList", List.class);
|
||||
if (dedicatedThreadField == null) {
|
||||
List<Field> matches = FuzzyReflection.fromObject(serverConnection, true).
|
||||
getFieldListByType(Thread.class);
|
||||
|
||||
// Verify the field count
|
||||
if (matches.size() != 1)
|
||||
reporter.reportWarning(this, "Unexpected number of threads in " + serverConnection.getClass().getName());
|
||||
else
|
||||
dedicatedThreadField = matches.get(0);
|
||||
}
|
||||
|
||||
// Next, try to get the dedicated thread
|
||||
try {
|
||||
if (dedicatedThreadField != null) {
|
||||
Object dedicatedThread = FieldUtils.readField(dedicatedThreadField, serverConnection, true);
|
||||
|
||||
// Inject server socket and NetServerHandlers.
|
||||
injectServerSocket(dedicatedThread);
|
||||
injectEveryListField(dedicatedThread, 1);
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
reporter.reportWarning(this, "Unable to retrieve net handler thread.", e);
|
||||
}
|
||||
|
||||
injectIntoList(serverConnection, listField);
|
||||
hasSuccess = true;
|
||||
}
|
||||
|
||||
private void injectServerSocket(Object container) {
|
||||
socketInjector.inject(container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically inject into every List-compatible public or private field of the given object.
|
||||
* @param container - container object with the fields to inject.
|
||||
* @param minimum - the minimum number of fields we expect exists.
|
||||
*/
|
||||
private void injectEveryListField(Object container, int minimum) {
|
||||
// Ok, great. Get every list field
|
||||
List<Field> lists = FuzzyReflection.fromObject(container, true).getFieldListByType(List.class);
|
||||
|
||||
for (Field list : lists) {
|
||||
injectIntoList(container, list);
|
||||
}
|
||||
|
||||
// Warn about unexpected errors
|
||||
if (lists.size() < minimum) {
|
||||
reporter.reportWarning(this, "Unable to inject " + minimum + " lists in " + container.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void injectIntoList(Object instance, Field field) {
|
||||
VolatileField listFieldRef = new VolatileField(field, instance, true);
|
||||
List<Object> list = (List<Object>) listFieldRef.getValue();
|
||||
|
||||
// Careful not to inject twice
|
||||
if (list instanceof ReplacedArrayList) {
|
||||
replacedLists.add((ReplacedArrayList<Object>) list);
|
||||
} else {
|
||||
ReplacedArrayList<Object> injectedList = createReplacement(list);
|
||||
|
||||
replacedLists.add(injectedList);
|
||||
listFieldRef.setValue(injectedList);
|
||||
listFields.add(listFieldRef);
|
||||
}
|
||||
}
|
||||
|
||||
// Hack to avoid the "moved to quickly" error
|
||||
private ReplacedArrayList<Object> createReplacement(List<Object> list) {
|
||||
return new ReplacedArrayList<Object>(list) {
|
||||
/**
|
||||
* Shut up Eclipse!
|
||||
*/
|
||||
private static final long serialVersionUID = 2070481080950500367L;
|
||||
|
||||
// Object writer we'll use
|
||||
private final ObjectWriter writer = new ObjectWriter();
|
||||
|
||||
@Override
|
||||
protected void onReplacing(Object inserting, Object replacement) {
|
||||
// Is this a normal Minecraft object?
|
||||
if (!(inserting instanceof Factory)) {
|
||||
// If so, copy the content of the old element to the new
|
||||
try {
|
||||
writer.copyTo(inserting, replacement, inserting.getClass());
|
||||
} catch (Throwable e) {
|
||||
reporter.reportDetailed(InjectedServerConnection.this, "Cannot copy old " + inserting +
|
||||
" to new.", e, inserting, replacement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onInserting(Object inserting) {
|
||||
// Ready for some login handler injection?
|
||||
if (MinecraftReflection.isLoginHandler(inserting)) {
|
||||
Object replaced = netLoginInjector.onNetLoginCreated(inserting);
|
||||
|
||||
// Only replace if it has changed
|
||||
if (inserting != replaced)
|
||||
addMapping(inserting, replaced, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRemoved(Object removing) {
|
||||
// Clean up?
|
||||
if (MinecraftReflection.isLoginHandler(removing)) {
|
||||
netLoginInjector.cleanup(removing);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the server handler instance kept by the "keep alive" object.
|
||||
* @param oldHandler - old server handler.
|
||||
* @param newHandler - new, proxied server handler.
|
||||
*/
|
||||
public void replaceServerHandler(Object oldHandler, Object newHandler) {
|
||||
if (!hasAttempted) {
|
||||
injectList();
|
||||
}
|
||||
|
||||
if (hasSuccess) {
|
||||
for (ReplacedArrayList<Object> replacedList : replacedLists) {
|
||||
replacedList.addMapping(oldHandler, newHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert to the old vanilla server handler, if it has been replaced.
|
||||
* @param oldHandler - old vanilla server handler.
|
||||
*/
|
||||
public void revertServerHandler(Object oldHandler) {
|
||||
if (hasSuccess) {
|
||||
for (ReplacedArrayList<Object> replacedList : replacedLists) {
|
||||
replacedList.removeMapping(oldHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Undoes everything.
|
||||
*/
|
||||
public void cleanupAll() {
|
||||
if (replacedLists.size() > 0) {
|
||||
// Repair the underlying lists
|
||||
for (ReplacedArrayList<Object> replacedList : replacedLists) {
|
||||
replacedList.revertAll();
|
||||
}
|
||||
for (VolatileField field : listFields) {
|
||||
field.revertValue();
|
||||
}
|
||||
|
||||
listFields.clear();
|
||||
replacedLists.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.injector.player;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import net.sf.cglib.proxy.Factory;
|
||||
|
||||
import org.bukkit.Server;
|
||||
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.error.Report;
|
||||
import com.comphenix.protocol.error.ReportType;
|
||||
import com.comphenix.protocol.injector.server.AbstractInputStreamLookup;
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.reflect.ObjectWriter;
|
||||
import com.comphenix.protocol.reflect.VolatileField;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
|
||||
/**
|
||||
* Used to ensure that the 1.3 server is referencing the correct server handler.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
class InjectedServerConnection {
|
||||
// A number of things can go wrong ...
|
||||
public static final ReportType REPORT_CANNOT_FIND_MINECRAFT_SERVER = new ReportType("Cannot extract minecraft server from Bukkit.");
|
||||
public static final ReportType REPORT_CANNOT_INJECT_SERVER_CONNECTION = new ReportType("Cannot inject into server connection. Bad things will happen.");
|
||||
|
||||
public static final ReportType REPORT_CANNOT_FIND_LISTENER_THREAD = new ReportType("Cannot find listener thread in MinecraftServer.");
|
||||
public static final ReportType REPORT_CANNOT_READ_LISTENER_THREAD = new ReportType("Unable to read the listener thread.");
|
||||
|
||||
public static final ReportType REPORT_CANNOT_FIND_SERVER_CONNECTION = new ReportType("Unable to retrieve server connection");
|
||||
public static final ReportType REPORT_UNEXPECTED_THREAD_COUNT = new ReportType("Unexpected number of threads in %s: %s");
|
||||
public static final ReportType REPORT_CANNOT_FIND_NET_HANDLER_THREAD = new ReportType("Unable to retrieve net handler thread.");
|
||||
public static final ReportType REPORT_INSUFFICENT_THREAD_COUNT = new ReportType("Unable to inject %s lists in %s.");
|
||||
|
||||
public static final ReportType REPORT_CANNOT_COPY_OLD_TO_NEW = new ReportType("Cannot copy old %s to new.");
|
||||
|
||||
private static Field listenerThreadField;
|
||||
private static Field minecraftServerField;
|
||||
private static Field listField;
|
||||
private static Field dedicatedThreadField;
|
||||
|
||||
private static Method serverConnectionMethod;
|
||||
|
||||
private List<VolatileField> listFields;
|
||||
private List<ReplacedArrayList<Object>> replacedLists;
|
||||
|
||||
// Used to inject net handlers
|
||||
private NetLoginInjector netLoginInjector;
|
||||
|
||||
// Inject server connections
|
||||
private AbstractInputStreamLookup socketInjector;
|
||||
|
||||
private Server server;
|
||||
private ErrorReporter reporter;
|
||||
private boolean hasAttempted;
|
||||
private boolean hasSuccess;
|
||||
|
||||
private Object minecraftServer = null;
|
||||
|
||||
public InjectedServerConnection(ErrorReporter reporter, AbstractInputStreamLookup socketInjector, Server server, NetLoginInjector netLoginInjector) {
|
||||
this.listFields = new ArrayList<VolatileField>();
|
||||
this.replacedLists = new ArrayList<ReplacedArrayList<Object>>();
|
||||
this.reporter = reporter;
|
||||
this.server = server;
|
||||
this.socketInjector = socketInjector;
|
||||
this.netLoginInjector = netLoginInjector;
|
||||
}
|
||||
|
||||
public void injectList() {
|
||||
// Only execute this method once
|
||||
if (!hasAttempted)
|
||||
hasAttempted = true;
|
||||
else
|
||||
return;
|
||||
|
||||
if (minecraftServerField == null)
|
||||
minecraftServerField = FuzzyReflection.fromObject(server, true).
|
||||
getFieldByType("MinecraftServer", MinecraftReflection.getMinecraftServerClass());
|
||||
|
||||
try {
|
||||
minecraftServer = FieldUtils.readField(minecraftServerField, server, true);
|
||||
} catch (IllegalAccessException e1) {
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_FIND_MINECRAFT_SERVER));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (serverConnectionMethod == null)
|
||||
serverConnectionMethod = FuzzyReflection.fromClass(minecraftServerField.getType()).
|
||||
getMethodByParameters("getServerConnection",
|
||||
MinecraftReflection.getServerConnectionClass(), new Class[] {});
|
||||
// We're using Minecraft 1.3.1
|
||||
injectServerConnection();
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
|
||||
// Minecraft 1.2.5 or lower
|
||||
injectListenerThread();
|
||||
|
||||
} catch (Exception e) {
|
||||
// Oh damn - inform the player
|
||||
reporter.reportDetailed(this, Report.newBuilder(REPORT_CANNOT_INJECT_SERVER_CONNECTION).error(e));
|
||||
}
|
||||
}
|
||||
|
||||
private void injectListenerThread() {
|
||||
try {
|
||||
if (listenerThreadField == null)
|
||||
listenerThreadField = FuzzyReflection.fromObject(minecraftServer).
|
||||
getFieldByType("networkListenThread", MinecraftReflection.getNetworkListenThreadClass());
|
||||
} catch (RuntimeException e) {
|
||||
reporter.reportDetailed(this,
|
||||
Report.newBuilder(REPORT_CANNOT_FIND_LISTENER_THREAD).callerParam(minecraftServer).error(e)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Object listenerThread = null;
|
||||
|
||||
// Attempt to get the thread
|
||||
try {
|
||||
listenerThread = listenerThreadField.get(minecraftServer);
|
||||
} catch (Exception e) {
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_READ_LISTENER_THREAD).error(e));
|
||||
return;
|
||||
}
|
||||
|
||||
// Inject the server socket too
|
||||
injectServerSocket(listenerThread);
|
||||
|
||||
// Just inject every list field we can get
|
||||
injectEveryListField(listenerThread, 1);
|
||||
hasSuccess = true;
|
||||
}
|
||||
|
||||
private void injectServerConnection() {
|
||||
Object serverConnection = null;
|
||||
|
||||
// Careful - we might fail
|
||||
try {
|
||||
serverConnection = serverConnectionMethod.invoke(minecraftServer);
|
||||
} catch (Exception e) {
|
||||
reporter.reportDetailed(this,
|
||||
Report.newBuilder(REPORT_CANNOT_FIND_SERVER_CONNECTION).callerParam(minecraftServer).error(e)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (listField == null)
|
||||
listField = FuzzyReflection.fromClass(serverConnectionMethod.getReturnType(), true).
|
||||
getFieldByType("netServerHandlerList", List.class);
|
||||
if (dedicatedThreadField == null) {
|
||||
List<Field> matches = FuzzyReflection.fromObject(serverConnection, true).
|
||||
getFieldListByType(Thread.class);
|
||||
|
||||
// Verify the field count
|
||||
if (matches.size() != 1)
|
||||
reporter.reportWarning(this,
|
||||
Report.newBuilder(REPORT_UNEXPECTED_THREAD_COUNT).messageParam(serverConnection.getClass(), matches.size())
|
||||
);
|
||||
else
|
||||
dedicatedThreadField = matches.get(0);
|
||||
}
|
||||
|
||||
// Next, try to get the dedicated thread
|
||||
try {
|
||||
if (dedicatedThreadField != null) {
|
||||
Object dedicatedThread = FieldUtils.readField(dedicatedThreadField, serverConnection, true);
|
||||
|
||||
// Inject server socket and NetServerHandlers.
|
||||
injectServerSocket(dedicatedThread);
|
||||
injectEveryListField(dedicatedThread, 1);
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_FIND_NET_HANDLER_THREAD).error(e));
|
||||
}
|
||||
|
||||
injectIntoList(serverConnection, listField);
|
||||
hasSuccess = true;
|
||||
}
|
||||
|
||||
private void injectServerSocket(Object container) {
|
||||
socketInjector.inject(container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically inject into every List-compatible public or private field of the given object.
|
||||
* @param container - container object with the fields to inject.
|
||||
* @param minimum - the minimum number of fields we expect exists.
|
||||
*/
|
||||
private void injectEveryListField(Object container, int minimum) {
|
||||
// Ok, great. Get every list field
|
||||
List<Field> lists = FuzzyReflection.fromObject(container, true).getFieldListByType(List.class);
|
||||
|
||||
for (Field list : lists) {
|
||||
injectIntoList(container, list);
|
||||
}
|
||||
|
||||
// Warn about unexpected errors
|
||||
if (lists.size() < minimum) {
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_INSUFFICENT_THREAD_COUNT).messageParam(minimum, container.getClass()));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void injectIntoList(Object instance, Field field) {
|
||||
VolatileField listFieldRef = new VolatileField(field, instance, true);
|
||||
List<Object> list = (List<Object>) listFieldRef.getValue();
|
||||
|
||||
// Careful not to inject twice
|
||||
if (list instanceof ReplacedArrayList) {
|
||||
replacedLists.add((ReplacedArrayList<Object>) list);
|
||||
} else {
|
||||
ReplacedArrayList<Object> injectedList = createReplacement(list);
|
||||
|
||||
replacedLists.add(injectedList);
|
||||
listFieldRef.setValue(injectedList);
|
||||
listFields.add(listFieldRef);
|
||||
}
|
||||
}
|
||||
|
||||
// Hack to avoid the "moved to quickly" error
|
||||
private ReplacedArrayList<Object> createReplacement(List<Object> list) {
|
||||
return new ReplacedArrayList<Object>(list) {
|
||||
/**
|
||||
* Shut up Eclipse!
|
||||
*/
|
||||
private static final long serialVersionUID = 2070481080950500367L;
|
||||
|
||||
// Object writer we'll use
|
||||
private final ObjectWriter writer = new ObjectWriter();
|
||||
|
||||
@Override
|
||||
protected void onReplacing(Object inserting, Object replacement) {
|
||||
// Is this a normal Minecraft object?
|
||||
if (!(inserting instanceof Factory)) {
|
||||
// If so, copy the content of the old element to the new
|
||||
try {
|
||||
writer.copyTo(inserting, replacement, inserting.getClass());
|
||||
} catch (Throwable e) {
|
||||
reporter.reportDetailed(InjectedServerConnection.this,
|
||||
Report.newBuilder(REPORT_CANNOT_COPY_OLD_TO_NEW).messageParam(inserting).callerParam(inserting, replacement).error(e)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onInserting(Object inserting) {
|
||||
// Ready for some login handler injection?
|
||||
if (MinecraftReflection.isLoginHandler(inserting)) {
|
||||
Object replaced = netLoginInjector.onNetLoginCreated(inserting);
|
||||
|
||||
// Only replace if it has changed
|
||||
if (inserting != replaced)
|
||||
addMapping(inserting, replaced, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRemoved(Object removing) {
|
||||
// Clean up?
|
||||
if (MinecraftReflection.isLoginHandler(removing)) {
|
||||
netLoginInjector.cleanup(removing);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the server handler instance kept by the "keep alive" object.
|
||||
* @param oldHandler - old server handler.
|
||||
* @param newHandler - new, proxied server handler.
|
||||
*/
|
||||
public void replaceServerHandler(Object oldHandler, Object newHandler) {
|
||||
if (!hasAttempted) {
|
||||
injectList();
|
||||
}
|
||||
|
||||
if (hasSuccess) {
|
||||
for (ReplacedArrayList<Object> replacedList : replacedLists) {
|
||||
replacedList.addMapping(oldHandler, newHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert to the old vanilla server handler, if it has been replaced.
|
||||
* @param oldHandler - old vanilla server handler.
|
||||
*/
|
||||
public void revertServerHandler(Object oldHandler) {
|
||||
if (hasSuccess) {
|
||||
for (ReplacedArrayList<Object> replacedList : replacedLists) {
|
||||
replacedList.removeMapping(oldHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Undoes everything.
|
||||
*/
|
||||
public void cleanupAll() {
|
||||
if (replacedLists.size() > 0) {
|
||||
// Repair the underlying lists
|
||||
for (ReplacedArrayList<Object> replacedList : replacedLists) {
|
||||
replacedList.revertAll();
|
||||
}
|
||||
for (VolatileField field : listFields) {
|
||||
field.revertValue();
|
||||
}
|
||||
|
||||
listFields.clear();
|
||||
replacedLists.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,141 +1,154 @@
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.injector.player;
|
||||
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.injector.GamePhase;
|
||||
import com.comphenix.protocol.injector.player.PlayerInjectionHandler.ConflictStrategy;
|
||||
import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
/**
|
||||
* Injects every NetLoginHandler created by the server.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
class NetLoginInjector {
|
||||
private ConcurrentMap<Object, PlayerInjector> injectedLogins = Maps.newConcurrentMap();
|
||||
|
||||
// Handles every hook
|
||||
private ProxyPlayerInjectionHandler injectionHandler;
|
||||
|
||||
// Create temporary players
|
||||
private TemporaryPlayerFactory playerFactory = new TemporaryPlayerFactory();
|
||||
|
||||
// The current error reporter
|
||||
private ErrorReporter reporter;
|
||||
private Server server;
|
||||
|
||||
public NetLoginInjector(ErrorReporter reporter, Server server, ProxyPlayerInjectionHandler injectionHandler) {
|
||||
this.reporter = reporter;
|
||||
this.server = server;
|
||||
this.injectionHandler = injectionHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a NetLoginHandler has been created.
|
||||
* @param inserting - the new NetLoginHandler.
|
||||
* @return An injected NetLoginHandler, or the original object.
|
||||
*/
|
||||
public Object onNetLoginCreated(Object inserting) {
|
||||
try {
|
||||
// Make sure we actually need to inject during this phase
|
||||
if (!injectionHandler.isInjectionNecessary(GamePhase.LOGIN))
|
||||
return inserting;
|
||||
|
||||
Player temporary = playerFactory.createTemporaryPlayer(server);
|
||||
// Note that we bail out if there's an existing player injector
|
||||
PlayerInjector injector = injectionHandler.injectPlayer(
|
||||
temporary, inserting, ConflictStrategy.BAIL_OUT, GamePhase.LOGIN);
|
||||
|
||||
if (injector != null) {
|
||||
// Update injector as well
|
||||
TemporaryPlayerFactory.setInjectorInPlayer(temporary, injector);
|
||||
injector.updateOnLogin = true;
|
||||
|
||||
// Save the login
|
||||
injectedLogins.putIfAbsent(inserting, injector);
|
||||
}
|
||||
|
||||
// NetServerInjector can never work (currently), so we don't need to replace the NetLoginHandler
|
||||
return inserting;
|
||||
|
||||
} catch (Throwable e) {
|
||||
// Minecraft can't handle this, so we'll deal with it here
|
||||
reporter.reportDetailed(this, "Unable to hook " +
|
||||
MinecraftReflection.getNetLoginHandlerName() + ".", e, inserting, injectionHandler);
|
||||
return inserting;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a NetLoginHandler should be reverted.
|
||||
* @param inserting - the original NetLoginHandler.
|
||||
* @return An injected NetLoginHandler, or the original object.
|
||||
*/
|
||||
public synchronized void cleanup(Object removing) {
|
||||
PlayerInjector injected = injectedLogins.get(removing);
|
||||
|
||||
if (injected != null) {
|
||||
try {
|
||||
PlayerInjector newInjector = null;
|
||||
Player player = injected.getPlayer();
|
||||
|
||||
// Clean up list
|
||||
injectedLogins.remove(removing);
|
||||
|
||||
// No need to clean up twice
|
||||
if (injected.isClean())
|
||||
return;
|
||||
|
||||
// Hack to clean up other references
|
||||
newInjector = injectionHandler.getInjectorByNetworkHandler(injected.getNetworkManager());
|
||||
injectionHandler.uninjectPlayer(player);
|
||||
|
||||
// Update NetworkManager
|
||||
if (newInjector != null) {
|
||||
if (injected instanceof NetworkObjectInjector) {
|
||||
newInjector.setNetworkManager(injected.getNetworkManager(), true);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Throwable e) {
|
||||
// Don't leak this to Minecraft
|
||||
reporter.reportDetailed(this, "Cannot cleanup " +
|
||||
MinecraftReflection.getNetLoginHandlerName() + ".", e, removing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all injected hooks.
|
||||
*/
|
||||
public void cleanupAll() {
|
||||
for (PlayerInjector injector : injectedLogins.values()) {
|
||||
injector.cleanupAll();
|
||||
}
|
||||
|
||||
injectedLogins.clear();
|
||||
}
|
||||
}
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.injector.player;
|
||||
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.error.Report;
|
||||
import com.comphenix.protocol.error.ReportType;
|
||||
import com.comphenix.protocol.injector.GamePhase;
|
||||
import com.comphenix.protocol.injector.player.PlayerInjectionHandler.ConflictStrategy;
|
||||
import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
/**
|
||||
* Injects every NetLoginHandler created by the server.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
class NetLoginInjector {
|
||||
public static final ReportType REPORT_CANNOT_HOOK_LOGIN_HANDLER = new ReportType("Unable to hook %s.");
|
||||
public static final ReportType REPORT_CANNOT_CLEANUP_LOGIN_HANDLER = new ReportType("Cannot cleanup %s.");
|
||||
|
||||
private ConcurrentMap<Object, PlayerInjector> injectedLogins = Maps.newConcurrentMap();
|
||||
|
||||
// Handles every hook
|
||||
private ProxyPlayerInjectionHandler injectionHandler;
|
||||
|
||||
// Create temporary players
|
||||
private TemporaryPlayerFactory playerFactory = new TemporaryPlayerFactory();
|
||||
|
||||
// The current error reporter
|
||||
private ErrorReporter reporter;
|
||||
private Server server;
|
||||
|
||||
public NetLoginInjector(ErrorReporter reporter, Server server, ProxyPlayerInjectionHandler injectionHandler) {
|
||||
this.reporter = reporter;
|
||||
this.server = server;
|
||||
this.injectionHandler = injectionHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a NetLoginHandler has been created.
|
||||
* @param inserting - the new NetLoginHandler.
|
||||
* @return An injected NetLoginHandler, or the original object.
|
||||
*/
|
||||
public Object onNetLoginCreated(Object inserting) {
|
||||
try {
|
||||
// Make sure we actually need to inject during this phase
|
||||
if (!injectionHandler.isInjectionNecessary(GamePhase.LOGIN))
|
||||
return inserting;
|
||||
|
||||
Player temporary = playerFactory.createTemporaryPlayer(server);
|
||||
// Note that we bail out if there's an existing player injector
|
||||
PlayerInjector injector = injectionHandler.injectPlayer(
|
||||
temporary, inserting, ConflictStrategy.BAIL_OUT, GamePhase.LOGIN);
|
||||
|
||||
if (injector != null) {
|
||||
// Update injector as well
|
||||
TemporaryPlayerFactory.setInjectorInPlayer(temporary, injector);
|
||||
injector.updateOnLogin = true;
|
||||
|
||||
// Save the login
|
||||
injectedLogins.putIfAbsent(inserting, injector);
|
||||
}
|
||||
|
||||
// NetServerInjector can never work (currently), so we don't need to replace the NetLoginHandler
|
||||
return inserting;
|
||||
|
||||
} catch (Throwable e) {
|
||||
// Minecraft can't handle this, so we'll deal with it here
|
||||
reporter.reportDetailed(this,
|
||||
Report.newBuilder(REPORT_CANNOT_HOOK_LOGIN_HANDLER).
|
||||
messageParam(MinecraftReflection.getNetLoginHandlerName()).
|
||||
callerParam(inserting, injectionHandler).
|
||||
error(e)
|
||||
);
|
||||
return inserting;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a NetLoginHandler should be reverted.
|
||||
* @param inserting - the original NetLoginHandler.
|
||||
* @return An injected NetLoginHandler, or the original object.
|
||||
*/
|
||||
public synchronized void cleanup(Object removing) {
|
||||
PlayerInjector injected = injectedLogins.get(removing);
|
||||
|
||||
if (injected != null) {
|
||||
try {
|
||||
PlayerInjector newInjector = null;
|
||||
Player player = injected.getPlayer();
|
||||
|
||||
// Clean up list
|
||||
injectedLogins.remove(removing);
|
||||
|
||||
// No need to clean up twice
|
||||
if (injected.isClean())
|
||||
return;
|
||||
|
||||
// Hack to clean up other references
|
||||
newInjector = injectionHandler.getInjectorByNetworkHandler(injected.getNetworkManager());
|
||||
injectionHandler.uninjectPlayer(player);
|
||||
|
||||
// Update NetworkManager
|
||||
if (newInjector != null) {
|
||||
if (injected instanceof NetworkObjectInjector) {
|
||||
newInjector.setNetworkManager(injected.getNetworkManager(), true);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Throwable e) {
|
||||
// Don't leak this to Minecraft
|
||||
reporter.reportDetailed(this,
|
||||
Report.newBuilder(REPORT_CANNOT_CLEANUP_LOGIN_HANDLER).
|
||||
messageParam(MinecraftReflection.getNetLoginHandlerName()).
|
||||
callerParam(removing).
|
||||
error(e)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all injected hooks.
|
||||
*/
|
||||
public void cleanupAll() {
|
||||
for (PlayerInjector injector : injectedLogins.values()) {
|
||||
injector.cleanupAll();
|
||||
}
|
||||
|
||||
injectedLogins.clear();
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.reflect.StructureModifier;
|
||||
import com.comphenix.protocol.reflect.VolatileField;
|
||||
import com.comphenix.protocol.utility.MinecraftVersion;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
/**
|
||||
@ -56,6 +57,12 @@ class NetworkFieldInjector extends PlayerInjector {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
// After commit 336a4e00668fd2518c41242755ed6b3bdc3b0e6c (Update CraftBukkit to Minecraft 1.4.4.),
|
||||
// CraftBukkit stopped redirecting map chunk and map chunk bulk packets to a separate queue.
|
||||
// Thus, NetworkFieldInjector can safely handle every packet (though not perfectly - some packets
|
||||
// will be slightly processed).
|
||||
private MinecraftVersion safeVersion = new MinecraftVersion("1.4.4");
|
||||
|
||||
// Packets to ignore
|
||||
private Set<Object> ignoredPackets = Sets.newSetFromMap(new ConcurrentHashMap<Object, Boolean>());
|
||||
|
||||
@ -99,7 +106,6 @@ class NetworkFieldInjector extends PlayerInjector {
|
||||
|
||||
@Override
|
||||
public void sendServerPacket(Object packet, boolean filtered) throws InvocationTargetException {
|
||||
|
||||
if (networkManager != null) {
|
||||
try {
|
||||
if (!filtered) {
|
||||
@ -122,14 +128,19 @@ class NetworkFieldInjector extends PlayerInjector {
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnsupportedListener checkListener(PacketListener listener) {
|
||||
int[] unsupported = { Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK };
|
||||
|
||||
// Unfortunately, we don't support chunk packets
|
||||
if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), unsupported)) {
|
||||
return new UnsupportedListener("The NETWORK_FIELD_INJECTOR hook doesn't support map chunk listeners.", unsupported);
|
||||
} else {
|
||||
public UnsupportedListener checkListener(MinecraftVersion version, PacketListener listener) {
|
||||
if (version != null && version.compareTo(safeVersion) > 0) {
|
||||
return null;
|
||||
|
||||
} else {
|
||||
int[] unsupported = { Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK };
|
||||
|
||||
// Unfortunately, we don't support chunk packets
|
||||
if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), unsupported)) {
|
||||
return new UnsupportedListener("The NETWORK_FIELD_INJECTOR hook doesn't support map chunk listeners.", unsupported);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,7 @@ import com.comphenix.protocol.injector.GamePhase;
|
||||
import com.comphenix.protocol.injector.ListenerInvoker;
|
||||
import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks;
|
||||
import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
|
||||
import com.comphenix.protocol.utility.MinecraftVersion;
|
||||
|
||||
/**
|
||||
* Injection method that overrides the NetworkHandler itself, and its queue-method.
|
||||
@ -53,6 +54,12 @@ public class NetworkObjectInjector extends PlayerInjector {
|
||||
// Used to construct proxy objects
|
||||
private ClassLoader classLoader;
|
||||
|
||||
// After commit 336a4e00668fd2518c41242755ed6b3bdc3b0e6c (Update CraftBukkit to Minecraft 1.4.4.),
|
||||
// CraftBukkit stopped redirecting map chunk and map chunk bulk packets to a separate queue.
|
||||
// Thus, NetworkFieldInjector can safely handle every packet (though not perfectly - some packets
|
||||
// will be slightly processed).
|
||||
private MinecraftVersion safeVersion = new MinecraftVersion("1.4.4");
|
||||
|
||||
// Shared callback filter - avoid creating a new class every time
|
||||
private volatile static CallbackFilter callbackFilter;
|
||||
|
||||
@ -117,14 +124,19 @@ public class NetworkObjectInjector extends PlayerInjector {
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnsupportedListener checkListener(PacketListener listener) {
|
||||
int[] unsupported = { Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK };
|
||||
|
||||
// Unfortunately, we don't support chunk packets
|
||||
if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), unsupported)) {
|
||||
return new UnsupportedListener("The NETWORK_OBJECT_INJECTOR hook doesn't support map chunk listeners.", unsupported);
|
||||
} else {
|
||||
public UnsupportedListener checkListener(MinecraftVersion version, PacketListener listener) {
|
||||
if (version != null && version.compareTo(safeVersion) > 0) {
|
||||
return null;
|
||||
|
||||
} else {
|
||||
int[] unsupported = { Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK };
|
||||
|
||||
// Unfortunately, we don't support chunk packets
|
||||
if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), unsupported)) {
|
||||
return new UnsupportedListener("The NETWORK_OBJECT_INJECTOR hook doesn't support map chunk listeners.", unsupported);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,324 +1,352 @@
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.injector.player;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import net.sf.cglib.proxy.*;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import com.comphenix.protocol.concurrency.IntegerSet;
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.events.PacketListener;
|
||||
import com.comphenix.protocol.injector.GamePhase;
|
||||
import com.comphenix.protocol.injector.ListenerInvoker;
|
||||
import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks;
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.reflect.ObjectWriter;
|
||||
import com.comphenix.protocol.reflect.VolatileField;
|
||||
import com.comphenix.protocol.reflect.instances.DefaultInstances;
|
||||
import com.comphenix.protocol.reflect.instances.ExistingGenerator;
|
||||
import com.comphenix.protocol.utility.MinecraftMethods;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
|
||||
/**
|
||||
* Represents a player hook into the NetServerHandler class.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
class NetworkServerInjector extends PlayerInjector {
|
||||
|
||||
private volatile static CallbackFilter callbackFilter;
|
||||
|
||||
private volatile static Field disconnectField;
|
||||
private InjectedServerConnection serverInjection;
|
||||
|
||||
// Determine if we're listening
|
||||
private IntegerSet sendingFilters;
|
||||
|
||||
// Used to create proxy objects
|
||||
private ClassLoader classLoader;
|
||||
|
||||
// Whether or not the player has disconnected
|
||||
private boolean hasDisconnected;
|
||||
|
||||
// Used to copy fields
|
||||
private final ObjectWriter writer = new ObjectWriter();
|
||||
|
||||
public NetworkServerInjector(
|
||||
ClassLoader classLoader, ErrorReporter reporter, Player player,
|
||||
ListenerInvoker invoker, IntegerSet sendingFilters,
|
||||
InjectedServerConnection serverInjection) throws IllegalAccessException {
|
||||
|
||||
super(reporter, player, invoker);
|
||||
this.classLoader = classLoader;
|
||||
this.sendingFilters = sendingFilters;
|
||||
this.serverInjection = serverInjection;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hasListener(int packetID) {
|
||||
return sendingFilters.contains(packetID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendServerPacket(Object packet, boolean filtered) throws InvocationTargetException {
|
||||
Object serverDelegate = filtered ? serverHandlerRef.getValue() : serverHandlerRef.getOldValue();
|
||||
|
||||
if (serverDelegate != null) {
|
||||
try {
|
||||
// Note that invocation target exception is a wrapper for a checked exception
|
||||
MinecraftMethods.getSendPacketMethod().invoke(serverDelegate, packet);
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw e;
|
||||
} catch (InvocationTargetException e) {
|
||||
throw e;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new IllegalStateException("Unable to access send packet method.", e);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("Unable to load server handler. Cannot send packet.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectManager() {
|
||||
|
||||
if (serverHandlerRef == null)
|
||||
throw new IllegalStateException("Cannot find server handler.");
|
||||
// Don't inject twice
|
||||
if (serverHandlerRef.getValue() instanceof Factory)
|
||||
return;
|
||||
|
||||
if (!tryInjectManager()) {
|
||||
Class<?> serverHandlerClass = MinecraftReflection.getNetServerHandlerClass();
|
||||
|
||||
// Try to override the proxied object
|
||||
if (proxyServerField != null) {
|
||||
serverHandlerRef = new VolatileField(proxyServerField, serverHandler, true);
|
||||
serverHandler = serverHandlerRef.getValue();
|
||||
|
||||
if (serverHandler == null)
|
||||
throw new RuntimeException("Cannot hook player: Inner proxy object is NULL.");
|
||||
else
|
||||
serverHandlerClass = serverHandler.getClass();
|
||||
|
||||
// Try again
|
||||
if (tryInjectManager()) {
|
||||
// It worked - probably
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException(
|
||||
"Cannot hook player: Unable to find a valid constructor for the "
|
||||
+ serverHandlerClass.getName() + " object.");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean tryInjectManager() {
|
||||
Class<?> serverClass = serverHandler.getClass();
|
||||
|
||||
Enhancer ex = new Enhancer();
|
||||
Callback sendPacketCallback = new MethodInterceptor() {
|
||||
@Override
|
||||
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
|
||||
Object packet = args[0];
|
||||
|
||||
if (packet != null) {
|
||||
packet = handlePacketSending(packet);
|
||||
|
||||
// A NULL packet indicate cancelling
|
||||
if (packet != null)
|
||||
args[0] = packet;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
// Call the method directly
|
||||
return proxy.invokeSuper(obj, args);
|
||||
};
|
||||
};
|
||||
Callback noOpCallback = NoOp.INSTANCE;
|
||||
|
||||
// Share callback filter - that way, we avoid generating a new class for
|
||||
// every logged in player.
|
||||
if (callbackFilter == null) {
|
||||
final Method sendPacket = MinecraftMethods.getSendPacketMethod();
|
||||
|
||||
callbackFilter = new CallbackFilter() {
|
||||
@Override
|
||||
public int accept(Method method) {
|
||||
if (method.equals(sendPacket))
|
||||
return 0;
|
||||
else
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ex.setClassLoader(classLoader);
|
||||
ex.setSuperclass(serverClass);
|
||||
ex.setCallbacks(new Callback[] { sendPacketCallback, noOpCallback });
|
||||
ex.setCallbackFilter(callbackFilter);
|
||||
|
||||
// Find the Minecraft NetServerHandler superclass
|
||||
Class<?> minecraftSuperClass = getFirstMinecraftSuperClass(serverHandler.getClass());
|
||||
ExistingGenerator generator = ExistingGenerator.fromObjectFields(serverHandler, minecraftSuperClass);
|
||||
DefaultInstances serverInstances = null;
|
||||
|
||||
// Maybe the proxy instance can help?
|
||||
Object proxyInstance = getProxyServerHandler();
|
||||
|
||||
// Use the existing server proxy when we create one
|
||||
if (proxyInstance != null && proxyInstance != serverHandler) {
|
||||
serverInstances = DefaultInstances.fromArray(generator,
|
||||
ExistingGenerator.fromObjectArray(new Object[] { proxyInstance }));
|
||||
} else {
|
||||
serverInstances = DefaultInstances.fromArray(generator);
|
||||
}
|
||||
|
||||
serverInstances.setNonNull(true);
|
||||
serverInstances.setMaximumRecursion(1);
|
||||
|
||||
Object proxyObject = serverInstances.forEnhancer(ex).getDefault(serverClass);
|
||||
|
||||
// Inject it now
|
||||
if (proxyObject != null) {
|
||||
// This will be done by InjectedServerConnection instead
|
||||
//copyTo(serverHandler, proxyObject);
|
||||
|
||||
serverInjection.replaceServerHandler(serverHandler, proxyObject);
|
||||
serverHandlerRef.setValue(proxyObject);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Object getProxyServerHandler() {
|
||||
if (proxyServerField != null && !proxyServerField.equals(serverHandlerRef.getField())) {
|
||||
try {
|
||||
return FieldUtils.readField(proxyServerField, serverHandler, true);
|
||||
} catch (Throwable e) {
|
||||
// Oh well
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Class<?> getFirstMinecraftSuperClass(Class<?> clazz) {
|
||||
if (clazz.getName().startsWith(MinecraftReflection.getMinecraftPackage()))
|
||||
return clazz;
|
||||
else if (clazz.equals(Object.class))
|
||||
return clazz;
|
||||
else
|
||||
return getFirstMinecraftSuperClass(clazz.getSuperclass());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanHook() {
|
||||
if (serverHandlerRef != null && serverHandlerRef.isCurrentSet()) {
|
||||
writer.copyTo(serverHandlerRef.getValue(), serverHandlerRef.getOldValue(), serverHandler.getClass());
|
||||
serverHandlerRef.revertValue();
|
||||
|
||||
try {
|
||||
if (getNetHandler() != null) {
|
||||
// Restore packet listener
|
||||
try {
|
||||
FieldUtils.writeField(netHandlerField, networkManager, serverHandlerRef.getOldValue(), true);
|
||||
} catch (IllegalAccessException e) {
|
||||
// Oh well
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// Prevent the PlayerQuitEvent from being sent twice
|
||||
if (hasDisconnected) {
|
||||
setDisconnect(serverHandlerRef.getValue(), true);
|
||||
}
|
||||
}
|
||||
|
||||
serverInjection.revertServerHandler(serverHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
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) {
|
||||
// Assume it's the first ...
|
||||
if (disconnectField == null) {
|
||||
disconnectField = FuzzyReflection.fromObject(handler).getFieldByType("disconnected", boolean.class);
|
||||
reporter.reportWarning(this, "Unable to find 'disconnected' field. Assuming " + disconnectField);
|
||||
|
||||
// Try again
|
||||
if (disconnectField != null) {
|
||||
setDisconnect(handler, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// This is really bad
|
||||
reporter.reportDetailed(this, "Cannot find disconnected field. Is ProtocolLib up to date?", e);
|
||||
|
||||
} catch (IllegalAccessException e) {
|
||||
reporter.reportWarning(this, "Unable to update disconnected field. Player quit event may be sent twice.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnsupportedListener checkListener(PacketListener listener) {
|
||||
// We support everything
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canInject(GamePhase phase) {
|
||||
// Doesn't work when logging in
|
||||
return phase == GamePhase.PLAYING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayerInjectHooks getHookType() {
|
||||
return PlayerInjectHooks.NETWORK_SERVER_OBJECT;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.injector.player;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
import net.sf.cglib.proxy.*;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import com.comphenix.protocol.concurrency.IntegerSet;
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.error.Report;
|
||||
import com.comphenix.protocol.error.ReportType;
|
||||
import com.comphenix.protocol.events.PacketListener;
|
||||
import com.comphenix.protocol.injector.GamePhase;
|
||||
import com.comphenix.protocol.injector.ListenerInvoker;
|
||||
import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks;
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.reflect.ObjectWriter;
|
||||
import com.comphenix.protocol.reflect.VolatileField;
|
||||
import com.comphenix.protocol.reflect.instances.DefaultInstances;
|
||||
import com.comphenix.protocol.reflect.instances.ExistingGenerator;
|
||||
import com.comphenix.protocol.utility.MinecraftMethods;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.utility.MinecraftVersion;
|
||||
|
||||
/**
|
||||
* Represents a player hook into the NetServerHandler class.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
class NetworkServerInjector extends PlayerInjector {
|
||||
// Disconnected field
|
||||
public static final ReportType REPORT_ASSUMING_DISCONNECT_FIELD = new ReportType("Unable to find 'disconnected' field. Assuming %s.");
|
||||
public static final ReportType REPORT_DISCONNECT_FIELD_MISSING = new ReportType("Cannot find disconnected field. Is ProtocolLib up to date?");
|
||||
public static final ReportType REPORT_DISCONNECT_FIELD_FAILURE = new ReportType("Unable to update disconnected field. Player quit event may be sent twice.");
|
||||
|
||||
private volatile static CallbackFilter callbackFilter;
|
||||
private volatile static boolean foundSendPacket;
|
||||
|
||||
private volatile static Field disconnectField;
|
||||
private InjectedServerConnection serverInjection;
|
||||
|
||||
// Determine if we're listening
|
||||
private IntegerSet sendingFilters;
|
||||
|
||||
// Used to create proxy objects
|
||||
private ClassLoader classLoader;
|
||||
|
||||
// Whether or not the player has disconnected
|
||||
private boolean hasDisconnected;
|
||||
|
||||
// Used to copy fields
|
||||
private final ObjectWriter writer = new ObjectWriter();
|
||||
|
||||
public NetworkServerInjector(
|
||||
ClassLoader classLoader, ErrorReporter reporter, Player player,
|
||||
ListenerInvoker invoker, IntegerSet sendingFilters,
|
||||
InjectedServerConnection serverInjection) throws IllegalAccessException {
|
||||
|
||||
super(reporter, player, invoker);
|
||||
this.classLoader = classLoader;
|
||||
this.sendingFilters = sendingFilters;
|
||||
this.serverInjection = serverInjection;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hasListener(int packetID) {
|
||||
return sendingFilters.contains(packetID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendServerPacket(Object packet, boolean filtered) throws InvocationTargetException {
|
||||
Object serverDelegate = filtered ? serverHandlerRef.getValue() : serverHandlerRef.getOldValue();
|
||||
|
||||
if (serverDelegate != null) {
|
||||
try {
|
||||
// Note that invocation target exception is a wrapper for a checked exception
|
||||
MinecraftMethods.getSendPacketMethod().invoke(serverDelegate, packet);
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw e;
|
||||
} catch (InvocationTargetException e) {
|
||||
throw e;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new IllegalStateException("Unable to access send packet method.", e);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("Unable to load server handler. Cannot send packet.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectManager() {
|
||||
|
||||
if (serverHandlerRef == null)
|
||||
throw new IllegalStateException("Cannot find server handler.");
|
||||
// Don't inject twice
|
||||
if (serverHandlerRef.getValue() instanceof Factory)
|
||||
return;
|
||||
|
||||
if (!tryInjectManager()) {
|
||||
Class<?> serverHandlerClass = MinecraftReflection.getNetServerHandlerClass();
|
||||
|
||||
// Try to override the proxied object
|
||||
if (proxyServerField != null) {
|
||||
serverHandlerRef = new VolatileField(proxyServerField, serverHandler, true);
|
||||
serverHandler = serverHandlerRef.getValue();
|
||||
|
||||
if (serverHandler == null)
|
||||
throw new RuntimeException("Cannot hook player: Inner proxy object is NULL.");
|
||||
else
|
||||
serverHandlerClass = serverHandler.getClass();
|
||||
|
||||
// Try again
|
||||
if (tryInjectManager()) {
|
||||
// It worked - probably
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException(
|
||||
"Cannot hook player: Unable to find a valid constructor for the "
|
||||
+ serverHandlerClass.getName() + " object.");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean tryInjectManager() {
|
||||
Class<?> serverClass = serverHandler.getClass();
|
||||
|
||||
Enhancer ex = new Enhancer();
|
||||
Callback sendPacketCallback = new MethodInterceptor() {
|
||||
@Override
|
||||
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
|
||||
Object packet = args[0];
|
||||
|
||||
if (packet != null) {
|
||||
packet = handlePacketSending(packet);
|
||||
|
||||
// A NULL packet indicate cancelling
|
||||
if (packet != null)
|
||||
args[0] = packet;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
// Call the method directly
|
||||
return proxy.invokeSuper(obj, args);
|
||||
};
|
||||
};
|
||||
Callback noOpCallback = NoOp.INSTANCE;
|
||||
|
||||
// Share callback filter - that way, we avoid generating a new class for
|
||||
// every logged in player.
|
||||
if (callbackFilter == null) {
|
||||
final Method sendPacket = MinecraftMethods.getSendPacketMethod();
|
||||
|
||||
callbackFilter = new CallbackFilter() {
|
||||
@Override
|
||||
public int accept(Method method) {
|
||||
if (isCallableEqual(sendPacket, method)) {
|
||||
foundSendPacket = true;
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ex.setClassLoader(classLoader);
|
||||
ex.setSuperclass(serverClass);
|
||||
ex.setCallbacks(new Callback[] { sendPacketCallback, noOpCallback });
|
||||
ex.setCallbackFilter(callbackFilter);
|
||||
|
||||
// Find the Minecraft NetServerHandler superclass
|
||||
Class<?> minecraftSuperClass = getFirstMinecraftSuperClass(serverHandler.getClass());
|
||||
ExistingGenerator generator = ExistingGenerator.fromObjectFields(serverHandler, minecraftSuperClass);
|
||||
DefaultInstances serverInstances = null;
|
||||
|
||||
// Maybe the proxy instance can help?
|
||||
Object proxyInstance = getProxyServerHandler();
|
||||
|
||||
// Use the existing server proxy when we create one
|
||||
if (proxyInstance != null && proxyInstance != serverHandler) {
|
||||
serverInstances = DefaultInstances.fromArray(generator,
|
||||
ExistingGenerator.fromObjectArray(new Object[] { proxyInstance }));
|
||||
} else {
|
||||
serverInstances = DefaultInstances.fromArray(generator);
|
||||
}
|
||||
|
||||
serverInstances.setNonNull(true);
|
||||
serverInstances.setMaximumRecursion(1);
|
||||
|
||||
Object proxyObject = serverInstances.forEnhancer(ex).getDefault(serverClass);
|
||||
|
||||
// Inject it now
|
||||
if (proxyObject != null) {
|
||||
// Did we override a sendPacket method?
|
||||
if (!foundSendPacket) {
|
||||
throw new IllegalArgumentException("Unable to find a sendPacket method in " + serverClass);
|
||||
}
|
||||
|
||||
serverInjection.replaceServerHandler(serverHandler, proxyObject);
|
||||
serverHandlerRef.setValue(proxyObject);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the two methods are equal in terms of call semantics.
|
||||
* <p>
|
||||
* Two methods are equal if they have the same name, parameter types and return type.
|
||||
* @param first - first method.
|
||||
* @param second - second method.
|
||||
* @return TRUE if they are, FALSE otherwise.
|
||||
*/
|
||||
private boolean isCallableEqual(Method first, Method second) {
|
||||
return first.getName().equals(second.getName()) &&
|
||||
first.getReturnType().equals(second.getReturnType()) &&
|
||||
Arrays.equals(first.getParameterTypes(), second.getParameterTypes());
|
||||
}
|
||||
|
||||
private Object getProxyServerHandler() {
|
||||
if (proxyServerField != null && !proxyServerField.equals(serverHandlerRef.getField())) {
|
||||
try {
|
||||
return FieldUtils.readField(proxyServerField, serverHandler, true);
|
||||
} catch (Throwable e) {
|
||||
// Oh well
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Class<?> getFirstMinecraftSuperClass(Class<?> clazz) {
|
||||
if (MinecraftReflection.isMinecraftClass(clazz))
|
||||
return clazz;
|
||||
else if (clazz.equals(Object.class))
|
||||
return clazz;
|
||||
else
|
||||
return getFirstMinecraftSuperClass(clazz.getSuperclass());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanHook() {
|
||||
if (serverHandlerRef != null && serverHandlerRef.isCurrentSet()) {
|
||||
writer.copyTo(serverHandlerRef.getValue(), serverHandlerRef.getOldValue(), serverHandler.getClass());
|
||||
serverHandlerRef.revertValue();
|
||||
|
||||
try {
|
||||
if (getNetHandler() != null) {
|
||||
// Restore packet listener
|
||||
try {
|
||||
FieldUtils.writeField(netHandlerField, networkManager, serverHandlerRef.getOldValue(), true);
|
||||
} catch (IllegalAccessException e) {
|
||||
// Oh well
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// Prevent the PlayerQuitEvent from being sent twice
|
||||
if (hasDisconnected) {
|
||||
setDisconnect(serverHandlerRef.getValue(), true);
|
||||
}
|
||||
}
|
||||
|
||||
serverInjection.revertServerHandler(serverHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
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) {
|
||||
// Assume it's the first ...
|
||||
if (disconnectField == null) {
|
||||
disconnectField = FuzzyReflection.fromObject(handler).getFieldByType("disconnected", boolean.class);
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_ASSUMING_DISCONNECT_FIELD).messageParam(disconnectField));
|
||||
|
||||
// Try again
|
||||
if (disconnectField != null) {
|
||||
setDisconnect(handler, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// This is really bad
|
||||
reporter.reportDetailed(this, Report.newBuilder(REPORT_DISCONNECT_FIELD_MISSING).error(e));
|
||||
|
||||
} catch (IllegalAccessException e) {
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_DISCONNECT_FIELD_FAILURE).error(e));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnsupportedListener checkListener(MinecraftVersion version, PacketListener listener) {
|
||||
// We support everything
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canInject(GamePhase phase) {
|
||||
// Doesn't work when logging in
|
||||
return phase == GamePhase.PLAYING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayerInjectHooks getHookType() {
|
||||
return PlayerInjectHooks.NETWORK_SERVER_OBJECT;
|
||||
}
|
||||
}
|
||||
|
@ -126,11 +126,18 @@ public interface PlayerInjectionHandler {
|
||||
* @throws IllegalAccessException If the reflection machinery failed.
|
||||
* @throws InvocationTargetException If the underlying method caused an error.
|
||||
*/
|
||||
public abstract void processPacket(Player player, Object mcPacket)
|
||||
public abstract void recieveClientPacket(Player player, Object mcPacket)
|
||||
throws IllegalAccessException, InvocationTargetException;
|
||||
|
||||
/**
|
||||
* Ensure that packet readers are informed of this player reference.
|
||||
* @param player - the player to update.
|
||||
*/
|
||||
public abstract void updatePlayer(Player player);
|
||||
|
||||
/**
|
||||
* Determine if the given listeners are valid.
|
||||
* @param version - the current Minecraft version, or NULL if unknown.
|
||||
* @param listeners - listeners to check.
|
||||
*/
|
||||
public abstract void checkListener(Set<PacketListener> listeners);
|
||||
@ -139,6 +146,7 @@ public interface PlayerInjectionHandler {
|
||||
* Determine if a listener is valid or not.
|
||||
* <p>
|
||||
* If not, a warning will be printed to the console.
|
||||
* @param version - the current Minecraft version, or NULL if unknown.
|
||||
* @param listener - listener to check.
|
||||
*/
|
||||
public abstract void checkListener(PacketListener listener);
|
||||
|
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
@ -14,6 +14,7 @@ import com.comphenix.protocol.events.PacketListener;
|
||||
import com.comphenix.protocol.injector.GamePhase;
|
||||
import com.comphenix.protocol.injector.ListenerInvoker;
|
||||
import com.comphenix.protocol.injector.PacketFilterManager;
|
||||
import com.comphenix.protocol.utility.MinecraftVersion;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Predicate;
|
||||
|
||||
@ -37,6 +38,7 @@ public class PlayerInjectorBuilder {
|
||||
protected ListenerInvoker invoker;
|
||||
protected Set<PacketListener> packetListeners;
|
||||
protected Server server;
|
||||
protected MinecraftVersion version;
|
||||
|
||||
/**
|
||||
* Set the class loader to use during class generation.
|
||||
@ -107,6 +109,16 @@ public class PlayerInjectorBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current Minecraft version.
|
||||
* @param server - the current Minecraft version, or NULL if unknown.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public PlayerInjectorBuilder version(MinecraftVersion version) {
|
||||
this.version = version;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before an object is created with this builder.
|
||||
*/
|
||||
@ -140,6 +152,6 @@ public class PlayerInjectorBuilder {
|
||||
|
||||
return new ProxyPlayerInjectionHandler(
|
||||
classLoader, reporter, injectionFilter,
|
||||
invoker, packetListeners, server);
|
||||
invoker, packetListeners, server, version);
|
||||
}
|
||||
}
|
||||
|
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
@ -1,119 +1,87 @@
|
||||
package com.comphenix.protocol.injector.server;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
|
||||
public abstract class AbstractInputStreamLookup {
|
||||
// Used to access the inner input stream of a filtered input stream
|
||||
private static Field filteredInputField;
|
||||
|
||||
// Error reporter
|
||||
protected final ErrorReporter reporter;
|
||||
|
||||
// Reference to the server itself
|
||||
protected final Server server;
|
||||
|
||||
protected AbstractInputStreamLookup(ErrorReporter reporter, Server server) {
|
||||
this.reporter = reporter;
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the underlying input stream that is associated with a given filter input stream.
|
||||
* @param filtered - the filter input stream.
|
||||
* @return The underlying input stream that is being filtered.
|
||||
* @throws FieldAccessException Unable to access input stream.
|
||||
*/
|
||||
protected static InputStream getInputStream(FilterInputStream filtered) {
|
||||
if (filteredInputField == null)
|
||||
filteredInputField = FuzzyReflection.fromClass(FilterInputStream.class, true).
|
||||
getFieldByType("in", InputStream.class);
|
||||
|
||||
InputStream current = filtered;
|
||||
|
||||
try {
|
||||
// Iterate until we find the real input stream
|
||||
while (current instanceof FilterInputStream) {
|
||||
current = (InputStream) FieldUtils.readField(filteredInputField, current, true);
|
||||
}
|
||||
return current;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new FieldAccessException("Cannot access filtered input field.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject the given server thread or dedicated connection.
|
||||
* @param container - class that contains a ServerSocket field.
|
||||
*/
|
||||
public abstract void inject(Object container);
|
||||
|
||||
/**
|
||||
* Invoked when the world has loaded.
|
||||
*/
|
||||
public abstract void postWorldLoaded();
|
||||
|
||||
/**
|
||||
* Retrieve the associated socket injector for a player.
|
||||
* @param input - the indentifying filtered input stream.
|
||||
* @return The socket injector we have associated with this player.
|
||||
*/
|
||||
public abstract SocketInjector waitSocketInjector(InputStream input);
|
||||
|
||||
/**
|
||||
* Retrieve an injector by its socket.
|
||||
* @param socket - the socket.
|
||||
* @return The socket injector.
|
||||
*/
|
||||
public abstract SocketInjector waitSocketInjector(Socket socket);
|
||||
|
||||
/**
|
||||
* Retrieve a injector by its address.
|
||||
* @param address - the address of the socket.
|
||||
* @return The socket injector, or NULL if not found.
|
||||
*/
|
||||
public abstract SocketInjector waitSocketInjector(SocketAddress address);
|
||||
|
||||
/**
|
||||
* Attempt to get a socket injector without blocking the thread.
|
||||
* @param address - the address to lookup.
|
||||
* @return The socket injector, or NULL if not found.
|
||||
*/
|
||||
public abstract SocketInjector peekSocketInjector(SocketAddress address);
|
||||
|
||||
/**
|
||||
* Associate a given socket address to the provided socket injector.
|
||||
* @param address - the socket address to associate.
|
||||
* @param injector - the injector.
|
||||
*/
|
||||
public abstract void setSocketInjector(SocketAddress address, SocketInjector injector);
|
||||
|
||||
/**
|
||||
* If a player can hold a reference to its parent injector, this method will update that reference.
|
||||
* @param previous - the previous injector.
|
||||
* @param current - the new injector.
|
||||
*/
|
||||
protected void onPreviousSocketOverwritten(SocketInjector previous, SocketInjector current) {
|
||||
Player player = previous.getPlayer();
|
||||
|
||||
// Default implementation
|
||||
if (player instanceof InjectorContainer) {
|
||||
TemporaryPlayerFactory.setInjectorInPlayer(player, current);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the injection should be undone.
|
||||
*/
|
||||
public abstract void cleanupAll();
|
||||
package com.comphenix.protocol.injector.server;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
|
||||
public abstract class AbstractInputStreamLookup {
|
||||
// Error reporter
|
||||
protected final ErrorReporter reporter;
|
||||
|
||||
// Reference to the server itself
|
||||
protected final Server server;
|
||||
|
||||
protected AbstractInputStreamLookup(ErrorReporter reporter, Server server) {
|
||||
this.reporter = reporter;
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject the given server thread or dedicated connection.
|
||||
* @param container - class that contains a ServerSocket field.
|
||||
*/
|
||||
public abstract void inject(Object container);
|
||||
|
||||
/**
|
||||
* Invoked when the world has loaded.
|
||||
*/
|
||||
public abstract void postWorldLoaded();
|
||||
|
||||
/**
|
||||
* Retrieve the associated socket injector for a player.
|
||||
* @param input - the indentifying filtered input stream.
|
||||
* @return The socket injector we have associated with this player.
|
||||
*/
|
||||
public abstract SocketInjector waitSocketInjector(InputStream input);
|
||||
|
||||
/**
|
||||
* Retrieve an injector by its socket.
|
||||
* @param socket - the socket.
|
||||
* @return The socket injector.
|
||||
*/
|
||||
public abstract SocketInjector waitSocketInjector(Socket socket);
|
||||
|
||||
/**
|
||||
* Retrieve a injector by its address.
|
||||
* @param address - the address of the socket.
|
||||
* @return The socket injector, or NULL if not found.
|
||||
*/
|
||||
public abstract SocketInjector waitSocketInjector(SocketAddress address);
|
||||
|
||||
/**
|
||||
* Attempt to get a socket injector without blocking the thread.
|
||||
* @param address - the address to lookup.
|
||||
* @return The socket injector, or NULL if not found.
|
||||
*/
|
||||
public abstract SocketInjector peekSocketInjector(SocketAddress address);
|
||||
|
||||
/**
|
||||
* Associate a given socket address to the provided socket injector.
|
||||
* @param address - the socket address to associate.
|
||||
* @param injector - the injector.
|
||||
*/
|
||||
public abstract void setSocketInjector(SocketAddress address, SocketInjector injector);
|
||||
|
||||
/**
|
||||
* If a player can hold a reference to its parent injector, this method will update that reference.
|
||||
* @param previous - the previous injector.
|
||||
* @param current - the new injector.
|
||||
*/
|
||||
protected void onPreviousSocketOverwritten(SocketInjector previous, SocketInjector current) {
|
||||
Player player = previous.getPlayer();
|
||||
|
||||
// Default implementation
|
||||
if (player instanceof InjectorContainer) {
|
||||
TemporaryPlayerFactory.setInjectorInPlayer(player, current);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the injection should be undone.
|
||||
*/
|
||||
public abstract void cleanupAll();
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package com.comphenix.protocol.injector.server;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class BukkitSocketInjector implements SocketInjector {
|
||||
/**
|
||||
* Represents a single send packet command.
|
||||
* @author Kristian
|
||||
*/
|
||||
static class SendPacketCommand {
|
||||
private final Object packet;
|
||||
private final boolean filtered;
|
||||
|
||||
public SendPacketCommand(Object packet, boolean filtered) {
|
||||
this.packet = packet;
|
||||
this.filtered = filtered;
|
||||
}
|
||||
|
||||
public Object getPacket() {
|
||||
return packet;
|
||||
}
|
||||
|
||||
public boolean isFiltered() {
|
||||
return filtered;
|
||||
}
|
||||
}
|
||||
|
||||
private Player player;
|
||||
|
||||
// Queue of server packets
|
||||
private List<SendPacketCommand> syncronizedQueue = Collections.synchronizedList(new ArrayList<SendPacketCommand>());
|
||||
|
||||
/**
|
||||
* Represents a temporary socket injector.
|
||||
* @param temporaryPlayer -
|
||||
*/
|
||||
public BukkitSocketInjector(Player player) {
|
||||
if (player == null)
|
||||
throw new IllegalArgumentException("Player cannot be NULL.");
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket getSocket() throws IllegalAccessException {
|
||||
throw new UnsupportedOperationException("Cannot get socket from Bukkit player.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketAddress getAddress() throws IllegalAccessException {
|
||||
return player.getAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect(String message) throws InvocationTargetException {
|
||||
player.kickPlayer(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendServerPacket(Object packet, boolean filtered)
|
||||
throws InvocationTargetException {
|
||||
SendPacketCommand command = new SendPacketCommand(packet, filtered);
|
||||
|
||||
// Queue until we can find something better
|
||||
syncronizedQueue.add(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Player getUpdatedPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transferState(SocketInjector delegate) {
|
||||
// Transmit all queued packets to a different injector.
|
||||
try {
|
||||
synchronized(syncronizedQueue) {
|
||||
for (SendPacketCommand command : syncronizedQueue) {
|
||||
delegate.sendServerPacket(command.getPacket(), command.isFiltered());
|
||||
}
|
||||
syncronizedQueue.clear();
|
||||
}
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException("Unable to transmit packets to " + delegate + " from old injector.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUpdatedPlayer(Player updatedPlayer) {
|
||||
this.player = updatedPlayer;
|
||||
}
|
||||
}
|
@ -1,164 +1,191 @@
|
||||
package com.comphenix.protocol.injector.server;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.bukkit.Server;
|
||||
|
||||
import com.comphenix.protocol.concurrency.BlockingHashMap;
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.google.common.collect.MapMaker;
|
||||
|
||||
class InputStreamReflectLookup extends AbstractInputStreamLookup {
|
||||
// The default lookup timeout
|
||||
private static final long DEFAULT_TIMEOUT = 2000; // ms
|
||||
|
||||
// Using weak keys and values ensures that we will not hold up garbage collection
|
||||
protected BlockingHashMap<SocketAddress, SocketInjector> addressLookup = new BlockingHashMap<SocketAddress, SocketInjector>();
|
||||
protected ConcurrentMap<InputStream, SocketAddress> inputLookup = new MapMaker().weakValues().makeMap();
|
||||
|
||||
// The timeout
|
||||
private final long injectorTimeout;
|
||||
|
||||
public InputStreamReflectLookup(ErrorReporter reporter, Server server) {
|
||||
this(reporter, server, DEFAULT_TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a reflect lookup with a given default injector timeout.
|
||||
* <p>
|
||||
* This timeout defines the maximum amount of time to wait until an injector has been discovered.
|
||||
* @param reporter - the error reporter.
|
||||
* @param server - the current Bukkit server.
|
||||
* @param injectorTimeout - the injector timeout.
|
||||
*/
|
||||
public InputStreamReflectLookup(ErrorReporter reporter, Server server, long injectorTimeout) {
|
||||
super(reporter, server);
|
||||
this.injectorTimeout = injectorTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inject(Object container) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postWorldLoaded() {
|
||||
// Nothing again
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketInjector peekSocketInjector(SocketAddress address) {
|
||||
try {
|
||||
return addressLookup.get(address, 0, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
// Whatever
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketInjector waitSocketInjector(SocketAddress address) {
|
||||
try {
|
||||
// Note that we actually SWALLOW interrupts here - this is because Minecraft uses interrupts to
|
||||
// periodically wake up waiting readers and writers. We have to wait for the dedicated server thread
|
||||
// to catch up, so we'll swallow these interrupts.
|
||||
//
|
||||
// TODO: Consider if we should raise the thread priority of the dedicated server listener thread.
|
||||
return addressLookup.get(address, injectorTimeout, TimeUnit.MILLISECONDS, true);
|
||||
} catch (InterruptedException e) {
|
||||
// This cannot be!
|
||||
throw new IllegalStateException("Impossible exception occured!", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketInjector waitSocketInjector(Socket socket) {
|
||||
return waitSocketInjector(socket.getRemoteSocketAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketInjector waitSocketInjector(InputStream input) {
|
||||
try {
|
||||
SocketAddress address = waitSocketAddress(input);
|
||||
|
||||
// Guard against NPE
|
||||
if (address != null)
|
||||
return waitSocketInjector(address);
|
||||
else
|
||||
return null;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new FieldAccessException("Cannot find or access socket field for " + input, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use reflection to get the underlying socket address from an input stream.
|
||||
* @param stream - the socket stream to lookup.
|
||||
* @return The underlying socket address, or NULL if not found.
|
||||
* @throws IllegalAccessException Unable to access socket field.
|
||||
*/
|
||||
private SocketAddress waitSocketAddress(InputStream stream) throws IllegalAccessException {
|
||||
// Extra check, just in case
|
||||
if (stream instanceof FilterInputStream)
|
||||
return waitSocketAddress(getInputStream((FilterInputStream) stream));
|
||||
|
||||
SocketAddress result = inputLookup.get(stream);
|
||||
|
||||
if (result == null) {
|
||||
Socket socket = lookupSocket(stream);
|
||||
|
||||
// Save it
|
||||
result = socket.getRemoteSocketAddress();
|
||||
inputLookup.put(stream, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSocketInjector(SocketAddress address, SocketInjector injector) {
|
||||
if (address == null)
|
||||
throw new IllegalArgumentException("address cannot be NULL");
|
||||
if (injector == null)
|
||||
throw new IllegalArgumentException("injector cannot be NULL.");
|
||||
|
||||
SocketInjector previous = addressLookup.put(address, injector);
|
||||
|
||||
// Any previous temporary players will also be associated
|
||||
if (previous != null) {
|
||||
// Update the reference to any previous injector
|
||||
onPreviousSocketOverwritten(previous, injector);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanupAll() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup the underlying socket of a stream through reflection.
|
||||
* @param stream - the socket stream.
|
||||
* @return The underlying socket.
|
||||
* @throws IllegalAccessException If reflection failed.
|
||||
*/
|
||||
private static Socket lookupSocket(InputStream stream) throws IllegalAccessException {
|
||||
if (stream instanceof FilterInputStream) {
|
||||
return lookupSocket(getInputStream((FilterInputStream) stream));
|
||||
} else {
|
||||
// Just do it
|
||||
Field socketField = FuzzyReflection.fromObject(stream, true).
|
||||
getFieldByType("socket", Socket.class);
|
||||
|
||||
return (Socket) FieldUtils.readField(socketField, stream, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.comphenix.protocol.injector.server;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.bukkit.Server;
|
||||
|
||||
import com.comphenix.protocol.concurrency.BlockingHashMap;
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.google.common.collect.MapMaker;
|
||||
|
||||
class InputStreamReflectLookup extends AbstractInputStreamLookup {
|
||||
// Used to access the inner input stream of a filtered input stream
|
||||
private static Field filteredInputField;
|
||||
|
||||
// The default lookup timeout
|
||||
private static final long DEFAULT_TIMEOUT = 2000; // ms
|
||||
|
||||
// Using weak keys and values ensures that we will not hold up garbage collection
|
||||
protected BlockingHashMap<SocketAddress, SocketInjector> addressLookup = new BlockingHashMap<SocketAddress, SocketInjector>();
|
||||
protected ConcurrentMap<InputStream, SocketAddress> inputLookup = new MapMaker().weakValues().makeMap();
|
||||
|
||||
// The timeout
|
||||
private final long injectorTimeout;
|
||||
|
||||
public InputStreamReflectLookup(ErrorReporter reporter, Server server) {
|
||||
this(reporter, server, DEFAULT_TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a reflect lookup with a given default injector timeout.
|
||||
* <p>
|
||||
* This timeout defines the maximum amount of time to wait until an injector has been discovered.
|
||||
* @param reporter - the error reporter.
|
||||
* @param server - the current Bukkit server.
|
||||
* @param injectorTimeout - the injector timeout.
|
||||
*/
|
||||
public InputStreamReflectLookup(ErrorReporter reporter, Server server, long injectorTimeout) {
|
||||
super(reporter, server);
|
||||
this.injectorTimeout = injectorTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inject(Object container) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postWorldLoaded() {
|
||||
// Nothing again
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketInjector peekSocketInjector(SocketAddress address) {
|
||||
try {
|
||||
return addressLookup.get(address, 0, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
// Whatever
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketInjector waitSocketInjector(SocketAddress address) {
|
||||
try {
|
||||
// Note that we actually SWALLOW interrupts here - this is because Minecraft uses interrupts to
|
||||
// periodically wake up waiting readers and writers. We have to wait for the dedicated server thread
|
||||
// to catch up, so we'll swallow these interrupts.
|
||||
//
|
||||
// TODO: Consider if we should raise the thread priority of the dedicated server listener thread.
|
||||
return addressLookup.get(address, injectorTimeout, TimeUnit.MILLISECONDS, true);
|
||||
} catch (InterruptedException e) {
|
||||
// This cannot be!
|
||||
throw new IllegalStateException("Impossible exception occured!", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketInjector waitSocketInjector(Socket socket) {
|
||||
return waitSocketInjector(socket.getRemoteSocketAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketInjector waitSocketInjector(InputStream input) {
|
||||
try {
|
||||
SocketAddress address = waitSocketAddress(input);
|
||||
|
||||
// Guard against NPE
|
||||
if (address != null)
|
||||
return waitSocketInjector(address);
|
||||
else
|
||||
return null;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new FieldAccessException("Cannot find or access socket field for " + input, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use reflection to get the underlying socket address from an input stream.
|
||||
* @param stream - the socket stream to lookup.
|
||||
* @return The underlying socket address, or NULL if not found.
|
||||
* @throws IllegalAccessException Unable to access socket field.
|
||||
*/
|
||||
private SocketAddress waitSocketAddress(InputStream stream) throws IllegalAccessException {
|
||||
// Extra check, just in case
|
||||
if (stream instanceof FilterInputStream)
|
||||
return waitSocketAddress(getInputStream((FilterInputStream) stream));
|
||||
|
||||
SocketAddress result = inputLookup.get(stream);
|
||||
|
||||
if (result == null) {
|
||||
Socket socket = lookupSocket(stream);
|
||||
|
||||
// Save it
|
||||
result = socket.getRemoteSocketAddress();
|
||||
inputLookup.put(stream, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the underlying input stream that is associated with a given filter input stream.
|
||||
* @param filtered - the filter input stream.
|
||||
* @return The underlying input stream that is being filtered.
|
||||
* @throws FieldAccessException Unable to access input stream.
|
||||
*/
|
||||
protected static InputStream getInputStream(FilterInputStream filtered) {
|
||||
if (filteredInputField == null)
|
||||
filteredInputField = FuzzyReflection.fromClass(FilterInputStream.class, true).
|
||||
getFieldByType("in", InputStream.class);
|
||||
|
||||
InputStream current = filtered;
|
||||
|
||||
try {
|
||||
// Iterate until we find the real input stream
|
||||
while (current instanceof FilterInputStream) {
|
||||
current = (InputStream) FieldUtils.readField(filteredInputField, current, true);
|
||||
}
|
||||
return current;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new FieldAccessException("Cannot access filtered input field.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSocketInjector(SocketAddress address, SocketInjector injector) {
|
||||
if (address == null)
|
||||
throw new IllegalArgumentException("address cannot be NULL");
|
||||
if (injector == null)
|
||||
throw new IllegalArgumentException("injector cannot be NULL.");
|
||||
|
||||
SocketInjector previous = addressLookup.put(address, injector);
|
||||
|
||||
// Any previous temporary players will also be associated
|
||||
if (previous != null) {
|
||||
// Update the reference to any previous injector
|
||||
onPreviousSocketOverwritten(previous, injector);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanupAll() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup the underlying socket of a stream through reflection.
|
||||
* @param stream - the socket stream.
|
||||
* @return The underlying socket.
|
||||
* @throws IllegalAccessException If reflection failed.
|
||||
*/
|
||||
private static Socket lookupSocket(InputStream stream) throws IllegalAccessException {
|
||||
if (stream instanceof FilterInputStream) {
|
||||
return lookupSocket(getInputStream((FilterInputStream) stream));
|
||||
} else {
|
||||
// Just do it
|
||||
Field socketField = FuzzyReflection.fromObject(stream, true).
|
||||
getFieldByType("socket", Socket.class);
|
||||
|
||||
return (Socket) FieldUtils.readField(socketField, stream, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,4 +58,10 @@ public interface SocketInjector {
|
||||
* @param delegate - the new injector.
|
||||
*/
|
||||
public abstract void transferState(SocketInjector delegate);
|
||||
|
||||
/**
|
||||
* Set the real Bukkit player that we will use.
|
||||
* @param updatedPlayer - the real Bukkit player.
|
||||
*/
|
||||
public abstract void setUpdatedPlayer(Player updatedPlayer);
|
||||
}
|
@ -1,195 +1,197 @@
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.injector.server;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import net.sf.cglib.proxy.Callback;
|
||||
import net.sf.cglib.proxy.CallbackFilter;
|
||||
import net.sf.cglib.proxy.Enhancer;
|
||||
import net.sf.cglib.proxy.MethodInterceptor;
|
||||
import net.sf.cglib.proxy.MethodProxy;
|
||||
import net.sf.cglib.proxy.NoOp;
|
||||
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import com.comphenix.protocol.injector.PacketConstructor;
|
||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||
|
||||
/**
|
||||
* Create fake player instances that represents pre-authenticated clients.
|
||||
*/
|
||||
public class TemporaryPlayerFactory {
|
||||
// Helpful constructors
|
||||
private final PacketConstructor chatPacket;
|
||||
|
||||
// Prevent too many class creations
|
||||
private static CallbackFilter callbackFilter;
|
||||
|
||||
public TemporaryPlayerFactory() {
|
||||
chatPacket = PacketConstructor.DEFAULT.withPacket(3, new Object[] { "DEMO" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the injector from a given player if it contains one.
|
||||
* @param player - the player that may contain a reference to a player injector.
|
||||
* @return The referenced player injector, or NULL if none can be found.
|
||||
*/
|
||||
public static SocketInjector getInjectorFromPlayer(Player player) {
|
||||
if (player instanceof InjectorContainer) {
|
||||
return ((InjectorContainer) player).getInjector();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the player injector, if possible.
|
||||
* @param player - the player to update.
|
||||
* @param injector - the injector to store.
|
||||
*/
|
||||
public static void setInjectorInPlayer(Player player, SocketInjector injector) {
|
||||
((InjectorContainer) player).setInjector(injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a temporary player that supports a subset of every player command.
|
||||
* <p>
|
||||
* Supported methods include:
|
||||
* <ul>
|
||||
* <li>getPlayer()</li>
|
||||
* <li>getAddress()</li>
|
||||
* <li>getServer()</li>
|
||||
* <li>chat(String)</li>
|
||||
* <li>sendMessage(String)</li>
|
||||
* <li>sendMessage(String[])</li>
|
||||
* <li>kickPlayer(String)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Note that a temporary player has not yet been assigned a name, and thus cannot be
|
||||
* uniquely identified. Use the address instead.
|
||||
* @param injector - the player injector used.
|
||||
* @param server - the current server.
|
||||
* @return A temporary player instance.
|
||||
*/
|
||||
public Player createTemporaryPlayer(final Server server) {
|
||||
|
||||
// Default implementation
|
||||
Callback implementation = new MethodInterceptor() {
|
||||
@Override
|
||||
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
|
||||
|
||||
String methodName = method.getName();
|
||||
SocketInjector injector = ((InjectorContainer) obj).getInjector();
|
||||
|
||||
if (injector == null)
|
||||
throw new IllegalStateException("Unable to find injector.");
|
||||
|
||||
// Use the socket to get the address
|
||||
if (methodName.equalsIgnoreCase("getName"))
|
||||
return "UNKNOWN[" + injector.getSocket().getRemoteSocketAddress() + "]";
|
||||
if (methodName.equalsIgnoreCase("getPlayer"))
|
||||
return injector.getUpdatedPlayer();
|
||||
if (methodName.equalsIgnoreCase("getAddress"))
|
||||
return injector.getAddress();
|
||||
if (methodName.equalsIgnoreCase("getServer"))
|
||||
return server;
|
||||
|
||||
try {
|
||||
// Handle send message methods
|
||||
if (methodName.equalsIgnoreCase("chat") || methodName.equalsIgnoreCase("sendMessage")) {
|
||||
Object argument = args[0];
|
||||
|
||||
// Dynamic overloading
|
||||
if (argument instanceof String) {
|
||||
return sendMessage(injector, (String) argument);
|
||||
} else if (argument instanceof String[]) {
|
||||
for (String message : (String[]) argument) {
|
||||
sendMessage(injector, message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch (InvocationTargetException e) {
|
||||
throw e.getCause();
|
||||
}
|
||||
|
||||
// Also, handle kicking
|
||||
if (methodName.equalsIgnoreCase("kickPlayer")) {
|
||||
injector.disconnect((String) args[0]);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ignore all other methods
|
||||
throw new UnsupportedOperationException(
|
||||
"The method " + method.getName() + " is not supported for temporary players.");
|
||||
}
|
||||
};
|
||||
|
||||
// Shared callback filter
|
||||
if (callbackFilter == null) {
|
||||
callbackFilter = new CallbackFilter() {
|
||||
@Override
|
||||
public int accept(Method method) {
|
||||
// Do not override the object method or the superclass methods
|
||||
if (method.getDeclaringClass().equals(Object.class) ||
|
||||
method.getDeclaringClass().equals(InjectorContainer.class))
|
||||
return 0;
|
||||
else
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// CGLib is amazing
|
||||
Enhancer ex = new Enhancer();
|
||||
ex.setSuperclass(InjectorContainer.class);
|
||||
ex.setInterfaces(new Class[] { Player.class });
|
||||
ex.setCallbacks(new Callback[] { NoOp.INSTANCE, implementation });
|
||||
ex.setCallbackFilter(callbackFilter);
|
||||
|
||||
return (Player) ex.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a temporary player with the given associated socket injector.
|
||||
* @param server - the parent server.
|
||||
* @param injector - the referenced socket injector.
|
||||
* @return The temporary player.
|
||||
*/
|
||||
public Player createTemporaryPlayer(Server server, SocketInjector injector) {
|
||||
Player temporary = createTemporaryPlayer(server);
|
||||
|
||||
((InjectorContainer) temporary).setInjector(injector);
|
||||
return temporary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the given client.
|
||||
* @param injector - the injector representing the client.
|
||||
* @param message - a message.
|
||||
* @return Always NULL.
|
||||
* @throws InvocationTargetException If the message couldn't be sent.
|
||||
* @throws FieldAccessException If we were unable to construct the message packet.
|
||||
*/
|
||||
private Object sendMessage(SocketInjector injector, String message) throws InvocationTargetException, FieldAccessException {
|
||||
injector.sendServerPacket(chatPacket.createPacket(message).getHandle(), false);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.injector.server;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import net.sf.cglib.proxy.Callback;
|
||||
import net.sf.cglib.proxy.CallbackFilter;
|
||||
import net.sf.cglib.proxy.Enhancer;
|
||||
import net.sf.cglib.proxy.MethodInterceptor;
|
||||
import net.sf.cglib.proxy.MethodProxy;
|
||||
import net.sf.cglib.proxy.NoOp;
|
||||
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import com.comphenix.protocol.injector.PacketConstructor;
|
||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||
|
||||
/**
|
||||
* Create fake player instances that represents pre-authenticated clients.
|
||||
*/
|
||||
public class TemporaryPlayerFactory {
|
||||
// Helpful constructors
|
||||
private final PacketConstructor chatPacket;
|
||||
|
||||
// Prevent too many class creations
|
||||
private static CallbackFilter callbackFilter;
|
||||
|
||||
public TemporaryPlayerFactory() {
|
||||
chatPacket = PacketConstructor.DEFAULT.withPacket(3, new Object[] { "DEMO" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the injector from a given player if it contains one.
|
||||
* @param player - the player that may contain a reference to a player injector.
|
||||
* @return The referenced player injector, or NULL if none can be found.
|
||||
*/
|
||||
public static SocketInjector getInjectorFromPlayer(Player player) {
|
||||
if (player instanceof InjectorContainer) {
|
||||
return ((InjectorContainer) player).getInjector();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the player injector, if possible.
|
||||
* @param player - the player to update.
|
||||
* @param injector - the injector to store.
|
||||
*/
|
||||
public static void setInjectorInPlayer(Player player, SocketInjector injector) {
|
||||
((InjectorContainer) player).setInjector(injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a temporary player that supports a subset of every player command.
|
||||
* <p>
|
||||
* Supported methods include:
|
||||
* <ul>
|
||||
* <li>getPlayer()</li>
|
||||
* <li>getAddress()</li>
|
||||
* <li>getServer()</li>
|
||||
* <li>chat(String)</li>
|
||||
* <li>sendMessage(String)</li>
|
||||
* <li>sendMessage(String[])</li>
|
||||
* <li>kickPlayer(String)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Note that a temporary player has not yet been assigned a name, and thus cannot be
|
||||
* uniquely identified. Use the address instead.
|
||||
* @param injector - the player injector used.
|
||||
* @param server - the current server.
|
||||
* @return A temporary player instance.
|
||||
*/
|
||||
public Player createTemporaryPlayer(final Server server) {
|
||||
|
||||
// Default implementation
|
||||
Callback implementation = new MethodInterceptor() {
|
||||
@Override
|
||||
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
|
||||
|
||||
String methodName = method.getName();
|
||||
SocketInjector injector = ((InjectorContainer) obj).getInjector();
|
||||
|
||||
if (injector == null)
|
||||
throw new IllegalStateException("Unable to find injector.");
|
||||
|
||||
// Use the socket to get the address
|
||||
if (methodName.equalsIgnoreCase("isOnline"))
|
||||
return injector.getSocket() != null && injector.getSocket().isConnected();
|
||||
if (methodName.equalsIgnoreCase("getName"))
|
||||
return "UNKNOWN[" + injector.getSocket().getRemoteSocketAddress() + "]";
|
||||
if (methodName.equalsIgnoreCase("getPlayer"))
|
||||
return injector.getUpdatedPlayer();
|
||||
if (methodName.equalsIgnoreCase("getAddress"))
|
||||
return injector.getAddress();
|
||||
if (methodName.equalsIgnoreCase("getServer"))
|
||||
return server;
|
||||
|
||||
try {
|
||||
// Handle send message methods
|
||||
if (methodName.equalsIgnoreCase("chat") || methodName.equalsIgnoreCase("sendMessage")) {
|
||||
Object argument = args[0];
|
||||
|
||||
// Dynamic overloading
|
||||
if (argument instanceof String) {
|
||||
return sendMessage(injector, (String) argument);
|
||||
} else if (argument instanceof String[]) {
|
||||
for (String message : (String[]) argument) {
|
||||
sendMessage(injector, message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch (InvocationTargetException e) {
|
||||
throw e.getCause();
|
||||
}
|
||||
|
||||
// Also, handle kicking
|
||||
if (methodName.equalsIgnoreCase("kickPlayer")) {
|
||||
injector.disconnect((String) args[0]);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ignore all other methods
|
||||
throw new UnsupportedOperationException(
|
||||
"The method " + method.getName() + " is not supported for temporary players.");
|
||||
}
|
||||
};
|
||||
|
||||
// Shared callback filter
|
||||
if (callbackFilter == null) {
|
||||
callbackFilter = new CallbackFilter() {
|
||||
@Override
|
||||
public int accept(Method method) {
|
||||
// Do not override the object method or the superclass methods
|
||||
if (method.getDeclaringClass().equals(Object.class) ||
|
||||
method.getDeclaringClass().equals(InjectorContainer.class))
|
||||
return 0;
|
||||
else
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// CGLib is amazing
|
||||
Enhancer ex = new Enhancer();
|
||||
ex.setSuperclass(InjectorContainer.class);
|
||||
ex.setInterfaces(new Class[] { Player.class });
|
||||
ex.setCallbacks(new Callback[] { NoOp.INSTANCE, implementation });
|
||||
ex.setCallbackFilter(callbackFilter);
|
||||
|
||||
return (Player) ex.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a temporary player with the given associated socket injector.
|
||||
* @param server - the parent server.
|
||||
* @param injector - the referenced socket injector.
|
||||
* @return The temporary player.
|
||||
*/
|
||||
public Player createTemporaryPlayer(Server server, SocketInjector injector) {
|
||||
Player temporary = createTemporaryPlayer(server);
|
||||
|
||||
((InjectorContainer) temporary).setInjector(injector);
|
||||
return temporary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the given client.
|
||||
* @param injector - the injector representing the client.
|
||||
* @param message - a message.
|
||||
* @return Always NULL.
|
||||
* @throws InvocationTargetException If the message couldn't be sent.
|
||||
* @throws FieldAccessException If we were unable to construct the message packet.
|
||||
*/
|
||||
private Object sendMessage(SocketInjector injector, String message) throws InvocationTargetException, FieldAccessException {
|
||||
injector.sendServerPacket(chatPacket.createPacket(message).getHandle(), false);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ class DummyPlayerHandler implements PlayerInjectionHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException {
|
||||
public void recieveClientPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException {
|
||||
injector.processPacket(player, mcPacket);
|
||||
}
|
||||
|
||||
@ -119,4 +119,9 @@ class DummyPlayerHandler implements PlayerInjectionHandler {
|
||||
public void postWorldLoaded() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePlayer(Player player) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,9 @@ import net.sf.cglib.proxy.NoOp;
|
||||
|
||||
import com.comphenix.protocol.Packets;
|
||||
import com.comphenix.protocol.concurrency.IntegerSet;
|
||||
import com.comphenix.protocol.error.DelegatedErrorReporter;
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.error.Report;
|
||||
import com.comphenix.protocol.events.PacketContainer;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.comphenix.protocol.injector.ListenerInvoker;
|
||||
@ -47,7 +49,7 @@ public class SpigotPacketInjector implements SpigotPacketListener {
|
||||
private static volatile boolean classChecked;
|
||||
|
||||
// Retrieve the entity player from a PlayerConnection
|
||||
private static Field playerConnectionPlayer;
|
||||
private static volatile Field playerConnectionPlayer;
|
||||
|
||||
// Packets that are not to be processed by the filters
|
||||
private Set<Object> ignoredPackets = Collections.newSetFromMap(new MapMaker().weakKeys().<Object, Boolean>makeMap());
|
||||
@ -275,7 +277,8 @@ public class SpigotPacketInjector implements SpigotPacketListener {
|
||||
if (dummyInjector == null) {
|
||||
// Inject the network manager
|
||||
try {
|
||||
NetworkObjectInjector created = new NetworkObjectInjector(classLoader, reporter, null, invoker, null);
|
||||
NetworkObjectInjector created = new NetworkObjectInjector(
|
||||
classLoader, filterImpossibleWarnings(reporter), null, invoker, null);
|
||||
|
||||
if (MinecraftReflection.isLoginHandler(connection)) {
|
||||
created.initialize(connection);
|
||||
@ -303,6 +306,23 @@ public class SpigotPacketInjector implements SpigotPacketListener {
|
||||
return dummyInjector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a delegated error reporter that ignores certain warnings that are irrelevant on Spigot.
|
||||
* @param reporter - error reporter to delegate.
|
||||
* @return The filtered error reporter.
|
||||
*/
|
||||
private ErrorReporter filterImpossibleWarnings(ErrorReporter reporter) {
|
||||
return new DelegatedErrorReporter(reporter) {
|
||||
@Override
|
||||
protected Report filterReport(Object sender, Report report, boolean detailed) {
|
||||
// This doesn't matter - ignore it
|
||||
if (report.getType() == NetworkObjectInjector.REPORT_DETECTED_CUSTOM_SERVER_HANDLER)
|
||||
return null;
|
||||
return report;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a given player injector for later.
|
||||
* @param networkManager - the associated network manager.
|
||||
@ -400,7 +420,8 @@ public class SpigotPacketInjector implements SpigotPacketListener {
|
||||
*/
|
||||
void injectPlayer(Player player) {
|
||||
try {
|
||||
NetworkObjectInjector dummy = new NetworkObjectInjector(classLoader, reporter, player, invoker, null);
|
||||
NetworkObjectInjector dummy = new NetworkObjectInjector(
|
||||
classLoader, filterImpossibleWarnings(reporter), player, invoker, null);
|
||||
dummy.initializePlayer(player);
|
||||
|
||||
// Save this player for the network manager
|
||||
|
@ -1,360 +1,365 @@
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.reflect.compiler;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryPoolMXBean;
|
||||
import java.lang.management.MemoryUsage;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.reflect.StructureModifier;
|
||||
import com.comphenix.protocol.reflect.compiler.StructureCompiler.StructureKey;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
|
||||
/**
|
||||
* Compiles structure modifiers on a background thread.
|
||||
* <p>
|
||||
* This is necessary as we cannot block the main thread.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class BackgroundCompiler {
|
||||
|
||||
/**
|
||||
* The default format for the name of new worker threads.
|
||||
*/
|
||||
public static final String THREAD_FORMAT = "ProtocolLib-StructureCompiler %s";
|
||||
|
||||
// How long to wait for a shutdown
|
||||
public static final int SHUTDOWN_DELAY_MS = 2000;
|
||||
|
||||
/**
|
||||
* The default fraction of perm gen space after which the background compiler will be disabled.
|
||||
*/
|
||||
public static final double DEFAULT_DISABLE_AT_PERM_GEN = 0.65;
|
||||
|
||||
// The single background compiler we're using
|
||||
private static BackgroundCompiler backgroundCompiler;
|
||||
|
||||
// Classes we're currently compiling
|
||||
private Map<StructureKey, List<CompileListener<?>>> listeners = Maps.newHashMap();
|
||||
private Object listenerLock = new Object();
|
||||
|
||||
private StructureCompiler compiler;
|
||||
private boolean enabled;
|
||||
private boolean shuttingDown;
|
||||
|
||||
private ExecutorService executor;
|
||||
private ErrorReporter reporter;
|
||||
|
||||
private double disablePermGenFraction = DEFAULT_DISABLE_AT_PERM_GEN;
|
||||
|
||||
/**
|
||||
* Retrieves the current background compiler.
|
||||
* @return Current background compiler.
|
||||
*/
|
||||
public static BackgroundCompiler getInstance() {
|
||||
return backgroundCompiler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the single background compiler we're using.
|
||||
* @param backgroundCompiler - current background compiler, or NULL if the library is not loaded.
|
||||
*/
|
||||
public static void setInstance(BackgroundCompiler backgroundCompiler) {
|
||||
BackgroundCompiler.backgroundCompiler = backgroundCompiler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a background compiler.
|
||||
* <p>
|
||||
* Uses the default {@link #THREAD_FORMAT} to name worker threads.
|
||||
* @param loader - class loader from Bukkit.
|
||||
* @param reporter - current error reporter.
|
||||
*/
|
||||
public BackgroundCompiler(ClassLoader loader, ErrorReporter reporter) {
|
||||
ThreadFactory factory = new ThreadFactoryBuilder().
|
||||
setDaemon(true).
|
||||
setNameFormat(THREAD_FORMAT).
|
||||
build();
|
||||
initializeCompiler(loader, reporter, Executors.newSingleThreadExecutor(factory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a background compiler utilizing the given thread pool.
|
||||
* @param loader - class loader from Bukkit.
|
||||
* @param reporter - current error reporter.
|
||||
* @param executor - thread pool we'll use.
|
||||
*/
|
||||
public BackgroundCompiler(ClassLoader loader, ErrorReporter reporter, ExecutorService executor) {
|
||||
initializeCompiler(loader, reporter, executor);
|
||||
}
|
||||
|
||||
// Avoid "Constructor call must be the first statement".
|
||||
private void initializeCompiler(ClassLoader loader, @Nullable ErrorReporter reporter, ExecutorService executor) {
|
||||
if (loader == null)
|
||||
throw new IllegalArgumentException("loader cannot be NULL");
|
||||
if (executor == null)
|
||||
throw new IllegalArgumentException("executor cannot be NULL");
|
||||
|
||||
this.compiler = new StructureCompiler(loader);
|
||||
this.reporter = reporter;
|
||||
this.executor = executor;
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the indirectly given structure modifier is eventually compiled.
|
||||
* @param cache - store of structure modifiers.
|
||||
* @param key - key of the structure modifier to compile.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public void scheduleCompilation(final Map<Class, StructureModifier> cache, final Class key) {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
final StructureModifier<Object> uncompiled = cache.get(key);
|
||||
|
||||
if (uncompiled != null) {
|
||||
scheduleCompilation(uncompiled, new CompileListener<Object>() {
|
||||
@Override
|
||||
public void onCompiled(StructureModifier<Object> compiledModifier) {
|
||||
// Update cache
|
||||
cache.put(key, compiledModifier);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the given structure modifier is eventually compiled.
|
||||
* @param uncompiled - structure modifier to compile.
|
||||
* @param listener - listener responsible for responding to the compilation.
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public <TKey> void scheduleCompilation(final StructureModifier<TKey> uncompiled, final CompileListener<TKey> listener) {
|
||||
// Only schedule if we're enabled
|
||||
if (enabled && !shuttingDown) {
|
||||
// Check perm gen
|
||||
if (getPermGenUsage() > disablePermGenFraction)
|
||||
return;
|
||||
|
||||
// Don't try to schedule anything
|
||||
if (executor == null || executor.isShutdown())
|
||||
return;
|
||||
|
||||
// Use to look up structure modifiers
|
||||
final StructureKey key = new StructureKey(uncompiled);
|
||||
|
||||
// Allow others to listen in too
|
||||
synchronized (listenerLock) {
|
||||
List list = listeners.get(key);
|
||||
|
||||
if (!listeners.containsKey(key)) {
|
||||
listeners.put(key, (List) Lists.newArrayList(listener));
|
||||
} else {
|
||||
// We're currently compiling
|
||||
list.add(listener);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the worker that will compile our modifier
|
||||
Callable<?> worker = new Callable<Object>() {
|
||||
@Override
|
||||
public Object call() throws Exception {
|
||||
StructureModifier<TKey> modifier = uncompiled;
|
||||
List list = null;
|
||||
|
||||
// Do our compilation
|
||||
try {
|
||||
modifier = compiler.compile(modifier);
|
||||
|
||||
synchronized (listenerLock) {
|
||||
list = listeners.get(key);
|
||||
}
|
||||
|
||||
// Only execute the listeners if there is a list
|
||||
if (list != null) {
|
||||
for (Object compileListener : list) {
|
||||
((CompileListener<TKey>) compileListener).onCompiled(modifier);
|
||||
}
|
||||
|
||||
// Remove it when we're done
|
||||
synchronized (listenerLock) {
|
||||
list = listeners.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Throwable e) {
|
||||
// Disable future compilations!
|
||||
setEnabled(false);
|
||||
|
||||
// Inform about this error as best as we can
|
||||
if (reporter != null) {
|
||||
reporter.reportDetailed(BackgroundCompiler.this,
|
||||
"Cannot compile structure. Disabing compiler.", e, uncompiled);
|
||||
} else {
|
||||
System.err.println("Exception occured in structure compiler: ");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// We'll also return the new structure modifier
|
||||
return modifier;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Lookup the previous class name on the main thread.
|
||||
// This is necessary as the Bukkit class loaders are not thread safe
|
||||
if (compiler.lookupClassLoader(uncompiled)) {
|
||||
try {
|
||||
worker.call();
|
||||
} catch (Exception e) {
|
||||
// Impossible!
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Perform the compilation on a seperate thread
|
||||
executor.submit(worker);
|
||||
}
|
||||
|
||||
} catch (RejectedExecutionException e) {
|
||||
// Occures when the underlying queue is overflowing. Since the compilation
|
||||
// is only an optmization and not really essential we'll just log this failure
|
||||
// and move on.
|
||||
reporter.reportWarning(this, "Unable to schedule compilation task.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a compile listener if we are still waiting for the structure modifier to be compiled.
|
||||
* @param uncompiled - the structure modifier that may get compiled.
|
||||
* @param listener - the listener to invoke in that case.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <TKey> void addListener(final StructureModifier<TKey> uncompiled, final CompileListener<TKey> listener) {
|
||||
synchronized (listenerLock) {
|
||||
StructureKey key = new StructureKey(uncompiled);
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
List list = listeners.get(key);
|
||||
|
||||
if (list != null) {
|
||||
list.add(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current usage of the Perm Gen space in percentage.
|
||||
* @return Usage of the perm gen space.
|
||||
*/
|
||||
private double getPermGenUsage() {
|
||||
for (MemoryPoolMXBean item : ManagementFactory.getMemoryPoolMXBeans()) {
|
||||
if (item.getName().contains("Perm Gen")) {
|
||||
MemoryUsage usage = item.getUsage();
|
||||
return usage.getUsed() / (double) usage.getCommitted();
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up after ourselves using the default timeout.
|
||||
*/
|
||||
public void shutdownAll() {
|
||||
shutdownAll(SHUTDOWN_DELAY_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up after ourselves.
|
||||
* @param timeout - the maximum time to wait.
|
||||
* @param unit - the time unit of the timeout argument.
|
||||
*/
|
||||
public void shutdownAll(long timeout, TimeUnit unit) {
|
||||
setEnabled(false);
|
||||
shuttingDown = true;
|
||||
executor.shutdown();
|
||||
|
||||
try {
|
||||
executor.awaitTermination(timeout, unit);
|
||||
} catch (InterruptedException e) {
|
||||
// Unlikely to ever occur - it's the main thread
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve whether or not the background compiler is enabled.
|
||||
* @return TRUE if it is enabled, FALSE otherwise.
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the background compiler is enabled.
|
||||
* @param enabled - TRUE to enable it, FALSE otherwise.
|
||||
*/
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the fraction of perm gen space used after which the background compiler will be disabled.
|
||||
* @return The fraction after which the background compiler is disabled.
|
||||
*/
|
||||
public double getDisablePermGenFraction() {
|
||||
return disablePermGenFraction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fraction of perm gen space used after which the background compiler will be disabled.
|
||||
* @param fraction - the maximum use of perm gen space.
|
||||
*/
|
||||
public void setDisablePermGenFraction(double fraction) {
|
||||
this.disablePermGenFraction = fraction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current structure compiler.
|
||||
* @return Current structure compiler.
|
||||
*/
|
||||
public StructureCompiler getCompiler() {
|
||||
return compiler;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program;
|
||||
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol.reflect.compiler;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryPoolMXBean;
|
||||
import java.lang.management.MemoryUsage;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.comphenix.protocol.error.ErrorReporter;
|
||||
import com.comphenix.protocol.error.Report;
|
||||
import com.comphenix.protocol.error.ReportType;
|
||||
import com.comphenix.protocol.reflect.StructureModifier;
|
||||
import com.comphenix.protocol.reflect.compiler.StructureCompiler.StructureKey;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
|
||||
/**
|
||||
* Compiles structure modifiers on a background thread.
|
||||
* <p>
|
||||
* This is necessary as we cannot block the main thread.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class BackgroundCompiler {
|
||||
public static final ReportType REPORT_CANNOT_COMPILE_STRUCTURE_MODIFIER = new ReportType("Cannot compile structure. Disabing compiler.");
|
||||
public static final ReportType REPORT_CANNOT_SCHEDULE_COMPILATION = new ReportType("Unable to schedule compilation task.");
|
||||
|
||||
/**
|
||||
* The default format for the name of new worker threads.
|
||||
*/
|
||||
public static final String THREAD_FORMAT = "ProtocolLib-StructureCompiler %s";
|
||||
|
||||
// How long to wait for a shutdown
|
||||
public static final int SHUTDOWN_DELAY_MS = 2000;
|
||||
|
||||
/**
|
||||
* The default fraction of perm gen space after which the background compiler will be disabled.
|
||||
*/
|
||||
public static final double DEFAULT_DISABLE_AT_PERM_GEN = 0.65;
|
||||
|
||||
// The single background compiler we're using
|
||||
private static BackgroundCompiler backgroundCompiler;
|
||||
|
||||
// Classes we're currently compiling
|
||||
private Map<StructureKey, List<CompileListener<?>>> listeners = Maps.newHashMap();
|
||||
private Object listenerLock = new Object();
|
||||
|
||||
private StructureCompiler compiler;
|
||||
private boolean enabled;
|
||||
private boolean shuttingDown;
|
||||
|
||||
private ExecutorService executor;
|
||||
private ErrorReporter reporter;
|
||||
|
||||
private double disablePermGenFraction = DEFAULT_DISABLE_AT_PERM_GEN;
|
||||
|
||||
/**
|
||||
* Retrieves the current background compiler.
|
||||
* @return Current background compiler.
|
||||
*/
|
||||
public static BackgroundCompiler getInstance() {
|
||||
return backgroundCompiler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the single background compiler we're using.
|
||||
* @param backgroundCompiler - current background compiler, or NULL if the library is not loaded.
|
||||
*/
|
||||
public static void setInstance(BackgroundCompiler backgroundCompiler) {
|
||||
BackgroundCompiler.backgroundCompiler = backgroundCompiler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a background compiler.
|
||||
* <p>
|
||||
* Uses the default {@link #THREAD_FORMAT} to name worker threads.
|
||||
* @param loader - class loader from Bukkit.
|
||||
* @param reporter - current error reporter.
|
||||
*/
|
||||
public BackgroundCompiler(ClassLoader loader, ErrorReporter reporter) {
|
||||
ThreadFactory factory = new ThreadFactoryBuilder().
|
||||
setDaemon(true).
|
||||
setNameFormat(THREAD_FORMAT).
|
||||
build();
|
||||
initializeCompiler(loader, reporter, Executors.newSingleThreadExecutor(factory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a background compiler utilizing the given thread pool.
|
||||
* @param loader - class loader from Bukkit.
|
||||
* @param reporter - current error reporter.
|
||||
* @param executor - thread pool we'll use.
|
||||
*/
|
||||
public BackgroundCompiler(ClassLoader loader, ErrorReporter reporter, ExecutorService executor) {
|
||||
initializeCompiler(loader, reporter, executor);
|
||||
}
|
||||
|
||||
// Avoid "Constructor call must be the first statement".
|
||||
private void initializeCompiler(ClassLoader loader, ErrorReporter reporter, ExecutorService executor) {
|
||||
if (loader == null)
|
||||
throw new IllegalArgumentException("loader cannot be NULL");
|
||||
if (executor == null)
|
||||
throw new IllegalArgumentException("executor cannot be NULL");
|
||||
if (reporter == null)
|
||||
throw new IllegalArgumentException("reporter cannot be NULL.");
|
||||
|
||||
this.compiler = new StructureCompiler(loader);
|
||||
this.reporter = reporter;
|
||||
this.executor = executor;
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the indirectly given structure modifier is eventually compiled.
|
||||
* @param cache - store of structure modifiers.
|
||||
* @param key - key of the structure modifier to compile.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public void scheduleCompilation(final Map<Class, StructureModifier> cache, final Class key) {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
final StructureModifier<Object> uncompiled = cache.get(key);
|
||||
|
||||
if (uncompiled != null) {
|
||||
scheduleCompilation(uncompiled, new CompileListener<Object>() {
|
||||
@Override
|
||||
public void onCompiled(StructureModifier<Object> compiledModifier) {
|
||||
// Update cache
|
||||
cache.put(key, compiledModifier);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the given structure modifier is eventually compiled.
|
||||
* @param uncompiled - structure modifier to compile.
|
||||
* @param listener - listener responsible for responding to the compilation.
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public <TKey> void scheduleCompilation(final StructureModifier<TKey> uncompiled, final CompileListener<TKey> listener) {
|
||||
// Only schedule if we're enabled
|
||||
if (enabled && !shuttingDown) {
|
||||
// Check perm gen
|
||||
if (getPermGenUsage() > disablePermGenFraction)
|
||||
return;
|
||||
|
||||
// Don't try to schedule anything
|
||||
if (executor == null || executor.isShutdown())
|
||||
return;
|
||||
|
||||
// Use to look up structure modifiers
|
||||
final StructureKey key = new StructureKey(uncompiled);
|
||||
|
||||
// Allow others to listen in too
|
||||
synchronized (listenerLock) {
|
||||
List list = listeners.get(key);
|
||||
|
||||
if (!listeners.containsKey(key)) {
|
||||
listeners.put(key, (List) Lists.newArrayList(listener));
|
||||
} else {
|
||||
// We're currently compiling
|
||||
list.add(listener);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the worker that will compile our modifier
|
||||
Callable<?> worker = new Callable<Object>() {
|
||||
@Override
|
||||
public Object call() throws Exception {
|
||||
StructureModifier<TKey> modifier = uncompiled;
|
||||
List list = null;
|
||||
|
||||
// Do our compilation
|
||||
try {
|
||||
modifier = compiler.compile(modifier);
|
||||
|
||||
synchronized (listenerLock) {
|
||||
list = listeners.get(key);
|
||||
|
||||
// Prevent ConcurrentModificationExceptions
|
||||
if (list != null) {
|
||||
list = Lists.newArrayList(list);
|
||||
}
|
||||
}
|
||||
|
||||
// Only execute the listeners if there is a list
|
||||
if (list != null) {
|
||||
for (Object compileListener : list) {
|
||||
((CompileListener<TKey>) compileListener).onCompiled(modifier);
|
||||
}
|
||||
|
||||
// Remove it when we're done
|
||||
synchronized (listenerLock) {
|
||||
list = listeners.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Throwable e) {
|
||||
// Disable future compilations!
|
||||
setEnabled(false);
|
||||
|
||||
// Inform about this error as best as we can
|
||||
reporter.reportDetailed(BackgroundCompiler.this,
|
||||
Report.newBuilder(REPORT_CANNOT_COMPILE_STRUCTURE_MODIFIER).callerParam(uncompiled).error(e)
|
||||
);
|
||||
}
|
||||
|
||||
// We'll also return the new structure modifier
|
||||
return modifier;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Lookup the previous class name on the main thread.
|
||||
// This is necessary as the Bukkit class loaders are not thread safe
|
||||
if (compiler.lookupClassLoader(uncompiled)) {
|
||||
try {
|
||||
worker.call();
|
||||
} catch (Exception e) {
|
||||
// Impossible!
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Perform the compilation on a seperate thread
|
||||
executor.submit(worker);
|
||||
}
|
||||
|
||||
} catch (RejectedExecutionException e) {
|
||||
// Occures when the underlying queue is overflowing. Since the compilation
|
||||
// is only an optmization and not really essential we'll just log this failure
|
||||
// and move on.
|
||||
reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_SCHEDULE_COMPILATION).error(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a compile listener if we are still waiting for the structure modifier to be compiled.
|
||||
* @param uncompiled - the structure modifier that may get compiled.
|
||||
* @param listener - the listener to invoke in that case.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <TKey> void addListener(final StructureModifier<TKey> uncompiled, final CompileListener<TKey> listener) {
|
||||
synchronized (listenerLock) {
|
||||
StructureKey key = new StructureKey(uncompiled);
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
List list = listeners.get(key);
|
||||
|
||||
if (list != null) {
|
||||
list.add(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current usage of the Perm Gen space in percentage.
|
||||
* @return Usage of the perm gen space.
|
||||
*/
|
||||
private double getPermGenUsage() {
|
||||
for (MemoryPoolMXBean item : ManagementFactory.getMemoryPoolMXBeans()) {
|
||||
if (item.getName().contains("Perm Gen")) {
|
||||
MemoryUsage usage = item.getUsage();
|
||||
return usage.getUsed() / (double) usage.getCommitted();
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up after ourselves using the default timeout.
|
||||
*/
|
||||
public void shutdownAll() {
|
||||
shutdownAll(SHUTDOWN_DELAY_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up after ourselves.
|
||||
* @param timeout - the maximum time to wait.
|
||||
* @param unit - the time unit of the timeout argument.
|
||||
*/
|
||||
public void shutdownAll(long timeout, TimeUnit unit) {
|
||||
setEnabled(false);
|
||||
shuttingDown = true;
|
||||
executor.shutdown();
|
||||
|
||||
try {
|
||||
executor.awaitTermination(timeout, unit);
|
||||
} catch (InterruptedException e) {
|
||||
// Unlikely to ever occur - it's the main thread
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve whether or not the background compiler is enabled.
|
||||
* @return TRUE if it is enabled, FALSE otherwise.
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the background compiler is enabled.
|
||||
* @param enabled - TRUE to enable it, FALSE otherwise.
|
||||
*/
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the fraction of perm gen space used after which the background compiler will be disabled.
|
||||
* @return The fraction after which the background compiler is disabled.
|
||||
*/
|
||||
public double getDisablePermGenFraction() {
|
||||
return disablePermGenFraction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fraction of perm gen space used after which the background compiler will be disabled.
|
||||
* @param fraction - the maximum use of perm gen space.
|
||||
*/
|
||||
public void setDisablePermGenFraction(double fraction) {
|
||||
this.disablePermGenFraction = fraction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current structure compiler.
|
||||
* @return Current structure compiler.
|
||||
*/
|
||||
public StructureCompiler getCompiler() {
|
||||
return compiler;
|
||||
}
|
||||
}
|
||||
|
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
@ -62,6 +62,25 @@ public abstract class AbstractFuzzyMatcher<T> implements Comparable<AbstractFuzz
|
||||
else
|
||||
return Math.max(roundA, roundB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine n round numbers by taking the highest non-zero number, or return zero.
|
||||
* @param rounds - the round numbers.
|
||||
* @return The combined round number.
|
||||
*/
|
||||
protected final int combineRounds(Integer... rounds) {
|
||||
if (rounds.length < 2)
|
||||
throw new IllegalArgumentException("Must supply at least two arguments.");
|
||||
|
||||
// Get the seed
|
||||
int reduced = combineRounds(rounds[0], rounds[1]);
|
||||
|
||||
// Aggregate it all
|
||||
for (int i = 2; i < rounds.length; i++) {
|
||||
reduced = combineRounds(reduced, rounds[i]);
|
||||
}
|
||||
return reduced;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(AbstractFuzzyMatcher<T> obj) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.comphenix.protocol.reflect.fuzzy;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@ -23,6 +24,9 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher<Class<?>> {
|
||||
private final ImmutableList<AbstractFuzzyMatcher<MethodInfo>> methodContracts;
|
||||
private final ImmutableList<AbstractFuzzyMatcher<MethodInfo>> constructorContracts;
|
||||
|
||||
private final ImmutableList<AbstractFuzzyMatcher<Class<?>>> baseclassContracts;
|
||||
private final ImmutableList<AbstractFuzzyMatcher<Class<?>>> interfaceContracts;
|
||||
|
||||
/**
|
||||
* Represents a class contract builder.
|
||||
* @author Kristian
|
||||
@ -33,6 +37,9 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher<Class<?>> {
|
||||
private List<AbstractFuzzyMatcher<MethodInfo>> methodContracts = Lists.newArrayList();
|
||||
private List<AbstractFuzzyMatcher<MethodInfo>> constructorContracts = Lists.newArrayList();
|
||||
|
||||
private List<AbstractFuzzyMatcher<Class<?>>> baseclassContracts = Lists.newArrayList();
|
||||
private List<AbstractFuzzyMatcher<Class<?>>> interfaceContracts = Lists.newArrayList();
|
||||
|
||||
/**
|
||||
* Add a new field contract.
|
||||
* @param matcher - new field contract.
|
||||
@ -89,18 +96,54 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher<Class<?>> {
|
||||
public Builder constructor(FuzzyMethodContract.Builder builder) {
|
||||
return constructor(builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new base class contract.
|
||||
* @param matcher - new base class contract.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder baseclass(AbstractFuzzyMatcher<Class<?>> matcher) {
|
||||
baseclassContracts.add(matcher);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new base class contract.
|
||||
* @param matcher - builder for the new base class contract.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder baseclass(FuzzyClassContract.Builder builder) {
|
||||
return baseclass(builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new interface contract.
|
||||
* @param matcher - new interface contract.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder interfaces(AbstractFuzzyMatcher<Class<?>> matcher) {
|
||||
interfaceContracts.add(matcher);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new interface contract.
|
||||
* @param matcher - builder for the new interface contract.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder interfaces(FuzzyClassContract.Builder builder) {
|
||||
return interfaces(builder.build());
|
||||
}
|
||||
|
||||
public FuzzyClassContract build() {
|
||||
Collections.sort(fieldContracts);
|
||||
Collections.sort(methodContracts);
|
||||
Collections.sort(constructorContracts);
|
||||
Collections.sort(baseclassContracts);
|
||||
Collections.sort(interfaceContracts);
|
||||
|
||||
// Construct a new class matcher
|
||||
return new FuzzyClassContract(
|
||||
ImmutableList.copyOf(fieldContracts),
|
||||
ImmutableList.copyOf(methodContracts),
|
||||
ImmutableList.copyOf(constructorContracts)
|
||||
);
|
||||
return new FuzzyClassContract(this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,17 +157,15 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher<Class<?>> {
|
||||
|
||||
/**
|
||||
* Constructs a new fuzzy class contract with the given contracts.
|
||||
* @param fieldContracts - field contracts.
|
||||
* @param methodContracts - method contracts.
|
||||
* @param constructorContracts - constructor contracts.
|
||||
* @param builder - the builder that is constructing us.
|
||||
*/
|
||||
private FuzzyClassContract(ImmutableList<AbstractFuzzyMatcher<Field>> fieldContracts,
|
||||
ImmutableList<AbstractFuzzyMatcher<MethodInfo>> methodContracts,
|
||||
ImmutableList<AbstractFuzzyMatcher<MethodInfo>> constructorContracts) {
|
||||
private FuzzyClassContract(Builder builder) {
|
||||
super();
|
||||
this.fieldContracts = fieldContracts;
|
||||
this.methodContracts = methodContracts;
|
||||
this.constructorContracts = constructorContracts;
|
||||
this.fieldContracts = ImmutableList.copyOf(builder.fieldContracts);
|
||||
this.methodContracts = ImmutableList.copyOf(builder.methodContracts);
|
||||
this.constructorContracts = ImmutableList.copyOf(builder.constructorContracts);
|
||||
this.baseclassContracts = ImmutableList.copyOf(builder.baseclassContracts);
|
||||
this.interfaceContracts = ImmutableList.copyOf(builder.interfaceContracts);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -157,12 +198,34 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher<Class<?>> {
|
||||
return constructorContracts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an immutable list of every baseclass contract.
|
||||
* <p>
|
||||
* This list is ordered in descending order of priority.
|
||||
* @return List of every baseclass contract.
|
||||
*/
|
||||
public ImmutableList<AbstractFuzzyMatcher<Class<?>>> getBaseclassContracts() {
|
||||
return baseclassContracts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an immutable list of every interface contract.
|
||||
* <p>
|
||||
* This list is ordered in descending order of priority.
|
||||
* @return List of every interface contract.
|
||||
*/
|
||||
public ImmutableList<AbstractFuzzyMatcher<Class<?>>> getInterfaceContracts() {
|
||||
return interfaceContracts;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int calculateRoundNumber() {
|
||||
// Find the highest round number
|
||||
return combineRounds(findHighestRound(fieldContracts),
|
||||
combineRounds(findHighestRound(methodContracts),
|
||||
findHighestRound(constructorContracts)));
|
||||
findHighestRound(methodContracts),
|
||||
findHighestRound(constructorContracts),
|
||||
findHighestRound(interfaceContracts),
|
||||
findHighestRound(baseclassContracts));
|
||||
}
|
||||
|
||||
private <T> int findHighestRound(Collection<AbstractFuzzyMatcher<T>> list) {
|
||||
@ -179,12 +242,19 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher<Class<?>> {
|
||||
FuzzyReflection reflection = FuzzyReflection.fromClass(value, true);
|
||||
|
||||
// Make sure all the contracts are valid
|
||||
return processContracts(reflection.getFields(), value, fieldContracts) &&
|
||||
processContracts(MethodInfo.fromMethods(reflection.getMethods()), value, methodContracts) &&
|
||||
processContracts(MethodInfo.fromConstructors(value.getDeclaredConstructors()), value, constructorContracts);
|
||||
return (fieldContracts.size() == 0 ||
|
||||
processContracts(reflection.getFields(), value, fieldContracts)) &&
|
||||
(methodContracts.size() == 0 ||
|
||||
processContracts(MethodInfo.fromMethods(reflection.getMethods()), value, methodContracts)) &&
|
||||
(constructorContracts.size() == 0 ||
|
||||
processContracts(MethodInfo.fromConstructors(value.getDeclaredConstructors()), value, constructorContracts)) &&
|
||||
(baseclassContracts.size() == 0 ||
|
||||
processValue(value.getSuperclass(), parent, baseclassContracts)) &&
|
||||
(interfaceContracts.size() == 0 ||
|
||||
processContracts(Arrays.asList(value.getInterfaces()), (Class<?>) parent, interfaceContracts));
|
||||
}
|
||||
|
||||
private <T> boolean processContracts(Collection<T> values, Class<?> parent, List<AbstractFuzzyMatcher<T>> matchers) {
|
||||
private <T> boolean processContracts(Collection<T> values, Object parent, List<AbstractFuzzyMatcher<T>> matchers) {
|
||||
boolean[] accepted = new boolean[matchers.size()];
|
||||
int count = accepted.length;
|
||||
|
||||
@ -205,7 +275,18 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher<Class<?>> {
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
private <T> int processValue(T value, Class<?> parent, boolean accepted[], List<AbstractFuzzyMatcher<T>> matchers) {
|
||||
private <T> boolean processValue(T value, Object parent, List<AbstractFuzzyMatcher<T>> matchers) {
|
||||
for (int i = 0; i < matchers.size(); i++) {
|
||||
if (matchers.get(i).isMatch(value, parent)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// No match
|
||||
return false;
|
||||
}
|
||||
|
||||
private <T> int processValue(T value, Object parent, boolean accepted[], List<AbstractFuzzyMatcher<T>> matchers) {
|
||||
// The order matters
|
||||
for (int i = 0; i < matchers.size(); i++) {
|
||||
if (!accepted[i]) {
|
||||
@ -235,6 +316,12 @@ public class FuzzyClassContract extends AbstractFuzzyMatcher<Class<?>> {
|
||||
if (constructorContracts.size() > 0) {
|
||||
params.put("constructors", constructorContracts);
|
||||
}
|
||||
if (baseclassContracts.size() > 0) {
|
||||
params.put("baseclasses", baseclassContracts);
|
||||
}
|
||||
if (interfaceContracts.size() > 0) {
|
||||
params.put("interfaces", interfaceContracts);
|
||||
}
|
||||
return "{\n " + Joiner.on(", \n ").join(params.entrySet()) + "\n}";
|
||||
}
|
||||
}
|
||||
|
@ -327,6 +327,7 @@ public class DefaultInstances implements InstanceProvider {
|
||||
try {
|
||||
return (T) constructor.newInstance(params);
|
||||
} catch (Exception e) {
|
||||
//e.printStackTrace();
|
||||
// Cannot create it
|
||||
return null;
|
||||
}
|
||||
|
@ -81,6 +81,9 @@ public class MinecraftReflection {
|
||||
private static Constructor<?> craftNMSConstructor;
|
||||
private static Constructor<?> craftBukkitConstructor;
|
||||
|
||||
// Matches classes
|
||||
private static AbstractFuzzyMatcher<Class<?>> fuzzyMatcher;
|
||||
|
||||
// New in 1.4.5
|
||||
private static Method craftNMSMethod;
|
||||
private static Method craftBukkitMethod;
|
||||
@ -88,7 +91,12 @@ public class MinecraftReflection {
|
||||
|
||||
// net.minecraft.server
|
||||
private static Class<?> itemStackArrayClass;
|
||||
|
||||
|
||||
/**
|
||||
* Whether or not we're currently initializing the reflection handler.
|
||||
*/
|
||||
private static boolean initializing;
|
||||
|
||||
private MinecraftReflection() {
|
||||
// No need to make this constructable.
|
||||
}
|
||||
@ -108,7 +116,9 @@ public class MinecraftReflection {
|
||||
* @return A matcher for Minecraft objects.
|
||||
*/
|
||||
public static AbstractFuzzyMatcher<Class<?>> getMinecraftObjectMatcher() {
|
||||
return FuzzyMatchers.matchRegex(getMinecraftObjectRegex(), 50);
|
||||
if (fuzzyMatcher == null)
|
||||
fuzzyMatcher = FuzzyMatchers.matchRegex(getMinecraftObjectRegex(), 50);
|
||||
return fuzzyMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -119,6 +129,9 @@ public class MinecraftReflection {
|
||||
// Speed things up
|
||||
if (MINECRAFT_FULL_PACKAGE != null)
|
||||
return MINECRAFT_FULL_PACKAGE;
|
||||
if (initializing)
|
||||
throw new IllegalStateException("Already initializing minecraft package!");
|
||||
initializing = true;
|
||||
|
||||
Server craftServer = Bukkit.getServer();
|
||||
|
||||
@ -129,6 +142,9 @@ public class MinecraftReflection {
|
||||
Class<?> craftClass = craftServer.getClass();
|
||||
CRAFTBUKKIT_PACKAGE = getPackage(craftClass.getCanonicalName());
|
||||
|
||||
// Libigot patch
|
||||
handleLibigot();
|
||||
|
||||
// Next, do the same for CraftEntity.getHandle() in order to get the correct Minecraft package
|
||||
Class<?> craftEntity = getCraftEntityClass();
|
||||
Method getHandle = craftEntity.getMethod("getHandle");
|
||||
@ -141,16 +157,16 @@ public class MinecraftReflection {
|
||||
MINECRAFT_PREFIX_PACKAGE = MINECRAFT_FULL_PACKAGE;
|
||||
|
||||
// The package is usualy flat, so go with that assumtion
|
||||
DYNAMIC_PACKAGE_MATCHER =
|
||||
String matcher =
|
||||
(MINECRAFT_PREFIX_PACKAGE.length() > 0 ?
|
||||
Pattern.quote(MINECRAFT_PREFIX_PACKAGE + ".") : "") + "\\w+";
|
||||
|
||||
// We'll still accept the default location, however
|
||||
DYNAMIC_PACKAGE_MATCHER = "(" + DYNAMIC_PACKAGE_MATCHER + ")|(" + MINECRAFT_OBJECT + ")";
|
||||
setDynamicPackageMatcher("(" + matcher + ")|(" + MINECRAFT_OBJECT + ")");
|
||||
|
||||
} else {
|
||||
// Use the standard matcher
|
||||
DYNAMIC_PACKAGE_MATCHER = MINECRAFT_OBJECT;
|
||||
setDynamicPackageMatcher(MINECRAFT_OBJECT);
|
||||
}
|
||||
|
||||
return MINECRAFT_FULL_PACKAGE;
|
||||
@ -159,12 +175,40 @@ public class MinecraftReflection {
|
||||
throw new RuntimeException("Security violation. Cannot get handle method.", e);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new IllegalStateException("Cannot find getHandle() method on server. Is this a modified CraftBukkit version?", e);
|
||||
} finally {
|
||||
initializing = false;
|
||||
}
|
||||
|
||||
} else {
|
||||
initializing = false;
|
||||
throw new IllegalStateException("Could not find Bukkit. Is it running?");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the dynamic package matcher.
|
||||
* @param regex - the Minecraft package regex.
|
||||
*/
|
||||
private static void setDynamicPackageMatcher(String regex) {
|
||||
DYNAMIC_PACKAGE_MATCHER = regex;
|
||||
|
||||
// Ensure that the matcher is regenerated
|
||||
fuzzyMatcher = null;
|
||||
}
|
||||
|
||||
// Patch for Libigot
|
||||
private static void handleLibigot() {
|
||||
try {
|
||||
getCraftEntityClass();
|
||||
} catch (RuntimeException e) {
|
||||
// Try reverting the package to the old format
|
||||
craftbukkitPackage = null;
|
||||
CRAFTBUKKIT_PACKAGE = "org.bukkit.craftbukkit";
|
||||
|
||||
// This might fail too
|
||||
getCraftEntityClass();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used during debugging and testing.
|
||||
@ -176,7 +220,7 @@ public class MinecraftReflection {
|
||||
CRAFTBUKKIT_PACKAGE = craftBukkitPackage;
|
||||
|
||||
// Standard matcher
|
||||
DYNAMIC_PACKAGE_MATCHER = MINECRAFT_OBJECT;
|
||||
setDynamicPackageMatcher(MINECRAFT_OBJECT);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -244,8 +288,7 @@ public class MinecraftReflection {
|
||||
if (clazz == null)
|
||||
throw new IllegalArgumentException("Class cannot be NULL.");
|
||||
|
||||
// Doesn't matter if we don't check for the version here
|
||||
return clazz.getName().startsWith(MINECRAFT_PREFIX_PACKAGE);
|
||||
return getMinecraftObjectMatcher().isMatch(clazz, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -809,9 +852,15 @@ public class MinecraftReflection {
|
||||
returnTypeMatches(tagCompoundContract).
|
||||
build()
|
||||
);
|
||||
Class<?> nbtBase = selected.getReturnType().getSuperclass();
|
||||
|
||||
// That can't be correct
|
||||
if (nbtBase == null || nbtBase.equals(Object.class)) {
|
||||
throw new IllegalStateException("Unable to find NBT base class: " + nbtBase);
|
||||
}
|
||||
|
||||
// Use the return type here too
|
||||
return setMinecraftClass("NBTBase", selected.getReturnType());
|
||||
return setMinecraftClass("NBTBase", nbtBase);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
|
||||
package com.comphenix.protocol;
|
||||
package com.comphenix.protocol.utility;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@ -31,7 +31,7 @@ import com.google.common.collect.Ordering;
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
class MinecraftVersion implements Comparable<MinecraftVersion> {
|
||||
public class MinecraftVersion implements Comparable<MinecraftVersion> {
|
||||
/**
|
||||
* Regular expression used to parse version strings.
|
||||
*/
|
@ -0,0 +1,104 @@
|
||||
package com.comphenix.protocol.wrappers;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||
import com.comphenix.protocol.reflect.fuzzy.AbstractFuzzyMatcher;
|
||||
import com.comphenix.protocol.reflect.fuzzy.FuzzyMatchers;
|
||||
|
||||
/**
|
||||
* Wrap a GNU Trove Collection class with an equivalent Java Collection class.
|
||||
* @author Kristian
|
||||
*/
|
||||
public class TroveWrapper {
|
||||
private volatile static Class<?> decorators;
|
||||
|
||||
/**
|
||||
* Retrieve a Java wrapper for the corresponding Trove map.
|
||||
* @param troveMap - the trove map to wrap.
|
||||
* @return The wrapped GNU Trove map.
|
||||
* @throws IllegalStateException If GNU Trove cannot be found in the class map.
|
||||
* @throws IllegalArgumentException If troveMap is NULL.
|
||||
* @throws FieldAccessException Error in wrapper method or lack of reflection permissions.
|
||||
*/
|
||||
public static <TKey, TValue> Map<TKey, TValue> getDecoratedMap(@Nonnull Object troveMap) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<TKey, TValue> result = (Map<TKey, TValue>) getDecorated(troveMap);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a Java wrapper for the corresponding Trove set.
|
||||
* @param troveSet - the trove set to wrap.
|
||||
* @return The wrapped GNU Trove set.
|
||||
* @throws IllegalStateException If GNU Trove cannot be found in the class map.
|
||||
* @throws IllegalArgumentException If troveSet is NULL.
|
||||
* @throws FieldAccessException Error in wrapper method or lack of reflection permissions.
|
||||
*/
|
||||
public static <TValue> Set<TValue> getDecoratedSet(@Nonnull Object troveSet) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<TValue> result = (Set<TValue>) getDecorated(troveSet);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a Java wrapper for the corresponding Trove list.
|
||||
* @param troveList - the trove list to wrap.
|
||||
* @return The wrapped GNU Trove list.
|
||||
* @throws IllegalStateException If GNU Trove cannot be found in the class map.
|
||||
* @throws IllegalArgumentException If troveList is NULL.
|
||||
* @throws FieldAccessException Error in wrapper method or lack of reflection permissions.
|
||||
*/
|
||||
public static <TValue> List<TValue> getDecoratedList(@Nonnull Object troveList) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<TValue> result = (List<TValue>) getDecorated(troveList);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Object getDecorated(@Nonnull Object trove) {
|
||||
if (trove == null)
|
||||
throw new IllegalArgumentException("trove instance cannot be non-null.");
|
||||
|
||||
AbstractFuzzyMatcher<Class<?>> match = FuzzyMatchers.matchSuper(trove.getClass());
|
||||
|
||||
if (decorators == null) {
|
||||
try {
|
||||
// Attempt to get decorator class
|
||||
decorators = TroveWrapper.class.getClassLoader().loadClass("gnu.trove.TDecorators");
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IllegalStateException("Cannot find TDecorators in Gnu Trove.", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Find an appropriate wrapper method in TDecorators
|
||||
for (Method method : decorators.getMethods()) {
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
|
||||
if (types.length == 1 && match.isMatch(types[0], null)) {
|
||||
try {
|
||||
Object result = method.invoke(null, trove);
|
||||
|
||||
if (result == null)
|
||||
throw new FieldAccessException("Wrapper returned NULL.");
|
||||
else
|
||||
return result;
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new FieldAccessException("Cannot invoke wrapper method.", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new FieldAccessException("Illegal access.", e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new FieldAccessException("Error in invocation.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Cannot find decorator for " + trove + " (" + trove.getClass() + ")");
|
||||
}
|
||||
}
|
@ -6,8 +6,6 @@ global:
|
||||
|
||||
# Number of seconds to wait until a new update is downloaded
|
||||
delay: 43200 # 12 hours
|
||||
# Last update time
|
||||
last: 0
|
||||
|
||||
metrics: true
|
||||
|
||||
@ -18,4 +16,10 @@ global:
|
||||
ignore version check:
|
||||
|
||||
# Override the starting injecting method
|
||||
injection method:
|
||||
injection method:
|
||||
|
||||
# Whether or not to enable the filter command
|
||||
debug: false
|
||||
|
||||
# The engine used by the filter command
|
||||
script engine: JavaScript
|
@ -1,9 +1,11 @@
|
||||
name: ProtocolLib
|
||||
version: 2.3.0
|
||||
version: 2.4.3
|
||||
description: Provides read/write access to the Minecraft protocol.
|
||||
author: Comphenix
|
||||
website: http://www.comphenix.net/ProtocolLib
|
||||
|
||||
main: com.comphenix.protocol.ProtocolLibrary
|
||||
load: startup
|
||||
database: false
|
||||
|
||||
commands:
|
||||
@ -17,6 +19,12 @@ commands:
|
||||
usage: /<command> add|remove|names client|server [ID start]-[ID stop] [detailed]
|
||||
permission: protocol.admin
|
||||
permission-message: You don't have <permission>
|
||||
filter:
|
||||
description: Add or remove programmable filters to the packet listeners.
|
||||
usage: /<command> add|remove name [ID start]-[ID stop]
|
||||
aliases: [packet_filter]
|
||||
permission: protocol.admin
|
||||
permission-message: You don't have <permission>
|
||||
|
||||
permissions:
|
||||
protocol.*:
|
||||
|
@ -4,8 +4,10 @@ import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import net.minecraft.server.v1_5_R2.StatisticList;
|
||||
|
||||
// Will have to be updated for every version though
|
||||
import org.bukkit.craftbukkit.v1_4_R1.inventory.CraftItemFactory;
|
||||
import org.bukkit.craftbukkit.v1_5_R2.inventory.CraftItemFactory;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
@ -34,6 +36,12 @@ public class BukkitInitialization {
|
||||
|
||||
initializePackage();
|
||||
|
||||
try {
|
||||
StatisticList.b();
|
||||
} catch (Exception e) {
|
||||
// Swallow
|
||||
}
|
||||
|
||||
// Mock the server object
|
||||
Server mockedServer = mock(Server.class);
|
||||
ItemFactory mockedFactory = mock(CraftItemFactory.class);
|
||||
@ -55,6 +63,6 @@ public class BukkitInitialization {
|
||||
*/
|
||||
public static void initializePackage() {
|
||||
// Initialize reflection
|
||||
MinecraftReflection.setMinecraftPackage("net.minecraft.server.v1_4_R1", "org.bukkit.craftbukkit.v1_4_R1");
|
||||
MinecraftReflection.setMinecraftPackage("net.minecraft.server.v1_5_R2", "org.bukkit.craftbukkit.v1_5_R2");
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,8 @@ import static org.junit.Assert.*;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import com.comphenix.protocol.utility.MinecraftVersion;
|
||||
|
||||
public class MinecraftVersionTest {
|
||||
|
||||
@Test
|
||||
|
@ -22,7 +22,7 @@ import java.lang.reflect.Array;
|
||||
import java.util.List;
|
||||
|
||||
// Will have to be updated for every version though
|
||||
import org.bukkit.craftbukkit.v1_4_R1.inventory.CraftItemFactory;
|
||||
import org.bukkit.craftbukkit.v1_5_R2.inventory.CraftItemFactory;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.WorldType;
|
||||
|
In neuem Issue referenzieren
Einen Benutzer sperren