From 97323b2cebb048f3cdb01443dd621aab82c8d351 Mon Sep 17 00:00:00 2001 From: Pasqual Koschmieder Date: Fri, 22 Sep 2023 18:59:21 +0200 Subject: [PATCH] fix custom payload --- .../protocol/events/AbstractStructure.java | 10 + .../wrappers/CustomPacketPayloadWrapper.java | 223 ++++++++++++++++++ .../protocol/events/PacketContainerTest.java | 36 +-- 3 files changed, 254 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/comphenix/protocol/wrappers/CustomPacketPayloadWrapper.java diff --git a/src/main/java/com/comphenix/protocol/events/AbstractStructure.java b/src/main/java/com/comphenix/protocol/events/AbstractStructure.java index 43746ebd..1d5dc1a2 100644 --- a/src/main/java/com/comphenix/protocol/events/AbstractStructure.java +++ b/src/main/java/com/comphenix/protocol/events/AbstractStructure.java @@ -867,6 +867,16 @@ public abstract class AbstractStructure { MinecraftKey.getConverter()); } + /** + * Retrieve a read/write structure for custom packet payloads (available since Minecraft 1.20.2). + * @return A modifier for CustomPacketPayloads fields. + */ + public StructureModifier getCustomPacketPayloads() { + return structureModifier.withType( + CustomPacketPayloadWrapper.getCustomPacketPayloadClass(), + CustomPacketPayloadWrapper.getConverter()); + } + /** * Retrieve a read/write structure for dimension IDs in 1.13.1+ * @return A modifier for dimension IDs diff --git a/src/main/java/com/comphenix/protocol/wrappers/CustomPacketPayloadWrapper.java b/src/main/java/com/comphenix/protocol/wrappers/CustomPacketPayloadWrapper.java new file mode 100644 index 00000000..44a38819 --- /dev/null +++ b/src/main/java/com/comphenix/protocol/wrappers/CustomPacketPayloadWrapper.java @@ -0,0 +1,223 @@ +package com.comphenix.protocol.wrappers; + +import com.comphenix.protocol.reflect.EquivalentConverter; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.StructureModifier; +import com.comphenix.protocol.reflect.accessors.Accessors; +import com.comphenix.protocol.reflect.accessors.ConstructorAccessor; +import com.comphenix.protocol.reflect.accessors.MethodAccessor; +import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract; +import com.comphenix.protocol.utility.ByteBuddyFactory; +import com.comphenix.protocol.utility.ByteBuddyGenerated; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.utility.StreamSerializer; +import io.netty.buffer.ByteBuf; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Objects; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.FieldAccessor; +import net.bytebuddy.implementation.MethodCall; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.implementation.bind.annotation.Argument; +import net.bytebuddy.implementation.bind.annotation.FieldValue; +import net.bytebuddy.matcher.ElementMatchers; + +/** + * A wrapper for the CustomPacketPayload class in 1.20.2. Due to the nature of the class, not all types are supported + * by default. Constructing a new wrapper instance will give out a handle to a completely new implemented type, that + * allows to set a key and some kind of data of any choice. + *

+ * Note that constructing this class from a generic handle is only possible for the spigot-specific UnknownPayload type. + * All other payloads should be accessed via a structure modifier directly. + * + * @author Pasqual Koschmieder + */ +public final class CustomPacketPayloadWrapper { + + private static final Class MINECRAFT_KEY_CLASS; + private static final Class CUSTOM_PACKET_PAYLOAD_CLASS; + + private static final MethodAccessor WRITE_BYTES_METHOD; + private static final ConstructorAccessor PAYLOAD_WRAPPER_CONSTRUCTOR; + + private static final EquivalentConverter CONVERTER; + + static { + try { + // using this method is a small hack to prevent fuzzy from finding the renamed "getBytes(byte[])" method + // the method we're extracting here is: writeBytes(byte[] data, int arrayStartInclusive, int arrayEndExclusive) + Class packetDataSerializer = MinecraftReflection.getPacketDataSerializerClass(); + Method writeBytes = FuzzyReflection.fromClass(packetDataSerializer, false).getMethod(FuzzyMethodContract.newBuilder() + .banModifier(Modifier.STATIC) + .requireModifier(Modifier.PUBLIC) + .parameterExactArray(byte[].class, int.class, int.class) + .returnTypeExact(packetDataSerializer) + .build()); + WRITE_BYTES_METHOD = Accessors.getMethodAccessor(writeBytes); + + MINECRAFT_KEY_CLASS = MinecraftReflection.getMinecraftKeyClass(); + CUSTOM_PACKET_PAYLOAD_CLASS = MinecraftReflection.getMinecraftClass("network.protocol.common.custom.CustomPacketPayload"); + + Constructor payloadWrapperConstructor = makePayloadWrapper(); + PAYLOAD_WRAPPER_CONSTRUCTOR = Accessors.getConstructorAccessor(payloadWrapperConstructor); + + CONVERTER = new EquivalentConverter() { + @Override + public Object getGeneric(CustomPacketPayloadWrapper specific) { + return specific.newHandle(); + } + + @Override + public CustomPacketPayloadWrapper getSpecific(Object generic) { + return fromUnknownPayload(generic); + } + + @Override + public Class getSpecificType() { + return CustomPacketPayloadWrapper.class; + } + }; + } catch (Exception exception) { + throw new ExceptionInInitializerError(exception); + } + } + + private static Constructor makePayloadWrapper() throws Exception { + return new ByteBuddy() + .subclass(Object.class) + .name("com.comphenix.protocol.wrappers.ProtocolLibCustomPacketPayload") + .implement(CUSTOM_PACKET_PAYLOAD_CLASS, ByteBuddyGenerated.class) + .defineField("payload", byte[].class, Modifier.PRIVATE | Modifier.FINAL) + .defineField("id", MinecraftReflection.getMinecraftKeyClass(), Modifier.PRIVATE | Modifier.FINAL) + .defineConstructor(Modifier.PUBLIC) + .withParameters(MinecraftReflection.getMinecraftKeyClass(), byte[].class) + .intercept(MethodCall.invoke(Object.class.getConstructor()) + .andThen(FieldAccessor.ofField("id").setsArgumentAt(0)) + .andThen(FieldAccessor.ofField("payload").setsArgumentAt(1))) + .method(ElementMatchers.returns(MinecraftReflection.getMinecraftKeyClass()).and(ElementMatchers.takesNoArguments())) + .intercept(FieldAccessor.ofField("id")) + .method(ElementMatchers.returns(void.class).and(ElementMatchers.takesArguments(MinecraftReflection.getPacketDataSerializerClass()))) + .intercept(MethodDelegation.to(CustomPacketPayloadInterceptionHandler.class)) + .make() + .load(ByteBuddyFactory.getInstance().getClassLoader(), ClassLoadingStrategy.Default.INJECTION) + .getLoaded() + .getConstructor(MinecraftReflection.getMinecraftKeyClass(), byte[].class); + } + + // ====== api methods ====== + + /** + * The wrapped payload in the message. + */ + private final byte[] payload; + /** + * The wrapped key of the message. + */ + private final MinecraftKey id; + /** + * The generic id of the message, lazy initialized when needed. + */ + private Object genericId; + + /** + * Constructs a new payload wrapper instance using the given message payload and id. + * + * @param payload the payload of the message. + * @param id the id of the message. + * @throws NullPointerException if the given payload or id is null. + */ + public CustomPacketPayloadWrapper(byte[] payload, MinecraftKey id) { + this.payload = Objects.requireNonNull(payload, "payload"); + this.id = Objects.requireNonNull(id, "id"); + } + + /** + * Get the CustomPacketPayload class that is backing this wrapper (available since Minecraft 1.20.2). + * + * @return the CustomPacketPayload class. + */ + public static Class getCustomPacketPayloadClass() { + return CUSTOM_PACKET_PAYLOAD_CLASS; + } + + /** + * Get a converter to convert this wrapper to a generic handle and an UnknownPayload type to this wrapper. + * + * @return a converter for this wrapper. + */ + public static EquivalentConverter getConverter() { + return CONVERTER; + } + + /** + * Constructs this wrapper from an incoming ServerboundCustomPayloadPacket.UnknownPayload. All other types of + * payloads are not supported and will result in an exception. + *

+ * Note: the buffer of the given UnknownPayload will NOT be released by this operation. Make sure + * to release the buffer manually if you discard the packet to prevent memory leaks. + * + * @param unknownPayload the instance of the unknown payload to convert to this wrapper. + * @return a wrapper holding the minecraft key and payload of the given UnknownPayload instance. + */ + public static CustomPacketPayloadWrapper fromUnknownPayload(Object unknownPayload) { + StructureModifier modifier = new StructureModifier<>(unknownPayload.getClass()).withTarget(unknownPayload); + Object messageId = modifier.withType(MINECRAFT_KEY_CLASS).read(0); + ByteBuf messagePayload = (ByteBuf) modifier.withType(ByteBuf.class).read(0); + + MinecraftKey id = MinecraftKey.getConverter().getSpecific(messageId); + byte[] payload = StreamSerializer.getDefault().getBytesAndRelease(messagePayload.retain()); + return new CustomPacketPayloadWrapper(payload, id); + } + + /** + * Get the generic id of the wrapped message id. + * + * @return the generic key id. + */ + private Object getGenericId() { + if (this.genericId == null) { + this.genericId = MinecraftKey.getConverter().getGeneric(this.id); + } + return this.genericId; + } + + /** + * Get the message payload of this wrapper. Changes made to the returned array will be reflected into this wrapper. + * + * @return the message payload. + */ + public byte[] getPayload() { + return this.payload; + } + + /** + * Get the message id of this wrapper. + * + * @return the message id of this wrapper. + */ + public MinecraftKey getId() { + return this.id; + } + + /** + * Constructs a NEW handle instance of a payload wrapper to use in a CustomPayload packet. + * + * @return a new payload wrapper instance using the provided message id and payload. + */ + public Object newHandle() { + return PAYLOAD_WRAPPER_CONSTRUCTOR.invoke(this.getGenericId(), this.payload); + } + + /** + * Handles interception of the ProtocolLib specific CustomPayloadWrapper implementation. For internal use only. + */ + @SuppressWarnings("unused") + static final class CustomPacketPayloadInterceptionHandler { + public static void intercept(@FieldValue("payload") byte[] payload, @Argument(0) Object packetBuffer) { + WRITE_BYTES_METHOD.invoke(packetBuffer, payload, 0, payload.length); + } + } +} diff --git a/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java b/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java index 1153c044..d18b9c96 100644 --- a/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java +++ b/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java @@ -15,6 +15,8 @@ */ package com.comphenix.protocol.events; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -50,13 +52,13 @@ import com.comphenix.protocol.wrappers.WrappedDataWatcher.Registry; import com.comphenix.protocol.wrappers.nbt.NbtCompound; import com.comphenix.protocol.wrappers.nbt.NbtFactory; import com.google.common.collect.Lists; -import io.netty.buffer.ByteBuf; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.ClickEvent; import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.chat.HoverEvent; import net.md_5.bungee.api.chat.hover.content.Text; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.protocol.common.ServerboundCustomPayloadPacket; import net.minecraft.network.protocol.game.PacketPlayOutGameStateChange; import net.minecraft.network.protocol.game.PacketPlayOutUpdateAttributes; import net.minecraft.network.protocol.game.PacketPlayOutUpdateAttributes.AttributeSnapshot; @@ -387,27 +389,31 @@ public class PacketContainerTest { @Test public void testBigPacketSerialization() { PacketContainer payload = new PacketContainer(PacketType.Play.Server.CUSTOM_PAYLOAD); - payload.getMinecraftKeys().write(0, new com.comphenix.protocol.wrappers.MinecraftKey("test")); byte[] randomData = new byte[8192]; ThreadLocalRandom.current().nextBytes(randomData); - - ByteBuf serializer = (ByteBuf) MinecraftReflection.createPacketDataSerializer(randomData.length); - serializer.writeBytes(randomData); - - payload.getModifier().withType(MinecraftReflection.getPacketDataSerializerClass()).write(0, serializer); + CustomPacketPayloadWrapper payloadWrapper = new CustomPacketPayloadWrapper(randomData, new com.comphenix.protocol.wrappers.MinecraftKey("test")); + payload.getCustomPacketPayloads().write(0, payloadWrapper); PacketContainer cloned = SerializableCloner.clone(payload); - com.comphenix.protocol.wrappers.MinecraftKey clonedKey = cloned.getMinecraftKeys().read(0); + Assertions.assertNotSame(payload, cloned); + } - byte[] clonedData = new byte[randomData.length]; - ByteBuf clonedBuffer = (ByteBuf) cloned.getModifier() - .withType(MinecraftReflection.getPacketDataSerializerClass()) - .read(0); - clonedBuffer.readBytes(clonedData); + @Test + public void testUnknownPayloadDeserialize() { + MinecraftKey id = new MinecraftKey("test"); + byte[] payloadData = new byte[]{0x00, 0x01, 0x05, 0x07}; + ByteBuf buffer = Unpooled.wrappedBuffer(payloadData); + ServerboundCustomPayloadPacket.UnknownPayload payload = new ServerboundCustomPayloadPacket.UnknownPayload(id, buffer); + ServerboundCustomPayloadPacket packet = new ServerboundCustomPayloadPacket(payload); - assertEquals("minecraft:test", clonedKey.getFullKey()); - assertArrayEquals(randomData, clonedData); + PacketContainer packetContainer = new PacketContainer(PacketType.Play.Client.CUSTOM_PAYLOAD, packet); + CustomPacketPayloadWrapper payloadWrapper = packetContainer.getCustomPacketPayloads().read(0); + + com.comphenix.protocol.wrappers.MinecraftKey key = payloadWrapper.getId(); + Assertions.assertEquals("minecraft", key.getPrefix()); + Assertions.assertEquals("test", key.getKey()); + Assertions.assertArrayEquals(payloadData, payloadWrapper.getPayload()); } @Test