diff --git a/src/main/java/com/comphenix/protocol/reflect/cloning/BukkitCloner.java b/src/main/java/com/comphenix/protocol/reflect/cloning/BukkitCloner.java index e964b0e8..cc1a304c 100644 --- a/src/main/java/com/comphenix/protocol/reflect/cloning/BukkitCloner.java +++ b/src/main/java/com/comphenix/protocol/reflect/cloning/BukkitCloner.java @@ -82,6 +82,7 @@ public class BukkitCloner implements Cloner { fromWrapper(MinecraftReflection::getIChatBaseComponentClass, WrappedChatComponent::fromHandle); fromManual(ComponentConverter::getBaseComponentArrayClass, source -> ComponentConverter.clone((BaseComponent[]) source)); + fromWrapper(WrappedVillagerData::getNmsClass, WrappedVillagerData::fromHandle); } private Function findCloner(Class type) { diff --git a/src/main/java/com/comphenix/protocol/reflect/cloning/ImmutableDetector.java b/src/main/java/com/comphenix/protocol/reflect/cloning/ImmutableDetector.java index 288b9a5d..82afef78 100644 --- a/src/main/java/com/comphenix/protocol/reflect/cloning/ImmutableDetector.java +++ b/src/main/java/com/comphenix/protocol/reflect/cloning/ImmutableDetector.java @@ -60,19 +60,24 @@ public class ImmutableDetector implements Cloner { static { add(MinecraftReflection::getGameProfileClass); add(MinecraftReflection::getDataWatcherSerializerClass); - add(() -> MinecraftReflection.getMinecraftClass("SoundEffect")); add(MinecraftReflection::getBlockClass); add(MinecraftReflection::getItemClass); + add("SoundEffect"); if (MinecraftVersion.atOrAbove(MinecraftVersion.AQUATIC_UPDATE)) { - add(() -> MinecraftReflection.getMinecraftClass("Particle")); add(MinecraftReflection::getFluidTypeClass); add(MinecraftReflection::getParticleTypeClass); + add("Particle"); } if (MinecraftVersion.atOrAbove(MinecraftVersion.VILLAGE_UPDATE)) { - add(() -> MinecraftReflection.getMinecraftClass("EntityTypes")); + add("EntityTypes"); + add("VillagerType"); + add("VillagerProfession"); } + + // TODO automatically detect the technically-not-an-enum enums that Mojang is so fond of + // Would also probably go in tandem with having the FieldCloner use this } private static void add(Supplier> getClass) { @@ -83,6 +88,15 @@ public class ImmutableDetector implements Cloner { } } catch (RuntimeException ignored) { } } + + private static void add(String className) { + try { + Class clazz = MinecraftReflection.getMinecraftClass(className); + if (clazz != null) { + immutableNMS.add(clazz); + } + } catch (RuntimeException ignored) { } + } @Override public boolean canClone(Object source) { diff --git a/src/main/java/com/comphenix/protocol/wrappers/EnumWrappers.java b/src/main/java/com/comphenix/protocol/wrappers/EnumWrappers.java index c67d6a43..9b4a1031 100644 --- a/src/main/java/com/comphenix/protocol/wrappers/EnumWrappers.java +++ b/src/main/java/com/comphenix/protocol/wrappers/EnumWrappers.java @@ -1,19 +1,22 @@ package com.comphenix.protocol.wrappers; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + import com.comphenix.protocol.PacketType; import com.comphenix.protocol.PacketType.Protocol; 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.MinecraftReflection; import com.google.common.collect.Maps; +import org.apache.commons.lang.Validate; import org.bukkit.GameMode; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - /** * Represents a generic enum converter. * @author Kristian @@ -693,4 +696,98 @@ public abstract class EnumWrappers { this.genericType = genericType; } } + + /** + * Used for classes where it's an enum in everything but name + * @param Generic type + */ + public static class FauxEnumConverter> implements EquivalentConverter { + private final Class specificClass; + private final Class genericClass; + private final Map lookup; + + public FauxEnumConverter(Class specific, Class generic) { + Validate.notNull(specific,"specific class cannot be null"); + Validate.notNull(generic,"generic class cannot be null"); + + this.specificClass = specific; + this.genericClass = generic; + this.lookup = new HashMap<>(); + } + + @Override + public Object getGeneric(T specific) { + Validate.notNull(specific, "specific object cannot be null"); + + return Accessors + .getFieldAccessor(genericClass, specific + .name(), false) + .get(null); + } + + @Override + public T getSpecific(Object generic) { + Validate.notNull(generic, "generic object cannot be null"); + + return lookup.computeIfAbsent(generic, x -> { + for (Field field : genericClass.getFields()) { + try { + if (!field.isAccessible()) { + field.setAccessible(true); + } + + if (field.get(null) == generic) { + return Enum.valueOf(specificClass, field.getName().toUpperCase()); + } + } catch (ReflectiveOperationException ignored) { } + } + + throw new IllegalArgumentException("Could not find ProtocolLib wrapper for " + generic); + }); + } + + @Override + public Class getSpecificType() { + return specificClass; + } + } + + public static class IndexedEnumConverter> implements EquivalentConverter { + private Class specificClass; + private Class genericClass; + + public IndexedEnumConverter(Class specificClass, Class genericClass) { + this.specificClass = specificClass; + this.genericClass = genericClass; + } + + @Override + public Object getGeneric(T specific) { + int ordinal = specific.ordinal(); + for (Object elem : genericClass.getEnumConstants()) { + if (((Enum) elem).ordinal() == ordinal) { + return elem; + } + } + + return null; + } + + @Override + public T getSpecific(Object generic) { + int ordinal = ((Enum) generic).ordinal(); + for (T elem : specificClass.getEnumConstants()) { + if (elem.ordinal() == ordinal) { + return elem; + } + } + + return null; + } + + @Override + public Class getSpecificType() { + return specificClass; + } + } } diff --git a/src/main/java/com/comphenix/protocol/wrappers/WrappedVillagerData.java b/src/main/java/com/comphenix/protocol/wrappers/WrappedVillagerData.java new file mode 100644 index 00000000..4b3316d7 --- /dev/null +++ b/src/main/java/com/comphenix/protocol/wrappers/WrappedVillagerData.java @@ -0,0 +1,68 @@ +package com.comphenix.protocol.wrappers; + +import com.comphenix.protocol.reflect.EquivalentConverter; +import com.comphenix.protocol.reflect.StructureModifier; +import com.comphenix.protocol.reflect.accessors.Accessors; +import com.comphenix.protocol.utility.MinecraftReflection; + +public class WrappedVillagerData extends AbstractWrapper implements ClonableWrapper { + private static final Class NMS_CLASS = MinecraftReflection.getMinecraftClass("VillagerData"); + private static final Class TYPE_CLASS = MinecraftReflection.getMinecraftClass("VillagerType"); + private static final Class PROF_CLASS = MinecraftReflection.getMinecraftClass("VillagerProfession"); + + private static final EquivalentConverter TYPE_CONVERTER = new EnumWrappers.FauxEnumConverter<>(Type.class, TYPE_CLASS); + private static final EquivalentConverter PROF_CONVERTER = new EnumWrappers.FauxEnumConverter<>(Profession.class, PROF_CLASS); + + public enum Type { + DESERT, JUNGLE, PLAINS, SAVANNA, SNOW, SWAMP, TAIGA; + } + + public enum Profession { + NONE, ARMORER, BUTCHER, CARTOGRAPHER, CLERIC, FARMER, FISHERMAN, + FLETCHER, LEATHERWORKER, LIBRARIAN, MASON, NITWIT, SHEPHERD, + TOOLSMITH, WEAPONSMITH; + } + + private StructureModifier modifier; + + private WrappedVillagerData(Object handle) { + super(NMS_CLASS); + setHandle(handle); + + modifier = new StructureModifier<>(NMS_CLASS).withTarget(handle); + } + + public static WrappedVillagerData fromHandle(Object handle) { + return new WrappedVillagerData(handle); + } + + public static WrappedVillagerData fromValues(Type type, Profession profession, int level) { + Object genericType = TYPE_CONVERTER.getGeneric(type); + Object genericProf = PROF_CONVERTER.getGeneric(profession); + + Object handle = Accessors.getConstructorAccessor(NMS_CLASS, TYPE_CLASS, PROF_CLASS, int.class) + .invoke(genericType, genericProf, level); + return fromHandle(handle); + } + + public static Class getNmsClass() { + return NMS_CLASS; + } + + public int getLevel() { + return modifier.withType(int.class).read(0); + } + + public Type getType() { + return modifier.withType(TYPE_CLASS, TYPE_CONVERTER).read(0); + } + + public Profession getProfession() { + return modifier.withType(PROF_CLASS, PROF_CONVERTER).read(0); + } + + @Override + public WrappedVillagerData deepClone() { + return WrappedVillagerData.fromValues(getType(), getProfession(), getLevel()); + } +} diff --git a/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java b/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java index eb00c4f3..4b39a9d1 100644 --- a/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java +++ b/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java @@ -535,7 +535,9 @@ public class PacketContainerTest { "String"), new WrappedWatchableObject(new WrappedDataWatcherObject(0, Registry.get(Float.class)), 1.0F), new WrappedWatchableObject(new WrappedDataWatcherObject(0, Registry.getChatComponentSerializer(true)), - com.google.common.base.Optional.of(ComponentConverter.fromBaseComponent(TEST_COMPONENT).getHandle())) + com.google.common.base.Optional.of(ComponentConverter.fromBaseComponent(TEST_COMPONENT).getHandle())), + new WrappedWatchableObject(new WrappedDataWatcherObject(0, Registry.get(VillagerData.class)), + new VillagerData(VillagerType.SNOW, VillagerProfession.ARMORER, 69)) )); } else if (type == PacketType.Play.Server.CHAT) { constructed.getChatComponents().write(0, ComponentConverter.fromBaseComponent(TEST_COMPONENT));