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.
This commit is contained in:
Kristian S. Stangeland 2013-07-28 02:02:27 +02:00
parent 7170bfcadc
commit 988026611c
14 changed files with 2402 additions and 907 deletions

View File

@ -439,7 +439,7 @@ class CommandPacket extends CommandBase {
@Override
public boolean print(StringBuilder output, Object value) {
if (value != null) {
EquivalentConverter<Object> converter = BukkitConverters.getGenericConverters().get(value.getClass());
EquivalentConverter<Object> converter = BukkitConverters.getConvertersForGeneric().get(value.getClass());
if (converter != null) {
output.append(converter.getSpecific(value));

View File

@ -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.
* <p>
* This modifier will automatically marshall between the visible ProtocolLib WrappedAttribute and the
* internal Minecraft AttributeSnapshot.
* @return A modifier for AttributeSnapshot collection fields.
*/
public StructureModifier<List<WrappedAttribute>> 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.
* <p>

View File

@ -89,6 +89,15 @@ public class StructureModifier<TField> {
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.

View File

@ -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.
* <p>
* This is necessary as we cannot block the main thread.
*
* @author Kristian
*/
public class BackgroundCompiler {
public static final ReportType REPORT_CANNOT_COMPILE_STRUCTURE_MODIFIER = new ReportType("Cannot compile structure. Disabing compiler.");
public static final ReportType REPORT_CANNOT_SCHEDULE_COMPILATION = new ReportType("Unable to schedule compilation task.");
/**
* The default format for the name of new worker threads.
*/
public static final String THREAD_FORMAT = "ProtocolLib-StructureCompiler %s";
// How long to wait for a shutdown
public static final int SHUTDOWN_DELAY_MS = 2000;
/**
* The default fraction of perm gen space after which the background compiler will be disabled.
*/
public static final double DEFAULT_DISABLE_AT_PERM_GEN = 0.65;
// The single background compiler we're using
private static BackgroundCompiler backgroundCompiler;
// Classes we're currently compiling
private Map<StructureKey, List<CompileListener<?>>> listeners = Maps.newHashMap();
private Object listenerLock = new Object();
private StructureCompiler compiler;
private boolean enabled;
private boolean shuttingDown;
private ExecutorService executor;
private ErrorReporter reporter;
private double disablePermGenFraction = DEFAULT_DISABLE_AT_PERM_GEN;
/**
* Retrieves the current background compiler.
* @return Current background compiler.
*/
public static BackgroundCompiler getInstance() {
return backgroundCompiler;
}
/**
* Sets the single background compiler we're using.
* @param backgroundCompiler - current background compiler, or NULL if the library is not loaded.
*/
public static void setInstance(BackgroundCompiler backgroundCompiler) {
BackgroundCompiler.backgroundCompiler = backgroundCompiler;
}
/**
* Initialize a background compiler.
* <p>
* Uses the default {@link #THREAD_FORMAT} to name worker threads.
* @param loader - class loader from Bukkit.
* @param reporter - current error reporter.
*/
public BackgroundCompiler(ClassLoader loader, ErrorReporter reporter) {
ThreadFactory factory = new ThreadFactoryBuilder().
setDaemon(true).
setNameFormat(THREAD_FORMAT).
build();
initializeCompiler(loader, reporter, Executors.newSingleThreadExecutor(factory));
}
/**
* Initialize a background compiler utilizing the given thread pool.
* @param loader - class loader from Bukkit.
* @param reporter - current error reporter.
* @param executor - thread pool we'll use.
*/
public BackgroundCompiler(ClassLoader loader, ErrorReporter reporter, ExecutorService executor) {
initializeCompiler(loader, reporter, executor);
}
// Avoid "Constructor call must be the first statement".
private void initializeCompiler(ClassLoader loader, ErrorReporter reporter, ExecutorService executor) {
if (loader == null)
throw new IllegalArgumentException("loader cannot be NULL");
if (executor == null)
throw new IllegalArgumentException("executor cannot be NULL");
if (reporter == null)
throw new IllegalArgumentException("reporter cannot be NULL.");
this.compiler = new StructureCompiler(loader);
this.reporter = reporter;
this.executor = executor;
this.enabled = true;
}
/**
* Ensure that the indirectly given structure modifier is eventually compiled.
* @param cache - store of structure modifiers.
* @param key - key of the structure modifier to compile.
*/
@SuppressWarnings("rawtypes")
public void scheduleCompilation(final Map<Class, StructureModifier> cache, final Class key) {
@SuppressWarnings("unchecked")
final StructureModifier<Object> uncompiled = cache.get(key);
if (uncompiled != null) {
scheduleCompilation(uncompiled, new CompileListener<Object>() {
@Override
public void onCompiled(StructureModifier<Object> compiledModifier) {
// Update cache
cache.put(key, compiledModifier);
}
});
}
}
/**
* Ensure that the given structure modifier is eventually compiled.
* @param uncompiled - structure modifier to compile.
* @param listener - listener responsible for responding to the compilation.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public <TKey> void scheduleCompilation(final StructureModifier<TKey> uncompiled, final CompileListener<TKey> listener) {
// Only schedule if we're enabled
if (enabled && !shuttingDown) {
// Check perm gen
if (getPermGenUsage() > disablePermGenFraction)
return;
// Don't try to schedule anything
if (executor == null || executor.isShutdown())
return;
// Use to look up structure modifiers
final StructureKey key = new StructureKey(uncompiled);
// Allow others to listen in too
synchronized (listenerLock) {
List list = listeners.get(key);
if (!listeners.containsKey(key)) {
listeners.put(key, (List) Lists.newArrayList(listener));
} else {
// We're currently compiling
list.add(listener);
return;
}
}
// Create the worker that will compile our modifier
Callable<?> worker = new Callable<Object>() {
@Override
public Object call() throws Exception {
StructureModifier<TKey> modifier = uncompiled;
List list = null;
// Do our compilation
try {
modifier = compiler.compile(modifier);
synchronized (listenerLock) {
list = listeners.get(key);
// Prevent ConcurrentModificationExceptions
if (list != null) {
list = Lists.newArrayList(list);
}
}
// Only execute the listeners if there is a list
if (list != null) {
for (Object compileListener : list) {
((CompileListener<TKey>) compileListener).onCompiled(modifier);
}
// Remove it when we're done
synchronized (listenerLock) {
list = listeners.remove(key);
}
}
} catch (Throwable e) {
// Disable future compilations!
setEnabled(false);
// Inform about this error as best as we can
reporter.reportDetailed(BackgroundCompiler.this,
Report.newBuilder(REPORT_CANNOT_COMPILE_STRUCTURE_MODIFIER).callerParam(uncompiled).error(e)
);
}
// We'll also return the new structure modifier
return modifier;
}
};
try {
// Lookup the previous class name on the main thread.
// This is necessary as the Bukkit class loaders are not thread safe
if (compiler.lookupClassLoader(uncompiled)) {
try {
worker.call();
} catch (Exception e) {
// Impossible!
e.printStackTrace();
}
} else {
// Perform the compilation on a seperate thread
executor.submit(worker);
}
} catch (RejectedExecutionException e) {
// Occures when the underlying queue is overflowing. Since the compilation
// is only an optmization and not really essential we'll just log this failure
// and move on.
reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_SCHEDULE_COMPILATION).error(e));
}
}
}
/**
* Add a compile listener if we are still waiting for the structure modifier to be compiled.
* @param uncompiled - the structure modifier that may get compiled.
* @param listener - the listener to invoke in that case.
*/
@SuppressWarnings("unchecked")
public <TKey> void addListener(final StructureModifier<TKey> uncompiled, final CompileListener<TKey> listener) {
synchronized (listenerLock) {
StructureKey key = new StructureKey(uncompiled);
@SuppressWarnings("rawtypes")
List list = listeners.get(key);
if (list != null) {
list.add(listener);
}
}
}
/**
* Retrieve the current usage of the Perm Gen space in percentage.
* @return Usage of the perm gen space.
*/
private double getPermGenUsage() {
for (MemoryPoolMXBean item : ManagementFactory.getMemoryPoolMXBeans()) {
if (item.getName().contains("Perm Gen")) {
MemoryUsage usage = item.getUsage();
return usage.getUsed() / (double) usage.getCommitted();
}
}
// Unknown
return 0;
}
/**
* Clean up after ourselves using the default timeout.
*/
public void shutdownAll() {
shutdownAll(SHUTDOWN_DELAY_MS, TimeUnit.MILLISECONDS);
}
/**
* Clean up after ourselves.
* @param timeout - the maximum time to wait.
* @param unit - the time unit of the timeout argument.
*/
public void shutdownAll(long timeout, TimeUnit unit) {
setEnabled(false);
shuttingDown = true;
executor.shutdown();
try {
executor.awaitTermination(timeout, unit);
} catch (InterruptedException e) {
// Unlikely to ever occur - it's the main thread
e.printStackTrace();
}
}
/**
* Retrieve whether or not the background compiler is enabled.
* @return TRUE if it is enabled, FALSE otherwise.
*/
public boolean isEnabled() {
return enabled;
}
/**
* Sets whether or not the background compiler is enabled.
* @param enabled - TRUE to enable it, FALSE otherwise.
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* Retrieve the fraction of perm gen space used after which the background compiler will be disabled.
* @return The fraction after which the background compiler is disabled.
*/
public double getDisablePermGenFraction() {
return disablePermGenFraction;
}
/**
* Set the fraction of perm gen space used after which the background compiler will be disabled.
* @param fraction - the maximum use of perm gen space.
*/
public void setDisablePermGenFraction(double fraction) {
this.disablePermGenFraction = fraction;
}
/**
* Retrieve the current structure compiler.
* @return Current structure compiler.
*/
public StructureCompiler getCompiler() {
return compiler;
}
}
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.reflect.compiler;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.lang.management.MemoryUsage;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import com.comphenix.protocol.error.ErrorReporter;
import com.comphenix.protocol.error.Report;
import com.comphenix.protocol.error.ReportType;
import com.comphenix.protocol.reflect.StructureModifier;
import com.comphenix.protocol.reflect.compiler.StructureCompiler.StructureKey;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
/**
* Compiles structure modifiers on a background thread.
* <p>
* This is necessary as we cannot block the main thread.
*
* @author Kristian
*/
public class BackgroundCompiler {
public static final ReportType REPORT_CANNOT_COMPILE_STRUCTURE_MODIFIER = new ReportType("Cannot compile structure. Disabing compiler.");
public static final ReportType REPORT_CANNOT_SCHEDULE_COMPILATION = new ReportType("Unable to schedule compilation task.");
/**
* The default format for the name of new worker threads.
*/
public static final String THREAD_FORMAT = "ProtocolLib-StructureCompiler %s";
// How long to wait for a shutdown
public static final int SHUTDOWN_DELAY_MS = 2000;
/**
* The default fraction of perm gen space after which the background compiler will be disabled.
*/
public static final double DEFAULT_DISABLE_AT_PERM_GEN = 0.65;
// The single background compiler we're using
private static BackgroundCompiler backgroundCompiler;
// Classes we're currently compiling
private Map<StructureKey, List<CompileListener<?>>> listeners = Maps.newHashMap();
private Object listenerLock = new Object();
private StructureCompiler compiler;
private boolean enabled;
private boolean shuttingDown;
private ExecutorService executor;
private ErrorReporter reporter;
private double disablePermGenFraction = DEFAULT_DISABLE_AT_PERM_GEN;
/**
* Retrieves the current background compiler.
* @return Current background compiler.
*/
public static BackgroundCompiler getInstance() {
return backgroundCompiler;
}
/**
* Sets the single background compiler we're using.
* @param backgroundCompiler - current background compiler, or NULL if the library is not loaded.
*/
public static void setInstance(BackgroundCompiler backgroundCompiler) {
BackgroundCompiler.backgroundCompiler = backgroundCompiler;
}
/**
* Initialize a background compiler.
* <p>
* Uses the default {@link #THREAD_FORMAT} to name worker threads.
* @param loader - class loader from Bukkit.
* @param reporter - current error reporter.
*/
public BackgroundCompiler(ClassLoader loader, ErrorReporter reporter) {
ThreadFactory factory = new ThreadFactoryBuilder().
setDaemon(true).
setNameFormat(THREAD_FORMAT).
build();
initializeCompiler(loader, reporter, Executors.newSingleThreadExecutor(factory));
}
/**
* Initialize a background compiler utilizing the given thread pool.
* @param loader - class loader from Bukkit.
* @param reporter - current error reporter.
* @param executor - thread pool we'll use.
*/
public BackgroundCompiler(ClassLoader loader, ErrorReporter reporter, ExecutorService executor) {
initializeCompiler(loader, reporter, executor);
}
// Avoid "Constructor call must be the first statement".
private void initializeCompiler(ClassLoader loader, ErrorReporter reporter, ExecutorService executor) {
if (loader == null)
throw new IllegalArgumentException("loader cannot be NULL");
if (executor == null)
throw new IllegalArgumentException("executor cannot be NULL");
if (reporter == null)
throw new IllegalArgumentException("reporter cannot be NULL.");
this.compiler = new StructureCompiler(loader);
this.reporter = reporter;
this.executor = executor;
this.enabled = true;
}
/**
* Ensure that the indirectly given structure modifier is eventually compiled.
* @param cache - store of structure modifiers.
* @param key - key of the structure modifier to compile.
*/
@SuppressWarnings("rawtypes")
public void scheduleCompilation(final Map<Class, StructureModifier> cache, final Class key) {
@SuppressWarnings("unchecked")
final StructureModifier<Object> uncompiled = cache.get(key);
if (uncompiled != null) {
scheduleCompilation(uncompiled, new CompileListener<Object>() {
@Override
public void onCompiled(StructureModifier<Object> compiledModifier) {
// Update cache
cache.put(key, compiledModifier);
}
});
}
}
/**
* Ensure that the given structure modifier is eventually compiled.
* @param uncompiled - structure modifier to compile.
* @param listener - listener responsible for responding to the compilation.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public <TKey> void scheduleCompilation(final StructureModifier<TKey> uncompiled, final CompileListener<TKey> listener) {
// Only schedule if we're enabled
if (enabled && !shuttingDown) {
// Check perm gen
if (getPermGenUsage() > disablePermGenFraction)
return;
// Don't try to schedule anything
if (executor == null || executor.isShutdown())
return;
// Use to look up structure modifiers
final StructureKey key = new StructureKey(uncompiled);
// Allow others to listen in too
synchronized (listenerLock) {
List list = listeners.get(key);
if (!listeners.containsKey(key)) {
listeners.put(key, (List) Lists.newArrayList(listener));
} else {
// We're currently compiling
list.add(listener);
return;
}
}
// Create the worker that will compile our modifier
Callable<?> worker = new Callable<Object>() {
@Override
public Object call() throws Exception {
StructureModifier<TKey> modifier = uncompiled;
List list = null;
// Do our compilation
try {
modifier = compiler.compile(modifier);
synchronized (listenerLock) {
list = listeners.get(key);
// Prevent ConcurrentModificationExceptions
if (list != null) {
list = Lists.newArrayList(list);
}
}
// Only execute the listeners if there is a list
if (list != null) {
for (Object compileListener : list) {
((CompileListener<TKey>) compileListener).onCompiled(modifier);
}
// Remove it when we're done
synchronized (listenerLock) {
list = listeners.remove(key);
}
}
} catch (Throwable e) {
// Disable future compilations!
setEnabled(false);
// Inform about this error as best as we can
reporter.reportDetailed(BackgroundCompiler.this,
Report.newBuilder(REPORT_CANNOT_COMPILE_STRUCTURE_MODIFIER).callerParam(uncompiled).error(e)
);
}
// We'll also return the new structure modifier
return modifier;
}
};
try {
// Lookup the previous class name on the main thread.
// This is necessary as the Bukkit class loaders are not thread safe
if (compiler.lookupClassLoader(uncompiled)) {
try {
worker.call();
} catch (Exception e) {
// Impossible!
e.printStackTrace();
}
} else {
// Perform the compilation on a seperate thread
executor.submit(worker);
}
} catch (RejectedExecutionException e) {
// Occures when the underlying queue is overflowing. Since the compilation
// is only an optmization and not really essential we'll just log this failure
// and move on.
reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_SCHEDULE_COMPILATION).error(e));
}
}
}
/**
* Add a compile listener if we are still waiting for the structure modifier to be compiled.
* @param uncompiled - the structure modifier that may get compiled.
* @param listener - the listener to invoke in that case.
*/
@SuppressWarnings("unchecked")
public <TKey> void addListener(final StructureModifier<TKey> uncompiled, final CompileListener<TKey> listener) {
synchronized (listenerLock) {
StructureKey key = new StructureKey(uncompiled);
@SuppressWarnings("rawtypes")
List list = listeners.get(key);
if (list != null) {
list.add(listener);
}
}
}
/**
* Retrieve the current usage of the Perm Gen space in percentage.
* @return Usage of the perm gen space.
*/
private double getPermGenUsage() {
for (MemoryPoolMXBean item : ManagementFactory.getMemoryPoolMXBeans()) {
if (item.getName().contains("Perm Gen")) {
MemoryUsage usage = item.getUsage();
return usage.getUsed() / (double) usage.getCommitted();
}
}
// Unknown
return 0;
}
/**
* Clean up after ourselves using the default timeout.
*/
public void shutdownAll() {
shutdownAll(SHUTDOWN_DELAY_MS, TimeUnit.MILLISECONDS);
}
/**
* Clean up after ourselves.
* @param timeout - the maximum time to wait.
* @param unit - the time unit of the timeout argument.
*/
public void shutdownAll(long timeout, TimeUnit unit) {
setEnabled(false);
shuttingDown = true;
executor.shutdown();
try {
executor.awaitTermination(timeout, unit);
} catch (InterruptedException e) {
// Unlikely to ever occur - it's the main thread
e.printStackTrace();
}
}
/**
* Retrieve whether or not the background compiler is enabled.
* @return TRUE if it is enabled, FALSE otherwise.
*/
public boolean isEnabled() {
return enabled;
}
/**
* Sets whether or not the background compiler is enabled.
* @param enabled - TRUE to enable it, FALSE otherwise.
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* Retrieve the fraction of perm gen space used after which the background compiler will be disabled.
* @return The fraction after which the background compiler is disabled.
*/
public double getDisablePermGenFraction() {
return disablePermGenFraction;
}
/**
* Set the fraction of perm gen space used after which the background compiler will be disabled.
* @param fraction - the maximum use of perm gen space.
*/
public void setDisablePermGenFraction(double fraction) {
this.disablePermGenFraction = fraction;
}
/**
* Retrieve the current structure compiler.
* @return Current structure compiler.
*/
public StructureCompiler getCompiler() {
return compiler;
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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.
* <p>
* 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 "<init>".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.

View File

@ -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<Class<?>, EquivalentConverter<Object>> 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 <T> EquivalentConverter<List<T>> getListConverter(final Class<?> genericItemType, final EquivalentConverter<T> itemConverter) {
// Convert to and from the wrapper
return new IgnoreNullConverter<List<T>>() {
@ -208,6 +221,29 @@ public class BukkitConverters {
};
}
/**
* Retrieve a converter for wrapped attribute snapshots.
* @return Wrapped attribute snapshot converter.
*/
public static EquivalentConverter<WrappedAttribute> getWrappedAttributeConverter() {
return new IgnoreNullConverter<WrappedAttribute>() {
@Override
protected Object getGenericValue(Class<?> genericType, WrappedAttribute specific) {
return specific.getHandle();
}
@Override
protected WrappedAttribute getSpecificValue(Object generic) {
return WrappedAttribute.fromHandle(generic);
}
@Override
public Class<WrappedAttribute> 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<Class<?>, EquivalentConverter<Object>> getSpecificConverters() {
public static Map<Class<?>, EquivalentConverter<Object>> getConvertersForSpecific() {
if (specificConverters == null) {
// Generics doesn't work, as usual
ImmutableMap.Builder<Class<?>, EquivalentConverter<Object>> 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<Class<?>, EquivalentConverter<Object>> getGenericConverters() {
public static Map<Class<?>, EquivalentConverter<Object>> getConvertersForGeneric() {
if (genericConverters == null) {
// Generics doesn't work, as usual
ImmutableMap.Builder<Class<?>, EquivalentConverter<Object>> 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;

View File

@ -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<Object> ATTRIBUTE_MODIFIER;
// The one constructor
private static Constructor<?> ATTRIBUTE_CONSTRUCTOR;
/**
* Reference to the underlying attribute snapshot.
*/
protected Object handle;
protected StructureModifier<Object> modifier;
// Cached computed value
private double computedValue = Double.NaN;
// Cached modifiers list
private Set<WrappedAttributeModifier> 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<Object>(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.
* <p>
* 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<WrappedAttributeModifier> getModifiers() {
if (attributeModifiers == null) {
@SuppressWarnings("unchecked")
Collection<Object> collection = (Collection<Object>) modifier.withType(Collection.class).read(0);
// Convert to an equivalent wrapper
ConvertedSet<Object, WrappedAttributeModifier> converted =
new ConvertedSet<Object, WrappedAttributeModifier>(getSetSafely(collection)) {
@Override
protected Object toInner(WrappedAttributeModifier outer) {
return outer.getHandle();
}
@Override
protected WrappedAttributeModifier toOuter(Object inner) {
return WrappedAttributeModifier.fromHandle(inner);
}
};
attributeModifiers = new CachedSet<WrappedAttributeModifier>(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<WrappedAttributeModifier> 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<WrappedAttributeModifier> 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 <U> Set<U> getSetSafely(Collection<U> collection) {
return collection instanceof Set ? (Set<U>) 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.
* <p>
* 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<WrappedAttributeModifier> 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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<WrappedAttributeModifier> 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<Object> getUnwrappedModifiers() {
Set<Object> 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);
}
}
}
}

View File

@ -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.
* <p>
* This is used to compute the final attribute value.
*
* @author Kristian
*/
public class WrappedAttributeModifier {
/**
* Represents the different modifier operations.
* <p>
* The final value is computed as follows:
* <ol>
* <li>Set X = base value.</li>
* <li>Execute all modifiers with {@link Operation#ADD_NUMBER}.
* <li>Set Y = X.</li>
* <li>Execute all modifiers with {@link Operation#MULTIPLY_PERCENTAGE}.</li>
* <li>Execute all modifiers with {@link Operation#ADD_PERCENTAGE}.</li>
* <li>Y is the final value.</li>
* </ol>
* @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<Object> BASE_MODIFIER;
// The constructor we are interested in
private static Constructor<?> ATTRIBUTE_MODIFIER_CONSTRUCTOR;
/**
* Handle to the underlying AttributeModifier.
*/
protected Object handle;
protected StructureModifier<Object> 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.
* <p>
* 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<Object>(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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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);
}
}
}
}

View File

@ -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.
* <p>
* The cache will be invalidated when data is removed.
*
* @author Kristian
* @param <T> - type of each element in the collection.
*/
public class CachedCollection<T> implements Collection<T> {
protected Set<T> delegate;
protected Object[] cache;
/**
* Construct a cached collection with the given delegate.
* <p>
* Objects are cached before they can be extracted from this collection.
* @param delegate - the delegate.
*/
public CachedCollection(Set<T> 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<T> iterator() {
final Iterator<T> source = delegate.iterator();
initializeCache();
return new Iterator<T>() {
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> 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<? extends T> 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();
}
}

View File

@ -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 <T> - the element type.
*/
public class CachedSet<T> extends CachedCollection<T> implements Set<T> {
/**
* Construct a cached set from the given delegate.
* @param delegate - the set delegate.
*/
public CachedSet(Set<T> delegate) {
super(delegate);
}
}

View File

@ -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<AttributeModifier> 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());
}
}