From 7fd4ec31723c6bebdb0152e19dc2fb6b51399910 Mon Sep 17 00:00:00 2001 From: Miklas <36079327+Etrayed@users.noreply.github.com> Date: Sun, 7 Aug 2022 00:52:42 +0200 Subject: [PATCH] Support for 1.18+ ClientboundLevelChunkWithLightPacket (#1592) --- .../protocol/events/AbstractStructure.java | 18 + .../injector/PrioritizedListener.java | 2 +- .../protocol/injector/StructureCache.java | 11 +- .../protocol/utility/MinecraftReflection.java | 26 + .../protocol/wrappers/BukkitConverters.java | 8 + .../protocol/wrappers/ComponentParser.java | 3 +- .../protocol/wrappers/EnumWrappers.java | 6 +- .../protocol/wrappers/MinecraftKey.java | 18 + .../wrappers/WrappedLevelChunkData.java | 465 ++++++++++++++++++ .../wrappers/WrappedLevelChunkDataTest.java | 147 ++++++ 10 files changed, 694 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/comphenix/protocol/wrappers/WrappedLevelChunkData.java create mode 100644 src/test/java/com/comphenix/protocol/wrappers/WrappedLevelChunkDataTest.java diff --git a/src/main/java/com/comphenix/protocol/events/AbstractStructure.java b/src/main/java/com/comphenix/protocol/events/AbstractStructure.java index 0c24d89e..79f68245 100644 --- a/src/main/java/com/comphenix/protocol/events/AbstractStructure.java +++ b/src/main/java/com/comphenix/protocol/events/AbstractStructure.java @@ -929,6 +929,24 @@ public abstract class AbstractStructure { BukkitConverters.getWrappedPublicKeyDataConverter()); } + /** + * Retrieve a read/write structure for LevelChunkPacketData in 1.18+ + * + * @return The Structure Modifier + */ + public StructureModifier getLevelChunkData() { + return structureModifier.withType(MinecraftReflection.getLevelChunkPacketDataClass(), BukkitConverters.getWrappedChunkDataConverter()); + } + + /** + * Retrieve a read/write structure for LightUpdatePacketData in 1.18+ + * + * @return The Structure Modifier + */ + public StructureModifier getLightUpdateData() { + return structureModifier.withType(MinecraftReflection.getLightUpdatePacketDataClass(), BukkitConverters.getWrappedLightDataConverter()); + } + /** * @return read/write structure for login encryption packets */ diff --git a/src/main/java/com/comphenix/protocol/injector/PrioritizedListener.java b/src/main/java/com/comphenix/protocol/injector/PrioritizedListener.java index 4f527ba3..4bc4c674 100644 --- a/src/main/java/com/comphenix/protocol/injector/PrioritizedListener.java +++ b/src/main/java/com/comphenix/protocol/injector/PrioritizedListener.java @@ -50,7 +50,7 @@ public class PrioritizedListener implements Comparable other = (PrioritizedListener) obj; return Objects.equal(listener, other.listener); } else { diff --git a/src/main/java/com/comphenix/protocol/injector/StructureCache.java b/src/main/java/com/comphenix/protocol/injector/StructureCache.java index deca2e70..f3fc37a8 100644 --- a/src/main/java/com/comphenix/protocol/injector/StructureCache.java +++ b/src/main/java/com/comphenix/protocol/injector/StructureCache.java @@ -65,9 +65,9 @@ public class StructureCache { return accessor.invoke(MinecraftReflection.getPacketDataSerializer(new ZeroBuffer())); } catch (Exception exception) { // try trick nms around as they want a non-null compound in the map_chunk packet constructor - ConstructorAccessor trickyDataSerializerAccessor = getTrickDataSerializerOrNull(); - if (trickyDataSerializerAccessor != null) { - return accessor.invoke(trickyDataSerializerAccessor.invoke(new ZeroBuffer())); + Object trickyDataSerializer = getTrickDataSerializerOrNull(); + if (trickyDataSerializer != null) { + return accessor.invoke(trickyDataSerializer); } // the tricks are over throw new IllegalArgumentException("Unable to create packet " + clazz, exception); @@ -127,7 +127,7 @@ public class StructureCache { * * @return an accessor to a constructor which creates a data serializer. */ - private static ConstructorAccessor getTrickDataSerializerOrNull() { + public static Object getTrickDataSerializerOrNull() { if (TRICKED_DATA_SERIALIZER == null && !TRICK_TRIED) { // ensure that we only try once to create the class TRICK_TRIED = true; @@ -152,6 +152,7 @@ public class StructureCache { // can happen if unsupported } } - return TRICKED_DATA_SERIALIZER; + + return TRICKED_DATA_SERIALIZER == null ? null : TRICKED_DATA_SERIALIZER.invoke(new ZeroBuffer()); } } diff --git a/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index b22e42b7..2ceb2d12 100644 --- a/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -21,6 +21,7 @@ import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -1594,4 +1595,29 @@ public final class MinecraftReflection { setMinecraftClass("MinecraftServer", params[0]); setMinecraftClass("PlayerList", params[1]); } + + public static Class getLevelChunkPacketDataClass() { + return getNullableNMS("network.protocol.game.ClientboundLevelChunkPacketData"); + } + + public static Class getLightUpdatePacketDataClass() { + return getNullableNMS("network.protocol.game.ClientboundLightUpdatePacketData"); + } + + public static Class getBlockEntityTypeClass() { + return getMinecraftClass("world.level.block.entity.BlockEntityType", "world.level.block.entity.TileEntityTypes", "TileEntityTypes"); + } + + public static Class getBlockEntityInfoClass() { + try { + return getMinecraftClass("BlockEntityInfo"); + } catch (RuntimeException expected) { + Class infoClass = (Class) ((ParameterizedType) FuzzyReflection.fromClass(getLevelChunkPacketDataClass(), + true).getFieldListByType(List.class).get(0).getGenericType()).getActualTypeArguments()[0]; + + setMinecraftClass("BlockEntityInfo", infoClass); + + return infoClass; + } + } } diff --git a/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java b/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java index ac21d9fc..6491ae58 100644 --- a/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java +++ b/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java @@ -610,6 +610,14 @@ public class BukkitConverters { return ignoreNull(handle(WrappedSaltedSignature::getHandle, WrappedSaltedSignature::new, WrappedSaltedSignature.class)); } + public static EquivalentConverter getWrappedChunkDataConverter() { + return ignoreNull(handle(WrappedLevelChunkData.ChunkData::getHandle, WrappedLevelChunkData.ChunkData::new, WrappedLevelChunkData.ChunkData.class)); + } + + public static EquivalentConverter getWrappedLightDataConverter() { + return ignoreNull(handle(WrappedLevelChunkData.LightData::getHandle, WrappedLevelChunkData.LightData::new, WrappedLevelChunkData.LightData.class)); + } + /** * Retrieve a converter for watchable objects and the respective wrapper. * @return A watchable object converter. diff --git a/src/main/java/com/comphenix/protocol/wrappers/ComponentParser.java b/src/main/java/com/comphenix/protocol/wrappers/ComponentParser.java index f6b8f924..97a57923 100644 --- a/src/main/java/com/comphenix/protocol/wrappers/ComponentParser.java +++ b/src/main/java/com/comphenix/protocol/wrappers/ComponentParser.java @@ -51,7 +51,8 @@ public class ComponentParser { // Should only be needed on 1.8. private static Object deserializeLegacy(Object gson, Class component, StringReader str) { try { - if(readerConstructor == null){ + if (readerConstructor == null) { + Class readerClass = Class.forName("org.bukkit.craftbukkit.libs.com.google.gson.stream.JsonReader"); readerConstructor = readerClass.getDeclaredConstructor(Reader.class); readerConstructor.setAccessible(true); diff --git a/src/main/java/com/comphenix/protocol/wrappers/EnumWrappers.java b/src/main/java/com/comphenix/protocol/wrappers/EnumWrappers.java index 67393899..745bf187 100644 --- a/src/main/java/com/comphenix/protocol/wrappers/EnumWrappers.java +++ b/src/main/java/com/comphenix/protocol/wrappers/EnumWrappers.java @@ -400,7 +400,7 @@ public abstract class EnumWrappers { * @return Wrapped {@link EntityPose} */ public static EntityPose fromNms(Object nms) { - if(POSE_CONVERTER == null) { + if (POSE_CONVERTER == null) { throw new IllegalStateException("EntityPose is only available in Minecraft version 1.13 +"); } return POSE_CONVERTER.getSpecific(nms); @@ -408,7 +408,7 @@ public abstract class EnumWrappers { /** @return net.minecraft.server.EntityPose enum equivalent to this wrapper enum */ public Object toNms() { - if(POSE_CONVERTER == null) { + if (POSE_CONVERTER == null) { throw new IllegalStateException("EntityPose is only available in Minecraft version 1.13 +"); } return POSE_CONVERTER.getGeneric(this); @@ -784,7 +784,7 @@ public abstract class EnumWrappers { * @return {@link EnumConverter} or null (if bellow 1.13 / nms EnumPose class cannot be found) */ public static EquivalentConverter getEntityPoseConverter() { - if(getEntityPoseClass() == null) return null; + if (getEntityPoseClass() == null) return null; return new EnumConverter<>(getEntityPoseClass(), EntityPose.class); } diff --git a/src/main/java/com/comphenix/protocol/wrappers/MinecraftKey.java b/src/main/java/com/comphenix/protocol/wrappers/MinecraftKey.java index 8ce6a57c..22a053e0 100644 --- a/src/main/java/com/comphenix/protocol/wrappers/MinecraftKey.java +++ b/src/main/java/com/comphenix/protocol/wrappers/MinecraftKey.java @@ -18,6 +18,7 @@ package com.comphenix.protocol.wrappers; import java.lang.reflect.Constructor; import java.util.Locale; +import java.util.Objects; import com.comphenix.protocol.reflect.EquivalentConverter; import com.comphenix.protocol.reflect.StructureModifier; @@ -112,6 +113,23 @@ public class MinecraftKey { return key.toUpperCase(Locale.ENGLISH).replace(".", "_"); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MinecraftKey that = (MinecraftKey) o; + return Objects.equals(prefix, that.prefix) && Objects.equals(key, that.key); + } + + @Override + public int hashCode() { + return Objects.hash(prefix, key); + } + private static Constructor constructor = null; public static EquivalentConverter getConverter() { diff --git a/src/main/java/com/comphenix/protocol/wrappers/WrappedLevelChunkData.java b/src/main/java/com/comphenix/protocol/wrappers/WrappedLevelChunkData.java new file mode 100644 index 00000000..e5f5f82f --- /dev/null +++ b/src/main/java/com/comphenix/protocol/wrappers/WrappedLevelChunkData.java @@ -0,0 +1,465 @@ +package com.comphenix.protocol.wrappers; + +import com.comphenix.protocol.injector.StructureCache; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.accessors.Accessors; +import com.comphenix.protocol.reflect.accessors.ConstructorAccessor; +import com.comphenix.protocol.reflect.accessors.FieldAccessor; +import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.utility.ZeroBuffer; +import com.comphenix.protocol.wrappers.nbt.NbtCompound; +import com.comphenix.protocol.wrappers.nbt.NbtFactory; +import com.google.common.collect.Lists; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; + +/** + * Wrapper classes for ClientboundLevelChunkWithLightPacket + * + * @author Etrayed + */ +public final class WrappedLevelChunkData { + + private WrappedLevelChunkData() { + } + + /** + * Wrapper for ClientboundLevelChunkPacketData + */ + public static final class ChunkData extends AbstractWrapper { + + private static final Class HANDLE_TYPE = MinecraftReflection.getLevelChunkPacketDataClass(); + + private static final ConstructorAccessor LEVEL_CHUNK_PACKET_DATA_CONSTRUCTOR; + + private static final FieldAccessor BLOCK_ENTITIES_DATA_ACCESSOR; + private static final FieldAccessor HEIGHTMAPS_ACCESSOR; + private static final FieldAccessor BUFFER_ACCESSOR; + + static { + FuzzyReflection reflection = FuzzyReflection.fromClass(HANDLE_TYPE, true); + + LEVEL_CHUNK_PACKET_DATA_CONSTRUCTOR = Accessors.getConstructorAccessor(HANDLE_TYPE, + MinecraftReflection.getPacketDataSerializerClass(), int.class, int.class); + BLOCK_ENTITIES_DATA_ACCESSOR = Accessors.getFieldAccessor(reflection.getField(FuzzyFieldContract.newBuilder() + .typeExact(List.class) + .build())); + HEIGHTMAPS_ACCESSOR = Accessors.getFieldAccessor(reflection.getField(FuzzyFieldContract.newBuilder() + .typeExact(MinecraftReflection.getNBTCompoundClass()) + .build())); + BUFFER_ACCESSOR = Accessors.getFieldAccessor(reflection.getField(FuzzyFieldContract.newBuilder().typeExact(byte[].class).build())); + } + + public ChunkData(Object handle) { + super(HANDLE_TYPE); + + setHandle(handle); + } + + /** + * The heightmap of this chunk. + * + * @return an NBT-Tag + */ + public NbtCompound getHeightmapsTag() { + return NbtFactory.fromNMSCompound(HEIGHTMAPS_ACCESSOR.get(handle)); + } + + /** + * Sets the heightmap tag of this chunk. + * + * @param heightmapsTag the new heightmaps tag. + */ + public void setHeightmapsTag(NbtCompound heightmapsTag) { + HEIGHTMAPS_ACCESSOR.set(handle, NbtFactory.fromBase(heightmapsTag).getHandle()); + } + + /** + * The actual structural data of this chunk as bytes. + * + * @return a byte array containing the chunks structural data. + */ + public byte[] getBuffer() { + return (byte[]) BUFFER_ACCESSOR.get(handle); + } + + /** + * Sets the structural data of this chunk. + * + * @param buffer the new buffer. + */ + public void setBuffer(byte[] buffer) { + BUFFER_ACCESSOR.set(handle, buffer); + } + + /** + * All block entities of this chunk. Supports removal and other edits. + * + * @return a mutable (remove only) list containing {@link BlockEntityInfo} + */ + public List getBlockEntityInfo() { + //noinspection StaticPseudoFunctionalStyleMethod + return Lists.transform((List) BLOCK_ENTITIES_DATA_ACCESSOR.get(handle), BlockEntityInfo::new); + } + + /** + * Sets the block entities of this chunk. Supports removal and other edits. + * + * @param blockEntityInfo the new list of block entities + */ + public void setBlockEntityInfo(List blockEntityInfo) { + List handleList = new ArrayList<>(blockEntityInfo.size()); + + for (BlockEntityInfo info : blockEntityInfo) { + //noinspection unchecked + handleList.add(info.getHandle()); + } + + BLOCK_ENTITIES_DATA_ACCESSOR.set(handle, handleList); + } + + /** + * Creates a new wrapper using predefined values. + * + * @param heightmapsTag the heightmaps tag + * @param buffer the buffer + * @param blockEntityInfo a list of wrapped block entities + * @return a newly created wrapper + */ + public static ChunkData fromValues(NbtCompound heightmapsTag, byte[] buffer, List blockEntityInfo) { + ChunkData data = new ChunkData(LEVEL_CHUNK_PACKET_DATA_CONSTRUCTOR.invoke(StructureCache.getTrickDataSerializerOrNull(), 0, 0)); + + data.setHeightmapsTag(heightmapsTag); + data.setBuffer(buffer); + data.setBlockEntityInfo(blockEntityInfo); + + return new ChunkData(data); + } + } + + /** + * Wrapper for ClientboundLightUpdatePacketData + */ + public static class LightData extends AbstractWrapper { + + private static final Class HANDLE_TYPE = MinecraftReflection.getLightUpdatePacketDataClass(); + + private static final ConstructorAccessor LIGHT_UPDATE_PACKET_DATA_CONSTRUCTOR; + + private static final FieldAccessor[] BIT_SET_ACCESSORS; + private static final FieldAccessor[] BYTE_ARRAY_LIST_ACCESSORS; + + private static final FieldAccessor TRUST_EDGES_ACCESSOR; + + static { + FuzzyReflection reflection = FuzzyReflection.fromClass(HANDLE_TYPE, true); + + LIGHT_UPDATE_PACKET_DATA_CONSTRUCTOR = Accessors.getConstructorAccessor(HANDLE_TYPE, + MinecraftReflection.getPacketDataSerializerClass(), int.class, int.class); + BIT_SET_ACCESSORS = Accessors.getFieldAccessorArray(HANDLE_TYPE, BitSet.class, true); + BYTE_ARRAY_LIST_ACCESSORS = Accessors.getFieldAccessorArray(HANDLE_TYPE, List.class, true); + TRUST_EDGES_ACCESSOR = Accessors.getFieldAccessor(reflection.getField(FuzzyFieldContract.newBuilder() + .typeExact(boolean.class) + .build())); + } + + public LightData(Object handle) { + super(HANDLE_TYPE); + + setHandle(handle); + } + + /** + * The sky light mask. + * + * @return a {@link BitSet} + */ + public BitSet getSkyYMask() { + return (BitSet) BIT_SET_ACCESSORS[0].get(handle); + } + + /** + * Sets the sky light mask + * + * @param skyYMask the new mask + */ + public void setSkyYMask(BitSet skyYMask) { + BIT_SET_ACCESSORS[0].set(handle, skyYMask); + } + + /** + * The block light mask. + * + * @return a {@link BitSet} + */ + public BitSet getBlockYMask() { + return (BitSet) BIT_SET_ACCESSORS[1].get(handle); + } + + /** + * Sets the block light mask + * + * @param blockYMask the new mask + */ + public void setBlockYMask(BitSet blockYMask) { + BIT_SET_ACCESSORS[1].set(handle, blockYMask); + } + + /** + * The empty sky light mask. + * + * @return a {@link BitSet} + */ + public BitSet getEmptySkyYMask() { + return (BitSet) BIT_SET_ACCESSORS[2].get(handle); + } + + /** + * Sets the empty sky light mask + * + * @param emptySkyYMask the new mask + */ + public void setEmptySkyYMask(BitSet emptySkyYMask) { + BIT_SET_ACCESSORS[2].set(handle, emptySkyYMask); + } + + /** + * The empty block light mask. + * + * @return a {@link BitSet} + */ + public BitSet getEmptyBlockYMask() { + return (BitSet) BIT_SET_ACCESSORS[3].get(handle); + } + + /** + * Sets the empty block light mask + * + * @param emptyBlockYMask the new mask + */ + public void setEmptyBlockYMask(BitSet emptyBlockYMask) { + BIT_SET_ACCESSORS[3].set(handle, emptyBlockYMask); + } + + /** + * A mutable list of sky light arrays. + * + * @return a mutable list of byte arrays. + */ + public List getSkyUpdates() { + //noinspection unchecked + return (List) BYTE_ARRAY_LIST_ACCESSORS[0].get(handle); + } + + /** + * A mutable list of block light arrays. + * + * @return a mutable list of byte arrays. + */ + public List getBlockUpdates() { + //noinspection unchecked + return (List) BYTE_ARRAY_LIST_ACCESSORS[1].get(handle); + } + + /** + * Whether edges can be trusted for light updates or not. + * + * @return {@code true} if edges can be trusted, {@code false} otherwise. + */ + public boolean isTrustEdges() { + return (boolean) TRUST_EDGES_ACCESSOR.get(handle); + } + + /** + * Sets whether edges can be trusted for light updates or not. + * + * @param trustEdges the new value + */ + public void setTrustEdges(boolean trustEdges) { + TRUST_EDGES_ACCESSOR.set(handle, trustEdges); + } + + public static LightData fromValues(BitSet skyYMask, BitSet blockYMask, BitSet emptySkyYMask, BitSet emptyBlockYMask, + List skyUpdates, List blockUpdates, boolean trustEdges) { + LightData data = new LightData(LIGHT_UPDATE_PACKET_DATA_CONSTRUCTOR.invoke(MinecraftReflection.getPacketDataSerializer(new ZeroBuffer()), 0, 0)); + + data.setTrustEdges(trustEdges); + data.setSkyYMask(skyYMask); + data.setBlockYMask(blockYMask); + data.setEmptySkyYMask(emptySkyYMask); + data.setEmptyBlockYMask(emptyBlockYMask); + data.getSkyUpdates().addAll(skyUpdates); + data.getBlockUpdates().addAll(blockUpdates); + + return data; + } + } + + /** + * Represents an immutable BlockEntityInfo in the MAP_CHUNK packet. + * + * @author Etrayed + */ + public static class BlockEntityInfo extends AbstractWrapper { + + private static final Class HANDLE_TYPE = MinecraftReflection.getBlockEntityInfoClass(); + private static final WrappedRegistry REGISTRY = WrappedRegistry.getRegistry(MinecraftReflection.getBlockEntityTypeClass()); + + private static final ConstructorAccessor BLOCK_ENTITY_INFO_CONSTRUCTOR; + + private static final FieldAccessor PACKED_XZ_ACCESSOR; + private static final FieldAccessor Y_ACCESSOR; + private static final FieldAccessor TYPE_ACCESSOR; + private static final FieldAccessor TAG_ACCESSOR; + + static { + FuzzyReflection reflection = FuzzyReflection.fromClass(HANDLE_TYPE, true); + List posFields = reflection.getFieldList(FuzzyFieldContract.newBuilder().typeExact(int.class).build()); + + BLOCK_ENTITY_INFO_CONSTRUCTOR = Accessors.getConstructorAccessor(HANDLE_TYPE, int.class, int.class, + MinecraftReflection.getBlockEntityTypeClass(), MinecraftReflection.getNBTCompoundClass()); + PACKED_XZ_ACCESSOR = Accessors.getFieldAccessor(posFields.get(0)); + Y_ACCESSOR = Accessors.getFieldAccessor(posFields.get(1)); + TYPE_ACCESSOR = Accessors.getFieldAccessor(reflection.getField(FuzzyFieldContract.newBuilder() + .typeExact(MinecraftReflection.getBlockEntityTypeClass()) + .build())); + TAG_ACCESSOR = Accessors.getFieldAccessor(reflection.getField(FuzzyFieldContract.newBuilder() + .typeExact(MinecraftReflection.getNBTCompoundClass()) + .build())); + } + + public BlockEntityInfo(Object handle) { + super(HANDLE_TYPE); + + setHandle(handle); + } + + /** + * The section-relative X-coordinate of the block entity. + * + * @return the unpacked X-coordinate. + */ + public int getSectionX() { + return (int) PACKED_XZ_ACCESSOR.get(handle) >> 4; + } + + /** + * Sets the section-relative X-coordinate of the block entity + * + * @param sectionX the section-relative x coordinate + */ + public void setSectionX(int sectionX) { + PACKED_XZ_ACCESSOR.set(handle, sectionX << 4 | getSectionZ()); + } + + /** + * The section-relative Z-coordinate of the block entity. + * + * @return the unpacked Z-coordinate. + */ + public int getSectionZ() { + return (int) PACKED_XZ_ACCESSOR.get(handle) & 0xF; + } + + /** + * Sets the section-relative Z-coordinate of the block entity + * + * @param sectionZ the section-relative z coordinate + */ + public void setSectionZ(int sectionZ) { + PACKED_XZ_ACCESSOR.set(handle, getSectionX() << 4 | sectionZ); + } + + /** + * The Y-coordinate of the block entity. + * + * @return the Y-coordinate. + */ + public int getY() { + return (int) Y_ACCESSOR.get(handle); + } + + /** + * Sets the Y-coordinate of the block entity. + * + * @param y the new y coordinate + */ + public void setY(int y) { + Y_ACCESSOR.set(handle, y); + } + + /** + * The registry key of the block entity type. + * + * @return the registry key. + */ + public MinecraftKey getTypeKey() { + return REGISTRY.getKey(TYPE_ACCESSOR.get(handle)); + } + + /** + * Sets the registry key of the block entity type + * + * @param typeKey the new block entity type key + */ + public void setTypeKey(MinecraftKey typeKey) { + TYPE_ACCESSOR.set(handle, REGISTRY.get(typeKey)); + } + + /** + * The NBT-Tag of this block entity containing additional information. (ex. text lines for a sign) + * + * @return the NBT-Tag or {@code null}. + */ + @Nullable + public NbtCompound getAdditionalData() { + Object tagHandle = TAG_ACCESSOR.get(handle); + + return tagHandle == null ? null : NbtFactory.fromNMSCompound(tagHandle); + } + + /** + * Edits the additional data specified for this block entity. + * + * @param additionalData the additional data for this block entity, can be {@code null} + */ + public void setAdditionalData(@Nullable NbtCompound additionalData) { + TAG_ACCESSOR.set(handle, additionalData == null ? null : NbtFactory.fromBase(additionalData).getHandle()); + } + + /** + * Creates a wrapper using raw values + * + * @param sectionX the section-relative X-coordinate of the block entity. + * @param sectionZ the section-relative Z-coordinate of the block entity. + * @param y the Y-coordinate of the block entity. + * @param typeKey the minecraft key of the block entity type. + */ + public static BlockEntityInfo fromValues(int sectionX, int sectionZ, int y, MinecraftKey typeKey) { + return fromValues(sectionX, sectionZ, y, typeKey, null); + } + + /** + * Creates a wrapper using raw values + * + * @param sectionX the section-relative X-coordinate of the block entity. + * @param sectionZ the section-relative Z-coordinate of the block entity. + * @param y the Y-coordinate of the block entity. + * @param typeKey the minecraft key of the block entity type. + * @param additionalData An NBT-Tag containing additional information. Can be {@code null}. + */ + public static BlockEntityInfo fromValues(int sectionX, int sectionZ, int y, MinecraftKey typeKey, @Nullable NbtCompound additionalData) { + return new BlockEntityInfo(BLOCK_ENTITY_INFO_CONSTRUCTOR.invoke( + sectionX << 4 | sectionZ, + y, + REGISTRY.get(typeKey), + additionalData == null ? null : NbtFactory.fromBase(additionalData).getHandle() + )); + } + } +} diff --git a/src/test/java/com/comphenix/protocol/wrappers/WrappedLevelChunkDataTest.java b/src/test/java/com/comphenix/protocol/wrappers/WrappedLevelChunkDataTest.java new file mode 100644 index 00000000..33a097b4 --- /dev/null +++ b/src/test/java/com/comphenix/protocol/wrappers/WrappedLevelChunkDataTest.java @@ -0,0 +1,147 @@ +package com.comphenix.protocol.wrappers; + +import com.comphenix.protocol.BukkitInitialization; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.accessors.Accessors; +import com.comphenix.protocol.reflect.accessors.FieldAccessor; +import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract; +import com.comphenix.protocol.utility.MinecraftReflection; +import net.minecraft.core.BlockPosition; +import net.minecraft.core.IRegistry; +import net.minecraft.core.IRegistryCustom; +import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; +import net.minecraft.resources.MinecraftKey; +import net.minecraft.server.level.WorldServer; +import net.minecraft.world.level.BlockAccessAir; +import net.minecraft.world.level.ChunkCoordIntPair; +import net.minecraft.world.level.block.entity.TileEntityBell; +import net.minecraft.world.level.block.state.IBlockData; +import net.minecraft.world.level.chunk.Chunk; +import net.minecraft.world.level.chunk.ILightAccess; +import net.minecraft.world.level.lighting.LightEngine; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.v1_19_R1.CraftWorld; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.internal.matchers.apachecommons.ReflectionEquals; + +import java.lang.reflect.Field; +import java.util.BitSet; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Etrayed + */ +public class WrappedLevelChunkDataTest { + + @BeforeAll + public static void initializeBukkitAndNMS() { + BukkitInitialization.initializeAll(); + + ILightAccess access = mock(ILightAccess.class); + + when(access.c(0, 0)).thenReturn(BlockAccessAir.a); + when(access.p()).thenReturn(BlockAccessAir.a); + + LightEngine engine = new LightEngine(access, true, true); + WorldServer nmsWorld = ((CraftWorld) Bukkit.getWorlds().get(0)).getHandle(); + + when(nmsWorld.s()).thenReturn(IRegistryCustom.d.get()); + // TODO: somehow find a way to always call the real code for all LevelHeightAccessor implementations + when(nmsWorld.v_()).thenReturn(256); + when(nmsWorld.ai()).thenReturn(16); // LevelHeightAccessor is mocked and therefore always returns 0, there are further methods like this which might cause errors in the future + + when(nmsWorld.l_()).thenReturn(engine); + } + + private final WorldServer nmsWorld; + + private final Chunk chunk; + + public WrappedLevelChunkDataTest() { + this.nmsWorld = ((CraftWorld) Bukkit.getWorlds().get(0)).getHandle(); + this.chunk = new Chunk(nmsWorld, new ChunkCoordIntPair(5, 5)); + + IBlockData bellData = IRegistry.V.a(new MinecraftKey("bell")).m(); + + chunk.b(0).a(0, 0, 0, bellData); + chunk.a(new TileEntityBell(BlockPosition.b, bellData)); + } + + @Test + public void testChunkData() { + ClientboundLevelChunkWithLightPacket packet = new ClientboundLevelChunkWithLightPacket(chunk, + nmsWorld.l_(), null, null, false); + PacketContainer container = PacketContainer.fromPacket(packet); + Object rawInstance = container.getSpecificModifier(MinecraftReflection.getLevelChunkPacketDataClass()).read(0); + Object virtualInstance = BukkitConverters.getWrappedChunkDataConverter().getGeneric(container.getLevelChunkData().read(0)); + + assertTrue(new ReflectionEquals(rawInstance, FuzzyReflection.fromClass(rawInstance.getClass(), true) + .getFieldListByType(List.class).get(0).getName()) + .matches(virtualInstance)); + assertTrue(blockEntitiesEqual(rawInstance, virtualInstance)); + } + + private boolean blockEntitiesEqual(Object raw, Object virtual) { + if (raw == null && virtual == null) { + return true; + } + + if (raw == null || virtual == null) { + return false; + } + + FieldAccessor accessor = Accessors.getFieldAccessor(FuzzyReflection.fromClass(raw.getClass(), true) + .getField(FuzzyFieldContract.newBuilder().typeExact(List.class).build())); + List rawList = (List) accessor.get(raw); + List virtualList = (List) accessor.get(virtual); + + if (rawList.size() != virtualList.size()) { + return false; + } + + for (int i = 0; i < rawList.size(); i++) { + if (!EqualsBuilder.reflectionEquals(rawList.get(0), virtualList.get(0))) { + return false; + } + } + + return true; + } + + @Test + public void testLightData() { + ClientboundLevelChunkWithLightPacket packet = new ClientboundLevelChunkWithLightPacket(chunk, + nmsWorld.l_(), null, null, false); + PacketContainer container = PacketContainer.fromPacket(packet); + + randomizeBitSets(container.getSpecificModifier(MinecraftReflection.getLightUpdatePacketDataClass()).read(0)); + + assertTrue(new ReflectionEquals(container.getSpecificModifier(MinecraftReflection.getLightUpdatePacketDataClass()).read(0)) + .matches(BukkitConverters.getWrappedLightDataConverter().getGeneric(container.getLightUpdateData().read(0)))); + } + + private void randomizeBitSets(Object lightData) { + for (Field field : FuzzyReflection.fromClass(MinecraftReflection.getLightUpdatePacketDataClass(), true).getFieldListByType(BitSet.class)) { + try { + field.setAccessible(true); + + randomizeBitSet((BitSet) field.get(lightData)); + } catch (IllegalAccessException ignored) {} + } + } + + private void randomizeBitSet(BitSet bitSet) { + for (int i = 0; i < bitSet.size(); i++) { + if (Math.random() >= 0.5D) { + bitSet.set(i); + } + } + } +}