From ad69b0caac4715811f3608c407124995d95f31b5 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Tue, 13 Nov 2012 14:34:07 +0100 Subject: [PATCH] Added the ability to read data watchers and watchable object lists. --- .../protocol/CleanupStaticMembers.java | 8 +- .../protocol/events/PacketContainer.java | 272 ++--------- .../protocol/reflect/EquivalentConverter.java | 19 +- .../protocol/reflect/StructureModifier.java | 11 +- .../compiler/CompiledStructureModifier.java | 2 +- .../protocol/wrappers/BukkitConverters.java | 307 +++++++++++++ .../protocol/wrappers/ChunkPosition.java | 54 ++- .../wrappers/WrappedChunkCoordinate.java | 105 +++++ .../protocol/wrappers/WrappedDataWatcher.java | 434 ++++++++++++++++++ .../wrappers/WrappedWatchableObject.java | 160 +++++++ 10 files changed, 1141 insertions(+), 231 deletions(-) create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedChunkCoordinate.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedDataWatcher.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedWatchableObject.java diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java index dbc2e819..91e86148 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java @@ -19,6 +19,8 @@ import com.comphenix.protocol.reflect.compiler.StructureCompiler; import com.comphenix.protocol.reflect.instances.CollectionGenerator; import com.comphenix.protocol.reflect.instances.DefaultInstances; import com.comphenix.protocol.reflect.instances.PrimitiveGenerator; +import com.comphenix.protocol.wrappers.ChunkPosition; +import com.comphenix.protocol.wrappers.WrappedDataWatcher; /** * Used to fix ClassLoader leaks that may lead to filling up the permanent generation. @@ -45,7 +47,8 @@ class CleanupStaticMembers { BukkitUnwrapper.class, DefaultInstances.class, CollectionGenerator.class, PrimitiveGenerator.class, FuzzyReflection.class, MethodUtils.class, BackgroundCompiler.class, StructureCompiler.class, - ObjectCloner.class, Packets.Server.class, Packets.Client.class + ObjectCloner.class, Packets.Server.class, Packets.Client.class, + ChunkPosition.class, WrappedDataWatcher.class }; String[] internalClasses = { @@ -62,7 +65,8 @@ class CleanupStaticMembers { "com.comphenix.protocol.injector.ReadPacketModifier", "com.comphenix.protocol.injector.StructureCache", "com.comphenix.protocol.reflect.compiler.BoxingHelper", - "com.comphenix.protocol.reflect.compiler.MethodDescriptor" + "com.comphenix.protocol.reflect.compiler.MethodDescriptor", + "com.comphenix.protocol.wrappers.WrappedWatchableObject" }; resetClasses(publicClasses); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java index af181463..e9ce9714 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java @@ -25,26 +25,22 @@ import java.io.ObjectOutputStream; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.Collection; import java.util.List; -import org.bukkit.Bukkit; -import org.bukkit.Server; import org.bukkit.World; import org.bukkit.WorldType; -import org.bukkit.craftbukkit.CraftWorld; -import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; 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.instances.DefaultInstances; +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 net.minecraft.server.Packet; @@ -66,24 +62,10 @@ public class PacketContainer implements Serializable { // Current structure modifier protected transient StructureModifier structureModifier; - // Check whether or not certain classes exists - private static boolean hasWorldType = false; - - // The getEntity method - private static Method getEntity; - // Support for serialization private static Method writeMethod; private static Method readMethod; - - static { - try { - Class.forName("net.minecraft.server.WorldType"); - hasWorldType = true; - } catch (ClassNotFoundException e) { - } - } - + /** * Creates a packet container for a new packet. * @param id - ID of the packet to create. @@ -150,22 +132,8 @@ public class PacketContainer implements Serializable { */ public StructureModifier getItemModifier() { // Convert to and from the Bukkit wrapper - return structureModifier.withType(net.minecraft.server.ItemStack.class, - getIgnoreNull(new EquivalentConverter() { - public Object getGeneric(ItemStack specific) { - return toStackNMS(specific); - } - - @Override - public ItemStack getSpecific(Object generic) { - return new CraftItemStack((net.minecraft.server.ItemStack) generic); - } - - @Override - public Class getSpecificType() { - return ItemStack.class; - } - })); + return structureModifier.withType( + net.minecraft.server.ItemStack.class, BukkitConverters.getItemStackConverter()); } /** @@ -176,17 +144,21 @@ public class PacketContainer implements Serializable { * @return A modifier for ItemStack array fields. */ public StructureModifier getItemArrayModifier() { + + final EquivalentConverter stackConverter = BukkitConverters.getItemStackConverter(); + // Convert to and from the Bukkit wrapper return structureModifier.withType( net.minecraft.server.ItemStack[].class, - getIgnoreNull(new EquivalentConverter() { + BukkitConverters.getIgnoreNull(new EquivalentConverter() { - public Object getGeneric(ItemStack[] specific) { + public Object getGeneric(ClassgenericType, ItemStack[] specific) { net.minecraft.server.ItemStack[] result = new net.minecraft.server.ItemStack[specific.length]; // Unwrap every item for (int i = 0; i < result.length; i++) { - result[i] = toStackNMS(specific[i]); + result[i] = (net.minecraft.server.ItemStack) stackConverter.getGeneric( + net.minecraft.server.ItemStack.class, specific[i]); } return result; } @@ -198,7 +170,7 @@ public class PacketContainer implements Serializable { // Add the wrapper for (int i = 0; i < result.length; i++) { - result[i] = new CraftItemStack(input[i]); + result[i] = stackConverter.getSpecific(input[i]); } return result; } @@ -210,20 +182,6 @@ public class PacketContainer implements Serializable { })); } - /** - * Convert an item stack to the NMS equivalent. - * @param stack - Bukkit stack to convert. - * @return A bukkit stack. - */ - private net.minecraft.server.ItemStack toStackNMS(ItemStack stack) { - // We must be prepared for an object that simply implements ItemStcak - if (stack instanceof CraftItemStack) { - return ((CraftItemStack) stack).getHandle(); - } else { - return (new CraftItemStack(stack)).getHandle(); - } - } - /** * Retrieves a read/write structure for the world type enum. *

@@ -232,33 +190,21 @@ public class PacketContainer implements Serializable { * @return A modifier for world type fields. */ public StructureModifier getWorldTypeModifier() { - - if (!hasWorldType) { - // We couldn't find the Minecraft equivalent - return structureModifier.withType(null); - } - // Convert to and from the Bukkit wrapper return structureModifier.withType( net.minecraft.server.WorldType.class, - getIgnoreNull(new EquivalentConverter() { - - @Override - public Object getGeneric(WorldType specific) { - return net.minecraft.server.WorldType.getType(specific.getName()); - } - - @Override - public WorldType getSpecific(Object generic) { - net.minecraft.server.WorldType type = (net.minecraft.server.WorldType) generic; - return WorldType.getByName(type.name()); - } - - @Override - public Class getSpecificType() { - return WorldType.class; - } - })); + BukkitConverters.getWorldTypeConverter()); + } + + /** + * Retrieves a read/write structure for data watchers. + * @return A modifier for data watchers. + */ + public StructureModifier getDataWatcherModifier() { + // Convert to and from the Bukkit wrapper + return structureModifier.withType( + net.minecraft.server.DataWatcher.class, + BukkitConverters.getDataWatcherConverter()); } /** @@ -272,64 +218,9 @@ public class PacketContainer implements Serializable { * @return A modifier entity types. */ public StructureModifier getEntityModifier(World world) { - - final Object worldServer = ((CraftWorld) world).getHandle(); - final Class nmsEntityClass = net.minecraft.server.Entity.class; - - if (getEntity == null) - getEntity = FuzzyReflection.fromObject(worldServer).getMethodByParameters( - "getEntity", nmsEntityClass, new Class[] { int.class }); - // Convert to and from the Bukkit wrapper return structureModifier.withType( - int.class, - getIgnoreNull(new EquivalentConverter() { - - @Override - public Object getGeneric(Entity specific) { - // Simple enough - return specific.getEntityId(); - } - - @Override - public Entity getSpecific(Object generic) { - try { - net.minecraft.server.Entity nmsEntity = (net.minecraft.server.Entity) - getEntity.invoke(worldServer, generic); - Integer id = (Integer) generic; - - // Attempt to get the Bukkit entity - if (nmsEntity != null) { - return nmsEntity.getBukkitEntity(); - } else { - Server server = Bukkit.getServer(); - - // Maybe it's a player that has just logged in? Try a search - if (server != null) { - for (Player player : server.getOnlinePlayers()) { - if (player.getEntityId() == id) { - return player; - } - } - } - - return null; - } - - } catch (IllegalArgumentException e) { - throw new RuntimeException("Incorrect arguments detected.", e); - } catch (IllegalAccessException e) { - throw new RuntimeException("Cannot read field due to a security limitation.", e); - } catch (InvocationTargetException e) { - throw new RuntimeException("Error occured in Minecraft method.", e.getCause()); - } - } - - @Override - public Class getSpecificType() { - return Entity.class; - } - })); + int.class, BukkitConverters.getEntityConverter(world)); } /** @@ -348,100 +239,35 @@ public class PacketContainer implements Serializable { *

* This modifier will automatically marshall between the visible ProtocolLib ChunkPosition and the * internal Minecraft ChunkPosition. - * @return A modifier for ChunkPosition array fields. + * @return A modifier for ChunkPosition list fields. */ public StructureModifier> getPositionCollectionModifier() { - final EquivalentConverter converter = ChunkPosition.getConverter(); - // Convert to and from the ProtocolLib wrapper - final StructureModifier> modifier = structureModifier.withType( + return structureModifier.withType( Collection.class, - getIgnoreNull(new EquivalentConverter>() { - private Class collectionType; - - @SuppressWarnings("unchecked") - @Override - public List getSpecific(Object generic) { - if (generic instanceof Collection) { - List positions = new ArrayList(); - - // Save the type - collectionType = generic.getClass(); - - // Copy everything to a new list - for (Object item : (Collection) generic) { - ChunkPosition result = converter.getSpecific(item); - - if (item != null) - positions.add(result); - } - return positions; - } - - // Not valid - return null; - } - - @SuppressWarnings("unchecked") - @Override - public Object getGeneric(List specific) { - // Just go by the first field - if (collectionType == null) { - collectionType = structureModifier.withType(Collection.class).getFields().get(0).getType(); - } - - Collection newContainer = (Collection) DefaultInstances.DEFAULT.getDefault(collectionType); - - // Convert each object - for (ChunkPosition position : specific) { - Object converted = converter.getGeneric(position); - - if (position == null) - newContainer.add(null); - else if (converted != null) - newContainer.add(converted); - } - return newContainer; - } - - @SuppressWarnings("unchecked") - @Override - public Class> getSpecificType() { - // Damn you Java - Class dummy = List.class; - return (Class>) dummy; - } - } - )); - - return modifier; + BukkitConverters.getListConverter( + net.minecraft.server.ChunkPosition.class, + ChunkPosition.getConverter()) + ); } - private EquivalentConverter getIgnoreNull(final EquivalentConverter delegate) { - // Automatically wrap all parameters to the delegate with a NULL check - return new EquivalentConverter() { - public Object getGeneric(TType specific) { - if (specific != null) - return delegate.getGeneric(specific); - else - return null; - } - - @Override - public TType getSpecific(Object generic) { - if (generic != null) - return delegate.getSpecific(generic); - else - return null; - } - - @Override - public Class getSpecificType() { - return delegate.getSpecificType(); - } - }; + /** + * Retrieves a read/write structure for collections of watchable objects. + *

+ * This modifier will automatically marshall between the visible WrappedWatchableObject and the + * internal Minecraft WatchableObject. + * @return A modifier for watchable object list fields. + */ + public StructureModifier> getWatchableCollectionModifier() { + // Convert to and from the ProtocolLib wrapper + return structureModifier.withType( + Collection.class, + BukkitConverters.getListConverter( + net.minecraft.server.WatchableObject.class, + BukkitConverters.getWatchableObjectConverter()) + ); } - + /** * Retrieves the ID of this packet. * @return Packet ID. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/EquivalentConverter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/EquivalentConverter.java index a03b5508..63733abe 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/EquivalentConverter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/EquivalentConverter.java @@ -24,8 +24,25 @@ package com.comphenix.protocol.reflect; * @param The specific type. */ public interface EquivalentConverter { + /** + * Retrieve a copy of the specific type using an instance of the generic type. + * @param generic - the generic type. + * @return The new specific type. + */ public TType getSpecific(Object generic); - public Object getGeneric(TType specific); + + /** + * Retrieve a copy of the generic type from a specific type. + * @param genericType - class or super class of the generic type. + * @param specific - the specific type we need to copy. + * @return A copy of the specific type. + */ + public Object getGeneric(Class genericType, TType specific); + + /** + * Due to type erasion, we need to explicitly keep a reference to the specific type. + * @return The specific type. + */ public Class getSpecificType(); } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java index 31715877..556af330 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java @@ -256,7 +256,7 @@ public class StructureModifier { throw new IllegalStateException("Cannot write to a NULL target."); // Use the converter, if it exists - Object obj = needConversion() ? converter.getGeneric(value) : value; + Object obj = needConversion() ? converter.getGeneric(getFieldType(fieldIndex), value) : value; try { FieldUtils.writeField(data.get(fieldIndex), target, obj, true); @@ -268,6 +268,15 @@ public class StructureModifier { return this; } + /** + * Retrieve the type of a specified field. + * @param index - the index. + * @return The type of the given field. + */ + protected Class getFieldType(int index) { + return data.get(index).getType(); + } + /** * Whether or not we should use the converter instance. * @return TRUE if we should, FALSE otherwise. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/CompiledStructureModifier.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/CompiledStructureModifier.java index 8641a6a9..45bca0f9 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/CompiledStructureModifier.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/CompiledStructureModifier.java @@ -108,7 +108,7 @@ public abstract class CompiledStructureModifier extends StructureModifie @Override public StructureModifier write(int index, Object value) throws FieldAccessException { if (converter != null) - value = converter.getGeneric((TField) value); + value = converter.getGeneric(getFieldType(index), (TField) value); return writeGenerated(index, value); } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java new file mode 100644 index 00000000..ee7faf9f --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java @@ -0,0 +1,307 @@ +package com.comphenix.protocol.wrappers; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import net.minecraft.server.DataWatcher; +import net.minecraft.server.WatchableObject; + +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.WorldType; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import com.comphenix.protocol.reflect.EquivalentConverter; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.instances.DefaultInstances; + +/** + * Contains several useful equivalent converters for normal Bukkit types. + * + * @author Kristian + */ +public class BukkitConverters { + // Check whether or not certain classes exists + private static boolean hasWorldType = false; + + // The getEntity method + private static Method getEntity; + + static { + try { + Class.forName("net.minecraft.server.WorldType"); + hasWorldType = true; + } catch (ClassNotFoundException e) { + } + } + + public static EquivalentConverter> getListConverter(final Class genericItemType, final EquivalentConverter itemConverter) { + // Convert to and from the wrapper + return getIgnoreNull(new EquivalentConverter>() { + @SuppressWarnings("unchecked") + @Override + public List getSpecific(Object generic) { + if (generic instanceof Collection) { + List items = new ArrayList(); + + // Copy everything to a new list + for (Object item : (Collection) generic) { + T result = itemConverter.getSpecific(item); + + if (item != null) + items.add(result); + } + return items; + } + + // Not valid + return null; + } + + @SuppressWarnings("unchecked") + @Override + public Object getGeneric(Class genericType, List specific) { + Collection newContainer = (Collection) DefaultInstances.DEFAULT.getDefault(genericType); + + // Convert each object + for (T position : specific) { + Object converted = itemConverter.getGeneric(genericItemType, position); + + if (position == null) + newContainer.add(null); + else if (converted != null) + newContainer.add(converted); + } + return newContainer; + } + + @SuppressWarnings("unchecked") + @Override + public Class> getSpecificType() { + // Damn you Java + Class dummy = List.class; + return (Class>) dummy; + } + } + ); + } + + /** + * Retrieve a converter for watchable objects and the respective wrapper. + * @return A watchable object converter. + */ + public static EquivalentConverter getWatchableObjectConverter() { + return getIgnoreNull(new EquivalentConverter() { + @Override + public Object getGeneric(Class genericType, WrappedWatchableObject specific) { + return specific.getHandle(); + } + + public WrappedWatchableObject getSpecific(Object generic) { + if (generic instanceof WatchableObject) + return new WrappedWatchableObject((WatchableObject) generic); + else if (generic instanceof WrappedWatchableObject) + return (WrappedWatchableObject) generic; + else + throw new IllegalArgumentException("Unrecognized type " + generic.getClass()); + }; + + @Override + public Class getSpecificType() { + return WrappedWatchableObject.class; + } + }); + } + + /** + * Retrieve a converter for the NMS DataWatcher class and our wrapper. + * @return A DataWatcher converter. + */ + public static EquivalentConverter getDataWatcherConverter() { + return getIgnoreNull(new EquivalentConverter() { + @Override + public Object getGeneric(Class genericType, WrappedDataWatcher specific) { + return specific.getHandle(); + } + + @Override + public WrappedDataWatcher getSpecific(Object generic) { + if (generic instanceof DataWatcher) + return new WrappedDataWatcher((DataWatcher) generic); + else if (generic instanceof WrappedDataWatcher) + return (WrappedDataWatcher) generic; + else + throw new IllegalArgumentException("Unrecognized type " + generic.getClass()); + } + + @Override + public Class getSpecificType() { + return WrappedDataWatcher.class; + } + }); + } + + /** + * Retrieve a converter for Bukkit's world type enum and the NMS equivalent. + * @return A world type enum converter. + */ + public static EquivalentConverter getWorldTypeConverter() { + // Check that we can actually use this converter + if (!hasWorldType) + return null; + + return getIgnoreNull(new EquivalentConverter() { + @Override + public Object getGeneric(Class genericType, WorldType specific) { + return net.minecraft.server.WorldType.getType(specific.getName()); + } + + @Override + public WorldType getSpecific(Object generic) { + net.minecraft.server.WorldType type = (net.minecraft.server.WorldType) generic; + return WorldType.getByName(type.name()); + } + + @Override + public Class getSpecificType() { + return WorldType.class; + } + }); + } + + /** + * Retrieve a converter for NMS entities and Bukkit entities. + * @param world - the current world. + * @return A converter between the underlying NMS entity and Bukkit's wrapper. + */ + public static EquivalentConverter getEntityConverter(World world) { + final Object worldServer = ((CraftWorld) world).getHandle(); + final Class nmsEntityClass = net.minecraft.server.Entity.class; + + if (getEntity == null) + getEntity = FuzzyReflection.fromObject(worldServer).getMethodByParameters( + "getEntity", nmsEntityClass, new Class[] { int.class }); + + return getIgnoreNull(new EquivalentConverter() { + + @Override + public Object getGeneric(Class genericType, Entity specific) { + // Simple enough + return specific.getEntityId(); + } + + @Override + public Entity getSpecific(Object generic) { + try { + net.minecraft.server.Entity nmsEntity = (net.minecraft.server.Entity) + getEntity.invoke(worldServer, generic); + Integer id = (Integer) generic; + + // Attempt to get the Bukkit entity + if (nmsEntity != null) { + return nmsEntity.getBukkitEntity(); + } else { + Server server = Bukkit.getServer(); + + // Maybe it's a player that has just logged in? Try a search + if (server != null) { + for (Player player : server.getOnlinePlayers()) { + if (player.getEntityId() == id) { + return player; + } + } + } + + return null; + } + + } catch (IllegalArgumentException e) { + throw new RuntimeException("Incorrect arguments detected.", e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Cannot read field due to a security limitation.", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Error occured in Minecraft method.", e.getCause()); + } + } + + @Override + public Class getSpecificType() { + return Entity.class; + } + }); + } + + /** + * Retrieve the converter used to convert NMS ItemStacks to Bukkit's ItemStack. + * @return Item stack converter. + */ + public static EquivalentConverter getItemStackConverter() { + return getIgnoreNull(new EquivalentConverter() { + public Object getGeneric(Class genericType, ItemStack specific) { + return toStackNMS(specific); + } + + @Override + public ItemStack getSpecific(Object generic) { + return new CraftItemStack((net.minecraft.server.ItemStack) generic); + } + + @Override + public Class getSpecificType() { + return ItemStack.class; + } + }); + } + + /** + * Convert an item stack to the NMS equivalent. + * @param stack - Bukkit stack to convert. + * @return A bukkit stack. + */ + private static net.minecraft.server.ItemStack toStackNMS(ItemStack stack) { + // We must be prepared for an object that simply implements ItemStcak + if (stack instanceof CraftItemStack) { + return ((CraftItemStack) stack).getHandle(); + } else { + return (new CraftItemStack(stack)).getHandle(); + } + } + + /** + * Wraps a given equivalent converter in NULL checks, ensuring that such values are ignored. + * @param delegate - the underlying equivalent converter. + * @return A equivalent converter that ignores NULL values. + */ + public static EquivalentConverter getIgnoreNull(final EquivalentConverter delegate) { + // Automatically wrap all parameters to the delegate with a NULL check + return new EquivalentConverter() { + public Object getGeneric(Class genericType, TType specific) { + if (specific != null) + return delegate.getGeneric(genericType, specific); + else + return null; + } + + @Override + public TType getSpecific(Object generic) { + if (generic != null) + return delegate.getSpecific(generic); + else + return null; + } + + @Override + public Class getSpecificType() { + return delegate.getSpecificType(); + } + }; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/ChunkPosition.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/ChunkPosition.java index 7cbd5840..e1066fbb 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/ChunkPosition.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/ChunkPosition.java @@ -8,11 +8,17 @@ import com.comphenix.protocol.reflect.StructureModifier; import com.google.common.base.Objects; /** - * Wraps a immutable net.minecraft.server.ChunkPosition, which represents a integer 3D vector. + * Copies a immutable net.minecraft.server.ChunkPosition, which represents a integer 3D vector. * * @author Kristian */ public class ChunkPosition { + + /** + * Represents the null (0, 0, 0) origin. + */ + public static ChunkPosition ORIGIN = new ChunkPosition(0, 0, 0); + // Use protected members, like Bukkit protected final int x; protected final int y; @@ -74,14 +80,56 @@ public class ChunkPosition { return z; } + /** + * Adds the current position and a given position together, producing a result position. + * @param other - the other position. + * @return The new result position. + */ + public ChunkPosition add(ChunkPosition other) { + if (other == null) + throw new IllegalArgumentException("other cannot be NULL"); + return new ChunkPosition(x + other.x, y + other.y, z + other.z); + } + + /** + * Adds the current position and a given position together, producing a result position. + * @param other - the other position. + * @return The new result position. + */ + public ChunkPosition subtract(ChunkPosition other) { + if (other == null) + throw new IllegalArgumentException("other cannot be NULL"); + return new ChunkPosition(x - other.x, y - other.y, z - other.z); + } + + /** + * Multiply each dimension in the current position by the given factor. + * @param factor - multiplier. + * @return The new result. + */ + public ChunkPosition multiply(int factor) { + return new ChunkPosition(x * factor, y * factor, z * factor); + } + + /** + * Divide each dimension in the current position by the given divisor. + * @param divisor - the divisor. + * @return The new result. + */ + public ChunkPosition divide(int divisor) { + if (divisor == 0) + throw new IllegalArgumentException("Cannot divide by null."); + return new ChunkPosition(x / divisor, y / divisor, z / divisor); + } + /** * Used to convert between NMS ChunkPosition and the wrapper instance. - * @return + * @return A new converter. */ public static EquivalentConverter getConverter() { return new EquivalentConverter() { @Override - public Object getGeneric(ChunkPosition specific) { + public Object getGeneric(Class genericType, ChunkPosition specific) { return new net.minecraft.server.ChunkPosition(specific.x, specific.z, specific.z); } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedChunkCoordinate.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedChunkCoordinate.java new file mode 100644 index 00000000..27926db2 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedChunkCoordinate.java @@ -0,0 +1,105 @@ +package com.comphenix.protocol.wrappers; + +import com.google.common.base.Objects; + +import net.minecraft.server.ChunkCoordinates; + +/** + * Allows access to a chunk coordinate. + * + * @author Kristian + */ +public class WrappedChunkCoordinate implements Comparable { + + /** + * If TRUE, NULLs should be put before non-null instances of this class. + */ + private static final boolean LARGER_THAN_NULL = true; + + protected ChunkCoordinates handle; + + public WrappedChunkCoordinate(ChunkCoordinates handle) { + if (handle == null) + throw new IllegalArgumentException("handle cannot be NULL"); + this.handle = handle; + } + + public ChunkCoordinates getHandle() { + return handle; + } + + /** + * Retrieve the x coordinate of the underlying coordiate. + * @return The x coordinate. + */ + public int getX() { + return handle.x; + } + + /** + * Set the x coordinate of the underlying coordiate. + * @param newX - the new x coordinate. + */ + public void setX(int newX) { + handle.x = newX; + } + + /** + * Retrieve the y coordinate of the underlying coordiate. + * @return The y coordinate. + */ + public int getY() { + return handle.y; + } + + /** + * Set the y coordinate of the underlying coordiate. + * @param newY - the new y coordinate. + */ + public void setY(int newY) { + handle.y = newY; + } + + /** + * Retrieve the z coordinate of the underlying coordiate. + * @return The z coordinate. + */ + public int getZ() { + return handle.z; + } + + /** + * Set the z coordinate of the underlying coordiate. + * @param newZ - the new z coordinate. + */ + public void setZ(int newZ) { + handle.z = newZ; + } + + @Override + public int compareTo(WrappedChunkCoordinate other) { + // We'll handle NULL objects too, unlike ChunkCoordinates + if (other.handle == null) + return LARGER_THAN_NULL ? -1 : 1; + else + return handle.compareTo(other.handle); + } + + @Override + public boolean equals(Object other) { + if (other instanceof WrappedChunkCoordinate) { + WrappedChunkCoordinate wrapper = (WrappedChunkCoordinate) other; + return Objects.equal(handle, wrapper.handle); + } + + // It's tempting to handle the ChunkCoordinate case too, but then + // the equals() method won't be commutative, causing a.equals(b) to + // be different to b.equals(a). + return false; + } + + @Override + public int hashCode() { + return handle.hashCode(); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedDataWatcher.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedDataWatcher.java new file mode 100644 index 00000000..5267b4dd --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedDataWatcher.java @@ -0,0 +1,434 @@ +package com.comphenix.protocol.wrappers; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.bukkit.inventory.ItemStack; + +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.google.common.base.Objects; + +import net.minecraft.server.ChunkCoordinates; +import net.minecraft.server.DataWatcher; +import net.minecraft.server.WatchableObject; + +/** + * Wraps a DataWatcher that is used to transmit arbitrary key-value pairs with a given entity. + * + * @author Kristian + */ +public class WrappedDataWatcher { + + /** + * Used to assign integer IDs to given types. + */ + private static Map, Integer> typeMap; + + // Fields + private static Field valueMapField; + private static Field readWriteLockField; + + // Methods + private static Method createKeyValueMethod; + private static Method updateKeyValueMethod; + private static Method getKeyValueMethod; + + /** + * Whether or not this class has already been initialized. + */ + private static boolean hasInitialized; + + // The underlying DataWatcher we're modifying + protected DataWatcher handle; + + // Lock + private ReadWriteLock readWriteLock; + + // Map of watchable objects + private Map watchableObjects; + + /** + * Initialize a new data watcher. + * @throws FieldAccessException If we're unable to wrap a DataWatcher. + */ + public WrappedDataWatcher() { + // Just create a new watcher + this(new DataWatcher()); + } + + /** + * Create a wrapper for a given data watcher. + * @param dataWatcher - the data watcher to wrap. + * @throws FieldAccessException If we're unable to wrap a DataWatcher. + */ + public WrappedDataWatcher(DataWatcher handle) { + this.handle = handle; + + try { + initialize(); + } catch (FieldAccessException e) { + throw new RuntimeException("Cannot initialize wrapper.", e); + } + } + + /** + * Retrieves the underlying data watcher. + * @return The underlying data watcher. + */ + public DataWatcher getHandle() { + return handle; + } + + /** + * Retrieve the ID of a given type, if it's allowed to be watched. + * @return The ID, or NULL if it cannot be watched. + * @throws FieldAccessException If we cannot initialize the reflection machinery. + */ + public static Integer getTypeID(Class clazz) throws FieldAccessException { + initialize(); + + return typeMap.get(clazz); + } + + /** + * Retrieve the type of a given ID, if it's allowed to be watched. + * @return The type using a given ID, or NULL if it cannot be watched. + * @throws FieldAccessException If we cannot initialize the reflection machinery. + */ + public static Class getTypeClass(int id) throws FieldAccessException { + initialize(); + + for (Map.Entry, Integer> entry : typeMap.entrySet()) { + if (Objects.equal(entry.getValue(), id)) { + return entry.getKey(); + } + } + + // Unknown class type + return null; + } + + /** + * Get a watched byte. + * @param index - index of the watched byte. + * @return The watched byte, or NULL if this value doesn't exist. + * @throws FieldAccessException Cannot read underlying field. + */ + public Byte getByte(int index) throws FieldAccessException { + return (Byte) getObjectRaw(index); + } + + /** + * Get a watched short. + * @param index - index of the watched short. + * @return The watched short, or NULL if this value doesn't exist. + * @throws FieldAccessException Cannot read underlying field. + */ + public Short getShort(int index) throws FieldAccessException { + return (Short) getObjectRaw(index); + } + + /** + * Get a watched integer. + * @param index - index of the watched integer. + * @return The watched integer, or NULL if this value doesn't exist. + * @throws FieldAccessException Cannot read underlying field. + */ + public Integer getInteger(int index) throws FieldAccessException { + return (Integer) getObjectRaw(index); + } + + /** + * Get a watched float. + * @param index - index of the watched float. + * @return The watched float, or NULL if this value doesn't exist. + * @throws FieldAccessException Cannot read underlying field. + */ + public Float getFloat(int index) throws FieldAccessException { + return (Float) getObjectRaw(index); + } + + /** + * Get a watched string. + * @param index - index of the watched string. + * @return The watched string, or NULL if this value doesn't exist. + * @throws FieldAccessException Cannot read underlying field. + */ + public String getString(int index) throws FieldAccessException { + return (String) getObjectRaw(index); + } + + /** + * Get a watched string. + * @param index - index of the watched string. + * @return The watched string, or NULL if this value doesn't exist. + * @throws FieldAccessException Cannot read underlying field. + */ + public ItemStack getItemStack(int index) throws FieldAccessException { + return (ItemStack) getObject(index); + } + + /** + * Get a watched string. + * @param index - index of the watched string. + * @return The watched string, or NULL if this value doesn't exist. + * @throws FieldAccessException Cannot read underlying field. + */ + public WrappedChunkCoordinate getChunkCoordinate(int index) throws FieldAccessException { + return (WrappedChunkCoordinate) getObject(index); + } + + /** + * Retrieve a watchable object by index. + * @param index - index of the object to retrieve. + * @return The watched object. + * @throws FieldAccessException Cannot read underlying field. + */ + public Object getObject(int index) throws FieldAccessException { + Object result = getObjectRaw(index); + + // Handle the special cases too + if (result instanceof net.minecraft.server.ItemStack) { + return BukkitConverters.getItemStackConverter().getSpecific(result); + } else if (result instanceof ChunkCoordinates) { + return new WrappedChunkCoordinate((ChunkCoordinates) result); + } else { + return result; + } + } + + /** + * Retrieve a watchable object by index. + * @param index - index of the object to retrieve. + * @return The watched object. + * @throws FieldAccessException Cannot read underlying field. + */ + private Object getObjectRaw(int index) throws FieldAccessException { + // The get method will take care of concurrency + WatchableObject watchable = getWatchedObject(index); + + if (watchable != null) { + return new WrappedWatchableObject(watchable).getValue(); + } else { + return null; + } + } + + /** + * Retrieve a copy of every index associated with a watched object. + * @return Every watched object index. + * @throws FieldAccessException If we're unable to read the underlying object. + */ + public Set indexSet() throws FieldAccessException { + try { + getReadWriteLock().readLock().lock(); + return new HashSet(getWatchableObjectMap().keySet()); + + } finally { + getReadWriteLock().readLock().unlock(); + } + } + + /** + * Retrieve the number of watched objects. + * @return Watched object count. + * @throws FieldAccessException If we're unable to read the underlying object. + */ + public int size() throws FieldAccessException { + try { + getReadWriteLock().readLock().lock(); + return getWatchableObjectMap().size(); + + } finally { + getReadWriteLock().readLock().unlock(); + } + } + + /** + * Set a watched byte. + * @param index - index of the watched byte. + * @param newValue - the new watched value. + * @throws FieldAccessException Cannot read underlying field. + */ + public void setObject(int index, Object newValue) throws FieldAccessException { + setObject(index, newValue, true); + } + + /** + * Set a watched byte. + * @param index - index of the watched byte. + * @param newValue - the new watched value. + * @param update - whether or not to refresh every listening clients. + * @throws FieldAccessException Cannot read underlying field. + */ + public void setObject(int index, Object newValue, boolean update) throws FieldAccessException { + // Convert special cases + if (newValue instanceof WrappedChunkCoordinate) + newValue = ((WrappedChunkCoordinate) newValue).getHandle(); + if (newValue instanceof ItemStack) + newValue = BukkitConverters.getItemStackConverter().getGeneric( + net.minecraft.server.ItemStack.class, (ItemStack) newValue); + + // Next, set the object + setObjectRaw(index, newValue, update); + } + + /** + * Set a watchable object by index. + * @param index - index of the object to retrieve. + * @param newValue - the new watched value. + * @param update - whether or not to refresh every listening clients. + * @return The watched object. + * @throws FieldAccessException Cannot read underlying field. + */ + private void setObjectRaw(int index, Object newValue, boolean update) throws FieldAccessException { + WatchableObject watchable; + + try { + // Aquire write lock + getReadWriteLock().writeLock().lock(); + watchable = getWatchedObject(index); + + if (watchable != null) { + new WrappedWatchableObject(watchable).setValue(newValue, update); + } + } finally { + getReadWriteLock().writeLock().unlock(); + } + } + + private WatchableObject getWatchedObject(int index) throws FieldAccessException { + // We use the get-method first and foremost + if (getKeyValueMethod != null) { + try { + return (WatchableObject) getKeyValueMethod.invoke(handle, index); + } catch (Exception e) { + throw new FieldAccessException("Cannot invoke get key method for index " + index, e); + } + } else { + try { + getReadWriteLock().readLock().lock(); + return (WatchableObject) getWatchableObjectMap().get(index); + + } finally { + getReadWriteLock().readLock().unlock(); + } + } + } + + /** + * Retrieve the current read write lock. + * @return Current read write lock. + * @throws FieldAccessException If we're unable to read the underlying field. + */ + protected ReadWriteLock getReadWriteLock() throws FieldAccessException { + try { + // Cache the read write lock + if (readWriteLock != null) + return readWriteLock; + else if (readWriteLockField != null) + return readWriteLock = (ReadWriteLock) FieldUtils.readField(readWriteLockField, handle, true); + else + return readWriteLock = new ReentrantReadWriteLock(); + } catch (IllegalAccessException e) { + throw new FieldAccessException("Unable to read lock field.", e); + } + } + + /** + * Retrieve the underlying map of key values that stores watchable objects. + * @return A map of watchable objects. + * @throws FieldAccessException If we don't have permission to perform reflection. + */ + @SuppressWarnings("unchecked") + protected Map getWatchableObjectMap() throws FieldAccessException { + if (watchableObjects == null) { + try { + watchableObjects = (Map) FieldUtils.readField(valueMapField, handle, true); + } catch (IllegalAccessException e) { + throw new FieldAccessException("Cannot read watchable object field.", e); + } + } + return watchableObjects; + } + + /** + * Invoked when a data watcher is first used. + */ + @SuppressWarnings("unchecked") + private static void initialize() throws FieldAccessException { + // This method should only be run once, even if an exception is thrown + if (!hasInitialized) + hasInitialized = true; + else + return; + + FuzzyReflection fuzzy = FuzzyReflection.fromClass(DataWatcher.class, true); + + for (Field lookup : fuzzy.getFieldListByType(Map.class)) { + if (Modifier.isStatic(lookup.getModifiers())) { + // This must be the type map + try { + typeMap = (Map, Integer>) FieldUtils.readStaticField(lookup, true); + } catch (IllegalAccessException e) { + throw new FieldAccessException("Cannot access type map field.", e); + } + + } else { + // If not, then we're probably dealing with the value map + valueMapField = lookup; + } + } + + try { + readWriteLockField = fuzzy.getFieldByType("readWriteLock", ReadWriteLock.class); + } catch (IllegalArgumentException e) { + // It's not a big deal + } + + initializeMethods(fuzzy); + } + + private static void initializeMethods(FuzzyReflection fuzzy) { + List candidates = fuzzy.getMethodListByParameters(Void.TYPE, + new Class[] { int.class, Object.class}); + + for (Method method : candidates) { + + if (!method.getName().startsWith("watch")) { + createKeyValueMethod = method; + } else { + updateKeyValueMethod = method; + } + } + + // Did we succeed? + if (updateKeyValueMethod == null || createKeyValueMethod == null) { + // Go by index instead + if (candidates.size() > 1) { + createKeyValueMethod = candidates.get(0); + updateKeyValueMethod = candidates.get(1); + } else { + throw new IllegalStateException("Unable to find create and update watchable object. Update ProtocolLib."); + } + } + + // Load the get-method + try { + getKeyValueMethod = fuzzy.getMethodByParameters( + "getWatchableObject", ".*WatchableObject", new String[] { int.class.getName() }); + getKeyValueMethod.setAccessible(true); + } catch (IllegalArgumentException e) { + // Use fallback method + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedWatchableObject.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedWatchableObject.java new file mode 100644 index 00000000..d9895f9a --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedWatchableObject.java @@ -0,0 +1,160 @@ +package com.comphenix.protocol.wrappers; + +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.StructureModifier; + +import net.minecraft.server.WatchableObject; + +/** + * Represents a watchable object. + * + * @author Kristian + */ +public class WrappedWatchableObject { + + // Whether or not the reflection machinery has been initialized + private static boolean hasInitialized; + + // The field containing the value itself + private static StructureModifier baseModifier; + + protected WatchableObject handle; + protected StructureModifier modifier; + + // Type of the stored value + private Class typeClass; + + public WrappedWatchableObject(WatchableObject handle) { + initialize(); + this.handle = handle; + this.modifier = baseModifier.withTarget(handle); + } + + /** + * Retrieves the underlying watchable object. + * @return The underlying watchable object. + */ + public WatchableObject getHandle() { + return handle; + } + + /** + * Initialize reflection machinery. + */ + private static void initialize() { + if (!hasInitialized) { + hasInitialized = true; + baseModifier = new StructureModifier(WatchableObject.class, null, false); + } + } + + /** + * Retrieve the correct super type of the current value. + * @return Super type. + * @throws FieldAccessException Unable to read values. + */ + public Class getValueType() throws FieldAccessException { + if (typeClass == null) { + typeClass = WrappedDataWatcher.getTypeClass(getTypeID()); + + if (typeClass == null) { + throw new IllegalStateException("Unrecognized data type: " + getTypeID()); + } + } + + return typeClass; + } + + /** + * Retrieve the index of this watchable object. This is used to identify a value. + * @return Object index. + * @throws FieldAccessException Reflection failed. + */ + public int getIndex() throws FieldAccessException { + return modifier.withType(int.class).read(1); + } + + /** + * Set the the index of this watchable object. + * @param index - the new object index. + * @throws FieldAccessException Reflection failed. + */ + public void setIndex(int index) throws FieldAccessException { + modifier.withType(int.class).write(1, index); + } + + /** + * Retrieve the type ID of a watchable object. + * @return Type ID that identifies the type of the value. + * @throws FieldAccessException Reflection failed. + */ + public int getTypeID() throws FieldAccessException { + return modifier.withType(int.class).read(0); + } + + /** + * Set the type ID of a watchable object. + * @param id - the new ID. + * @throws FieldAccessException Reflection failed. + */ + public void setTypeID(int id) throws FieldAccessException { + modifier.withType(int.class).write(0, id); + } + + /** + * Update the value field. + * @param newValue - new value. + * @throws FieldAccessException Unable to use reflection. + */ + public void setValue(Object newValue) throws FieldAccessException { + setValue(newValue, true); + } + + /** + * Update the value field. + * @param newValue - new value. + * @param updateClient - whether or not to update listening clients. + * @throws FieldAccessException Unable to use reflection. + */ + public void setValue(Object newValue, boolean updateClient) throws FieldAccessException { + // Verify a few quick things + if (newValue == null) + throw new IllegalArgumentException("Cannot watch a NULL value."); + if (!getValueType().isAssignableFrom(newValue.getClass())) + throw new IllegalArgumentException("Object " + newValue + " must be of type " + getValueType().getName()); + + // See if we should update the client to + if (updateClient) + setDirtyState(true); + + // Use the modifier to set the value + modifier.withType(Object.class).write(0, newValue); + } + + /** + * Read the value field. + * @return The watched value. + * @throws FieldAccessException Unable to use reflection. + */ + public Object getValue() throws FieldAccessException { + return modifier.withType(Object.class).read(0); + } + + /** + * Set whether or not the value must be synchronized with the client. + * @param dirty - TRUE if the value should be synchronized, FALSE otherwise. + * @throws FieldAccessException Unable to use reflection. + */ + public void setDirtyState(boolean dirty) throws FieldAccessException { + modifier.withType(boolean.class).write(0, dirty); + } + + /** + * Retrieve whether or not the value must be synchronized with the client. + * @return TRUE if the value should be synchronized, FALSE otherwise. + * @throws FieldAccessException Unable to use reflection. + */ + public boolean getDirtyState() throws FieldAccessException { + return modifier.withType(boolean.class).read(0); + } +}