From 988026611c2419a455afc3c4097865544b54e2b0 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Sun, 28 Jul 2013 02:02:27 +0200 Subject: [PATCH] Added support for modifying attributes in UPDATE_ATTRIBUTES. This contains a fully-fledged API for reading and modifying Attribute- Snapshot and AttributeModifier. Keep in mind that these objects are immutable, so modification must be made through object builders. The packets are also shared, so packet cloning might be necessary if attributes should differ per player. --- .../com/comphenix/protocol/CommandPacket.java | 2 +- .../protocol/events/PacketContainer.java | 18 + .../protocol/reflect/StructureModifier.java | 9 + .../reflect/compiler/BackgroundCompiler.java | 730 +++++------ .../reflect/compiler/EmptyClassVisitor.java | 57 + .../reflect/compiler/EmptyMethodVisitor.java | 132 ++ .../reflect/compiler/StructureCompiler.java | 1066 ++++++++--------- .../protocol/utility/MinecraftReflection.java | 95 ++ .../protocol/wrappers/BukkitConverters.java | 54 +- .../protocol/wrappers/WrappedAttribute.java | 419 +++++++ .../wrappers/WrappedAttributeModifier.java | 412 +++++++ .../wrappers/collection/CachedCollection.java | 203 ++++ .../wrappers/collection/CachedSet.java | 19 + .../wrappers/WrappedAttributeTest.java | 93 ++ 14 files changed, 2402 insertions(+), 907 deletions(-) create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/EmptyClassVisitor.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/EmptyMethodVisitor.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedAttribute.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedAttributeModifier.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/CachedCollection.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/CachedSet.java create mode 100644 ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/WrappedAttributeTest.java 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()); + } +}