Fix EntityUseAction & Hand read for minecraft 1.17 (#1230)

This commit is contained in:
Pasqual Koschmieder 2021-06-18 16:47:48 +02:00 committed by GitHub
parent 1c2bc274dd
commit 0a32f24f08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 339 additions and 19 deletions

View File

@ -732,7 +732,7 @@ public class PacketContainer implements Serializable {
EnumWrappers.getDifficultyClass(),
EnumWrappers.getDifficultyConverter());
}
/**
* Retrieve a read/write structure for the EntityUse enum in 1.7.2.
* @return A modifier for EntityUse enum fields.
@ -744,6 +744,17 @@ public class PacketContainer implements Serializable {
EnumWrappers.getEntityUseActionConverter());
}
/**
* Retrieves a read/write structure for the EntityUseAction class in the UseEntity packet sent by the client for
* 1.17 and above.
* @return A modifier for EntityUseAction class fields.
*/
public StructureModifier<WrappedEnumEntityUseAction> getEnumEntityUseActions() {
return structureModifier.withType(
MinecraftReflection.getEnumEntityUseActionClass(),
WrappedEnumEntityUseAction.CONVERTER);
}
/**
* Retrieve a read/write structure for the NativeGameMode enum in 1.7.2.
* @return A modifier for NativeGameMode enum fields.
@ -916,7 +927,7 @@ public class PacketContainer implements Serializable {
public StructureModifier<Hand> getHands() {
return structureModifier.withType(
EnumWrappers.getHandClass(),
EnumWrappers.getHandConverter());
EnumWrappers.getHandConverter());
}
/**

View File

@ -289,6 +289,7 @@ public final class Accessors {
* @return The method accessor.
*/
public static ConstructorAccessor getConstructorAccessor(final Constructor<?> constructor) {
constructor.setAccessible(true); // let us in even if we are not allowed to
return new DefaultConstrutorAccessor(constructor);
}

View File

