diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index 8b869c38..f356e5e2 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -1628,6 +1628,14 @@ public class MinecraftReflection { } } + /** + * Retrieve the NMS tile entity class. + * @return The tile entity class. + */ + public static Class getTileEntityClass() { + return getMinecraftClass("TileEntity"); + } + /** * Retrieve the google.gson.Gson class used by Minecraft. * @return The GSON class. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtFactory.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtFactory.java index 596da076..10b52775 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtFactory.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtFactory.java @@ -32,6 +32,8 @@ import java.util.zip.GZIPOutputStream; import javax.annotation.Nonnull; import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; import org.bukkit.inventory.ItemStack; import com.comphenix.protocol.reflect.FieldAccessException; @@ -215,6 +217,35 @@ public class NbtFactory { } } + /** + * Retrieve the NBT tile entity that represents the given block. + * @param state - the block state. + * @return The NBT compound, or NULL if the state doesn't have a tile entity. + */ + public static NbtCompound readBlockState(Block block) { + BlockState state = block.getState(); + TileEntityAccessor accessor = TileEntityAccessor.getAccessor(state); + + return accessor != null ? accessor.readBlockState(state) : null; + } + + /** + * Write to the NBT tile entity in the given block. + * @param target - the target block. + * @param blockState - the new tile entity. + * @throws IllegalArgumentException If the block doesn't contain a tile entity. + */ + public static void writeBlockState(Block target, NbtCompound blockState) { + BlockState state = target.getState(); + TileEntityAccessor accessor = TileEntityAccessor.getAccessor(state); + + if (accessor != null) { + accessor.writeBlockState(state, blockState); + } else { + throw new IllegalArgumentException("Unable to find tile entity in " + target); + } + } + /** * Ensure that the given stack can store arbitrary NBT information. * @param stack - the stack to check. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/TileEntityAccessor.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/TileEntityAccessor.java new file mode 100644 index 00000000..1bfed734 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/TileEntityAccessor.java @@ -0,0 +1,191 @@ +package com.comphenix.protocol.wrappers.nbt; + +import java.io.IOException; +import java.util.concurrent.ConcurrentMap; + +import net.minecraft.server.v1_7_R3.NBTTagCompound; +import net.sf.cglib.asm.ClassReader; +import net.sf.cglib.asm.MethodVisitor; +import net.sf.cglib.asm.Opcodes; + +import org.bukkit.block.BlockState; + +import com.comphenix.protocol.reflect.accessors.Accessors; +import com.comphenix.protocol.reflect.accessors.FieldAccessor; +import com.comphenix.protocol.reflect.accessors.MethodAccessor; +import com.comphenix.protocol.reflect.compiler.EmptyClassVisitor; +import com.comphenix.protocol.reflect.compiler.EmptyMethodVisitor; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.google.common.collect.Maps; + +/** + * Manipulate tile entities. + * @author Kristian + */ +class TileEntityAccessor { + /** + * Token indicating that the given block state doesn't contany any tile entities. + */ + private static final TileEntityAccessor EMPTY_ACCESSOR = new TileEntityAccessor(); + + /** + * Cached field accessors - {@link #EMPTY_ACCESSOR} represents no valid tile entity. + */ + private static final ConcurrentMap, TileEntityAccessor> cachedAccessors = Maps.newConcurrentMap(); + + private FieldAccessor tileEntityField; + private MethodAccessor readCompound; + private MethodAccessor writeCompound; + + private TileEntityAccessor() { + // Do nothing + } + + /** + * Construct a new tile entity accessor. + * @param tileEntityField - the tile entity field. + * @param tileEntity - the tile entity. + * @throws IOException Cannot read tile entity. + */ + private TileEntityAccessor(FieldAccessor tileEntityField) { + if (tileEntityField != null) { + this.tileEntityField = tileEntityField; + + // Possible read/write methods + try { + findSerializationMethods(tileEntityField.getField().getType()); + } catch (IOException e) { + throw new RuntimeException("Cannot find read/write methods.", e); + } + } + } + + /** + * Find the read/write methods in TileEntity. + * @param tileEntityClass - the tile entity class. + * @param nbtCompoundClass - the compound clas. + * @throws IOException If we cannot find these methods. + */ + private void findSerializationMethods(final Class tileEntityClass) throws IOException { + final Class nbtCompoundClass = MinecraftReflection.getNBTCompoundClass(); + + final ClassReader reader = new ClassReader(tileEntityClass.getCanonicalName()); + final String tagCompoundName = getJarName(NBTTagCompound.class); + final String expectedDesc = "(L" + tagCompoundName + ";)V"; + + reader.accept(new EmptyClassVisitor() { + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + final String methodName = name; + + // Detect read/write calls to NBTTagCompound + if (expectedDesc.equals(desc)) { + return new EmptyMethodVisitor() { + private int readMethods; + private int writeMethods; + + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + // This must be a virtual call on NBTTagCompound that accepts a String + if (opcode == Opcodes.INVOKEVIRTUAL && + tagCompoundName.equals(owner) && + desc.startsWith("(Ljava/lang/String")) { + + // Is this a write call? + if (desc.endsWith(")V")) { + writeMethods++; + } else { + readMethods++; + } + } + } + + @Override + public void visitEnd() { + if (readMethods > writeMethods) { + readCompound = Accessors.getMethodAccessor(tileEntityClass, methodName, nbtCompoundClass); + } else if (writeMethods > readMethods) { + writeCompound = Accessors.getMethodAccessor(tileEntityClass, methodName, nbtCompoundClass); + } + super.visitEnd(); + } + }; + } + return null; + } + }, 0); + + // Ensure we found them + if (readCompound == null) + throw new RuntimeException("Unable to find read method in " + tileEntityClass); + if (writeCompound == null) + throw new RuntimeException("Unable to find write method in " + tileEntityClass); + } + + /** + * Retrieve the JAR name (slash instead of dots) of the given clas. + * @param clazz - the class. + * @return The JAR name. + */ + private static String getJarName(Class clazz) { + return clazz.getCanonicalName().replace('.', '/'); + } + + /** + * Read the NBT compound that represents a given tile entity. + * @param state - tile entity represented by a block state. + * @return The compound. + */ + public NbtCompound readBlockState(T state) { + NbtCompound output = NbtFactory.ofCompound(""); + Object tileEntity = tileEntityField.get(state); + + // Write the block state to the output compound + writeCompound.invoke(tileEntity, NbtFactory.fromBase(output).getHandle()); + return output; + } + + /** + * Write the NBT compound as a tile entity. + * @param state - target block state. + * @param compound - the compound. + */ + public void writeBlockState(T state, NbtCompound compound) { + Object tileEntity = tileEntityField.get(state); + + // Ensure the block state is set to the compound + readCompound.invoke(tileEntity, NbtFactory.fromBase(compound).getHandle()); + } + + /** + * Retrieve an accessor for the tile entity at a specific location. + * @param state - the block state. + * @return The accessor, or NULL if this block state doesn't contain any tile entities. + */ + @SuppressWarnings("unchecked") + public static TileEntityAccessor getAccessor(T state) { + Class craftBlockState = state.getClass(); + TileEntityAccessor accessor = cachedAccessors.get(craftBlockState); + + // Attempt to construct the accessor + if (accessor == null ) { + TileEntityAccessor created = null; + FieldAccessor field = null; + + try { + field = Accessors.getFieldAccessor(craftBlockState, MinecraftReflection.getTileEntityClass(), true); + } catch (Exception e) { + created = EMPTY_ACCESSOR; + } + if (field != null) { + created = new TileEntityAccessor(field); + } + accessor = cachedAccessors.putIfAbsent(craftBlockState, created); + + // We won the race + if (accessor == null) { + accessor = created; + } + } + return (TileEntityAccessor) (accessor != EMPTY_ACCESSOR ? accessor : null); + } +}