diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java index c9051ebd..337f3b3d 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java @@ -439,7 +439,7 @@ class CommandPacket extends CommandBase { @Override public boolean print(StringBuilder output, Object value) { if (value != null) { - EquivalentConverter converter = BukkitConverters.getGenericConverters().get(value.getClass()); + EquivalentConverter converter = BukkitConverters.getConvertersForGeneric().get(value.getClass()); if (converter != null) { output.append(converter.getSpecific(value)); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java index e0ec72a9..247d5150 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java @@ -62,6 +62,7 @@ import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.utility.StreamSerializer; import com.comphenix.protocol.wrappers.BukkitConverters; import com.comphenix.protocol.wrappers.ChunkPosition; +import com.comphenix.protocol.wrappers.WrappedAttribute; import com.comphenix.protocol.wrappers.WrappedDataWatcher; import com.comphenix.protocol.wrappers.WrappedWatchableObject; import com.comphenix.protocol.wrappers.nbt.NbtBase; @@ -386,6 +387,23 @@ public class PacketContainer implements Serializable { BukkitConverters.getNbtConverter()); } + /** + * Retrieves a read/write structure for collections of attribute snapshots. + *

+ * This modifier will automatically marshall between the visible ProtocolLib WrappedAttribute and the + * internal Minecraft AttributeSnapshot. + * @return A modifier for AttributeSnapshot collection fields. + */ + public StructureModifier> getAttributeCollectionModifier() { + // Convert to and from the ProtocolLib wrapper + return structureModifier.withType( + Collection.class, + BukkitConverters.getListConverter( + MinecraftReflection.getAttributeSnapshotClass(), + BukkitConverters.getWrappedAttributeConverter()) + ); + } + /** * Retrieves a read/write structure for collections of chunk positions. *

diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java index 752a14d9..664e8ea9 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java @@ -89,6 +89,15 @@ public class StructureModifier { this(targetType, null, true); } + /** + * Creates a structure modifier. + * @param targetType - the structure to modify. + * @param useStructureCompiler - whether or not to use a structure compiler. + */ + public StructureModifier(Class targetType, boolean useStructureCompiler) { + this(targetType, null, true, useStructureCompiler); + } + /** * Creates a structure modifier. * @param targetType - the structure to modify. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java index 19e9e56c..4883523a 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java @@ -1,365 +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 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. - *

- * 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>> 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. - *

- * 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 cache, final Class key) { - - @SuppressWarnings("unchecked") - final StructureModifier uncompiled = cache.get(key); - - if (uncompiled != null) { - scheduleCompilation(uncompiled, new CompileListener() { - @Override - public void onCompiled(StructureModifier 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 void scheduleCompilation(final StructureModifier uncompiled, final CompileListener 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() { - @Override - public Object call() throws Exception { - StructureModifier 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) 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 void addListener(final StructureModifier uncompiled, final CompileListener 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. + *

+ * 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>> 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. + *

+ * 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 cache, final Class key) { + + @SuppressWarnings("unchecked") + final StructureModifier uncompiled = cache.get(key); + + if (uncompiled != null) { + scheduleCompilation(uncompiled, new CompileListener() { + @Override + public void onCompiled(StructureModifier 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 void scheduleCompilation(final StructureModifier uncompiled, final CompileListener 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() { + @Override + public Object call() throws Exception { + StructureModifier 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) 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 void addListener(final StructureModifier uncompiled, final CompileListener 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; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/EmptyClassVisitor.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/EmptyClassVisitor.java new file mode 100644 index 00000000..495eb6c7 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/EmptyClassVisitor.java @@ -0,0 +1,57 @@ +package com.comphenix.protocol.reflect.compiler; + +import net.sf.cglib.asm.AnnotationVisitor; +import net.sf.cglib.asm.Attribute; +import net.sf.cglib.asm.ClassVisitor; +import net.sf.cglib.asm.FieldVisitor; +import net.sf.cglib.asm.MethodVisitor; + +public abstract class EmptyClassVisitor implements ClassVisitor { + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + // NOP + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // NOP + return null; + } + + @Override + public void visitAttribute(Attribute attr) { + // NOP + } + + @Override + public void visitEnd() { + // NOP + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { + // NOP + return null; + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + // NOP + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + // NOP + return null; + } + + @Override + public void visitOuterClass(String owner, String name, String desc) { + // NOP + } + + @Override + public void visitSource(String source, String debug) { + // NOP + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/EmptyMethodVisitor.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/EmptyMethodVisitor.java new file mode 100644 index 00000000..de648408 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/EmptyMethodVisitor.java @@ -0,0 +1,132 @@ +package com.comphenix.protocol.reflect.compiler; + +import net.sf.cglib.asm.AnnotationVisitor; +import net.sf.cglib.asm.Attribute; +import net.sf.cglib.asm.Label; +import net.sf.cglib.asm.MethodVisitor; + +public class EmptyMethodVisitor implements MethodVisitor { + @Override + public AnnotationVisitor visitAnnotationDefault() { + // NOP + return null; + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // NOP + return null; + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { + // NOP + return null; + } + + @Override + public void visitAttribute(Attribute attr) { + // NOP + } + + @Override + public void visitCode() { + // NOP + } + + @Override + public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) { + // NOP + } + + @Override + public void visitInsn(int opcode) { + // NOP + } + + @Override + public void visitIntInsn(int opcode, int operand) { + // NOP + } + + @Override + public void visitVarInsn(int opcode, int var) { + // NOP + } + + @Override + public void visitTypeInsn(int opcode, String type) { + // NOP + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + // NOP + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + // NOP + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + // NOP + } + + @Override + public void visitLabel(Label label) { + // NOP + } + + @Override + public void visitLdcInsn(Object cst) { + // NOP + } + + @Override + public void visitIincInsn(int var, int increment) { + // NOP + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) { + // NOP + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + // NOP + } + + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + // NOP + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + // NOP + } + + @Override + public void visitLocalVariable(String name, String desc, String signature, Label start, + Label end, int index) { + // NOP + } + + @Override + public void visitLineNumber(int line, Label start) { + // NOP + } + + @Override + public void visitMaxs(int maxStack, int maxLocals) { + // NOP + } + + @Override + public void visitEnd() { + // NOP + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/StructureCompiler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/StructureCompiler.java index 70dbe262..b681e77e 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/StructureCompiler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/StructureCompiler.java @@ -1,533 +1,533 @@ -/* - * 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.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import com.comphenix.protocol.ProtocolLibrary; -import com.comphenix.protocol.error.Report; -import com.comphenix.protocol.error.ReportType; -import com.comphenix.protocol.reflect.StructureModifier; -import com.google.common.base.Objects; -import com.google.common.primitives.Primitives; - -import net.sf.cglib.asm.*; - -// public class CompiledStructureModifierPacket20 extends CompiledStructureModifier { -// -// private Packet20NamedEntitySpawn typedTarget; -// -// public CompiledStructureModifierPacket20(StructureModifier other, StructureCompiler compiler) { -// super(); -// initialize(other); -// this.target = other.getTarget(); -// this.typedTarget = (Packet20NamedEntitySpawn) target; -// this.compiler = compiler; -// } -// -// @Override -// protected Object readGenerated(int fieldIndex) throws FieldAccessException { -// -// Packet20NamedEntitySpawn target = typedTarget; -// -// switch (fieldIndex) { -// case 0: return (Object) target.a; -// case 1: return (Object) target.b; -// case 2: return (Object) target.c; -// case 3: return super.readReflected(fieldIndex); -// case 4: return super.readReflected(fieldIndex); -// case 5: return (Object) target.f; -// case 6: return (Object) target.g; -// case 7: return (Object) target.h; -// default: -// throw new FieldAccessException("Invalid index " + fieldIndex); -// } -// } -// -// @Override -// protected StructureModifier writeGenerated(int index, Object value) throws FieldAccessException { -// -// 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: super.writeReflected(index, value); break; -// case 5: super.writeReflected(index, value); break; -// case 6: target.g = (Byte) value; break; -// case 7: target.h = (Integer) value; break; -// default: -// throw new FieldAccessException("Invalid index " + index); -// } -// -// // Chaining -// return this; -// } -// } - -/** - * Represents a StructureModifier compiler. - * - * @author Kristian - */ -public final class StructureCompiler { - public static final ReportType REPORT_TOO_MANY_GENERATED_CLASSES = new ReportType("Generated too many classes (count: %s)"); - - // Used to store generated classes of different types - @SuppressWarnings("rawtypes") - static class StructureKey { - private Class targetType; - private Class fieldType; - - public StructureKey(StructureModifier source) { - this(source.getTargetType(), source.getFieldType()); - } - - 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 volatile static Method defineMethod; - - @SuppressWarnings("rawtypes") - private Map compiledCache = new ConcurrentHashMap(); - - // The class loader we'll store our classes - private ClassLoader loader; - - // References to other classes - private static String PACKAGE_NAME = "com/comphenix/protocol/reflect/compiler"; - private static String SUPER_CLASS = "com/comphenix/protocol/reflect/StructureModifier"; - private static String COMPILED_CLASS = PACKAGE_NAME + "/CompiledStructureModifier"; - private static String FIELD_EXCEPTION_CLASS = "com/comphenix/protocol/reflect/FieldAccessException"; - - /** - * Construct a structure compiler. - * @param loader - main class loader. - */ - StructureCompiler(ClassLoader loader) { - this.loader = loader; - } - - /** - * Lookup the current class loader for any previously generated classes before we attempt to generate something. - * @param source - the structure modifier to look up. - * @return TRUE if we successfully found a previously generated class, FALSE otherwise. - */ - public boolean lookupClassLoader(StructureModifier source) { - StructureKey key = new StructureKey(source); - - // See if there's a need to lookup the class name - if (compiledCache.containsKey(key)) { - return true; - } - - try { - String className = getCompiledName(source); - - // This class might have been generated before. Try to load it. - Class before = loader.loadClass(PACKAGE_NAME.replace('/', '.') + "." + className); - - if (before != null) { - compiledCache.put(key, before); - return true; - } - } catch (ClassNotFoundException e) { - // That's ok. - } - - // We need to compile the class - return false; - } - - /** - * 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); - 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 (OutOfMemoryError e) { - // Print the number of generated classes by the current instances - ProtocolLibrary.getErrorReporter().reportWarning( - this, Report.newBuilder(REPORT_TOO_MANY_GENERATED_CLASSES).messageParam(compiledCache.size()) - ); - throw e; - } 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); - } - } - - /** - * Retrieve a variable identifier that can uniquely represent the given type. - * @param type - a type. - * @return A unique and legal identifier for the given type. - */ - private String getSafeTypeName(Class type) { - return type.getCanonicalName().replace("[]", "Array").replace(".", "_"); - } - - /** - * Retrieve the compiled name of a given structure modifier. - * @param source - the structure modifier. - * @return The unique, compiled name of a compiled structure modifier. - */ - private String getCompiledName(StructureModifier source) { - Class targetType = source.getTargetType(); - - // Concat class and field type - return "CompiledStructure$" + - getSafeTypeName(targetType) + "$" + - getSafeTypeName(source.getFieldType()); - } - - /** - * Compile a structure modifier. - * @param source - structure modifier. - * @return The compiled structure modifier. - */ - private Class generateClass(StructureModifier source) { - - ClassWriter cw = new ClassWriter(0); - Class targetType = source.getTargetType(); - - String className = getCompiledName(source); - String targetSignature = Type.getDescriptor(targetType); - String targetName = targetType.getName().replace('.', '/'); - - // Define class - cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, PACKAGE_NAME + "/" + className, - null, 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) { - Method defined = ClassLoader.class.getDeclaredMethod("defineClass", - new Class[] { String.class, byte[].class, int.class, int.class }); - - // Awesome. Now, create and return it. - defined.setAccessible(true); - defineMethod = defined; - } - - @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 boolean isNonFinal(Field field) { - return !Modifier.isFinal(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 = "(ILjava/lang/Object;)L" + SUPER_CLASS + ";"; - MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PROTECTED, "writeGenerated", methodDescriptor, methodSignature, - new String[] { FIELD_EXCEPTION_CLASS }); - BoxingHelper boxingHelper = new BoxingHelper(mv); - - String generatedClassName = PACKAGE_NAME + "/" + className; - - mv.visitCode(); - mv.visitVarInsn(Opcodes.ALOAD, 0); - mv.visitFieldInsn(Opcodes.GETFIELD, generatedClassName, "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++) { - - Field field = fields.get(i); - Class outputType = field.getType(); - Class inputType = Primitives.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 non-final fields - if (isPublic(field) && isNonFinal(field)) { - mv.visitVarInsn(Opcodes.ALOAD, 3); - mv.visitVarInsn(Opcodes.ALOAD, 2); - - if (!outputType.isPrimitive()) - mv.visitTypeInsn(Opcodes.CHECKCAST, inputPath); - else - boxingHelper.unbox(Type.getType(outputType)); - - mv.visitFieldInsn(Opcodes.PUTFIELD, targetName, field.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.INVOKEVIRTUAL, generatedClassName, "writeReflected", "(ILjava/lang/Object;)V"); - } - - mv.visitJumpInsn(Opcodes.GOTO, returnLabel); - } - - mv.visitLabel(errorLabel); - mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); - mv.visitTypeInsn(Opcodes.NEW, FIELD_EXCEPTION_CLASS); - 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, FIELD_EXCEPTION_CLASS, "", "(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_PROTECTED, "readGenerated", "(I)Ljava/lang/Object;", null, - new String[] { "com/comphenix/protocol/reflect/FieldAccessException" }); - BoxingHelper boxingHelper = new BoxingHelper(mv); - - String generatedClassName = PACKAGE_NAME + "/" + className; - - mv.visitCode(); - mv.visitVarInsn(Opcodes.ALOAD, 0); - mv.visitFieldInsn(Opcodes.GETFIELD, generatedClassName, "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++) { - - Field field = fields.get(i); - Class outputType = field.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(field)) { - mv.visitVarInsn(Opcodes.ALOAD, 2); - mv.visitFieldInsn(Opcodes.GETFIELD, targetName, field.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.INVOKEVIRTUAL, generatedClassName, "readReflected", "(I)Ljava/lang/Object;"); - } - - mv.visitInsn(Opcodes.ARETURN); - } - - mv.visitLabel(errorLabel); - mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); - mv.visitTypeInsn(Opcodes.NEW, FIELD_EXCEPTION_CLASS); - 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, FIELD_EXCEPTION_CLASS, "", "(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" + PACKAGE_NAME + "/StructureCompiler;)V", null); - String fullClassName = PACKAGE_NAME + "/" + className; - - 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, fullClassName, "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, fullClassName, "target", "Ljava/lang/Object;"); - mv.visitVarInsn(Opcodes.ALOAD, 0); - mv.visitVarInsn(Opcodes.ALOAD, 0); - mv.visitFieldInsn(Opcodes.GETFIELD, fullClassName, "target", "Ljava/lang/Object;"); - mv.visitTypeInsn(Opcodes.CHECKCAST, targetName); - mv.visitFieldInsn(Opcodes.PUTFIELD, fullClassName, "typedTarget", targetSignature); - mv.visitVarInsn(Opcodes.ALOAD, 0); - mv.visitVarInsn(Opcodes.ALOAD, 2); - mv.visitFieldInsn(Opcodes.PUTFIELD, fullClassName, "compiler", "L" + PACKAGE_NAME + "/StructureCompiler;"); - mv.visitInsn(Opcodes.RETURN); - mv.visitMaxs(2, 3); - mv.visitEnd(); - } -} +/* + * 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.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.reflect.StructureModifier; +import com.google.common.base.Objects; +import com.google.common.primitives.Primitives; + +import net.sf.cglib.asm.*; + +// public class CompiledStructureModifierPacket20 extends CompiledStructureModifier { +// +// private Packet20NamedEntitySpawn typedTarget; +// +// public CompiledStructureModifierPacket20(StructureModifier other, StructureCompiler compiler) { +// super(); +// initialize(other); +// this.target = other.getTarget(); +// this.typedTarget = (Packet20NamedEntitySpawn) target; +// this.compiler = compiler; +// } +// +// @Override +// protected Object readGenerated(int fieldIndex) throws FieldAccessException { +// +// Packet20NamedEntitySpawn target = typedTarget; +// +// switch (fieldIndex) { +// case 0: return (Object) target.a; +// case 1: return (Object) target.b; +// case 2: return (Object) target.c; +// case 3: return super.readReflected(fieldIndex); +// case 4: return super.readReflected(fieldIndex); +// case 5: return (Object) target.f; +// case 6: return (Object) target.g; +// case 7: return (Object) target.h; +// default: +// throw new FieldAccessException("Invalid index " + fieldIndex); +// } +// } +// +// @Override +// protected StructureModifier writeGenerated(int index, Object value) throws FieldAccessException { +// +// 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: super.writeReflected(index, value); break; +// case 5: super.writeReflected(index, value); break; +// case 6: target.g = (Byte) value; break; +// case 7: target.h = (Integer) value; break; +// default: +// throw new FieldAccessException("Invalid index " + index); +// } +// +// // Chaining +// return this; +// } +// } + +/** + * Represents a StructureModifier compiler. + * + * @author Kristian + */ +public final class StructureCompiler { + public static final ReportType REPORT_TOO_MANY_GENERATED_CLASSES = new ReportType("Generated too many classes (count: %s)"); + + // Used to store generated classes of different types + @SuppressWarnings("rawtypes") + static class StructureKey { + private Class targetType; + private Class fieldType; + + public StructureKey(StructureModifier source) { + this(source.getTargetType(), source.getFieldType()); + } + + 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 volatile static Method defineMethod; + + @SuppressWarnings("rawtypes") + private Map compiledCache = new ConcurrentHashMap(); + + // The class loader we'll store our classes + private ClassLoader loader; + + // References to other classes + private static String PACKAGE_NAME = "com/comphenix/protocol/reflect/compiler"; + private static String SUPER_CLASS = "com/comphenix/protocol/reflect/StructureModifier"; + private static String COMPILED_CLASS = PACKAGE_NAME + "/CompiledStructureModifier"; + private static String FIELD_EXCEPTION_CLASS = "com/comphenix/protocol/reflect/FieldAccessException"; + + /** + * Construct a structure compiler. + * @param loader - main class loader. + */ + StructureCompiler(ClassLoader loader) { + this.loader = loader; + } + + /** + * Lookup the current class loader for any previously generated classes before we attempt to generate something. + * @param source - the structure modifier to look up. + * @return TRUE if we successfully found a previously generated class, FALSE otherwise. + */ + public boolean lookupClassLoader(StructureModifier source) { + StructureKey key = new StructureKey(source); + + // See if there's a need to lookup the class name + if (compiledCache.containsKey(key)) { + return true; + } + + try { + String className = getCompiledName(source); + + // This class might have been generated before. Try to load it. + Class before = loader.loadClass(PACKAGE_NAME.replace('/', '.') + "." + className); + + if (before != null) { + compiledCache.put(key, before); + return true; + } + } catch (ClassNotFoundException e) { + // That's ok. + } + + // We need to compile the class + return false; + } + + /** + * 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); + 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 (OutOfMemoryError e) { + // Print the number of generated classes by the current instances + ProtocolLibrary.getErrorReporter().reportWarning( + this, Report.newBuilder(REPORT_TOO_MANY_GENERATED_CLASSES).messageParam(compiledCache.size()) + ); + throw e; + } 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); + } + } + + /** + * Retrieve a variable identifier that can uniquely represent the given type. + * @param type - a type. + * @return A unique and legal identifier for the given type. + */ + private String getSafeTypeName(Class type) { + return type.getCanonicalName().replace("[]", "Array").replace(".", "_"); + } + + /** + * Retrieve the compiled name of a given structure modifier. + * @param source - the structure modifier. + * @return The unique, compiled name of a compiled structure modifier. + */ + private String getCompiledName(StructureModifier source) { + Class targetType = source.getTargetType(); + + // Concat class and field type + return "CompiledStructure$" + + getSafeTypeName(targetType) + "$" + + getSafeTypeName(source.getFieldType()); + } + + /** + * Compile a structure modifier. + * @param source - structure modifier. + * @return The compiled structure modifier. + */ + private Class generateClass(StructureModifier source) { + + ClassWriter cw = new ClassWriter(0); + Class targetType = source.getTargetType(); + + String className = getCompiledName(source); + String targetSignature = Type.getDescriptor(targetType); + String targetName = targetType.getName().replace('.', '/'); + + // Define class + cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, PACKAGE_NAME + "/" + className, + null, 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) { + Method defined = ClassLoader.class.getDeclaredMethod("defineClass", + new Class[] { String.class, byte[].class, int.class, int.class }); + + // Awesome. Now, create and return it. + defined.setAccessible(true); + defineMethod = defined; + } + + @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 boolean isNonFinal(Field field) { + return !Modifier.isFinal(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 = "(ILjava/lang/Object;)L" + SUPER_CLASS + ";"; + MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PROTECTED, "writeGenerated", methodDescriptor, methodSignature, + new String[] { FIELD_EXCEPTION_CLASS }); + BoxingHelper boxingHelper = new BoxingHelper(mv); + + String generatedClassName = PACKAGE_NAME + "/" + className; + + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, generatedClassName, "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++) { + + Field field = fields.get(i); + Class outputType = field.getType(); + Class inputType = Primitives.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 non-final fields + if (isPublic(field) && isNonFinal(field)) { + mv.visitVarInsn(Opcodes.ALOAD, 3); + mv.visitVarInsn(Opcodes.ALOAD, 2); + + if (!outputType.isPrimitive()) + mv.visitTypeInsn(Opcodes.CHECKCAST, inputPath); + else + boxingHelper.unbox(Type.getType(outputType)); + + mv.visitFieldInsn(Opcodes.PUTFIELD, targetName, field.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.INVOKEVIRTUAL, generatedClassName, "writeReflected", "(ILjava/lang/Object;)V"); + } + + mv.visitJumpInsn(Opcodes.GOTO, returnLabel); + } + + mv.visitLabel(errorLabel); + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + mv.visitTypeInsn(Opcodes.NEW, FIELD_EXCEPTION_CLASS); + 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, FIELD_EXCEPTION_CLASS, "", "(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_PROTECTED, "readGenerated", "(I)Ljava/lang/Object;", null, + new String[] { "com/comphenix/protocol/reflect/FieldAccessException" }); + BoxingHelper boxingHelper = new BoxingHelper(mv); + + String generatedClassName = PACKAGE_NAME + "/" + className; + + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, generatedClassName, "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++) { + + Field field = fields.get(i); + Class outputType = field.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(field)) { + mv.visitVarInsn(Opcodes.ALOAD, 2); + mv.visitFieldInsn(Opcodes.GETFIELD, targetName, field.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.INVOKEVIRTUAL, generatedClassName, "readReflected", "(I)Ljava/lang/Object;"); + } + + mv.visitInsn(Opcodes.ARETURN); + } + + mv.visitLabel(errorLabel); + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + mv.visitTypeInsn(Opcodes.NEW, FIELD_EXCEPTION_CLASS); + 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, FIELD_EXCEPTION_CLASS, "", "(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" + PACKAGE_NAME + "/StructureCompiler;)V", null); + String fullClassName = PACKAGE_NAME + "/" + className; + + 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, fullClassName, "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, fullClassName, "target", "Ljava/lang/Object;"); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, fullClassName, "target", "Ljava/lang/Object;"); + mv.visitTypeInsn(Opcodes.CHECKCAST, targetName); + mv.visitFieldInsn(Opcodes.PUTFIELD, fullClassName, "typedTarget", targetSignature); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 2); + mv.visitFieldInsn(Opcodes.PUTFIELD, fullClassName, "compiler", "L" + PACKAGE_NAME + "/StructureCompiler;"); + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(2, 3); + mv.visitEnd(); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index d6bef053..cdff70f3 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -19,6 +19,7 @@ package com.comphenix.protocol.utility; import java.io.DataInputStream; import java.io.DataOutput; +import java.io.IOException; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -33,12 +34,19 @@ import java.util.regex.Pattern; import javax.annotation.Nonnull; +import net.sf.cglib.asm.ClassReader; +import net.sf.cglib.asm.MethodVisitor; +import net.sf.cglib.asm.Opcodes; + import org.bukkit.Bukkit; import org.bukkit.Server; import org.bukkit.inventory.ItemStack; import com.comphenix.protocol.injector.BukkitUnwrapper; +import com.comphenix.protocol.injector.packet.PacketRegistry; import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.compiler.EmptyClassVisitor; +import com.comphenix.protocol.reflect.compiler.EmptyMethodVisitor; import com.comphenix.protocol.reflect.fuzzy.AbstractFuzzyMatcher; import com.comphenix.protocol.reflect.fuzzy.FuzzyClassContract; import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract; @@ -952,6 +960,79 @@ public class MinecraftReflection { } } + /** + * Retrieve the attribute snapshot class. + *

+ * This stores the final value of an attribute, along with all the associated computational steps. + * @return The attribute snapshot class. + */ + public static Class getAttributeSnapshotClass() { + try { + return getMinecraftClass("AttributeSnapshot"); + } catch (RuntimeException e) { + final Class packetUpdateAttributes = PacketRegistry.getPacketClassFromID(44, true); + final String packetSignature = packetUpdateAttributes.getCanonicalName().replace('.', '/'); + + // HACK - class is found by inspecting code + try { + ClassReader reader = new ClassReader(packetUpdateAttributes.getCanonicalName()); + + reader.accept(new EmptyClassVisitor() { + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + // The read method + if (desc.startsWith("(Ljava/io/DataInput")) { + return new EmptyMethodVisitor() { + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + if (opcode == Opcodes.INVOKESPECIAL && isConstructor(name)) { + String className = owner.replace('/', '.'); + + // Use signature to distinguish between constructors + if (desc.startsWith("(L" + packetSignature)) { + setMinecraftClass("AttributeSnapshot", MinecraftReflection.getClass(className)); + } else if (desc.startsWith("(Ljava/util/UUID;Ljava/lang/String")) { + setMinecraftClass("AttributeModifier", MinecraftReflection.getClass(className)); + } + } + }; + }; + } + return null; + } + }, 0); + + } catch (IOException e1) { + throw new RuntimeException("Unable to read the content of Packet44UpdateAttributes.", e1); + } + + // If our dirty ASM trick failed, this will throw an exception + return getMinecraftClass("AttributeSnapshot"); + } + } + + /** + * Retrieve the attribute modifier class. + * @return Attribute modifier class. + */ + public static Class getAttributeModifierClass() { + try { + return getMinecraftClass("AttributeModifier"); + } catch (RuntimeException e) { + // Initialize first + getAttributeSnapshotClass(); + return getMinecraftClass("AttributeModifier"); + } + } + + /** + * Determine if a given method retrieved by ASM is a constructor. + * @param name - the name of the method. + * @return TRUE if it is, FALSE otherwise. + */ + private static boolean isConstructor(String name) { + return "".equals(name); + } + /** * Retrieve the ItemStack[] class. * @return The ItemStack[] class. @@ -1106,6 +1187,20 @@ public class MinecraftReflection { return unwrapper.unwrapItem(stack); } + /** + * Retrieve the given class by name. + * @param className - name of the class. + * @return The class. + */ + @SuppressWarnings("rawtypes") + private static Class getClass(String className) { + try { + return MinecraftReflection.class.getClassLoader().loadClass(className); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Cannot find class " + className, e); + } + } + /** * Retrieve the class object of a specific CraftBukkit class. * @param className - the specific CraftBukkit class. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java index 622092ff..e4a2d698 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java @@ -49,6 +49,7 @@ import com.google.common.collect.ImmutableMap; public class BukkitConverters { // Check whether or not certain classes exists private static boolean hasWorldType = false; + private static boolean hasAttributeSnapshot = false; // The static maps private static Map, EquivalentConverter> specificConverters; @@ -60,9 +61,15 @@ public class BukkitConverters { static { try { - Class.forName(MinecraftReflection.getMinecraftPackage() + ".WorldType"); + MinecraftReflection.getWorldTypeClass(); hasWorldType = true; - } catch (ClassNotFoundException e) { + } catch (Exception e) { + } + + try { + MinecraftReflection.getAttributeSnapshotClass(); + hasAttributeSnapshot = true; + } catch (Exception e) { } } @@ -158,6 +165,12 @@ public class BukkitConverters { } } + /** + * Retrieve an equivalent converter for a list of generic items. + * @param genericItemType - the generic item type. + * @param itemConverter - an equivalent converter for the generic type. + * @return An equivalent converter. + */ public static EquivalentConverter> getListConverter(final Class genericItemType, final EquivalentConverter itemConverter) { // Convert to and from the wrapper return new IgnoreNullConverter>() { @@ -208,6 +221,29 @@ public class BukkitConverters { }; } + /** + * Retrieve a converter for wrapped attribute snapshots. + * @return Wrapped attribute snapshot converter. + */ + public static EquivalentConverter getWrappedAttributeConverter() { + return new IgnoreNullConverter() { + @Override + protected Object getGenericValue(Class genericType, WrappedAttribute specific) { + return specific.getHandle(); + } + + @Override + protected WrappedAttribute getSpecificValue(Object generic) { + return WrappedAttribute.fromHandle(generic); + } + + @Override + public Class getSpecificType() { + return WrappedAttribute.class; + } + }; + } + /** * Retrieve a converter for watchable objects and the respective wrapper. * @return A watchable object converter. @@ -429,7 +465,7 @@ public class BukkitConverters { * @return Every converter with a unique specific class. */ @SuppressWarnings({"rawtypes", "unchecked"}) - public static Map, EquivalentConverter> getSpecificConverters() { + public static Map, EquivalentConverter> getConvertersForSpecific() { if (specificConverters == null) { // Generics doesn't work, as usual ImmutableMap.Builder, EquivalentConverter> builder = @@ -440,9 +476,10 @@ public class BukkitConverters { put(NbtCompound.class, (EquivalentConverter) getNbtConverter()). put(WrappedWatchableObject.class, (EquivalentConverter) getWatchableObjectConverter()); - if (hasWorldType) { + if (hasWorldType) builder.put(WorldType.class, (EquivalentConverter) getWorldTypeConverter()); - } + if (hasAttributeSnapshot) + builder.put(WrappedAttribute.class, (EquivalentConverter) getWrappedAttributeConverter()); specificConverters = builder.build(); } return specificConverters; @@ -453,7 +490,7 @@ public class BukkitConverters { * @return Every converter with a unique generic class. */ @SuppressWarnings({"rawtypes", "unchecked"}) - public static Map, EquivalentConverter> getGenericConverters() { + public static Map, EquivalentConverter> getConvertersForGeneric() { if (genericConverters == null) { // Generics doesn't work, as usual ImmutableMap.Builder, EquivalentConverter> builder = @@ -464,9 +501,10 @@ public class BukkitConverters { put(MinecraftReflection.getNBTCompoundClass(), (EquivalentConverter) getNbtConverter()). put(MinecraftReflection.getWatchableObjectClass(), (EquivalentConverter) getWatchableObjectConverter()); - if (hasWorldType) { + if (hasWorldType) builder.put(MinecraftReflection.getWorldTypeClass(), (EquivalentConverter) getWorldTypeConverter()); - } + if (hasAttributeSnapshot) + builder.put(MinecraftReflection.getAttributeSnapshotClass(), (EquivalentConverter) getWrappedAttributeConverter()); genericConverters = builder.build(); } return genericConverters; diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedAttribute.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedAttribute.java new file mode 100644 index 00000000..3e0a08cd --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedAttribute.java @@ -0,0 +1,419 @@ +package com.comphenix.protocol.wrappers; + +import java.lang.reflect.Constructor; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.UUID; + +import javax.annotation.Nonnull; + +import com.comphenix.protocol.Packets; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.StructureModifier; +import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.wrappers.collection.CachedSet; +import com.comphenix.protocol.wrappers.collection.ConvertedSet; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.google.common.collect.Sets; + +/** + * Represents a single attribute sent in packet 44. + * @author Kristian + */ +public class WrappedAttribute { + // Shared structure modifier + private static StructureModifier ATTRIBUTE_MODIFIER; + + // The one constructor + private static Constructor ATTRIBUTE_CONSTRUCTOR; + + /** + * Reference to the underlying attribute snapshot. + */ + protected Object handle; + protected StructureModifier modifier; + + // Cached computed value + private double computedValue = Double.NaN; + + // Cached modifiers list + private Set attributeModifiers; + + /** + * Construct a new wrapped attribute around a specific NMS instance. + * @param handle - handle to a NMS AttributeSnapshot. + * @return The attribute wrapper. + * @throws IllegalArgumentException If the handle is not a AttributeSnapshot. + */ + public static WrappedAttribute fromHandle(@Nonnull Object handle) { + return new WrappedAttribute(handle); + } + + /** + * Construct a new wrapped attribute builder. + * @return The new builder. + */ + public static Builder newBuilder() { + return new Builder(null); + } + + /** + * Construct a new wrapped attribute builder initialized to the values from a template. + * @param template - the attribute template. + * @return The new builder. + */ + public static Builder newBuilder(@Nonnull WrappedAttribute template) { + return new Builder(Preconditions.checkNotNull(template, "template cannot be NULL.")); + } + + /** + * Construct a wrapper around a specific NMS instance. + * @param handle - the NMS instance. + */ + private WrappedAttribute(@Nonnull Object handle) { + this.handle = Preconditions.checkNotNull(handle, "handle cannot be NULL."); + + // Check handle type + if (!MinecraftReflection.getAttributeSnapshotClass().isAssignableFrom(handle.getClass())) { + throw new IllegalArgumentException("handle (" + handle + ") must be a AttributeSnapshot."); + } + + // Initialize modifier + if (ATTRIBUTE_MODIFIER == null) { + ATTRIBUTE_MODIFIER = new StructureModifier(MinecraftReflection.getAttributeSnapshotClass()); + } + this.modifier = ATTRIBUTE_MODIFIER.withTarget(handle); + } + + /** + * Retrieve the underlying NMS attribute snapshot. + * @return The underlying attribute snapshot. + */ + public Object getHandle() { + return handle; + } + + /** + * Retrieve the unique attribute key that identifies its function. + *

+ * Example: "generic.maxHealth" + * @return The attribute key. + */ + public String getAttributeKey() { + return (String) modifier.withType(String.class).read(0); + } + + /** + * Retrieve the base value of this attribute, before any of the modifiers have been taken into account. + * @return The base value. + */ + public double getBaseValue() { + return (Double) modifier.withType(double.class).read(0); + } + + /** + * Retrieve the final computed value. + * @return The final value. + */ + public double getFinalValue() { + if (Double.isNaN(computedValue)) { + computedValue = computeValue(); + } + return computedValue; + } + + /** + * Retrieve the parent update attributes packet. + * @return The parent packet. + */ + public PacketContainer getParentPacket() { + return new PacketContainer( + Packets.Server.UPDATE_ATTRIBUTES, + modifier.withType(MinecraftReflection.getPacketClass()).read(0) + ); + } + + /** + * Determine if the attribute has a given attribute modifier, identified by UUID. + * @return TRUE if it does, FALSE otherwise. + */ + public boolean hasModifier(UUID id) { + return getModifiers().contains(WrappedAttributeModifier.newBuilder(id).build()); + } + + /** + * Retrieve an attribute modifier by UUID. + * @param id - the id to look for. + * @return The single attribute modifier with the given ID. + */ + public WrappedAttributeModifier getModifierByUUID(UUID id) { + if (hasModifier(id)) { + for (WrappedAttributeModifier modifier : getModifiers()) { + if (Objects.equal(modifier.getUUID(), id)) { + return modifier; + } + } + } + return null; + } + + /** + * Retrieve an immutable set of all the attribute modifiers that will compute the final value of this attribute. + * @return Every attribute modifier. + */ + public Set getModifiers() { + if (attributeModifiers == null) { + @SuppressWarnings("unchecked") + Collection collection = (Collection) modifier.withType(Collection.class).read(0); + + // Convert to an equivalent wrapper + ConvertedSet converted = + new ConvertedSet(getSetSafely(collection)) { + @Override + protected Object toInner(WrappedAttributeModifier outer) { + return outer.getHandle(); + } + + @Override + protected WrappedAttributeModifier toOuter(Object inner) { + return WrappedAttributeModifier.fromHandle(inner); + } + }; + + attributeModifiers = new CachedSet(converted); + } + return Collections.unmodifiableSet(attributeModifiers); + } + + /** + * Construct an attribute with the same key and name, but a different list of modifiers. + * @param modifiers - attribute modifiers. + * @return The new attribute. + */ + public WrappedAttribute withModifiers(Collection modifiers) { + return newBuilder(this).modifiers(modifiers).build(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj instanceof WrappedAttribute) { + WrappedAttribute other = (WrappedAttribute) obj; + + return getBaseValue() == other.getBaseValue() && + Objects.equal(getAttributeKey(), other.getAttributeKey()) && + Sets.symmetricDifference( + getModifiers(), + other.getModifiers() + ).isEmpty(); + } + return false; + } + + @Override + public int hashCode() { + if (attributeModifiers == null) + getModifiers(); + return Objects.hashCode(getAttributeKey(), getBaseValue(), attributeModifiers); + } + + /** + * Compute the final value from the current attribute modifers. + * @return The final value. + */ + private double computeValue() { + Collection modifiers = getModifiers(); + double x = getBaseValue(); + double y = 0; + + // Compute each phase + for (int phase = 0; phase < 3; phase++) { + for (WrappedAttributeModifier modifier : modifiers) { + if (modifier.getOperation().getId() == phase) { + switch (phase) { + case 0: // Adding phase + x += modifier.getAmount(); + break; + case 1: // Multiply percentage + y += x * modifier.getAmount(); + break; + case 2: + y *= 1 + modifier.getAmount(); + break; + default : + throw new IllegalStateException("Unknown phase: " + phase); + } + } + } + + // The additive phase is finished + if (phase == 0) { + y = x; + } + } + return y; + } + + @Override + public String toString() { + return Objects.toStringHelper("WrappedAttribute"). + add("key", getAttributeKey()). + add("baseValue", getBaseValue()). + add("finalValue", getFinalValue()). + add("modifiers", getModifiers()). + toString(); + } + + /** + * If the collection is a set, retrieve it - otherwise, create a new set with the same elements. + * @param collection - the collection. + * @return A set with the same elements. + */ + private static Set getSetSafely(Collection collection) { + return collection instanceof Set ? (Set) collection : Sets.newHashSet(collection); + } + + /** + * Ensure that the given double is not infinite nor NaN. + * @param value - the value to check. + */ + static double checkDouble(double value) { + if (Double.isInfinite(value)) + throw new IllegalArgumentException("value cannot be infinite."); + if (Double.isNaN(value)) + throw new IllegalArgumentException("value cannot be NaN."); + return value; + } + + /** + * Represents a builder for wrapped attributes. + *

+ * Use {@link WrappedAttribute#newBuilder()} to construct it. + * @author Kristian + */ + public static class Builder { + private double baseValue = Double.NaN; + private String attributeKey; + private PacketContainer packet; + private Collection modifiers = Collections.emptyList(); + + private Builder(WrappedAttribute template) { + if (template != null) { + baseValue = template.getBaseValue(); + attributeKey = template.getAttributeKey(); + packet = template.getParentPacket(); + modifiers = template.getModifiers(); + } + } + + /** + * Change the base value of the attribute. + *

+ * The modifiers will automatically supply a value if this is unset. + * @param value - the final value. + * @return This builder, for chaining. + */ + public Builder baseValue(double baseValue) { + this.baseValue = checkDouble(baseValue); + return this; + } + + /** + * Set the unique attribute key that identifies its function. + *

+ * This is required. + * @param attributeKey - the unique attribute key. + * @return This builder, for chaining. + */ + public Builder attributeKey(String attributeKey) { + this.attributeKey = Preconditions.checkNotNull(attributeKey, "attributeKey cannot be NULL."); + return this; + } + + /** + * Set the modifers that will be supplied to the client, and used to compute the final value. + *

+ * Call {@link #recomputeValue()} to force the builder to recompute the final value. + * @param modifiers - the attribute modifiers. + * @return This builder, for chaining. + */ + public Builder modifiers(Collection modifiers) { + this.modifiers = Preconditions.checkNotNull(modifiers, "modifiers cannot be NULL - use an empty list instead."); + return this; + } + + /** + * Set the parent update attributes packet (44). + * @param packet - the parent packet. + * @return This builder, for chaining. + */ + public Builder packet(PacketContainer packet) { + if (Preconditions.checkNotNull(packet, "packet cannot be NULL").getID() != Packets.Server.UPDATE_ATTRIBUTES) { + throw new IllegalArgumentException("Packet must be UPDATE_ATTRIBUTES (44)"); + } + this.packet = packet; + return this; + } + + /** + * Retrieve the unwrapped modifiers. + * @return Unwrapped modifiers. + */ + private Set getUnwrappedModifiers() { + Set output = Sets.newHashSet(); + + for (WrappedAttributeModifier modifier : modifiers) { + output.add(modifier.getHandle()); + } + return output; + } + + /** + * Build a new wrapped attribute with the values of this builder. + * @return The wrapped attribute. + * @throws RuntimeException If anything went wrong with the reflection. + */ + public WrappedAttribute build() { + Preconditions.checkNotNull(packet, "packet cannot be NULL."); + Preconditions.checkNotNull(attributeKey, "attributeKey cannot be NULL."); + + // Remember to set the base value + if (Double.isNaN(baseValue)) { + throw new IllegalStateException("Base value has not been set."); + } + + // Retrieve the correct constructor + if (ATTRIBUTE_CONSTRUCTOR == null) { + ATTRIBUTE_CONSTRUCTOR = FuzzyReflection.fromClass(MinecraftReflection.getAttributeSnapshotClass(), true).getConstructor( + FuzzyMethodContract.newBuilder().parameterCount(4). + parameterDerivedOf(MinecraftReflection.getPacketClass(), 0). + parameterExactType(String.class, 1). + parameterExactType(double.class, 2). + parameterDerivedOf(Collection.class, 3). + build() + ); + // Just in case + ATTRIBUTE_CONSTRUCTOR.setAccessible(true); + } + + try { + Object handle = ATTRIBUTE_CONSTRUCTOR.newInstance( + packet.getHandle(), + attributeKey, + baseValue, + getUnwrappedModifiers()); + + // Create it + return new WrappedAttribute(handle); + + } catch (Exception e) { + throw new RuntimeException("Cannot construct AttributeSnapshot.", e); + } + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedAttributeModifier.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedAttributeModifier.java new file mode 100644 index 00000000..328e3e2e --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedAttributeModifier.java @@ -0,0 +1,412 @@ +package com.comphenix.protocol.wrappers; + +import java.lang.reflect.Constructor; +import java.util.UUID; + +import javax.annotation.Nonnull; + +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.StructureModifier; +import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; + +/** + * Represents a wrapper around a AttributeModifier. + *

+ * This is used to compute the final attribute value. + * + * @author Kristian + */ +public class WrappedAttributeModifier { + /** + * Represents the different modifier operations. + *

+ * The final value is computed as follows: + *

    + *
  1. Set X = base value.
  2. + *
  3. Execute all modifiers with {@link Operation#ADD_NUMBER}. + *
  4. Set Y = X.
  5. + *
  6. Execute all modifiers with {@link Operation#MULTIPLY_PERCENTAGE}.
  7. + *
  8. Execute all modifiers with {@link Operation#ADD_PERCENTAGE}.
  9. + *
  10. Y is the final value.
  11. + *
+ * @author Kristian + */ + public enum Operation { + /** + * Increment X by amount. + */ + ADD_NUMBER(0), + + /** + * Increment Y by X * amount. + */ + MULTIPLY_PERCENTAGE(1), + + /** + * Multiply Y by (1 + amount) + */ + ADD_PERCENTAGE(2); + + private int id; + + private Operation(int id) { + this.id = id; + } + + /** + * Retrieve the unique operation ID. + * @return Operation ID. + */ + public int getId() { + return id; + } + + /** + * Retrieve the associated operation from an ID. + * @param id - the ID. + * @return The operation. + */ + public static Operation fromId(int id) { + // Linear scan is very fast for small N + for (Operation op : values()) { + if (op.getId() == id) { + return op; + } + } + throw new IllegalArgumentException("Corrupt operation ID " + id + " detected."); + } + } + + // Shared structure modifier + private static StructureModifier BASE_MODIFIER; + + // The constructor we are interested in + private static Constructor ATTRIBUTE_MODIFIER_CONSTRUCTOR; + + /** + * Handle to the underlying AttributeModifier. + */ + protected Object handle; + protected StructureModifier modifier; + + // Cached values + private final UUID uuid; + private final String name; + private final Operation operation; + private final double amount; + + /** + * Construct a new attribute modifier builder. + *

+ * It will automatically be supplied with a random UUID. + * @return The new builder. + */ + public static Builder newBuilder() { + return new Builder(null).uuid(UUID.randomUUID()); + } + + /** + * Construct a new attribute modifier builder with the given UUID. + * @param id - the new UUID. + * @return Thew new builder. + */ + public static Builder newBuilder(UUID id) { + return new Builder(null).uuid(id); + } + + /** + * Construct a new wrapped attribute modifier builder initialized to the values from a template. + * @param template - the attribute modifier template. + * @return The new builder. + */ + public static Builder newBuilder(@Nonnull WrappedAttributeModifier template) { + return new Builder(Preconditions.checkNotNull(template, "template cannot be NULL.")); + } + + /** + * Construct an attribute modifier wrapper around a given NMS instance. + * @param handle - the NMS instance. + * @return The created attribute modifier. + * @throws IllegalArgumentException If the handle is not an AttributeModifier. + */ + public static WrappedAttributeModifier fromHandle(@Nonnull Object handle) { + return new WrappedAttributeModifier(handle); + } + + /** + * Construct a new wrapped attribute modifier with no associated handle. + * @param uuid - the UUID. + * @param name - the human readable name. + * @param amount - the amount. + * @param operation - the operation. + */ + protected WrappedAttributeModifier(UUID uuid, String name, double amount, Operation operation) { + // Use the supplied values instead of reading from the NMS instance + this.uuid = uuid; + this.name = name; + this.amount = amount; + this.operation = operation; + } + + /** + * Construct an attribute modifier wrapper around a given NMS instance. + * @param handle - the NMS instance. + */ + protected WrappedAttributeModifier(@Nonnull Object handle) { + // Update handle and modifier + setHandle(handle); + initializeModifier(handle); + + // Load final values, caching them + this.uuid = (UUID) modifier.withType(UUID.class).read(0); + this.name = (String) modifier.withType(String.class).read(0); + this.amount = (Double) modifier.withType(double.class).read(0); + this.operation = Operation.fromId((Integer) modifier.withType(int.class).read(0)); + } + + /** + * Construct an attribute modifier wrapper around a NMS instance. + * @param handle - the NMS instance. + * @param uuid - the UUID. + * @param name - the human readable name. + * @param amount - the amount. + * @param operation - the operation. + */ + protected WrappedAttributeModifier(@Nonnull Object handle, UUID uuid, String name, double amount, Operation operation) { + this(uuid, name, amount, operation); + + // Initialize handle and modifier + setHandle(handle); + initializeModifier(handle); + } + + /** + * Initialize modifier from a given handle. + * @param handle - the handle. + * @return The given handle. + */ + private void initializeModifier(@Nonnull Object handle) { + // Initialize modifier + if (BASE_MODIFIER == null) { + BASE_MODIFIER = new StructureModifier(MinecraftReflection.getAttributeModifierClass()); + } + this.modifier = BASE_MODIFIER.withTarget(handle); + } + + /** + * Set the handle of a modifier. + * @param handle - the underlying handle. + */ + private void setHandle(Object handle) { + // Check handle type + if (!MinecraftReflection.getAttributeModifierClass().isAssignableFrom(handle.getClass())) + throw new IllegalArgumentException("handle (" + handle + ") must be a AttributeModifier."); + this.handle = handle; + } + + /** + * Retrieve the unique UUID that identifies the origin of this modifier. + * @return The unique UUID. + */ + public UUID getUUID() { + return uuid; + } + + /** + * Retrieve a human readable name of this modifier. + *

+ * Note that this will be "Unknown synced attribute modifier" on the client side. + * @return The attribute key. + */ + public String getName() { + return name; + } + + /** + * Retrieve the operation that is used to compute the final attribute value. + * @return The operation. + */ + public Operation getOperation() { + return operation; + } + + /** + * Retrieve the amount to modify in the operation. + * @return The amount. + */ + public double getAmount() { + return amount; + } + + /** + * Invoked when we need to construct a handle object. + */ + protected void checkHandle() { + if (handle == null) { + handle = newBuilder(this).build().getHandle(); + initializeModifier(handle); + } + } + + /** + * Retrieve the underlying attribute modifier. + * @return The underlying modifier. + */ + public Object getHandle() { + return handle; + } + + /** + * Set whether or not the modifier is pending synchronization with the client. + *

+ * This value will be disregarded for {@link #equals(Object)}. + * @param pending - TRUE if is is, FALSE otherwise. + */ + public void setPendingSynchronization(boolean pending) { + modifier.withType(boolean.class).write(0, pending); + } + + /** + * Whether or not the modifier is pending synchronization with the client. + * @return TRUE if it is, FALSE otherwise. + */ + public boolean isPendingSynchronization() { + return (Boolean) modifier.withType(boolean.class).read(0); + } + + /** + * Determine if a given modifier is equal to the current modifier. + *

+ * Two modifiers are considered equal if they use the same UUID. + * @param obj - the object to check against. + * @return TRUE if the given object is the same, FALSE otherwise. + */ + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj instanceof WrappedAttributeModifier) { + WrappedAttributeModifier other = (WrappedAttributeModifier) obj; + + // Ensure they are equal + return Objects.equal(uuid, other.getUUID()); + } + return false; + } + + @Override + public int hashCode() { + return uuid != null ? uuid.hashCode() : 0; + } + + @Override + public String toString() { + return "[amount=" + amount + ", operation=" + operation + ", name='" + name + "', id=" + uuid + ", serialize=" + isPendingSynchronization() + "]"; + } + + /** + * Represents a builder of attribute modifiers. + *

+ * Use {@link WrappedAttributeModifier#newBuilder()} to construct an instance of the builder. + * @author Kristian + */ + public static class Builder { + private Operation operation = Operation.ADD_NUMBER; + private String name = "Unknown"; + private double amount; + private UUID uuid; + + private Builder(WrappedAttributeModifier template) { + if (template != null) { + operation = template.getOperation(); + name = template.getName(); + amount = template.getAmount(); + uuid = template.getUUID(); + } + } + + /** + * Set the unique UUID that identifies the origin of this modifier. + *

+ * This parameter is automatically supplied with a random UUID, or the + * UUID from an attribute modifier to clone. + * + * @param uuid - the uuid to supply to the new object. + * @return This builder, for chaining. + */ + public Builder uuid(@Nonnull UUID uuid) { + this.uuid = Preconditions.checkNotNull(uuid, "uuid cannot be NULL."); + return this; + } + + /** + * Set the operation that is used to compute the final attribute value. + * + * @param operation - the operation to supply to the new object. + * @return This builder, for chaining. + */ + public Builder operation(@Nonnull Operation operation) { + this.operation = Preconditions.checkNotNull(operation, "operation cannot be NULL."); + return this; + } + + /** + * Set a human readable name of this modifier. + * @param attributeKey - the attribute key to supply to the new object. + * @return This builder, for chaining. + */ + public Builder name(@Nonnull String name) { + this.name = Preconditions.checkNotNull(name, "name cannot be NULL."); + return this; + } + + /** + * Set the amount to modify in the operation. + * + * @param amount - the amount to supply to the new object. + * @return This builder, for chaining. + */ + public Builder amount(double amount) { + this.amount = WrappedAttribute.checkDouble(amount); + return this; + } + + /** + * Construct a new attribute modifier and its wrapper using the supplied values in this builder. + * @return The new attribute modifier. + * @throws NullPointerException If UUID has not been set. + * @throws RuntimeException If we are unable to construct the underlying attribute modifier. + */ + public WrappedAttributeModifier build() { + Preconditions.checkNotNull(uuid, "uuid cannot be NULL."); + + // Retrieve the correct constructor + if (ATTRIBUTE_MODIFIER_CONSTRUCTOR == null) { + ATTRIBUTE_MODIFIER_CONSTRUCTOR = FuzzyReflection.fromClass( + MinecraftReflection.getAttributeModifierClass(), true).getConstructor( + FuzzyMethodContract.newBuilder().parameterCount(4). + parameterDerivedOf(UUID.class, 0). + parameterExactType(String.class, 1). + parameterExactType(double.class, 2). + parameterExactType(int.class, 3).build()); + + // Just in case + ATTRIBUTE_MODIFIER_CONSTRUCTOR.setAccessible(true); + } + + // Construct it + try { + // No need to read these values with a modifier + return new WrappedAttributeModifier( + ATTRIBUTE_MODIFIER_CONSTRUCTOR.newInstance( + uuid, name, amount, operation.getId()), + uuid, name, amount, operation + ); + } catch (Exception e) { + throw new RuntimeException("Cannot construct AttributeModifier.", e); + } + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/CachedCollection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/CachedCollection.java new file mode 100644 index 00000000..66890363 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/CachedCollection.java @@ -0,0 +1,203 @@ +package com.comphenix.protocol.wrappers.collection; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.Set; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterators; + +/** + * Represents a set that will (best effort) cache elements before using + * an underlying set to retrieve the actual element. + *

+ * The cache will be invalidated when data is removed. + * + * @author Kristian + * @param - type of each element in the collection. + */ +public class CachedCollection implements Collection { + protected Set delegate; + protected Object[] cache; + + /** + * Construct a cached collection with the given delegate. + *

+ * Objects are cached before they can be extracted from this collection. + * @param delegate - the delegate. + */ + public CachedCollection(Set delegate) { + this.delegate = Preconditions.checkNotNull(delegate, "delegate cannot be NULL."); + } + + /** + * Construct the cache if needed. + */ + private void initializeCache() { + if (cache == null) { + cache = new Object[delegate.size()]; + } + } + + /** + * Ensure that the cache is big enough. + */ + private void growCache() { + // We'll delay making the cache + if (cache == null) + return; + int newLength = cache.length; + + // Ensure that the cache is big enoigh + while (newLength < delegate.size()) { + newLength *= 2; + } + if (newLength != cache.length) { + cache = Arrays.copyOf(cache, newLength); + } + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return delegate.contains(o); + } + + @Override + public Iterator iterator() { + final Iterator source = delegate.iterator(); + initializeCache(); + + return new Iterator() { + int currentIndex = -1; + int iteratorIndex = -1; + + @Override + public boolean hasNext() { + return currentIndex < delegate.size() - 1; + } + + @SuppressWarnings("unchecked") + @Override + public T next() { + currentIndex++; + + if (cache[currentIndex] == null) { + cache[currentIndex] = getSourceValue(); + } + return (T) cache[currentIndex]; + } + + @Override + public void remove() { + // Increment iterator + getSourceValue(); + source.remove(); + } + + /** + * Retrieve the corresponding value from the source iterator. + */ + private T getSourceValue() { + T last = null; + + while (iteratorIndex < currentIndex) { + iteratorIndex++; + last = source.next(); + } + return last; + } + }; + } + + @Override + public Object[] toArray() { + Iterators.size(iterator()); + return cache.clone(); + } + + @SuppressWarnings({"unchecked", "hiding", "rawtypes"}) + @Override + public T[] toArray(T[] a) { + Iterators.size(iterator()); + return (T[]) Arrays.copyOf(cache, size(), (Class) a.getClass().getComponentType()); + } + + @Override + public boolean add(T e) { + boolean result = delegate.add(e); + + growCache(); + return result; + } + + @Override + public boolean addAll(Collection c) { + boolean result = delegate.addAll(c); + + growCache(); + return result; + } + + @Override + public boolean containsAll(Collection c) { + return delegate.containsAll(c); + } + + @Override + public boolean remove(Object o) { + cache = null; + return delegate.remove(o); + } + + @Override + public boolean removeAll(Collection c) { + cache = null; + return delegate.removeAll(c); + } + + @Override + public boolean retainAll(Collection c) { + cache = null; + return delegate.retainAll(c); + } + + @Override + public void clear() { + cache = null; + delegate.clear(); + } + + @Override + public int hashCode() { + int result = 1; + + // Combine all the hashCodes() + for (Object element : this) + result = 31 * result + (element == null ? 0 : element.hashCode()); + return result; + } + + @Override + public String toString() { + Iterators.size(iterator()); + StringBuilder result = new StringBuilder("["); + + for (T element : this) { + if (result.length() > 1) + result.append(", "); + result.append(element); + } + return result.append("]").toString(); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/CachedSet.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/CachedSet.java new file mode 100644 index 00000000..1c18d0bc --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/CachedSet.java @@ -0,0 +1,19 @@ +package com.comphenix.protocol.wrappers.collection; + +import java.util.Set; + +/** + * Represents a cached set. Enumeration of the set will use a cached inner list. + * + * @author Kristian + * @param - the element type. + */ +public class CachedSet extends CachedCollection implements Set { + /** + * Construct a cached set from the given delegate. + * @param delegate - the set delegate. + */ + public CachedSet(Set delegate) { + super(delegate); + } +} diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/WrappedAttributeTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/WrappedAttributeTest.java new file mode 100644 index 00000000..5e704fa5 --- /dev/null +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/WrappedAttributeTest.java @@ -0,0 +1,93 @@ +package com.comphenix.protocol.wrappers; + +import static org.junit.Assert.*; + +import java.util.List; + +import net.minecraft.server.v1_6_R2.AttributeModifier; +import net.minecraft.server.v1_6_R2.AttributeSnapshot; +import net.minecraft.server.v1_6_R2.Packet44UpdateAttributes; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.comphenix.protocol.BukkitInitialization; +import com.comphenix.protocol.Packets; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.wrappers.WrappedAttributeModifier.Operation; +import com.google.common.collect.Lists; + +public class WrappedAttributeTest { + private WrappedAttributeModifier doubleModifier; + private WrappedAttributeModifier constantModifier; + private WrappedAttribute attribute; + + @BeforeClass + public static void initializeBukkit() throws IllegalAccessException { + BukkitInitialization.initializePackage(); + } + + @Before + public void setUp() { + // Create a couple of modifiers + doubleModifier = + WrappedAttributeModifier.newBuilder(). + name("Double Damage"). + amount(1). + operation(Operation.ADD_PERCENTAGE). + build(); + constantModifier = + WrappedAttributeModifier.newBuilder(). + name("Damage Bonus"). + amount(5). + operation(Operation.ADD_NUMBER). + build(); + + // Create attribute + attribute = WrappedAttribute.newBuilder(). + attributeKey("generic.attackDamage"). + baseValue(2). + packet(new PacketContainer(Packets.Server.UPDATE_ATTRIBUTES)). + modifiers(Lists.newArrayList(constantModifier, doubleModifier)). + build(); + } + + @Test + public void testEquality() { + // Check wrapped equality + assertEquals(doubleModifier, doubleModifier); + assertNotSame(constantModifier, doubleModifier); + + assertEquals(doubleModifier.getHandle(), getModifierCopy(doubleModifier)); + assertEquals(constantModifier.getHandle(), getModifierCopy(constantModifier)); + } + + @Test + public void testAttribute() { + assertEquals(attribute, WrappedAttribute.fromHandle(getAttributeCopy(attribute))); + + assertTrue(attribute.hasModifier(doubleModifier.getUUID())); + assertTrue(attribute.hasModifier(constantModifier.getUUID())); + } + + /** + * Retrieve the equivalent NMS attribute. + * @param attribute - the wrapped attribute. + * @return The equivalent NMS attribute. + */ + private AttributeSnapshot getAttributeCopy(WrappedAttribute attribute) { + List modifiers = Lists.newArrayList(); + + for (WrappedAttributeModifier wrapper : attribute.getModifiers()) { + modifiers.add((AttributeModifier) wrapper.getHandle()); + } + return new AttributeSnapshot( + (Packet44UpdateAttributes) attribute.getParentPacket().getHandle(), + attribute.getAttributeKey(), attribute.getBaseValue(), modifiers); + } + + private AttributeModifier getModifierCopy(WrappedAttributeModifier modifier) { + return new AttributeModifier(modifier.getUUID(), modifier.getName(), modifier.getAmount(), modifier.getOperation().getId()); + } +}