Archiviert
13
0

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.
Dieser Commit ist enthalten in:
Kristian S. Stangeland 2013-07-28 02:02:27 +02:00
Ursprung 7170bfcadc
Commit 988026611c
14 geänderte Dateien mit 2402 neuen und 907 gelöschten Zeilen

Datei anzeigen

@ -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));

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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