@ -27,10 +27,8 @@ import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
@ -57,6 +55,7 @@ import com.comphenix.protocol.reflect.ClassAnalyser;
import com.comphenix.protocol.reflect.ClassAnalyser.AsmMethod;
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.accessors.MethodAccessor;
import com.comphenix.protocol.reflect.fuzzy.AbstractFuzzyMatcher;
import com.comphenix.protocol.reflect.fuzzy.FuzzyClassContract;
@ -65,6 +64,7 @@ import com.comphenix.protocol.reflect.fuzzy.FuzzyMatchers;
import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract;
import com.comphenix.protocol.utility.RemappedClassSource.RemapperUnavailableException;
import com.comphenix.protocol.utility.RemappedClassSource.RemapperUnavailableException.Reason;
import com.comphenix.protocol.wrappers.EnumWrappers;
import com.comphenix.protocol.wrappers.nbt.NbtFactory;
import com.comphenix.protocol.wrappers.nbt.NbtType;
import com.google.common.base.Joiner;
@ -1793,6 +1793,52 @@ public class MinecraftReflection {
"PacketPlayOutPlayerInfo$PlayerInfoData", "PlayerInfoData");
}
/**
* Retrieves the entity use action class in 1.17.
* @return The EntityUseAction class
*/
public static Class<?> getEnumEntityUseActionClass() {
Class<?> packetClass = PacketType.Play.Client.USE_ENTITY.getPacketClass();
return FuzzyReflection.fromClass(packetClass, true).getFieldByType("^.*(EnumEntityUseAction)").getType();
}
/**
* Get a method accessor to get the actual use action out of the wrapping EnumEntityUseAction in 1.17.
* @return a method accessor to get the actual use action
*/
public static MethodAccessor getEntityUseActionEnumMethodAccessor() {
FuzzyReflection fuzzy = FuzzyReflection.fromClass(MinecraftReflection.getEnumEntityUseActionClass(), true);
return Accessors.getMethodAccessor(fuzzy.getMethod(FuzzyMethodContract.newBuilder()
.returnTypeExact(EnumWrappers.getEntityUseActionClass())
.build()));
}
/**
* Get a field accessor for the hand in the wrapping EnumEntityUseAction in 1.17.
*
* @param enumEntityUseAction the object instance of the action, the field is not present in attack.
* @return a field accessor for the hand in the wrapping EnumEntityUseAction
*/
public static FieldAccessor getHandEntityUseActionEnumFieldAccessor(Object enumEntityUseAction) {
FuzzyReflection fuzzy = FuzzyReflection.fromObject(enumEntityUseAction, true);
return Accessors.getFieldAccessor(fuzzy.getField(FuzzyFieldContract.newBuilder()
.typeExact(EnumWrappers.getHandClass())
.build()));
}
/**
* Get a field accessor for the vec3d in the wrapping EnumEntityUseAction in 1.17.
*
* @param enumEntityUseAction the object instance of the action, the field is not present in attack.
* @return a field accessor for the hand in the wrapping EnumEntityUseAction
*/
public static FieldAccessor getVec3EntityUseActionEnumFieldAccessor(Object enumEntityUseAction) {
FuzzyReflection fuzzy = FuzzyReflection.fromObject(enumEntityUseAction, true);
return Accessors.getFieldAccessor(fuzzy.getField(FuzzyFieldContract.newBuilder()
.typeExact(MinecraftReflection.getVec3DClass())
.build()));
}
/**
* Determine if the given object is a PlayerInfoData.
* @param obj - the given object.

View File

@ -10,6 +10,7 @@ import com.comphenix.protocol.ProtocolLogger;
import com.comphenix.protocol.reflect.EquivalentConverter;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.utility.MinecraftVersion;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.google.common.collect.Maps;
@ -481,7 +482,6 @@ public abstract class EnumWrappers {
DIFFICULTY_CLASS = getEnum(PacketType.Play.Server.LOGIN.getPacketClass(), 1);
}
ENTITY_USE_ACTION_CLASS = getEnum(PacketType.Play.Client.USE_ENTITY.getPacketClass(), 0);
GAMEMODE_CLASS = getEnum(PacketType.Play.Server.LOGIN.getPacketClass(), 0);
RESOURCE_PACK_STATUS_CLASS = getEnum(PacketType.Play.Client.RESOURCE_PACK_STATUS.getPacketClass(), 0);
PLAYER_INFO_ACTION_CLASS = getEnum(PacketType.Play.Server.PLAYER_INFO.getPacketClass(), 0);
@ -501,7 +501,16 @@ public abstract class EnumWrappers {
ITEM_SLOT_CLASS = getEnum(PacketType.Play.Server.ENTITY_EQUIPMENT.getPacketClass(), 0);
}
HAND_CLASS = getEnum(PacketType.Play.Client.USE_ENTITY.getPacketClass(), 1);
// In 1.17 the hand and use action class is no longer a field in the packet
if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove()) {
HAND_CLASS = MinecraftReflection.getMinecraftClass("world.EnumHand");
// class is named 'b' in the packet but class order differs in spigot and paper so we can only use the first method's return type (safest way)
ENTITY_USE_ACTION_CLASS = MinecraftReflection.getEnumEntityUseActionClass().getMethods()[0].getReturnType();
} else {
HAND_CLASS = getEnum(PacketType.Play.Client.USE_ENTITY.getPacketClass(), 1);
ENTITY_USE_ACTION_CLASS = getEnum(PacketType.Play.Client.USE_ENTITY.getPacketClass(), 0);
}
DIRECTION_CLASS = getEnum(PacketType.Play.Server.SPAWN_ENTITY_PAINTING.getPacketClass(), 0);
CHAT_TYPE_CLASS = getEnum(PacketType.Play.Server.CHAT.getPacketClass(), 0);
ENTITY_POSE_CLASS = MinecraftReflection.getNullableNMS("world.entity.EntityPose", "EntityPose");
@ -510,7 +519,6 @@ public abstract class EnumWrappers {
associate(CLIENT_COMMAND_CLASS, ClientCommand.class, getClientCommandConverter());
associate(CHAT_VISIBILITY_CLASS, ChatVisibility.class, getChatVisibilityConverter());
associate(DIFFICULTY_CLASS, Difficulty.class, getDifficultyConverter());
associate(ENTITY_USE_ACTION_CLASS, EntityUseAction.class, getEntityUseActionConverter());
associate(GAMEMODE_CLASS, NativeGameMode.class, getGameModeConverter());
associate(RESOURCE_PACK_STATUS_CLASS, ResourcePackStatus.class, getResourcePackStatusConverter());
associate(PLAYER_INFO_ACTION_CLASS, PlayerInfoAction.class, getPlayerInfoActionConverter());
@ -523,15 +531,14 @@ public abstract class EnumWrappers {
associate(PARTICLE_CLASS, Particle.class, getParticleConverter());
associate(SOUND_CATEGORY_CLASS, SoundCategory.class, getSoundCategoryConverter());
associate(ITEM_SLOT_CLASS, ItemSlot.class, getItemSlotConverter());
associate(HAND_CLASS, Hand.class, getHandConverter());
associate(DIRECTION_CLASS, Direction.class, getDirectionConverter());
associate(CHAT_TYPE_CLASS, ChatType.class, getChatTypeConverter());
associate(HAND_CLASS, Hand.class, getHandConverter());
associate(ENTITY_USE_ACTION_CLASS, EntityUseAction.class, getEntityUseActionConverter());
if (ENTITY_POSE_CLASS != null) {
associate(ENTITY_POSE_CLASS, EntityPose.class, getEntityPoseConverter());
}
INITIALIZED = true;
}
private static void associate(Class<?> nativeClass, Class<?> wrapperClass, EquivalentConverter<?> converter) {

View File

@ -0,0 +1,200 @@
package com.comphenix.protocol.wrappers;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.reflect.EquivalentConverter;
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.accessors.MethodAccessor;
import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.EnumWrappers.EntityUseAction;
import com.comphenix.protocol.wrappers.EnumWrappers.Hand;
import org.bukkit.util.Vector;
/**
* Represents an entity used action used in the UseEntity packet sent by the client.
* @author derklaro
*/
public class WrappedEnumEntityUseAction extends AbstractWrapper implements ClonableWrapper {
public static final EquivalentConverter<WrappedEnumEntityUseAction> CONVERTER = Converters.handle(AbstractWrapper::getHandle,
WrappedEnumEntityUseAction::fromHandle, WrappedEnumEntityUseAction.class);
private static final Class<?> PACKET_CLASS = PacketType.Play.Client.USE_ENTITY.getPacketClass();
private static final Class<?>[] DECLARED_CLASSES = PACKET_CLASS.getDeclaredClasses();
private static final Class<?> HANDLE_TYPE = MinecraftReflection.getEnumEntityUseActionClass();
private static final MethodAccessor ACTION_USE = MinecraftReflection.getEntityUseActionEnumMethodAccessor();
private static final ConstructorAccessor INTERACT = useAction(EnumWrappers.getHandClass());
private static final ConstructorAccessor INTERACT_AT = useAction(EnumWrappers.getHandClass(),
MinecraftReflection.getVec3DClass());
private static final Object ATTACK = Accessors.getFieldAccessor(FuzzyReflection.fromClass(PACKET_CLASS, true)
.getField(FuzzyFieldContract.newBuilder()
.requireModifier(Modifier.STATIC)
.typeExact(MinecraftReflection.getEnumEntityUseActionClass())
.build())
).get(null);
private static final WrappedEnumEntityUseAction ATTACK_WRAPPER = new WrappedEnumEntityUseAction(ATTACK);
private final EntityUseAction action;
// these fields are only available for interact & interact_at
private FieldAccessor handAccessor;
private FieldAccessor positionAccessor;
/**
* Construct a new wrapper for the entity use action class in the UseEntity packet.
* @param handle - the NMS handle.
*/
private WrappedEnumEntityUseAction(Object handle) {
super(HANDLE_TYPE);
setHandle(handle);
action = EnumWrappers.getEntityUseActionConverter().getSpecific(ACTION_USE.invoke(handle));
}
/**
* Finds a constructor of a declared class in the UseEntity class. Used to find the action class implementations.
* @param parameterTypes - the types the constructor of the class must have.
* @return a constructor for a matching class.
* @throws IllegalArgumentException if no constructor was found.
*/
private static ConstructorAccessor useAction(Class<?>... parameterTypes) {
for (Class<?> subClass : DECLARED_CLASSES) {
ConstructorAccessor accessor = Accessors.getConstructorAccessorOrNull(subClass, parameterTypes);
if (accessor != null) {
return accessor;
}
}
throw new IllegalArgumentException(
"No constructor with " + Arrays.toString(parameterTypes) + " in " + PACKET_CLASS);
}
/**
* Construct a new wrapper for the entity use action class in the UseEntity packet.
* @param handle - the NMS handle.
* @return the created wrapper.
*/
public static WrappedEnumEntityUseAction fromHandle(Object handle) {
return new WrappedEnumEntityUseAction(handle);
}
/**
* Get the jvm static action for attacking an entity.
* @return the action for an entity attack.
*/
public static WrappedEnumEntityUseAction attack() {
return ATTACK_WRAPPER;
}
/**
* Get an action for interacting with an entity.
* @param hand - the hand used for the interact.
* @return the action for an interact.
*/
public static WrappedEnumEntityUseAction interact(Hand hand) {
Object handle = INTERACT.invoke(EnumWrappers.getHandConverter().getGeneric(hand));
return new WrappedEnumEntityUseAction(handle);
}
/**
* Get an action for interacting with an entity at a specific location.
* @param hand - the hand used for the interact.
* @param vector - the position of the interact.
* @return the action for an interact_at.
*/
public static WrappedEnumEntityUseAction interactAt(Hand hand, Vector vector) {
Object handle = INTERACT_AT.invoke(EnumWrappers.getHandConverter().getGeneric(hand),
BukkitConverters.getVectorConverter().getGeneric(vector));
return new WrappedEnumEntityUseAction(handle);
}
/**
* Get the action used for the interact.
* @return the interact action.
*/
public EntityUseAction getAction() {
return action;
}
/**
* Get the hand used for the interact. Only available if this represents interact or interact_at.
* @return the hand used for the interact.
* @throws IllegalArgumentException if called for attack.
*/
public Hand getHand() {
return EnumWrappers.getHandConverter().getSpecific(getHandAccessor().get(handle));
}
/**
* Sets the hand used for the interact.
* @param hand the used hand.
* @throws IllegalArgumentException if called for attack.
*/
public void setHand(Hand hand) {
getHandAccessor().set(handle, EnumWrappers.getHandConverter().getGeneric(hand));
}
/**
* Get the position of the interact. Only available if this represents interact_at.
* @return the position of the interact.
* @throws IllegalArgumentException if called for attack or interact.
*/
public Vector getPosition() {
return BukkitConverters.getVectorConverter().getSpecific(getPositionAccessor().get(handle));
}
/**
* Sets the position of the interact.
* @param position the position.
* @throws IllegalArgumentException if called for attack or interact.
*/
public void setPosition(Vector position) {
getPositionAccessor().set(handle, BukkitConverters.getVectorConverter().getGeneric(position));
}
@Override
public WrappedEnumEntityUseAction deepClone() {
switch (action) {
case ATTACK:
return WrappedEnumEntityUseAction.attack();
case INTERACT:
return WrappedEnumEntityUseAction.interact(getHand());
case INTERACT_AT:
return WrappedEnumEntityUseAction.interactAt(getHand(), getPosition());
default:
throw new IllegalArgumentException("Invalid EntityUseAction: " + action);
}
}
/**
* Get a field accessor for the hand in the interact and interact_at type.
* @return a field accessor for the hand field.
* @throws IllegalArgumentException if called for attack.
*/
private FieldAccessor getHandAccessor() {
if (handAccessor == null) {
handAccessor = MinecraftReflection.getHandEntityUseActionEnumFieldAccessor(handle);
}
return handAccessor;
}
/**
* Get a field accessor for the position in the interact_at type.
* @return a field accessor for the position field.
* @throws IllegalArgumentException if called for attack or interact.
*/
public FieldAccessor getPositionAccessor() {
if (positionAccessor == null) {
positionAccessor = MinecraftReflection.getVec3EntityUseActionEnumFieldAccessor(handle);
}
return positionAccessor;
}
}

