ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/CustomPacketPayloadWrapper....

246 lines
10 KiB
Java

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 io.netty.buffer.Unpooled;
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.
* <p>
* 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 ConstructorAccessor PAYLOAD_WRAPPER_CONSTRUCTOR;
private static final MethodAccessor GET_ID_PAYLOAD_METHOD;
private static final MethodAccessor SERIALIZE_PAYLOAD_METHOD;
private static final EquivalentConverter<CustomPacketPayloadWrapper> CONVERTER;
static {
try {
MINECRAFT_KEY_CLASS = MinecraftReflection.getMinecraftKeyClass();
CUSTOM_PACKET_PAYLOAD_CLASS = MinecraftReflection.getMinecraftClass("network.protocol.common.custom.CustomPacketPayload");
Method getPayloadId = FuzzyReflection.fromClass(CUSTOM_PACKET_PAYLOAD_CLASS).getMethod(FuzzyMethodContract.newBuilder()
.banModifier(Modifier.STATIC)
.returnTypeExact(MINECRAFT_KEY_CLASS)
.parameterCount(0)
.build());
GET_ID_PAYLOAD_METHOD = Accessors.getMethodAccessor(getPayloadId);
Method serializePayloadData = FuzzyReflection.fromClass(CUSTOM_PACKET_PAYLOAD_CLASS).getMethod(FuzzyMethodContract.newBuilder()
.banModifier(Modifier.STATIC)
.returnTypeVoid()
.parameterCount(1)
.parameterDerivedOf(ByteBuf.class, 0)
.build());
SERIALIZE_PAYLOAD_METHOD = Accessors.getMethodAccessor(serializePayloadData);
Constructor<?> payloadWrapperConstructor = makePayloadWrapper();
PAYLOAD_WRAPPER_CONSTRUCTOR = Accessors.getConstructorAccessor(payloadWrapperConstructor);
CONVERTER = new EquivalentConverter<CustomPacketPayloadWrapper>() {
@Override
public Object getGeneric(CustomPacketPayloadWrapper specific) {
return specific.newHandle();
}
@Override
public CustomPacketPayloadWrapper getSpecific(Object generic) {
return fromUnknownPayload(generic);
}
@Override
public Class<CustomPacketPayloadWrapper> 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<CustomPacketPayloadWrapper> getConverter() {
return CONVERTER;
}
/**
* Constructs this wrapper from any CustomPayload type.
* <p>
* Note: the buffer of the given payload (if any) will <strong>NOT</strong> be released by this operation. Make sure
* to release the buffer manually if you discard the packet to prevent memory leaks.
*
* @param payload the instance of the custom payload to convert to this wrapper.
* @return a wrapper holding the minecraft key and payload of the given custom payload instance.
*/
public static CustomPacketPayloadWrapper fromUnknownPayload(Object payload) {
Object messageId = GET_ID_PAYLOAD_METHOD.invoke(payload);
MinecraftKey id = MinecraftKey.getConverter().getSpecific(messageId);
// we read and retain the underlying buffer in case the class uses a buffer to store the data
// this way, when passing the packet to further handling, the buffer is not released and can be re-used
StructureModifier<Object> modifier = new StructureModifier<>(payload.getClass()).withTarget(payload);
byte[] messagePayload = modifier.withType(ByteBuf.class).optionRead(0)
.map(buffer -> {
ByteBuf buf = (ByteBuf) buffer;
byte[] data = StreamSerializer.getDefault().getBytesAndRelease(buf.markReaderIndex().retain());
buf.resetReaderIndex();
return data;
})
.orElseGet(() -> {
ByteBuf buffer = Unpooled.buffer();
Object serializer = MinecraftReflection.getPacketDataSerializer(buffer);
SERIALIZE_PAYLOAD_METHOD.invoke(payload, serializer);
return StreamSerializer.getDefault().getBytesAndRelease(buffer);
});
return new CustomPacketPayloadWrapper(messagePayload, 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 <strong>NEW</strong> 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) {
((ByteBuf) packetBuffer).writeBytes(payload);
}
}
}