diff --git a/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java
index 7abc4ff1..39075681 100644
--- a/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java
+++ b/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java
@@ -25,6 +25,7 @@ import org.bukkit.Server;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
+import com.comphenix.protocol.compiler.BackgroundCompiler;
import com.comphenix.protocol.injector.PacketFilterManager;
import com.comphenix.protocol.metrics.Statistics;
@@ -39,6 +40,9 @@ public class ProtocolLibrary extends JavaPlugin {
// Metrics and statistisc
private Statistics statistisc;
+ // Structure compiler
+ private BackgroundCompiler backgroundCompiler;
+
@Override
public void onLoad() {
logger = getLoggerSafely();
@@ -50,6 +54,12 @@ public class ProtocolLibrary extends JavaPlugin {
Server server = getServer();
PluginManager manager = server.getPluginManager();
+ // Initialize background compiler
+ if (backgroundCompiler == null) {
+ backgroundCompiler = new BackgroundCompiler(getClassLoader());
+ BackgroundCompiler.setInstance(backgroundCompiler);
+ }
+
// Notify server managers of incompatible plugins
checkForIncompatibility(manager);
@@ -83,6 +93,13 @@ public class ProtocolLibrary extends JavaPlugin {
@Override
public void onDisable() {
+ // Disable compiler
+ if (backgroundCompiler != null) {
+ backgroundCompiler.shutdownAll();
+ backgroundCompiler = null;
+ BackgroundCompiler.setInstance(null);
+ }
+
protocolManager.close();
protocolManager = null;
statistisc = null;
diff --git a/ProtocolLib/src/com/comphenix/protocol/compiler/BackgroundCompiler.java b/ProtocolLib/src/com/comphenix/protocol/compiler/BackgroundCompiler.java
new file mode 100644
index 00000000..313593ab
--- /dev/null
+++ b/ProtocolLib/src/com/comphenix/protocol/compiler/BackgroundCompiler.java
@@ -0,0 +1,162 @@
+package com.comphenix.protocol.compiler;
+
+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.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.comphenix.protocol.reflect.StructureModifier;
+
+/**
+ * Compiles structure modifiers on a background thread.
+ *
+ * This is necessary as we cannot block the main thread.
+ *
+ * @author Kristian
+ */
+public class BackgroundCompiler {
+
+ // How long to wait for a shutdown
+ public static final int SHUTDOWN_DELAY_MS = 2000;
+
+ // The single background compiler we're using
+ private static BackgroundCompiler backgroundCompiler;
+
+ private StructureCompiler compiler;
+ private boolean enabled;
+ private boolean shuttingDown;
+
+ private ExecutorService executor;
+
+ /**
+ * 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.
+ * @param loader - class loader from Bukkit.
+ */
+ public BackgroundCompiler(ClassLoader loader) {
+ this(loader, Executors.newSingleThreadExecutor());
+ }
+
+ /**
+ * Initialize a background compiler utilizing the given thread pool.
+ * @param loader - class loader from Bukkit.
+ * @param executor - thread pool we'll use.
+ */
+ public BackgroundCompiler(ClassLoader loader, 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.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 cache, final Class key) {
+
+ // Only schedule if we're enabled
+ if (enabled && !shuttingDown) {
+
+ // Don't try to schedule anything
+ if (executor == null || executor.isShutdown())
+ return;
+
+ try {
+ executor.submit(new Callable() {
+ @Override
+ public Object call() throws Exception {
+
+ StructureModifier> modifier = cache.get(key);
+
+ // Update the cache!
+ modifier = compiler.compile(modifier);
+ cache.put(key, modifier);
+
+ // We'll also return the new structure modifier
+ return modifier;
+ }
+ });
+ } 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.
+ Logger.getLogger("Minecraft").log(Level.WARNING, "Unable to schedule compilation task.", e);
+ }
+ }
+ }
+
+ /**
+ * 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.
+ 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 current structure compiler.
+ * @return Current structure compiler.
+ */
+ public StructureCompiler getCompiler() {
+ return compiler;
+ }
+}
diff --git a/ProtocolLib/src/com/comphenix/protocol/compiler/BoxingHelper.java b/ProtocolLib/src/com/comphenix/protocol/compiler/BoxingHelper.java
new file mode 100644
index 00000000..3692381e
--- /dev/null
+++ b/ProtocolLib/src/com/comphenix/protocol/compiler/BoxingHelper.java
@@ -0,0 +1,275 @@
+package com.comphenix.protocol.compiler;
+
+import net.sf.cglib.asm.*;
+
+/**
+ * Used by the compiler to automatically box and unbox values.
+ */
+class BoxingHelper {
+
+ private final static Type BYTE_TYPE = Type.getObjectType("java/lang/Byte");
+ private final static Type BOOLEAN_TYPE = Type.getObjectType("java/lang/Boolean");
+ private final static Type SHORT_TYPE = Type.getObjectType("java/lang/Short");
+ private final static Type CHARACTER_TYPE = Type.getObjectType("java/lang/Character");
+ private final static Type INTEGER_TYPE = Type.getObjectType("java/lang/Integer");
+ private final static Type FLOAT_TYPE = Type.getObjectType("java/lang/Float");
+ private final static Type LONG_TYPE = Type.getObjectType("java/lang/Long");
+ private final static Type DOUBLE_TYPE = Type.getObjectType("java/lang/Double");
+ private final static Type NUMBER_TYPE = Type.getObjectType("java/lang/Number");
+ private final static Type OBJECT_TYPE = Type.getObjectType("java/lang/Object");
+
+ private final static MethodDescriptor BOOLEAN_VALUE = MethodDescriptor.getMethod("boolean booleanValue()");
+ private final static MethodDescriptor CHAR_VALUE = MethodDescriptor.getMethod("char charValue()");
+ private final static MethodDescriptor INT_VALUE = MethodDescriptor.getMethod("int intValue()");
+ private final static MethodDescriptor FLOAT_VALUE = MethodDescriptor.getMethod("float floatValue()");
+ private final static MethodDescriptor LONG_VALUE = MethodDescriptor.getMethod("long longValue()");
+ private final static MethodDescriptor DOUBLE_VALUE = MethodDescriptor.getMethod("double doubleValue()");
+
+ private MethodVisitor mv;
+
+ public BoxingHelper(MethodVisitor mv) {
+ this.mv = mv;
+ }
+
+ /**
+ * Generates the instructions to box the top stack value. This value is
+ * replaced by its boxed equivalent on top of the stack.
+ *
+ * @param type the type of the top stack value.
+ */
+ public void box(final Type type){
+ if(type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) {
+ return;
+ }
+
+ if(type == Type.VOID_TYPE) {
+ push((String) null);
+ } else {
+ Type boxed = type;
+
+ switch(type.getSort()) {
+ case Type.BYTE:
+ boxed = BYTE_TYPE;
+ break;
+ case Type.BOOLEAN:
+ boxed = BOOLEAN_TYPE;
+ break;
+ case Type.SHORT:
+ boxed = SHORT_TYPE;
+ break;
+ case Type.CHAR:
+ boxed = CHARACTER_TYPE;
+ break;
+ case Type.INT:
+ boxed = INTEGER_TYPE;
+ break;
+ case Type.FLOAT:
+ boxed = FLOAT_TYPE;
+ break;
+ case Type.LONG:
+ boxed = LONG_TYPE;
+ break;
+ case Type.DOUBLE:
+ boxed = DOUBLE_TYPE;
+ break;
+ }
+
+ newInstance(boxed);
+ if(type.getSize() == 2) {
+ // Pp -> Ppo -> oPpo -> ooPpo -> ooPp -> o
+ dupX2();
+ dupX2();
+ pop();
+ } else {
+ // p -> po -> opo -> oop -> o
+ dupX1();
+ swap();
+ }
+
+ invokeConstructor(boxed, new MethodDescriptor("", Type.VOID_TYPE, new Type[] {type}));
+ }
+ }
+
+ /**
+ * Generates the instruction to invoke a constructor.
+ *
+ * @param type the class in which the constructor is defined.
+ * @param method the constructor to be invoked.
+ */
+ public void invokeConstructor(final Type type, final MethodDescriptor method){
+ invokeInsn(Opcodes.INVOKESPECIAL, type, method);
+ }
+
+ /**
+ * Generates a DUP_X1 instruction.
+ */
+ public void dupX1(){
+ mv.visitInsn(Opcodes.DUP_X1);
+ }
+
+ /**
+ * Generates a DUP_X2 instruction.
+ */
+ public void dupX2(){
+ mv.visitInsn(Opcodes.DUP_X2);
+ }
+
+ /**
+ * Generates a POP instruction.
+ */
+ public void pop(){
+ mv.visitInsn(Opcodes.POP);
+ }
+
+ /**
+ * Generates a SWAP instruction.
+ */
+ public void swap(){
+ mv.visitInsn(Opcodes.SWAP);
+ }
+
+ /**
+ * Generates the instruction to push the given value on the stack.
+ *
+ * @param value the value to be pushed on the stack.
+ */
+ public void push(final boolean value){
+ push(value ? 1 : 0);
+ }
+
+ /**
+ * Generates the instruction to push the given value on the stack.
+ *
+ * @param value the value to be pushed on the stack.
+ */
+ public void push(final int value) {
+ if (value >= -1 && value <= 5) {
+ mv.visitInsn(Opcodes.ICONST_0 + value);
+ } else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) {
+ mv.visitIntInsn(Opcodes.BIPUSH, value);
+ } else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) {
+ mv.visitIntInsn(Opcodes.SIPUSH, value);
+ } else {
+ mv.visitLdcInsn(new Integer(value));
+ }
+ }
+
+ /**
+ * Generates the instruction to create a new object.
+ *
+ * @param type the class of the object to be created.
+ */
+ public void newInstance(final Type type){
+ typeInsn(Opcodes.NEW, type);
+ }
+
+ /**
+ * Generates the instruction to push the given value on the stack.
+ *
+ * @param value the value to be pushed on the stack. May be null .
+ */
+ public void push(final String value) {
+ if (value == null) {
+ mv.visitInsn(Opcodes.ACONST_NULL);
+ } else {
+ mv.visitLdcInsn(value);
+ }
+ }
+
+ /**
+ * Generates the instructions to unbox the top stack value. This value is
+ * replaced by its unboxed equivalent on top of the stack.
+ *
+ * @param type
+ * the type of the top stack value.
+ */
+ public void unbox(final Type type){
+ Type t = NUMBER_TYPE;
+ MethodDescriptor sig = null;
+
+ switch(type.getSort()) {
+ case Type.VOID:
+ return;
+ case Type.CHAR:
+ t = CHARACTER_TYPE;
+ sig = CHAR_VALUE;
+ break;
+ case Type.BOOLEAN:
+ t = BOOLEAN_TYPE;
+ sig = BOOLEAN_VALUE;
+ break;
+ case Type.DOUBLE:
+ sig = DOUBLE_VALUE;
+ break;
+ case Type.FLOAT:
+ sig = FLOAT_VALUE;
+ break;
+ case Type.LONG:
+ sig = LONG_VALUE;
+ break;
+ case Type.INT:
+ case Type.SHORT:
+ case Type.BYTE:
+ sig = INT_VALUE;
+ }
+
+ if(sig == null) {
+ checkCast(type);
+ } else {
+ checkCast(t);
+ invokeVirtual(t, sig);
+ }
+ }
+
+ /**
+ * Generates the instruction to check that the top stack value is of the
+ * given type.
+ *
+ * @param type a class or interface type.
+ */
+ public void checkCast(final Type type){
+ if(!type.equals(OBJECT_TYPE)) {
+ typeInsn(Opcodes.CHECKCAST, type);
+ }
+ }
+
+ /**
+ * Generates the instruction to invoke a normal method.
+ *
+ * @param owner the class in which the method is defined.
+ * @param method the method to be invoked.
+ */
+ public void invokeVirtual(final Type owner, final MethodDescriptor method){
+ invokeInsn(Opcodes.INVOKEVIRTUAL, owner, method);
+ }
+
+ /**
+ * Generates an invoke method instruction.
+ *
+ * @param opcode the instruction's opcode.
+ * @param type the class in which the method is defined.
+ * @param method the method to be invoked.
+ */
+ private void invokeInsn(final int opcode, final Type type, final MethodDescriptor method){
+ String owner = type.getSort() == Type.ARRAY ? type.getDescriptor() : type.getInternalName();
+ mv.visitMethodInsn(opcode, owner, method.getName(), method.getDescriptor());
+ }
+
+ /**
+ * Generates a type dependent instruction.
+ *
+ * @param opcode the instruction's opcode.
+ * @param type the instruction's operand.
+ */
+ private void typeInsn(final int opcode, final Type type){
+ String desc;
+
+ if(type.getSort() == Type.ARRAY) {
+ desc = type.getDescriptor();
+ } else {
+ desc = type.getInternalName();
+ }
+
+ mv.visitTypeInsn(opcode, desc);
+ }
+}
diff --git a/ProtocolLib/src/com/comphenix/protocol/compiler/CompiledStructureModifier.java b/ProtocolLib/src/com/comphenix/protocol/compiler/CompiledStructureModifier.java
new file mode 100644
index 00000000..570a06b6
--- /dev/null
+++ b/ProtocolLib/src/com/comphenix/protocol/compiler/CompiledStructureModifier.java
@@ -0,0 +1,45 @@
+package com.comphenix.protocol.compiler;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+
+import com.comphenix.protocol.reflect.FieldAccessException;
+import com.comphenix.protocol.reflect.StructureModifier;
+import com.comphenix.protocol.reflect.instances.DefaultInstances;
+
+/**
+ * Represents a compiled structure modifier.
+ *
+ * @author Kristian
+ * @param Field type.
+ */
+public class CompiledStructureModifier extends StructureModifier {
+ // Used to compile instances of structure modifiers
+ protected StructureCompiler compiler;
+
+ // Speed up the default writer
+ @SuppressWarnings("unchecked")
+ @Override
+ public StructureModifier writeDefaults() throws FieldAccessException {
+
+ DefaultInstances generator = DefaultInstances.DEFAULT;
+
+ // Write a default instance to every field
+ for (Map.Entry entry : defaultFields.entrySet()) {
+ Integer index = entry.getValue();
+ Field field = entry.getKey();
+
+ write(index, (TField) generator.getDefault(field.getType()));
+ }
+
+ return this;
+ }
+
+ @Override
+ public StructureModifier withTarget(Object target) {
+ if (compiler != null)
+ return compiler.compile(super.withTarget(target));
+ else
+ return super.withTarget(target);
+ }
+}
diff --git a/ProtocolLib/src/com/comphenix/protocol/compiler/MethodDescriptor.java b/ProtocolLib/src/com/comphenix/protocol/compiler/MethodDescriptor.java
new file mode 100644
index 00000000..4f5e8c5c
--- /dev/null
+++ b/ProtocolLib/src/com/comphenix/protocol/compiler/MethodDescriptor.java
@@ -0,0 +1,220 @@
+package com.comphenix.protocol.compiler;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import net.sf.cglib.asm.Type;
+
+/**
+ * Represents a method.
+ */
+class MethodDescriptor {
+
+ /**
+ * The method name.
+ */
+ private final String name;
+
+ /**
+ * The method descriptor.
+ */
+ private final String desc;
+
+ /**
+ * Maps primitive Java type names to their descriptors.
+ */
+ private static final Map DESCRIPTORS;
+
+ static {
+ DESCRIPTORS = new HashMap();
+ DESCRIPTORS.put("void", "V");
+ DESCRIPTORS.put("byte", "B");
+ DESCRIPTORS.put("char", "C");
+ DESCRIPTORS.put("double", "D");
+ DESCRIPTORS.put("float", "F");
+ DESCRIPTORS.put("int", "I");
+ DESCRIPTORS.put("long", "J");
+ DESCRIPTORS.put("short", "S");
+ DESCRIPTORS.put("boolean", "Z");
+ }
+
+ /**
+ * Creates a new {@link Method}.
+ *
+ * @param name the method's name.
+ * @param desc the method's descriptor.
+ */
+ public MethodDescriptor(final String name, final String desc) {
+ this.name = name;
+ this.desc = desc;
+ }
+
+ /**
+ * Creates a new {@link Method}.
+ *
+ * @param name the method's name.
+ * @param returnType the method's return type.
+ * @param argumentTypes the method's argument types.
+ */
+ public MethodDescriptor(
+ final String name,
+ final Type returnType,
+ final Type[] argumentTypes)
+ {
+ this(name, Type.getMethodDescriptor(returnType, argumentTypes));
+ }
+
+ /**
+ * Returns a {@link Method} corresponding to the given Java method
+ * declaration.
+ *
+ * @param method a Java method declaration, without argument names, of the
+ * form "returnType name (argumentType1, ... argumentTypeN)", where
+ * the types are in plain Java (e.g. "int", "float",
+ * "java.util.List", ...). Classes of the java.lang package can be
+ * specified by their unqualified name; all other classes names must
+ * be fully qualified.
+ * @return a {@link Method} corresponding to the given Java method
+ * declaration.
+ * @throws IllegalArgumentException if method
could not get
+ * parsed.
+ */
+ public static MethodDescriptor getMethod(final String method)
+ throws IllegalArgumentException
+ {
+ return getMethod(method, false);
+ }
+
+ /**
+ * Returns a {@link Method} corresponding to the given Java method
+ * declaration.
+ *
+ * @param method a Java method declaration, without argument names, of the
+ * form "returnType name (argumentType1, ... argumentTypeN)", where
+ * the types are in plain Java (e.g. "int", "float",
+ * "java.util.List", ...). Classes of the java.lang package may be
+ * specified by their unqualified name, depending on the
+ * defaultPackage argument; all other classes names must be fully
+ * qualified.
+ * @param defaultPackage true if unqualified class names belong to the
+ * default package, or false if they correspond to java.lang classes.
+ * For instance "Object" means "Object" if this option is true, or
+ * "java.lang.Object" otherwise.
+ * @return a {@link Method} corresponding to the given Java method
+ * declaration.
+ * @throws IllegalArgumentException if method
could not get
+ * parsed.
+ */
+ public static MethodDescriptor getMethod(
+ final String method,
+ final boolean defaultPackage) throws IllegalArgumentException
+ {
+ int space = method.indexOf(' ');
+ int start = method.indexOf('(', space) + 1;
+ int end = method.indexOf(')', start);
+ if (space == -1 || start == -1 || end == -1) {
+ throw new IllegalArgumentException();
+ }
+ String returnType = method.substring(0, space);
+ String methodName = method.substring(space + 1, start - 1).trim();
+ StringBuffer sb = new StringBuffer();
+ sb.append('(');
+ int p;
+ do {
+ String s;
+ p = method.indexOf(',', start);
+ if (p == -1) {
+ s = map(method.substring(start, end).trim(), defaultPackage);
+ } else {
+ s = map(method.substring(start, p).trim(), defaultPackage);
+ start = p + 1;
+ }
+ sb.append(s);
+ } while (p != -1);
+ sb.append(')');
+ sb.append(map(returnType, defaultPackage));
+ return new MethodDescriptor(methodName, sb.toString());
+ }
+
+ private static String map(final String type, final boolean defaultPackage) {
+ if ("".equals(type)) {
+ return type;
+ }
+
+ StringBuffer sb = new StringBuffer();
+ int index = 0;
+ while ((index = type.indexOf("[]", index) + 1) > 0) {
+ sb.append('[');
+ }
+
+ String t = type.substring(0, type.length() - sb.length() * 2);
+ String desc = (String) DESCRIPTORS.get(t);
+ if (desc != null) {
+ sb.append(desc);
+ } else {
+ sb.append('L');
+ if (t.indexOf('.') < 0) {
+ if (!defaultPackage) {
+ sb.append("java/lang/");
+ }
+ sb.append(t);
+ } else {
+ sb.append(t.replace('.', '/'));
+ }
+ sb.append(';');
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns the name of the method described by this object.
+ *
+ * @return the name of the method described by this object.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the descriptor of the method described by this object.
+ *
+ * @return the descriptor of the method described by this object.
+ */
+ public String getDescriptor() {
+ return desc;
+ }
+
+ /**
+ * Returns the return type of the method described by this object.
+ *
+ * @return the return type of the method described by this object.
+ */
+ public Type getReturnType() {
+ return Type.getReturnType(desc);
+ }
+
+ /**
+ * Returns the argument types of the method described by this object.
+ *
+ * @return the argument types of the method described by this object.
+ */
+ public Type[] getArgumentTypes() {
+ return Type.getArgumentTypes(desc);
+ }
+
+ public String toString() {
+ return name + desc;
+ }
+
+ public boolean equals(final Object o) {
+ if (!(o instanceof MethodDescriptor)) {
+ return false;
+ }
+ MethodDescriptor other = (MethodDescriptor) o;
+ return name.equals(other.name) && desc.equals(other.desc);
+ }
+
+ public int hashCode() {
+ return name.hashCode() ^ desc.hashCode();
+ }
+}
diff --git a/ProtocolLib/src/com/comphenix/protocol/compiler/StructureCompiler.java b/ProtocolLib/src/com/comphenix/protocol/compiler/StructureCompiler.java
new file mode 100644
index 00000000..f79a949b
--- /dev/null
+++ b/ProtocolLib/src/com/comphenix/protocol/compiler/StructureCompiler.java
@@ -0,0 +1,432 @@
+package com.comphenix.protocol.compiler;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.comphenix.protocol.reflect.PrimitiveUtils;
+import com.comphenix.protocol.reflect.StructureModifier;
+import com.google.common.base.Objects;
+
+import net.sf.cglib.asm.*;
+
+// This class will automatically generate the following type of structure modifier:
+//
+// public class CompiledStructure$Packet20NamedEntitySpawnObject extends CompiledStructureModifier {
+//
+// private Packet20NamedEntitySpawn typedTarget;
+//
+// public CompiledStructure$Packet20NamedEntitySpawnObject(StructureModifier other, StructureCompiler compiler) {
+// initialize(other);
+// this.typedTarget = (Packet20NamedEntitySpawn) other.getTarget();
+// this.compiler = compiler;
+// }
+//
+// @SuppressWarnings("unchecked")
+// @Override
+// public TField read(int fieldIndex) throws FieldAccessException {
+//
+// Packet20NamedEntitySpawn target = typedTarget;
+//
+// switch (fieldIndex) {
+// case 0: return (TField) (Object) target.a;
+// case 1: return (TField) (Object) target.b;
+// case 2: return (TField) (Object) target.c;
+// case 3: return (TField) (Object) target.d;
+// case 4: return (TField) (Object) target.e;
+// case 5: return (TField) (Object) target.f;
+// case 6: return (TField) (Object) target.g;
+// case 7: return (TField) (Object) target.h;
+// default:
+// throw new IllegalArgumentException("Invalid index " + fieldIndex);
+// }
+// }
+//
+// @Override
+// public StructureModifier write(int index, Object value) {
+//
+// Packet20NamedEntitySpawn target = typedTarget;
+//
+// switch (index) {
+// case 0: target.a = (Integer) value; break;
+// case 1: target.b = (String) value; break;
+// case 2: target.c = (Integer) value; break;
+// case 3: target.d = (Integer) value; break;
+// case 4: target.e = (Integer) value; break;
+// case 5: target.f = (Byte) value; break;
+// case 6: target.g = (Byte) value; break;
+// case 7: target.h = (Integer) value; break;
+// default:
+// throw new IllegalArgumentException("Invalid index " + index);
+// }
+//
+// // Chaining
+// return this;
+// }
+// }
+
+/**
+ * Represents a StructureModifier compiler.
+ *
+ * @author Kristian
+ */
+public final class StructureCompiler {
+
+ // Used to store generated classes of different types
+ @SuppressWarnings("rawtypes")
+ private class StructureKey {
+ private Class targetType;
+ private Class fieldType;
+
+ public StructureKey(Class targetType, Class fieldType) {
+ this.targetType = targetType;
+ this.fieldType = fieldType;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(targetType, fieldType);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof StructureKey) {
+ StructureKey other = (StructureKey) obj;
+ return Objects.equal(targetType, other.targetType) &&
+ Objects.equal(fieldType, other.fieldType);
+ }
+ return false;
+ }
+ }
+
+ // Used to load classes
+ private static Method defineMethod;
+
+ @SuppressWarnings("rawtypes")
+ private Map compiledCache = new HashMap();
+
+ // The class loader we'll store our classes
+ private ClassLoader loader;
+
+ // References to other classes
+ private static String PACKAGE_NAME = "com/comphenix/protocol/compiler";
+ private static String SUPER_CLASS = "com/comphenix/protocol/reflect/StructureModifier";
+ private static String COMPILED_CLASS = PACKAGE_NAME + "/CompiledStructureModifier";
+
+ /**
+ * Construct a structure compiler.
+ * @param loader - main class loader.
+ */
+ StructureCompiler(ClassLoader loader) {
+ this.loader = loader;
+ }
+
+ /**
+ * Compiles the given structure modifier.
+ *
+ * WARNING: Do NOT call this method in the main thread. Compiling may easily take 10 ms, which is already
+ * over 1/4 of a tick (50 ms). Let the background thread automatically compile the structure modifiers instead.
+ * @param source - structure modifier to compile.
+ * @return A compiled structure modifier.
+ */
+ @SuppressWarnings("unchecked")
+ public synchronized StructureModifier compile(StructureModifier source) {
+
+ // We cannot optimize a structure modifier with no public fields
+ if (!isAnyPublic(source.getFields())) {
+ return source;
+ }
+
+ StructureKey key = new StructureKey(source.getTargetType(), source.getFieldType());
+ Class> compiledClass = compiledCache.get(key);
+
+ if (!compiledCache.containsKey(key)) {
+ compiledClass = generateClass(source);
+ compiledCache.put(key, compiledClass);
+ }
+
+ // Next, create an instance of this class
+ try {
+ return (StructureModifier) compiledClass.getConstructor(
+ StructureModifier.class, StructureCompiler.class).
+ newInstance(source, this);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalStateException("Used invalid parameters in instance creation", e);
+ } catch (SecurityException e) {
+ throw new RuntimeException("Security limitation!", e);
+ } catch (InstantiationException e) {
+ throw new RuntimeException("Error occured while instancing generated class.", e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Security limitation! Cannot create instance of dynamic class.", e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException("Error occured while instancing generated class.", e);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalStateException("Cannot happen.", e);
+ }
+ }
+
+ private Class> generateClass(StructureModifier source) {
+
+ ClassWriter cw = new ClassWriter(0);
+
+ @SuppressWarnings("rawtypes")
+ Class targetType = source.getTargetType();
+
+ String className = "CompiledStructure$" + targetType.getSimpleName() + source.getFieldType().getSimpleName();
+ String targetSignature = Type.getDescriptor(targetType);
+ String targetName = targetType.getName().replace('.', '/');
+
+ cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, PACKAGE_NAME + "/" + className,
+ "L" + COMPILED_CLASS + ";",
+ COMPILED_CLASS, null);
+
+ createFields(cw, targetSignature);
+ createConstructor(cw, className, targetSignature, targetName);
+ createReadMethod(cw, className, source.getFields(), targetSignature, targetName);
+ createWriteMethod(cw, className, source.getFields(), targetSignature, targetName);
+ cw.visitEnd();
+
+ byte[] data = cw.toByteArray();
+
+ // Call the define method
+ try {
+ if (defineMethod == null) {
+ defineMethod = ClassLoader.class.getDeclaredMethod("defineClass",
+ new Class>[] { String.class, byte[].class, int.class, int.class });
+
+ // Awesome. Now, create and return it.
+ defineMethod.setAccessible(true);
+ }
+
+ @SuppressWarnings("rawtypes")
+ Class clazz = (Class) defineMethod.invoke(loader, null, data, 0, data.length);
+
+ // DEBUG CODE: Print the content of the generated class.
+ //org.objectweb.asm.ClassReader cr = new org.objectweb.asm.ClassReader(data);
+ //cr.accept(new ASMifierClassVisitor(new PrintWriter(System.out)), 0);
+
+ return clazz;
+
+ } catch (SecurityException e) {
+ throw new RuntimeException("Cannot use reflection to dynamically load a class.", e);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalStateException("Incompatible JVM.", e);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalStateException("Cannot call defineMethod - wrong JVM?", e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Security limitation! Cannot dynamically load class.", e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException("Error occured in code generator.", e);
+ }
+ }
+
+ /**
+ * Determine if at least one of the given fields is public.
+ * @param fields - field to test.
+ * @return TRUE if one or more field is publically accessible, FALSE otherwise.
+ */
+ private boolean isAnyPublic(List fields) {
+ // Are any of the fields public?
+ for (int i = 0; i < fields.size(); i++) {
+ if (isPublic(fields.get(i))) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean isPublic(Field field) {
+ return Modifier.isPublic(field.getModifiers());
+ }
+
+ private void createFields(ClassWriter cw, String targetSignature) {
+ FieldVisitor typedField = cw.visitField(Opcodes.ACC_PRIVATE, "typedTarget", targetSignature, null, null);
+ typedField.visitEnd();
+ }
+
+ private void createWriteMethod(ClassWriter cw, String className, List fields, String targetSignature, String targetName) {
+
+ String methodDescriptor = "(ILjava/lang/Object;)L" + SUPER_CLASS + ";";
+ String methodSignature = "(ITTField;)L" + SUPER_CLASS + ";";
+ MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL, "write", methodDescriptor, methodSignature,
+ new String[] { "com/comphenix/protocol/reflect/FieldAccessException" });
+ BoxingHelper boxingHelper = new BoxingHelper(mv);
+
+ mv.visitCode();
+ mv.visitVarInsn(Opcodes.ALOAD, 0);
+ mv.visitFieldInsn(Opcodes.GETFIELD, PACKAGE_NAME + "/" + className, "typedTarget", targetSignature);
+ mv.visitVarInsn(Opcodes.ASTORE, 3);
+ mv.visitVarInsn(Opcodes.ILOAD, 1);
+
+ // The last label is for the default switch
+ Label[] labels = new Label[fields.size()];
+ Label errorLabel = new Label();
+ Label returnLabel = new Label();
+
+ // Generate labels
+ for (int i = 0; i < fields.size(); i++) {
+ labels[i] = new Label();
+ }
+
+ mv.visitTableSwitchInsn(0, labels.length - 1, errorLabel, labels);
+
+ for (int i = 0; i < fields.size(); i++) {
+
+ Class> outputType = fields.get(i).getType();
+ Class> inputType = PrimitiveUtils.wrap(outputType);
+ String typeDescriptor = Type.getDescriptor(outputType);
+ String inputPath = inputType.getName().replace('.', '/');
+
+ mv.visitLabel(labels[i]);
+
+ // Push the compare object
+ if (i == 0)
+ mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] { targetName }, 0, null);
+ else
+ mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+
+ // Only write to public fields
+ if (isPublic(fields.get(i))) {
+ mv.visitVarInsn(Opcodes.ALOAD, 3);
+ mv.visitVarInsn(Opcodes.ALOAD, 2);
+
+ if (!PrimitiveUtils.isPrimitive(outputType))
+ mv.visitTypeInsn(Opcodes.CHECKCAST, inputPath);
+ else
+ boxingHelper.unbox(Type.getType(outputType));
+
+ mv.visitFieldInsn(Opcodes.PUTFIELD, targetName, fields.get(i).getName(), typeDescriptor);
+
+ } else {
+ // Use reflection. We don't have a choice, unfortunately.
+ mv.visitVarInsn(Opcodes.ALOAD, 0);
+ mv.visitVarInsn(Opcodes.ILOAD, 1);
+ mv.visitVarInsn(Opcodes.ALOAD, 2);
+ mv.visitMethodInsn(Opcodes.INVOKESPECIAL, COMPILED_CLASS, "write", "(ILjava/lang/Object;)L" + SUPER_CLASS + ";");
+ mv.visitInsn(Opcodes.POP);
+ }
+
+ mv.visitJumpInsn(Opcodes.GOTO, returnLabel);
+ }
+
+ mv.visitLabel(errorLabel);
+ mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+ mv.visitTypeInsn(Opcodes.NEW, "java/lang/IllegalArgumentException");
+ mv.visitInsn(Opcodes.DUP);
+ mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
+ mv.visitInsn(Opcodes.DUP);
+ mv.visitLdcInsn("Invalid index ");
+ mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "(Ljava/lang/String;)V");
+ mv.visitVarInsn(Opcodes.ILOAD, 1);
+ mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;");
+ mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;");
+ mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/IllegalArgumentException", "", "(Ljava/lang/String;)V");
+ mv.visitInsn(Opcodes.ATHROW);
+
+ mv.visitLabel(returnLabel);
+ mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+ mv.visitVarInsn(Opcodes.ALOAD, 0);
+ mv.visitInsn(Opcodes.ARETURN);
+ mv.visitMaxs(5, 4);
+ mv.visitEnd();
+ }
+
+ private void createReadMethod(ClassWriter cw, String className, List fields, String targetSignature, String targetName) {
+ MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL, "read", "(I)Ljava/lang/Object;", "(I)TTField;",
+ new String[] { "com/comphenix/protocol/reflect/FieldAccessException" });
+ BoxingHelper boxingHelper = new BoxingHelper(mv);
+
+ mv.visitCode();
+ mv.visitVarInsn(Opcodes.ALOAD, 0);
+ mv.visitFieldInsn(Opcodes.GETFIELD, PACKAGE_NAME + "/" + className, "typedTarget", targetSignature);
+ mv.visitVarInsn(Opcodes.ASTORE, 2);
+ mv.visitVarInsn(Opcodes.ILOAD, 1);
+
+ // The last label is for the default switch
+ Label[] labels = new Label[fields.size()];
+ Label errorLabel = new Label();
+
+ // Generate labels
+ for (int i = 0; i < fields.size(); i++) {
+ labels[i] = new Label();
+ }
+
+ mv.visitTableSwitchInsn(0, fields.size() - 1, errorLabel, labels);
+
+ for (int i = 0; i < fields.size(); i++) {
+ Class> outputType = fields.get(i).getType();
+ String typeDescriptor = Type.getDescriptor(outputType);
+
+ mv.visitLabel(labels[i]);
+
+ // Push the compare object
+ if (i == 0)
+ mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] { targetName }, 0, null);
+ else
+ mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+
+ // Note that byte code cannot access non-public fields
+ if (isPublic(fields.get(i))) {
+ mv.visitVarInsn(Opcodes.ALOAD, 2);
+ mv.visitFieldInsn(Opcodes.GETFIELD, targetName, fields.get(i).getName(), typeDescriptor);
+
+ boxingHelper.box(Type.getType(outputType));
+ } else {
+ // We have to use reflection for private and protected fields.
+ mv.visitVarInsn(Opcodes.ALOAD, 0);
+ mv.visitVarInsn(Opcodes.ILOAD, 1);
+ mv.visitMethodInsn(Opcodes.INVOKESPECIAL, COMPILED_CLASS, "read", "(I)Ljava/lang/Object;");
+ }
+
+ mv.visitInsn(Opcodes.ARETURN);
+ }
+
+ mv.visitLabel(errorLabel);
+ mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+ mv.visitTypeInsn(Opcodes.NEW, "java/lang/IllegalArgumentException");
+ mv.visitInsn(Opcodes.DUP);
+ mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
+ mv.visitInsn(Opcodes.DUP);
+ mv.visitLdcInsn("Invalid index ");
+ mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "(Ljava/lang/String;)V");
+ mv.visitVarInsn(Opcodes.ILOAD, 1);
+ mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;");
+ mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;");
+ mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/IllegalArgumentException", "", "(Ljava/lang/String;)V");
+ mv.visitInsn(Opcodes.ATHROW);
+ mv.visitMaxs(5, 3);
+ mv.visitEnd();
+ }
+
+ private void createConstructor(ClassWriter cw, String className, String targetSignature, String targetName) {
+ MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "",
+ "(L" + SUPER_CLASS + ";L" + PACKAGE_NAME + "/StructureCompiler;)V",
+ "(L" + SUPER_CLASS + ";L" + SUPER_CLASS + ";)V", null);
+ mv.visitCode();
+ mv.visitVarInsn(Opcodes.ALOAD, 0);
+ mv.visitMethodInsn(Opcodes.INVOKESPECIAL, COMPILED_CLASS, "", "()V");
+ mv.visitVarInsn(Opcodes.ALOAD, 0);
+ mv.visitVarInsn(Opcodes.ALOAD, 1);
+ mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, PACKAGE_NAME + "/" + className, "initialize", "(L" + SUPER_CLASS + ";)V");
+ mv.visitVarInsn(Opcodes.ALOAD, 0);
+ mv.visitVarInsn(Opcodes.ALOAD, 1);
+ mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, SUPER_CLASS, "getTarget", "()Ljava/lang/Object;");
+ mv.visitFieldInsn(Opcodes.PUTFIELD, PACKAGE_NAME + "/" + className, "target", "Ljava/lang/Object;");
+ mv.visitVarInsn(Opcodes.ALOAD, 0);
+ mv.visitVarInsn(Opcodes.ALOAD, 0);
+ mv.visitFieldInsn(Opcodes.GETFIELD, PACKAGE_NAME + "/" + className, "target", "Ljava/lang/Object;");
+ mv.visitTypeInsn(Opcodes.CHECKCAST, targetName);
+ mv.visitFieldInsn(Opcodes.PUTFIELD, PACKAGE_NAME + "/" + className, "typedTarget", targetSignature);
+ mv.visitVarInsn(Opcodes.ALOAD, 0);
+ mv.visitVarInsn(Opcodes.ALOAD, 2);
+ mv.visitFieldInsn(Opcodes.PUTFIELD, PACKAGE_NAME + "/" + className, "compiler", "L" + PACKAGE_NAME + "/StructureCompiler;");
+ mv.visitInsn(Opcodes.RETURN);
+ mv.visitMaxs(2, 3);
+ mv.visitEnd();
+ }
+}
diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/StructureModifier.java b/ProtocolLib/src/com/comphenix/protocol/reflect/StructureModifier.java
index aecd71bd..d79733b2 100644
--- a/ProtocolLib/src/com/comphenix/protocol/reflect/StructureModifier.java
+++ b/ProtocolLib/src/com/comphenix/protocol/reflect/StructureModifier.java
@@ -21,15 +21,23 @@ import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import com.comphenix.protocol.compiler.BackgroundCompiler;
import com.comphenix.protocol.reflect.instances.DefaultInstances;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
+/**
+ * Provides list-oriented access to the fields of a Minecraft packet.
+ *
+ * Implemented by using reflection. Use a CompiledStructureModifier, if speed is essential.
+ *
+ * @author Kristian
+ * @param Type of the fields to retrieve.
+ */
@SuppressWarnings("rawtypes")
public class StructureModifier {
@@ -45,7 +53,7 @@ public class StructureModifier {
protected List data = new ArrayList();
// Improved default values
- protected Set defaultFields;
+ protected Map defaultFields;
// Cache of previous types
protected Map subtypeCache;
@@ -58,9 +66,9 @@ public class StructureModifier {
*/
public StructureModifier(Class targetType, Class superclassExclude, boolean requireDefault) {
List fields = getFields(targetType, superclassExclude);
- Set defaults = requireDefault ? generateDefaultFields(fields) : new HashSet();
+ Map defaults = requireDefault ? generateDefaultFields(fields) : new HashMap();
- initialize(targetType, Object.class, fields, defaults, null, new HashMap());
+ initialize(targetType, Object.class, fields, defaults, null, new ConcurrentHashMap());
}
/**
@@ -89,7 +97,7 @@ public class StructureModifier {
* @param subTypeCache - a structure modifier cache.
*/
protected void initialize(Class targetType, Class fieldType,
- List data, Set defaultFields,
+ List data, Map defaultFields,
EquivalentConverter converter, Map subTypeCache) {
this.targetType = targetType;
this.fieldType = fieldType;
@@ -213,7 +221,7 @@ public class StructureModifier {
DefaultInstances generator = DefaultInstances.DEFAULT;
// Write a default instance to every field
- for (Field field : defaultFields) {
+ for (Field field : defaultFields.keySet()) {
try {
FieldUtils.writeField(field, target,
generator.getDefault(field.getType()), true);
@@ -239,22 +247,32 @@ public class StructureModifier {
// Do we need to update the cache?
if (result == null) {
List filtered = new ArrayList();
- Set defaults = new HashSet();
+ Map defaults = new HashMap();
+ int index = 0;
for (Field field : data) {
if (fieldType != null && fieldType.isAssignableFrom(field.getType())) {
filtered.add(field);
- if (defaultFields.contains(field))
- defaults.add(field);
+ // Don't use the original index
+ if (defaultFields.containsKey(field))
+ defaults.put(field, index);
}
+
+ // Keep track of the field index
+ index++;
}
// Cache structure modifiers
result = withFieldType(fieldType, filtered, defaults, converter);
- if (fieldType != null)
+ if (fieldType != null) {
subtypeCache.put(fieldType, result);
+
+ // Automatically compile the structure modifier
+ if (BackgroundCompiler.getInstance() != null)
+ BackgroundCompiler.getInstance().scheduleCompilation(subtypeCache, fieldType);
+ }
}
// Add the target too
@@ -320,11 +338,11 @@ public class StructureModifier {
*/
protected StructureModifier withFieldType(
Class fieldType, List filtered,
- Set defaults, EquivalentConverter converter) {
+ Map defaults, EquivalentConverter converter) {
StructureModifier result = new StructureModifier();
result.initialize(targetType, fieldType, filtered, defaults,
- converter, new HashMap());
+ converter, new ConcurrentHashMap());
return result;
}
@@ -387,10 +405,11 @@ public class StructureModifier {
}
// Used to generate plausible default values
- private static Set generateDefaultFields(List fields) {
+ private static Map generateDefaultFields(List fields) {
- Set requireDefaults = new HashSet();
+ Map requireDefaults = new HashMap();
DefaultInstances generator = DefaultInstances.DEFAULT;
+ int index = 0;
for (Field field : fields) {
Class> type = field.getType();
@@ -400,9 +419,12 @@ public class StructureModifier {
// Next, see if we actually can generate a default value
if (generator.getDefault(type) != null) {
// If so, require it
- requireDefaults.add(field);
+ requireDefaults.put(field, index);
}
}
+
+ // Increment field index
+ index++;
}
return requireDefaults;