View File

@ -16,6 +16,8 @@
*/
package com.comphenix.protocol.events;
import com.comphenix.protocol.wrappers.EnumWrappers.EntityUseAction;
import com.comphenix.protocol.wrappers.EnumWrappers.Hand;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.*;
@ -585,6 +587,51 @@ public class PacketContainerTest {
assertTrue(packet.getGameStateIDs().read(0) == 2);
}
@Test
public void testUseEntity() {
PacketContainer packet = new PacketContainer(PacketType.Play.Client.USE_ENTITY);
WrappedEnumEntityUseAction action;
WrappedEnumEntityUseAction clone;
// test attack
packet.getEnumEntityUseActions().write(0, WrappedEnumEntityUseAction.attack());
action = packet.getEnumEntityUseActions().read(0);
// attack's handle should always be the same
assertEquals(WrappedEnumEntityUseAction.attack(), action);
assertEquals(EntityUseAction.ATTACK, action.getAction());
// hand & position should not be available
assertThrows(IllegalArgumentException.class, action::getHand);
assertThrows(IllegalArgumentException.class, action::getPosition);
// test cloning
clone = action.deepClone();
assertSame(WrappedEnumEntityUseAction.attack(), clone);
// test interact
packet.getEnumEntityUseActions().write(0, WrappedEnumEntityUseAction.interact(Hand.OFF_HAND));
action = packet.getEnumEntityUseActions().read(0);
assertEquals(EntityUseAction.INTERACT, action.getAction());
assertEquals(Hand.OFF_HAND, action.getHand());
// position should not be available
assertThrows(IllegalArgumentException.class, action::getPosition);
// test cloning
clone = action.deepClone();
assertEquals(EntityUseAction.INTERACT, clone.getAction());
assertEquals(Hand.OFF_HAND, clone.getHand());
// test interact_at
Vector position = new Vector(1, 199, 4);
packet.getEnumEntityUseActions().write(0, WrappedEnumEntityUseAction.interactAt(Hand.MAIN_HAND, position));
action = packet.getEnumEntityUseActions().read(0);
assertEquals(EntityUseAction.INTERACT_AT, action.getAction());
assertEquals(Hand.MAIN_HAND, action.getHand());
assertEquals(position, action.getPosition());
// test cloning
clone = action.deepClone();
assertEquals(EntityUseAction.INTERACT_AT, clone.getAction());
assertEquals(Hand.MAIN_HAND, clone.getHand());
assertEquals(position, clone.getPosition());
}
/**
* Actions from the outbound Boss packet. Used for testing generic enums.
* @author dmulloy2

View File

@ -150,4 +150,10 @@ public class MinecraftReflectionTest {
public void testGameProfile() {
assertEquals(GameProfile.class, MinecraftReflection.getGameProfileClass());
}
@Test
public void testEnumEntityUseAction() {
// this class is package-private in PacketPlayInUseEntity, so we can only check if no exception is thrown during retrieval
MinecraftReflection.getEnumEntityUseActionClass();
}
}

View File

@ -12,12 +12,11 @@ import com.comphenix.protocol.reflect.EquivalentConverter;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import net.minecraft.network.EnumProtocol;
import net.minecraft.network.protocol.game.PacketPlayInClientCommand.EnumClientCommand;
import net.minecraft.world.EnumHand;
import net.minecraft.world.EnumDifficulty;
import net.minecraft.world.entity.player.EnumChatVisibility;
import net.minecraft.world.level.EnumGamemode;
@ -28,15 +27,16 @@ public class EnumWrappersTest {
public EnumClientCommand command;
public EnumChatVisibility visibility;
public EnumDifficulty difficulty;
// public EnumEntityUseAction action;
public EnumHand hand;
// public EnumEntityUseAction action; // moved to PacketPlayInUseEntity but is private
public EnumGamemode mode;
}
@BeforeClass
public static void initializeBukkit() {
BukkitInitialization.initializePackage();
}
@Test
public void testEnum() {
EnumClass obj = new EnumClass();
@ -44,13 +44,15 @@ public class EnumWrappersTest {
obj.command = EnumClientCommand.b;
obj.visibility = EnumChatVisibility.c;
obj.difficulty = EnumDifficulty.d;
obj.hand = EnumHand.b;
// obj.action = EnumEntityUseAction.INTERACT;
obj.mode = EnumGamemode.e;
assertEquals(obj.protocol, roundtrip(obj, "protocol", EnumWrappers.getProtocolConverter()) );
assertEquals(obj.command, roundtrip(obj, "command", EnumWrappers.getClientCommandConverter()) );
assertEquals(obj.visibility, roundtrip(obj, "visibility", EnumWrappers.getChatVisibilityConverter()) );
assertEquals(obj.difficulty, roundtrip(obj, "difficulty", EnumWrappers.getDifficultyConverter()) );
assertEquals(obj.hand, roundtrip(obj, "hand", EnumWrappers.getHandConverter()) );
// assertEquals(obj.action, roundtrip(obj, "action", EnumWrappers.getEntityUseActionConverter()) );
assertEquals(obj.mode, roundtrip(obj, "mode", EnumWrappers.getGameModeConverter()) );
}
@ -58,14 +60,14 @@ public class EnumWrappersTest {
@SuppressWarnings("unchecked")
public <T extends Enum<T>> T roundtrip(Object target, String fieldName, EquivalentConverter<T> converter) {
FieldAccessor accessor = Accessors.getFieldAccessor(target.getClass(), fieldName, true);
return (T) converter.getGeneric(
converter.getSpecific(accessor.get(target))
);
}
private static final Set<String> KNOWN_INVALID = Sets.newHashSet(
"Particle", "WorldBorderAction", "CombatEventType", "EntityUseAction", "TitleAction", "Hand"
"Particle", "WorldBorderAction", "CombatEventType", "TitleAction"
);
@Test