mirror of
https://github.com/dmulloy2/ProtocolLib.git
synced 2025-01-26 10:11:33 +01:00
Added a generic cloning library to properly support deepClone().
This commit is contained in:
parent
fdbd3d6495
commit
8b91e3034d
@ -17,8 +17,6 @@
|
||||
|
||||
package com.comphenix.protocol.events;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
@ -41,6 +39,7 @@ import com.comphenix.protocol.injector.StructureCache;
|
||||
import com.comphenix.protocol.reflect.EquivalentConverter;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.reflect.StructureModifier;
|
||||
import com.comphenix.protocol.reflect.cloning.AggregateCloner;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.wrappers.BukkitConverters;
|
||||
import com.comphenix.protocol.wrappers.ChunkPosition;
|
||||
@ -370,35 +369,8 @@ public class PacketContainer implements Serializable {
|
||||
* @return A deep copy of the current packet.
|
||||
*/
|
||||
public PacketContainer deepClone() {
|
||||
ObjectOutputStream output = null;
|
||||
ObjectInputStream input = null;
|
||||
|
||||
try {
|
||||
// Use a small buffer of 32 bytes initially.
|
||||
ByteArrayOutputStream bufferOut = new ByteArrayOutputStream();
|
||||
output = new ObjectOutputStream(bufferOut);
|
||||
output.writeObject(this);
|
||||
|
||||
ByteArrayInputStream bufferIn = new ByteArrayInputStream(bufferOut.toByteArray());
|
||||
input = new ObjectInputStream(bufferIn);
|
||||
return (PacketContainer) input.readObject();
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Unexpected error occured during object cloning.", e);
|
||||
} catch (ClassNotFoundException e) {
|
||||
// Cannot happen
|
||||
throw new IllegalStateException("Unexpected failure with serialization.", e);
|
||||
} finally {
|
||||
try {
|
||||
if (output != null)
|
||||
output.close();
|
||||
if (input != null)
|
||||
input.close();
|
||||
|
||||
} catch (IOException e) {
|
||||
// STOP IT
|
||||
}
|
||||
}
|
||||
Object clonedPacket = AggregateCloner.DEFAULT.clone(getHandle());
|
||||
return new PacketContainer(getID(), clonedPacket);
|
||||
}
|
||||
|
||||
private void writeObject(ObjectOutputStream output) throws IOException {
|
||||
|
@ -182,6 +182,22 @@ class MinecraftRegistry {
|
||||
throw new IllegalArgumentException("The packet ID " + packetID + " is not registered.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the packet ID of a given packet.
|
||||
* @param packet - the type of packet to check.
|
||||
* @return The ID of the given packet.
|
||||
* @throws IllegalArgumentException If this is not a valid packet.
|
||||
*/
|
||||
public static int getPacketID(Class<?> packet) {
|
||||
if (packet == null)
|
||||
throw new IllegalArgumentException("Packet type class cannot be NULL.");
|
||||
if (!MinecraftReflection.getPacketClass().isAssignableFrom(packet))
|
||||
throw new IllegalArgumentException("Type must be a packet.");
|
||||
|
||||
// The registry contains both the overridden and original packets
|
||||
return getPacketToID().get(packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first superclass that is not a CBLib proxy object.
|
||||
* @param clazz - the class whose hierachy we're going to search through.
|
||||
|
@ -64,6 +64,27 @@ public class StructureCache {
|
||||
return getStructure(id, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a cached structure modifier given a packet type.
|
||||
* @param packetType - packet type.
|
||||
* @return A structure modifier.
|
||||
*/
|
||||
public static StructureModifier<Object> getStructure(Class<?> packetType) {
|
||||
// Compile structures by default
|
||||
return getStructure(packetType, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a cached structure modifier given a packet type.
|
||||
* @param packetType - packet type.
|
||||
* @param compile - whether or not to asynchronously compile the structure modifier.
|
||||
* @return A structure modifier.
|
||||
*/
|
||||
public static StructureModifier<Object> getStructure(Class<?> packetType, boolean compile) {
|
||||
// Get the ID from the class
|
||||
return getStructure(MinecraftRegistry.getPacketID(packetType), compile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a cached structure modifier for the given packet id.
|
||||
* @param id - packet ID.
|
||||
|
@ -17,21 +17,63 @@
|
||||
|
||||
package com.comphenix.protocol.reflect;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import com.comphenix.protocol.injector.StructureCache;
|
||||
import com.comphenix.protocol.reflect.cloning.Cloner;
|
||||
import com.comphenix.protocol.reflect.cloning.IdentityCloner;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
|
||||
/**
|
||||
* Can copy an object field by field.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class ObjectWriter {
|
||||
|
||||
// Cache structure modifiers
|
||||
@SuppressWarnings("rawtypes")
|
||||
private static ConcurrentMap<Class, StructureModifier<Object>> cache =
|
||||
new ConcurrentHashMap<Class, StructureModifier<Object>>();
|
||||
|
||||
/**
|
||||
* The default value cloner to use.
|
||||
*/
|
||||
private static final Cloner DEFAULT_CLONER = new IdentityCloner();
|
||||
|
||||
/**
|
||||
* Retrieve a usable structure modifier for the given object type.
|
||||
* <p>
|
||||
* Will attempt to reuse any other structure modifiers we have cached.
|
||||
* @param type - the type of the object we are modifying.
|
||||
* @return A structure modifier for the given type.
|
||||
*/
|
||||
private static StructureModifier<Object> getModifier(Class<?> type) {
|
||||
Class<?> packetClass = MinecraftReflection.getPacketClass();
|
||||
|
||||
// Handle subclasses of the packet class with our custom structure cache
|
||||
if (!type.equals(packetClass) && packetClass.isAssignableFrom(type)) {
|
||||
// Delegate to our already existing registry of structure modifiers
|
||||
return StructureCache.getStructure(type);
|
||||
}
|
||||
|
||||
StructureModifier<Object> modifier = cache.get(type);
|
||||
|
||||
// Create the structure modifier if we haven't already
|
||||
if (modifier == null) {
|
||||
StructureModifier<Object> value = new StructureModifier<Object>(type, null, false);
|
||||
modifier = cache.putIfAbsent(type, value);
|
||||
|
||||
if (modifier == null)
|
||||
modifier = value;
|
||||
}
|
||||
|
||||
// And we're done
|
||||
return modifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy every field in object A to object B. Each value is copied directly, and is not cloned.
|
||||
* <p>
|
||||
@ -41,22 +83,31 @@ public class ObjectWriter {
|
||||
* @param commonType - type containing each field to copy.
|
||||
*/
|
||||
public static void copyTo(Object source, Object destination, Class<?> commonType) {
|
||||
|
||||
// Note that we indicate that public fields will be copied the first time around
|
||||
copyToInternal(source, destination, commonType, DEFAULT_CLONER, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy every field in object A to object B. Each value is copied using the supplied cloner.
|
||||
* <p>
|
||||
* The two objects must have the same number of fields of the same type.
|
||||
* @param source - fields to copy.
|
||||
* @param destination - fields to copy to.
|
||||
* @param commonType - type containing each field to copy.
|
||||
* @param valueCloner - a object responsible for copying the content of each field.
|
||||
*/
|
||||
public static void copyTo(Object source, Object destination, Class<?> commonType, Cloner valueCloner) {
|
||||
copyToInternal(source, destination, commonType, valueCloner, true);
|
||||
}
|
||||
|
||||
// Internal method that will actually implement the recursion
|
||||
private static void copyToInternal(Object source, Object destination, Class<?> commonType, Cloner valueCloner, boolean copyPublic) {
|
||||
if (source == null)
|
||||
throw new IllegalArgumentException("Source cannot be NULL");
|
||||
if (destination == null)
|
||||
throw new IllegalArgumentException("Destination cannot be NULL");
|
||||
|
||||
StructureModifier<Object> modifier = cache.get(commonType);
|
||||
|
||||
// Create the structure modifier if we haven't already
|
||||
if (modifier == null) {
|
||||
StructureModifier<Object> value = new StructureModifier<Object>(commonType, null, false);
|
||||
modifier = cache.putIfAbsent(commonType, value);
|
||||
|
||||
if (modifier == null)
|
||||
modifier = value;
|
||||
}
|
||||
StructureModifier<Object> modifier = getModifier(commonType);
|
||||
|
||||
// Add target
|
||||
StructureModifier<Object> modifierSource = modifier.withTarget(source);
|
||||
@ -65,20 +116,21 @@ public class ObjectWriter {
|
||||
// Copy every field
|
||||
try {
|
||||
for (int i = 0; i < modifierSource.size(); i++) {
|
||||
if (!modifierDest.isReadOnly(i)) {
|
||||
Object value = modifierSource.read(i);
|
||||
modifierDest.write(i, value);
|
||||
}
|
||||
Field field = modifierSource.getField(i);
|
||||
int mod = field.getModifiers();
|
||||
|
||||
// System.out.println(String.format("Writing value %s to %s",
|
||||
// value, modifier.getFields().get(i).getName()));
|
||||
// Skip static fields. We also get the "public" field fairly often, so we'll skip that.
|
||||
if (!Modifier.isStatic(mod) && (!Modifier.isPublic(mod) || copyPublic)) {
|
||||
Object value = modifierSource.read(i);
|
||||
modifierDest.write(i, valueCloner.clone(value));
|
||||
}
|
||||
}
|
||||
|
||||
// Copy private fields underneath
|
||||
Class<?> superclass = commonType.getSuperclass();
|
||||
|
||||
if (!superclass.equals(Object.class)) {
|
||||
copyTo(source, destination, superclass);
|
||||
copyToInternal(source, destination, superclass, valueCloner, false);
|
||||
}
|
||||
|
||||
} catch (FieldAccessException e) {
|
||||
|
@ -201,10 +201,16 @@ public class StructureModifier<TField> {
|
||||
* @return TRUE if the field by the given index is read-only, FALSE otherwise.
|
||||
*/
|
||||
public boolean isReadOnly(int fieldIndex) {
|
||||
if (fieldIndex < 0 || fieldIndex >= data.size())
|
||||
throw new IllegalArgumentException("Index parameter is not within [0 - " + data.size() + ")");
|
||||
|
||||
return Modifier.isFinal(data.get(fieldIndex).getModifiers());
|
||||
return Modifier.isFinal(getField(fieldIndex).getModifiers());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a given field is public or not.
|
||||
* @param fieldIndex - field index.
|
||||
* @return TRUE if the field is public, FALSE otherwise.
|
||||
*/
|
||||
public boolean isPublic(int fieldIndex) {
|
||||
return Modifier.isPublic(getField(fieldIndex).getModifiers());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -499,6 +505,19 @@ public class StructureModifier<TField> {
|
||||
return ImmutableList.copyOf(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a field by index.
|
||||
* @param fieldIndex - index of the field to retrieve.
|
||||
* @return The field represented with the given index.
|
||||
* @throws IllegalArgumentException If no field with the given index can be found.
|
||||
*/
|
||||
public Field getField(int fieldIndex) {
|
||||
if (fieldIndex < 0 || fieldIndex >= data.size())
|
||||
throw new IllegalArgumentException("Index parameter is not within [0 - " + data.size() + ")");
|
||||
|
||||
return data.get(fieldIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve every value stored in the fields of the current type.
|
||||
* @return Every field value.
|
||||
@ -560,4 +579,6 @@ public class StructureModifier<TField> {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,236 @@
|
||||
package com.comphenix.protocol.reflect.cloning;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.comphenix.protocol.reflect.instances.DefaultInstances;
|
||||
import com.comphenix.protocol.reflect.instances.ExistingGenerator;
|
||||
import com.comphenix.protocol.reflect.instances.InstanceProvider;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
/**
|
||||
* Implements a cloning procedure by trying multiple methods in turn until one is successful.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class AggregateCloner implements Cloner {
|
||||
public static class BuilderParameters {
|
||||
// Can only be modified by the builder
|
||||
private InstanceProvider instanceProvider;
|
||||
private Cloner aggregateCloner;
|
||||
|
||||
// Used to construct the different types
|
||||
private InstanceProvider typeConstructor;
|
||||
|
||||
private BuilderParameters() {
|
||||
// Only allow inner classes to construct it.
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the instance provider last set in the builder.
|
||||
* @return Current instance provider.
|
||||
*/
|
||||
public InstanceProvider getInstanceProvider() {
|
||||
return instanceProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the aggregate cloner that is being built.
|
||||
* @return The parent cloner.
|
||||
*/
|
||||
public Cloner getAggregateCloner() {
|
||||
return aggregateCloner;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private List<Function<BuilderParameters, Cloner>> factories = Lists.newArrayList();
|
||||
private BuilderParameters parameters;
|
||||
|
||||
/**
|
||||
* Create a new aggregate builder.
|
||||
*/
|
||||
public Builder() {
|
||||
this.parameters = new BuilderParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the instance provider supplied to all cloners in this builder.
|
||||
* @param provider - new instance provider.
|
||||
* @return The current builder.
|
||||
*/
|
||||
public Builder instanceProvider(InstanceProvider provider) {
|
||||
this.parameters.instanceProvider = provider;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the next cloner that will be considered in turn.
|
||||
* @param type - the type of the next cloner.
|
||||
* @return This builder.
|
||||
*/
|
||||
public Builder andThen(final Class<? extends Cloner> type) {
|
||||
// Use reflection to generate a factory on the fly
|
||||
return orCloner(new Function<BuilderParameters, Cloner>() {
|
||||
@Override
|
||||
public Cloner apply(@Nullable BuilderParameters param) {
|
||||
Object result = param.typeConstructor.create(type);
|
||||
|
||||
if (result == null) {
|
||||
throw new IllegalStateException("Constructed NULL instead of " + type);
|
||||
}
|
||||
|
||||
if (type.isAssignableFrom(result.getClass()))
|
||||
return (Cloner) result;
|
||||
else
|
||||
throw new IllegalStateException("Constructed " + result.getClass() + " instead of " + type);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the next cloner that will be considered in turn.
|
||||
* @param factory - factory constructing the next cloner.
|
||||
* @return This builder.
|
||||
*/
|
||||
public Builder orCloner(Function<BuilderParameters, Cloner> factory) {
|
||||
factories.add(factory);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new aggregate cloner using the supplied values.
|
||||
* @return A new aggregate cloner.
|
||||
*/
|
||||
public AggregateCloner build() {
|
||||
AggregateCloner newCloner = new AggregateCloner();
|
||||
|
||||
// The parameters we will pass to our cloners
|
||||
Cloner paramCloner = new NullableCloner(newCloner);
|
||||
InstanceProvider paramProvider = parameters.instanceProvider;
|
||||
|
||||
// Initialize parameters
|
||||
parameters.aggregateCloner = paramCloner;
|
||||
parameters.typeConstructor = DefaultInstances.fromArray(
|
||||
ExistingGenerator.fromObjectArray(new Object[] { paramCloner, paramProvider })
|
||||
);
|
||||
|
||||
// Build every cloner in the correct order
|
||||
List<Cloner> cloners = Lists.newArrayList();
|
||||
|
||||
for (int i = 0; i < factories.size(); i++) {
|
||||
Cloner cloner = factories.get(i).apply(parameters);
|
||||
|
||||
// See if we were successful
|
||||
if (cloner != null)
|
||||
cloners.add(cloner);
|
||||
else
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Cannot create cloner from %s (%s)", factories.get(i), i)
|
||||
);
|
||||
}
|
||||
|
||||
// We're done
|
||||
newCloner.setCloners(cloners);
|
||||
return newCloner;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a default aggregate cloner.
|
||||
*/
|
||||
public static final AggregateCloner DEFAULT = newBuilder().
|
||||
instanceProvider(DefaultInstances.DEFAULT).
|
||||
andThen(BukkitCloner.class).
|
||||
andThen(ImmutableDetector.class).
|
||||
andThen(CollectionCloner.class).
|
||||
andThen(FieldCloner.class).
|
||||
build();
|
||||
|
||||
// List of clone methods
|
||||
private List<Cloner> cloners;
|
||||
|
||||
private WeakReference<Object> lastObject;
|
||||
private int lastResult;
|
||||
|
||||
/**
|
||||
* Begins constructing a new aggregate cloner.
|
||||
* @return A builder for a new aggregate cloner.
|
||||
*/
|
||||
public static Builder newBuilder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new, empty aggregate cloner.
|
||||
*/
|
||||
private AggregateCloner() {
|
||||
// Only used by our builder above.
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a view of the current list of cloners.
|
||||
* @return Current cloners.
|
||||
*/
|
||||
public List<Cloner> getCloners() {
|
||||
return Collections.unmodifiableList(cloners);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cloners that will be used.
|
||||
* @param cloners - the cloners that will be used.
|
||||
*/
|
||||
private void setCloners(Iterable<? extends Cloner> cloners) {
|
||||
this.cloners = Lists.newArrayList(cloners);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canClone(Object source) {
|
||||
// Optimize a bit
|
||||
lastResult = getFirstCloner(source);
|
||||
lastObject = new WeakReference<Object>(source);
|
||||
return lastResult >= 0 && lastResult < cloners.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the index of the first cloner capable of cloning the given object.
|
||||
* <p>
|
||||
* Returns an invalid index if no cloner is able to clone the object.
|
||||
* @param source - the object to clone.
|
||||
* @return The index of the cloner object.
|
||||
*/
|
||||
private int getFirstCloner(Object source) {
|
||||
for (int i = 0; i < cloners.size(); i++) {
|
||||
if (cloners.get(i).canClone(source))
|
||||
return i;
|
||||
}
|
||||
|
||||
return cloners.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object clone(Object source) {
|
||||
if (source == null)
|
||||
throw new IllegalAccessError("source cannot be NULL.");
|
||||
int index = 0;
|
||||
|
||||
// Are we dealing with the same object?
|
||||
if (lastObject != null && lastObject.get() == source) {
|
||||
index = lastResult;
|
||||
} else {
|
||||
index = getFirstCloner(source);
|
||||
}
|
||||
|
||||
// Make sure the object is valid
|
||||
if (index < cloners.size()) {
|
||||
return cloners.get(index).clone(source);
|
||||
}
|
||||
|
||||
// Damn - failure
|
||||
throw new IllegalArgumentException("Cannot clone " + source + ": No cloner is sutable.");
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package com.comphenix.protocol.reflect.cloning;
|
||||
|
||||
import com.comphenix.protocol.reflect.EquivalentConverter;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.wrappers.BukkitConverters;
|
||||
import com.comphenix.protocol.wrappers.ChunkPosition;
|
||||
import com.comphenix.protocol.wrappers.WrappedDataWatcher;
|
||||
|
||||
/**
|
||||
* Represents an object that can clone a specific list of Bukkit- and Minecraft-related objects.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class BukkitCloner implements Cloner {
|
||||
// List of classes we support
|
||||
private Class<?>[] clonableClasses = { MinecraftReflection.getItemStackClass(), MinecraftReflection.getChunkPositionClass(),
|
||||
MinecraftReflection.getDataWatcherClass() };
|
||||
|
||||
private int findMatchingClass(Class<?> type) {
|
||||
// See if is a subclass of any of our supported superclasses
|
||||
for (int i = 0; i < clonableClasses.length; i++) {
|
||||
if (clonableClasses[i].isAssignableFrom(type))
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canClone(Object source) {
|
||||
if (source == null)
|
||||
return false;
|
||||
|
||||
return findMatchingClass(source.getClass()) >= 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object clone(Object source) {
|
||||
if (source == null)
|
||||
throw new IllegalArgumentException("source cannot be NULL.");
|
||||
|
||||
// Convert to a wrapper
|
||||
switch (findMatchingClass(source.getClass())) {
|
||||
case 0:
|
||||
return MinecraftReflection.getMinecraftItemStack(MinecraftReflection.getBukkitItemStack(source).clone());
|
||||
case 1:
|
||||
EquivalentConverter<ChunkPosition> chunkConverter = ChunkPosition.getConverter();
|
||||
return chunkConverter.getGeneric(clonableClasses[1], chunkConverter.getSpecific(source));
|
||||
case 2:
|
||||
EquivalentConverter<WrappedDataWatcher> dataConverter = BukkitConverters.getDataWatcherConverter();
|
||||
return dataConverter.getGeneric(clonableClasses[2], dataConverter.getSpecific(source).deepClone());
|
||||
default:
|
||||
throw new IllegalArgumentException("Cannot clone objects of type " + source.getClass());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package com.comphenix.protocol.reflect.cloning;
|
||||
|
||||
/**
|
||||
* Represents an object that is capable of cloning other objects.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public interface Cloner {
|
||||
/**
|
||||
* Determine whether or not the current cloner can clone the given object.
|
||||
* @param source - the object that is being considered.
|
||||
* @return TRUE if this cloner can actually clone the given object, FALSE otherwise.
|
||||
*/
|
||||
public boolean canClone(Object source);
|
||||
|
||||
/**
|
||||
* Perform the clone.
|
||||
* <p>
|
||||
* This method should never be called unless a corresponding {@link #canClone(Object)} returns TRUE.
|
||||
* @param source - the value to clone.
|
||||
* @return A cloned value.
|
||||
* @throws IllegalArgumentException If this cloner cannot perform the clone.
|
||||
*/
|
||||
public Object clone(Object source);
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
package com.comphenix.protocol.reflect.cloning;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Attempts to clone collection and array classes.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class CollectionCloner implements Cloner {
|
||||
private final Cloner defaultCloner;
|
||||
|
||||
/**
|
||||
* Constructs a new collection and array cloner with the given inner element cloner.
|
||||
* @param defaultCloner - default inner element cloner.
|
||||
*/
|
||||
public CollectionCloner(Cloner defaultCloner) {
|
||||
this.defaultCloner = defaultCloner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canClone(Object source) {
|
||||
if (source == null)
|
||||
return false;
|
||||
|
||||
Class<?> clazz = source.getClass();
|
||||
return Collection.class.isAssignableFrom(clazz) || clazz.isArray();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object clone(Object source) {
|
||||
if (source == null)
|
||||
throw new IllegalArgumentException("source cannot be NULL.");
|
||||
|
||||
Class<?> clazz = source.getClass();
|
||||
|
||||
if (source instanceof Collection) {
|
||||
Collection<Object> copy = null;
|
||||
|
||||
// Not all collections implement "clone", but most *do* implement the "copy constructor" pattern
|
||||
try {
|
||||
Constructor<?> constructCopy = clazz.getConstructor(Collection.class);
|
||||
copy = (Collection<Object>) constructCopy.newInstance(source);
|
||||
} catch (NoSuchMethodException e) {
|
||||
copy = (Collection<Object>) cloneObject(clazz, source);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Cannot construct collection.", e);
|
||||
}
|
||||
|
||||
// Next, clone each element in the collection
|
||||
copy.clear();
|
||||
|
||||
for (Object element : (Collection<Object>) source) {
|
||||
if (defaultCloner.canClone(element))
|
||||
copy.add(defaultCloner.clone(element));
|
||||
else
|
||||
throw new IllegalArgumentException("Cannot clone " + element + " in collection " + source);
|
||||
}
|
||||
|
||||
return copy;
|
||||
|
||||
// Second possibility
|
||||
} else if (clazz.isArray()) {
|
||||
// Get the length
|
||||
int lenght = Array.getLength(source);
|
||||
Class<?> component = clazz.getComponentType();
|
||||
|
||||
// Can we speed things up by making a shallow copy instead?
|
||||
if (ImmutableDetector.isImmutable(component)) {
|
||||
return clonePrimitive(component, source);
|
||||
}
|
||||
|
||||
// Create a new copy
|
||||
Object copy = Array.newInstance(clazz.getComponentType(), lenght);
|
||||
|
||||
// Set each element
|
||||
for (int i = 0; i < lenght; i++) {
|
||||
Object element = Array.get(source, i);
|
||||
|
||||
if (defaultCloner.canClone(element))
|
||||
Array.set(copy, i, defaultCloner.clone(element));
|
||||
else
|
||||
throw new IllegalArgumentException("Cannot clone " + element + " in array " + source);
|
||||
}
|
||||
|
||||
// And we're done
|
||||
return copy;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(source + " is not an array nor a Collection.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone a primitive or immutable array by calling its clone method.
|
||||
* @param component - the component type of the array.
|
||||
* @param source - the array itself.
|
||||
* @return The cloned array.
|
||||
*/
|
||||
private Object clonePrimitive(Class<?> component, Object source) {
|
||||
// Cast and call the correct version
|
||||
if (byte.class.equals(component))
|
||||
return ((byte[]) source).clone();
|
||||
else if (short.class.equals(component))
|
||||
return ((short[]) source).clone();
|
||||
else if (int.class.equals(component))
|
||||
return ((int[]) source).clone();
|
||||
else if (long.class.equals(component))
|
||||
return ((long[]) source).clone();
|
||||
else if (float.class.equals(component))
|
||||
return ((float[]) source).clone();
|
||||
else if (double.class.equals(component))
|
||||
return ((double[]) source).clone();
|
||||
else if (char.class.equals(component))
|
||||
return ((char[]) source).clone();
|
||||
else if (boolean.class.equals(component))
|
||||
return ((boolean[]) source).clone();
|
||||
else
|
||||
return ((Object[]) source).clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone an object by calling "clone" using reflection.
|
||||
* @param clazz - the class type.
|
||||
* @param obj - the object to clone.
|
||||
* @return The cloned object.
|
||||
*/
|
||||
private Object cloneObject(Class<?> clazz, Object source) {
|
||||
// Try to clone it instead
|
||||
try {
|
||||
return clazz.getMethod("clone").invoke(source);
|
||||
} catch (Exception e1) {
|
||||
throw new RuntimeException("Cannot copy " + source + " (" + clazz + ")", e1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the default cloner used to clone the content of each element in the collection.
|
||||
* @return Cloner used to clone elements.
|
||||
*/
|
||||
public Cloner getDefaultCloner() {
|
||||
return defaultCloner;
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package com.comphenix.protocol.reflect.cloning;
|
||||
|
||||
import com.comphenix.protocol.reflect.ObjectWriter;
|
||||
import com.comphenix.protocol.reflect.instances.InstanceProvider;
|
||||
|
||||
/**
|
||||
* Represents a class capable of cloning objects by deeply copying its fields.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class FieldCloner implements Cloner {
|
||||
private final Cloner defaultCloner;
|
||||
private final InstanceProvider instanceProvider;
|
||||
|
||||
/**
|
||||
* Constructs a field cloner that copies objects by reading and writing the internal fields directly.
|
||||
* @param defaultCloner - the default cloner used while copying fields.
|
||||
* @param instanceProvider - used to construct new, empty copies of a given type.
|
||||
*/
|
||||
public FieldCloner(Cloner defaultCloner, InstanceProvider instanceProvider) {
|
||||
this.defaultCloner = defaultCloner;
|
||||
this.instanceProvider = instanceProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canClone(Object source) {
|
||||
if (source == null)
|
||||
return false;
|
||||
|
||||
// Attempt to create the type
|
||||
return instanceProvider.create(source.getClass()) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object clone(Object source) {
|
||||
if (source == null)
|
||||
throw new IllegalArgumentException("source cannot be NULL.");
|
||||
|
||||
Object copy = instanceProvider.create(source.getClass());
|
||||
|
||||
// Copy public and private fields alike. Skip static and transient fields.
|
||||
ObjectWriter.copyTo(source, copy, source.getClass(), defaultCloner);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the default cloner used to clone the content of each field.
|
||||
* @return Cloner used to clone fields.
|
||||
*/
|
||||
public Cloner getDefaultCloner() {
|
||||
return defaultCloner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the instance provider this cloner is using to create new, empty classes.
|
||||
* @return The instance provider in use.
|
||||
*/
|
||||
public InstanceProvider getInstanceProvider() {
|
||||
return instanceProvider;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.comphenix.protocol.reflect.cloning;
|
||||
|
||||
/**
|
||||
* Represents a cloner that simply returns the given object.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class IdentityCloner implements Cloner {
|
||||
@Override
|
||||
public boolean canClone(Object source) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object clone(Object source) {
|
||||
return source;
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package com.comphenix.protocol.reflect.cloning;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.google.common.primitives.Primitives;
|
||||
|
||||
/**
|
||||
* Detects classes that are immutable, and thus doesn't require cloning.
|
||||
* <p>
|
||||
* This ought to have no false positives, but plenty of false negatives.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class ImmutableDetector implements Cloner {
|
||||
// Notable immutable classes we might encounter
|
||||
private static final Class<?>[] immutableClasses = {
|
||||
StackTraceElement.class, BigDecimal.class,
|
||||
BigInteger.class, Locale.class, UUID.class,
|
||||
URL.class, URI.class, Inet4Address.class,
|
||||
Inet6Address.class, InetSocketAddress.class
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean canClone(Object source) {
|
||||
// Don't accept NULL
|
||||
if (source == null)
|
||||
return false;
|
||||
|
||||
return isImmutable(source.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given type is probably immutable.
|
||||
* @param type - the type to check.
|
||||
* @return TRUE if the type is immutable, FALSE otherwise.
|
||||
*/
|
||||
public static boolean isImmutable(Class<?> type) {
|
||||
// Cases that are definitely not true
|
||||
if (type.isArray())
|
||||
return false;
|
||||
|
||||
// All primitive types
|
||||
if (Primitives.isWrapperType(type) || String.class.equals(type))
|
||||
return true;
|
||||
// May not be true, but if so, that kind of code is broken anyways
|
||||
if (type.isEnum())
|
||||
return true;
|
||||
|
||||
for (Class<?> clazz : immutableClasses)
|
||||
if (clazz.equals(type))
|
||||
return true;
|
||||
|
||||
// Probably not
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object clone(Object source) {
|
||||
// Safe if the class is immutable
|
||||
return source;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package com.comphenix.protocol.reflect.cloning;
|
||||
|
||||
/**
|
||||
* Creates a cloner wrapper that accepts and clones NULL values.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class NullableCloner implements Cloner {
|
||||
protected Cloner wrapped;
|
||||
|
||||
public NullableCloner(Cloner wrapped) {
|
||||
this.wrapped = wrapped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canClone(Object source) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object clone(Object source) {
|
||||
// Don't pass the NULL value to the cloner
|
||||
if (source == null)
|
||||
return null;
|
||||
else
|
||||
return wrapped.clone(source);
|
||||
}
|
||||
|
||||
public Cloner getWrapped() {
|
||||
return wrapped;
|
||||
}
|
||||
}
|
@ -20,6 +20,8 @@ package com.comphenix.protocol.reflect.instances;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.*;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import net.sf.cglib.proxy.Enhancer;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
@ -30,7 +32,7 @@ import com.google.common.collect.ImmutableList;
|
||||
* @author Kristian
|
||||
*
|
||||
*/
|
||||
public class DefaultInstances {
|
||||
public class DefaultInstances implements InstanceProvider {
|
||||
|
||||
/**
|
||||
* Standard default instance provider.
|
||||
@ -326,4 +328,9 @@ public class DefaultInstances {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object create(@Nullable Class<?> type) {
|
||||
return getDefault(type);
|
||||
}
|
||||
}
|
||||
|
@ -18,13 +18,16 @@
|
||||
package com.comphenix.protocol.reflect.instances;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
/**
|
||||
* Provides instance constructors using a list of existing values.
|
||||
@ -33,8 +36,52 @@ import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
* @author Kristian
|
||||
*/
|
||||
public class ExistingGenerator implements InstanceProvider {
|
||||
/**
|
||||
* Represents a single node in the tree of possible values.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
private static final class Node {
|
||||
private Map<Class<?>, Node> children;
|
||||
private Class<?> key;
|
||||
private Object value;
|
||||
private int level;
|
||||
|
||||
public Node(Class<?> key, Object value, int level) {
|
||||
this.children = new HashMap<Class<?>, Node>();
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
private Map<String, Object> existingValues = new HashMap<String, Object>();
|
||||
public Node addChild(Node node) {
|
||||
children.put(node.key, node);
|
||||
return node;
|
||||
}
|
||||
|
||||
public int getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public Collection<Node> getChildren() {
|
||||
return children.values();
|
||||
}
|
||||
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(Object value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public Node getChild(Class<?> clazz) {
|
||||
return children.get(clazz);
|
||||
}
|
||||
}
|
||||
|
||||
// Represents the root node
|
||||
private Node root = new Node(null, null, 0);
|
||||
|
||||
private ExistingGenerator() {
|
||||
// Only accessible to the constructors
|
||||
@ -110,18 +157,94 @@ public class ExistingGenerator implements InstanceProvider {
|
||||
if (value == null)
|
||||
throw new IllegalArgumentException("Value cannot be NULL.");
|
||||
|
||||
existingValues.put(value.getClass().getName(), value);
|
||||
}
|
||||
|
||||
private void addObject(Class<?> type, Object value) {
|
||||
existingValues.put(type.getName(), value);
|
||||
addObject(value.getClass(), value);
|
||||
}
|
||||
|
||||
private void addObject(Class<?> type, Object value) {
|
||||
Node node = getLeafNode(root, type, false);
|
||||
|
||||
// Set the value
|
||||
node.setValue(value);
|
||||
}
|
||||
|
||||
private Node getLeafNode(final Node start, Class<?> type, boolean readOnly) {
|
||||
Class<?>[] path = getHierachy(type);
|
||||
Node current = start;
|
||||
|
||||
for (int i = 0; i < path.length; i++) {
|
||||
Node next = getNext(current, path[i], readOnly);
|
||||
|
||||
// Try every interface too
|
||||
if (next == null && readOnly) {
|
||||
current = null;
|
||||
break;
|
||||
}
|
||||
|
||||
current = next;
|
||||
}
|
||||
|
||||
// And we're done
|
||||
return current;
|
||||
}
|
||||
|
||||
private Node getNext(Node current, Class<?> clazz, boolean readOnly) {
|
||||
Node next = current.getChild(clazz);
|
||||
|
||||
// Add a new node if needed
|
||||
if (next == null && !readOnly) {
|
||||
next = current.addChild(new Node(clazz, null, current.getLevel() + 1));
|
||||
}
|
||||
|
||||
// Add interfaces
|
||||
if (next != null && !readOnly && !clazz.isInterface()) {
|
||||
for (Class<?> clazzInterface : clazz.getInterfaces()) {
|
||||
getLeafNode(root, clazzInterface, readOnly).addChild(next);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
private Node getLowestLeaf(Node current) {
|
||||
Node candidate = current;
|
||||
|
||||
// Depth-first search
|
||||
for (Node child : current.getChildren()) {
|
||||
Node subtree = getLowestLeaf(child);
|
||||
|
||||
// Get the lowest node
|
||||
if (subtree.getValue() != null && candidate.getLevel() < subtree.getLevel()) {
|
||||
candidate = subtree;
|
||||
}
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private Class<?>[] getHierachy(Class<?> type) {
|
||||
LinkedList<Class<?>> levels = Lists.newLinkedList();
|
||||
|
||||
// Add each class from the hierachy
|
||||
for (; type != null; type = type.getSuperclass()) {
|
||||
levels.addFirst(type);
|
||||
}
|
||||
|
||||
return levels.toArray(new Class<?>[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object create(@Nullable Class<?> type) {
|
||||
Object value = existingValues.get(type.getName());
|
||||
|
||||
// Locate the type in the hierachy
|
||||
Node node = getLeafNode(root, type, true);
|
||||
|
||||
// Next, get the lowest leaf node
|
||||
if (node != null) {
|
||||
node = getLowestLeaf(node);
|
||||
}
|
||||
|
||||
// NULL values indicate that the generator failed
|
||||
return value;
|
||||
if (node != null)
|
||||
return node.getValue();
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +112,24 @@ public class MinecraftReflection {
|
||||
return fullName.substring(0, fullName.lastIndexOf("."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically retrieve the Bukkit entity from a given entity.
|
||||
* @param nmsObject - the NMS entity.
|
||||
* @return A bukkit entity.
|
||||
* @throws RuntimeException If we were unable to retrieve the Bukkit entity.
|
||||
*/
|
||||
public static Object getBukkitEntity(Object nmsObject) {
|
||||
if (nmsObject == null)
|
||||
return null;
|
||||
|
||||
// We will have to do this dynamically, unfortunately
|
||||
try {
|
||||
return nmsObject.getClass().getMethod("getBukkitEntity").invoke(nmsObject);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Cannot get Bukkit entity from " + nmsObject, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a given object can be found within the package net.minecraft.server.
|
||||
* @param obj - the object to test.
|
||||
@ -138,25 +156,7 @@ public class MinecraftReflection {
|
||||
String javaName = obj.getClass().getName();
|
||||
return javaName.startsWith(MINECRAFT_PREFIX_PACKAGE) && javaName.endsWith(className);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically retrieve the Bukkit entity from a given entity.
|
||||
* @param nmsObject - the NMS entity.
|
||||
* @return A bukkit entity.
|
||||
* @throws RuntimeException If we were unable to retrieve the Bukkit entity.
|
||||
*/
|
||||
public static Object getBukkitEntity(Object nmsObject) {
|
||||
if (nmsObject == null)
|
||||
return null;
|
||||
|
||||
// We will have to do this dynamically, unfortunately
|
||||
try {
|
||||
return nmsObject.getClass().getMethod("getBukkitEntity").invoke(nmsObject);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Cannot get Bukkit entity from " + nmsObject, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if a given object is a ChunkPosition.
|
||||
* @param obj - the object to test.
|
||||
|
@ -5,6 +5,7 @@ import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.List;
|
||||
|
||||
// Will have to be updated for every version though
|
||||
@ -23,12 +24,15 @@ import org.junit.runner.RunWith;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
|
||||
import com.comphenix.protocol.Packets;
|
||||
import com.comphenix.protocol.reflect.EquivalentConverter;
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.StructureModifier;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.wrappers.BukkitConverters;
|
||||
import com.comphenix.protocol.wrappers.ChunkPosition;
|
||||
import com.comphenix.protocol.wrappers.WrappedDataWatcher;
|
||||
import com.comphenix.protocol.wrappers.WrappedWatchableObject;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
@ -36,6 +40,9 @@ import com.google.common.collect.Lists;
|
||||
@RunWith(org.powermock.modules.junit4.PowerMockRunner.class)
|
||||
@PrepareForTest(CraftItemFactory.class)
|
||||
public class PacketContainerTest {
|
||||
// Helper converters
|
||||
private EquivalentConverter<WrappedDataWatcher> watchConvert = BukkitConverters.getDataWatcherConverter();
|
||||
private EquivalentConverter<ItemStack> itemConvert = BukkitConverters.getItemStackConverter();
|
||||
|
||||
@BeforeClass
|
||||
public static void initializeBukkit() throws IllegalAccessException {
|
||||
@ -203,23 +210,15 @@ public class PacketContainerTest {
|
||||
}
|
||||
|
||||
private boolean equivalentItem(ItemStack first, ItemStack second) {
|
||||
if (first == null)
|
||||
if (first == null) {
|
||||
return second == null;
|
||||
else if (second == null)
|
||||
} else if (second == null) {
|
||||
return false;
|
||||
else
|
||||
} else {
|
||||
return first.getType().equals(second.getType());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean equivalentPacket(PacketContainer first, PacketContainer second) {
|
||||
if (first == null)
|
||||
return second == null;
|
||||
else if (second == null)
|
||||
return false;
|
||||
else
|
||||
return first.getModifier().getValues().equals(second.getModifier().getValues());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWorldTypeModifier() {
|
||||
PacketContainer loginPacket = new PacketContainer(Packets.Server.LOGIN);
|
||||
@ -324,7 +323,16 @@ public class PacketContainerTest {
|
||||
PacketContainer cloned = constructed.deepClone();
|
||||
|
||||
// Make sure they're equivalent
|
||||
assertTrue("Packet " + id + " could not be cloned.", equivalentPacket(constructed, cloned));
|
||||
StructureModifier<Object> firstMod = constructed.getModifier(), secondMod = cloned.getModifier();
|
||||
assertEquals(firstMod.size(), secondMod.size());
|
||||
|
||||
// Make sure all the fields are equivalent
|
||||
for (int i = 0; i < firstMod.size(); i++) {
|
||||
if (firstMod.getField(i).getType().isArray())
|
||||
assertArrayEquals(getArray(firstMod.read(i)), getArray(secondMod.read(i)));
|
||||
else
|
||||
testEquality(firstMod.read(i), secondMod.read(i));
|
||||
}
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (!registered) {
|
||||
@ -337,4 +345,36 @@ public class PacketContainerTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to objects that support equals()
|
||||
private void testEquality(Object a, Object b) {
|
||||
if (a != null && b != null) {
|
||||
if (MinecraftReflection.isDataWatcher(a)) {
|
||||
a = watchConvert.getSpecific(a);
|
||||
b = watchConvert.getSpecific(b);
|
||||
} else if (MinecraftReflection.isItemStack(a)) {
|
||||
a = itemConvert.getSpecific(a);
|
||||
b = itemConvert.getSpecific(b);
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying array as an object array.
|
||||
* @param val - array wrapped as an Object.
|
||||
* @return An object array.
|
||||
*/
|
||||
private Object[] getArray(Object val) {
|
||||
if (val instanceof Object[])
|
||||
return (Object[]) val;
|
||||
|
||||
int arrlength = Array.getLength(val);
|
||||
Object[] outputArray = new Object[arrlength];
|
||||
|
||||
for (int i = 0; i < arrlength; ++i)
|
||||
outputArray[i] = Array.get(val, i);
|
||||
return outputArray;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user