From 154d73ae5178f5ce7c95ed4fc5ac4cc03192167d Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Sun, 8 Dec 2013 23:45:35 +0100 Subject: [PATCH] Added the ability to read and modify server-side chat messages. This introduces the new WrappedChatComponent class, which can be accessed through PacketContainer.getChatComponents(). --- .../protocol/events/PacketContainer.java | 16 ++- .../protocol/reflect/ClassAnalyser.java | 25 +++- .../protocol/utility/MinecraftReflection.java | 56 +++++++- .../protocol/wrappers/BukkitConverters.java | 37 +++++- .../wrappers/WrappedChatComponent.java | 125 ++++++++++++++++++ .../protocol/events/PacketContainerTest.java | 12 ++ .../utility/MinecraftReflectionTest.java | 12 ++ 7 files changed, 278 insertions(+), 5 deletions(-) create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedChatComponent.java diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java index 87f0139c..9fcdea35 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java @@ -66,6 +66,7 @@ import com.comphenix.protocol.utility.StreamSerializer; import com.comphenix.protocol.wrappers.BukkitConverters; import com.comphenix.protocol.wrappers.ChunkPosition; import com.comphenix.protocol.wrappers.WrappedAttribute; +import com.comphenix.protocol.wrappers.WrappedChatComponent; import com.comphenix.protocol.wrappers.WrappedDataWatcher; import com.comphenix.protocol.wrappers.WrappedGameProfile; import com.comphenix.protocol.wrappers.WrappedWatchableObject; @@ -478,7 +479,7 @@ public class PacketContainer implements Serializable { } /** - * Retrieves a read/write structure for game profiles. + * Retrieves a read/write structure for game profiles in Minecraft 1.7.2. *

* This modifier will automatically marshall between WrappedGameProfile and the * internal Minecraft GameProfile. @@ -490,6 +491,19 @@ public class PacketContainer implements Serializable { GameProfile.class, BukkitConverters.getWrappedGameProfileConverter()); } + /** + * Retrieves a read/write structure for chat components in Minecraft 1.7.2. + *

+ * This modifier will automatically marshall between WrappedChatComponent and the + * internal Minecraft GameProfile. + * @return A modifier for GameProfile fields. + */ + public StructureModifier getChatComponents() { + // Convert to and from the Bukkit wrapper + return structureModifier.withType( + MinecraftReflection.getIChatBaseComponent(), BukkitConverters.getWrappedChatComponentConverter()); + } + /** * Retrieves the ID of this packet. *

diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/ClassAnalyser.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/ClassAnalyser.java index 03c62442..5a4b4eff 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/ClassAnalyser.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/ClassAnalyser.java @@ -6,6 +6,7 @@ import java.util.List; import com.comphenix.protocol.reflect.compiler.EmptyClassVisitor; import com.comphenix.protocol.reflect.compiler.EmptyMethodVisitor; +import com.comphenix.protocol.utility.MinecraftReflection; import com.google.common.collect.Lists; import net.sf.cglib.asm.ClassReader; @@ -28,10 +29,19 @@ public class ClassAnalyser { this.signature = signature; } - public String getOwnerClass() { + public String getOwnerName() { return ownerClass; } + /** + * Retrieve the associated owner class. + * @return The owner class. + * @throws ClassNotFoundException + */ + public Class getOwnerClass() throws ClassNotFoundException { + return AsmMethod.class.getClassLoader().loadClass(getOwnerName().replace('/', '.')); + } + public String getMethodName() { return methodName; } @@ -57,7 +67,18 @@ public class ClassAnalyser { * @throws IOException Cannot access the parent class. */ public List getMethodCalls(Method method) throws IOException { - final ClassReader reader = new ClassReader(method.getDeclaringClass().getCanonicalName()); + return getMethodCalls(method.getDeclaringClass(), method); + } + + /** + * Retrieve every method calls in the given method. + * @param clazz - the parent class. + * @param method - the method to analyse. + * @return The method calls. + * @throws IOException Cannot access the parent class. + */ + public List getMethodCalls(Class clazz, Method method) throws IOException { + final ClassReader reader = new ClassReader(clazz.getCanonicalName()); final List output = Lists.newArrayList(); // The method we are looking for diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index c552ae72..7cde4435 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -543,6 +543,14 @@ public class MinecraftReflection { } } + /** + * Retrieve the CraftChatMessage in Minecraft 1.7.2. + * @return The CraftChatMessage class. + */ + public static Class getCraftChatMessage() { + return getCraftBukkitClass("util.CraftChatMessage"); + } + /** * Retrieve the WorldServer (NMS) class. * @return The WorldServer class. @@ -637,6 +645,52 @@ public class MinecraftReflection { } } + /** + * Retrieve the IChatBaseComponent class. + * @return The IChatBaseComponent. + */ + public static Class getIChatBaseComponent() { + try { + return getMinecraftClass("IChatBaseComponent"); + } catch (RuntimeException e) { + return setMinecraftClass("IChatBaseComponent", + FuzzyReflection.getMethodAccessor(getCraftChatMessage(), "fromString", String.class). + getMethod().getReturnType().getComponentType() + ); + } + } + + /** + * Attempt to find the ChatSerializer class. + * @return The serializer class. + * @throws IllegalStateException If the class could not be found or deduced. + */ + public static Class getChatSerializer() { + try { + return getMinecraftClass("ChatSerializer"); + } catch (RuntimeException e) { + // Analyse the ASM + try { + List methodCalls = ClassAnalyser.getDefault().getMethodCalls( + PacketType.Play.Server.CHAT.getPacketClass(), + MinecraftMethods.getPacketReadByteBufMethod() + ); + Class packetSerializer = getPacketDataSerializerClass(); + + for (AsmMethod method : methodCalls) { + Class owner = method.getOwnerClass(); + + if (isMinecraftClass(owner) && !owner.equals(packetSerializer)) { + return setMinecraftClass("ChatSerializer", owner); + } + } + } catch (Exception e1) { + throw new IllegalStateException("Cannot find ChatSerializer class.", e); + } + } + throw new IllegalStateException("Cannot find ChatSerializer class."); + } + /** * Determine if this Minecraft version is using Netty. *

@@ -1300,7 +1354,7 @@ public class MinecraftReflection { try { // Now -- we inspect all the method calls within that method, and use the first foreign Minecraft class for (AsmMethod method : ClassAnalyser.getDefault().getMethodCalls(writeNbt)) { - Class owner = MinecraftReflection.class.getClassLoader().loadClass(method.getOwnerClass().replace('/', '.')); + Class owner = method.getOwnerClass(); if (!packetSerializer.equals(owner) && isMinecraftClass(owner)) { return setMinecraftClass("NBTCompressedStreamTools", owner); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java index 301f9a0f..d26fd7c5 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java @@ -257,6 +257,29 @@ public class BukkitConverters { }; } + /** + * Retrieve a converter for wrapped chat components. + * @return Wrapped chat componetns. + */ + public static EquivalentConverter getWrappedChatComponentConverter() { + return new IgnoreNullConverter() { + @Override + protected Object getGenericValue(Class genericType, WrappedChatComponent specific) { + return specific.getHandle(); + } + + @Override + protected WrappedChatComponent getSpecificValue(Object generic) { + return WrappedChatComponent.fromHandle(generic); + } + + @Override + public Class getSpecificType() { + return WrappedChatComponent.class; + } + }; + } + /** * Retrieve a converter for wrapped attribute snapshots. * @return Wrapped attribute snapshot converter. @@ -601,6 +624,12 @@ public class BukkitConverters { put(WrappedWatchableObject.class, (EquivalentConverter) getWatchableObjectConverter()). put(PotionEffect.class, (EquivalentConverter) getPotionEffectConverter()); + // Types added in 1.7.2 + if (MinecraftReflection.isUsingNetty()) { + builder.put(WrappedGameProfile.class, (EquivalentConverter) getWrappedGameProfileConverter()); + builder.put(WrappedChatComponent.class, (EquivalentConverter) getWrappedChatComponentConverter()); + } + if (hasWorldType) builder.put(WorldType.class, (EquivalentConverter) getWorldTypeConverter()); if (hasAttributeSnapshot) @@ -626,11 +655,17 @@ public class BukkitConverters { put(MinecraftReflection.getNBTCompoundClass(), (EquivalentConverter) getNbtConverter()). put(MinecraftReflection.getWatchableObjectClass(), (EquivalentConverter) getWatchableObjectConverter()). put(MinecraftReflection.getMobEffectClass(), (EquivalentConverter) getPotionEffectConverter()); - + if (hasWorldType) builder.put(MinecraftReflection.getWorldTypeClass(), (EquivalentConverter) getWorldTypeConverter()); if (hasAttributeSnapshot) builder.put(MinecraftReflection.getAttributeSnapshotClass(), (EquivalentConverter) getWrappedAttributeConverter()); + + // Types added in 1.7.2 + if (MinecraftReflection.isUsingNetty()) { + builder.put(MinecraftReflection.getGameProfileClass(), (EquivalentConverter) getWrappedGameProfileConverter()); + builder.put(MinecraftReflection.getIChatBaseComponent(), (EquivalentConverter) getWrappedChatComponentConverter()); + } genericConverters = builder.build(); } return genericConverters; diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedChatComponent.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedChatComponent.java new file mode 100644 index 00000000..f14dbbd5 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedChatComponent.java @@ -0,0 +1,125 @@ +package com.comphenix.protocol.wrappers; + +import org.bukkit.ChatColor; + +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.FuzzyReflection.MethodAccessor; +import com.comphenix.protocol.utility.MinecraftReflection; + +/** + * Represents a chat component added in Minecraft 1.7.2 + * @author Kristian + */ +public class WrappedChatComponent { + private static final Class SERIALIZER = MinecraftReflection.getChatSerializer(); + private static final Class COMPONENT = MinecraftReflection.getIChatBaseComponent(); + private static MethodAccessor SERIALIZE_COMPONENT = null; + private static MethodAccessor DESERIALIZE_COMPONENT = null; + private static MethodAccessor CONSTRUCT_COMPONENT = null; + + static { + FuzzyReflection fuzzy = FuzzyReflection.fromClass(SERIALIZER); + + // Retrieve the correct methods + SERIALIZE_COMPONENT = FuzzyReflection.getMethodAccessor( + fuzzy.getMethodByParameters("serialize", String.class, new Class[] { COMPONENT })); + DESERIALIZE_COMPONENT = FuzzyReflection.getMethodAccessor( + fuzzy.getMethodByParameters("serialize", COMPONENT, new Class[] { String.class })); + + // Get a component from a standard Minecraft message + CONSTRUCT_COMPONENT = FuzzyReflection.getMethodAccessor( + MinecraftReflection.getCraftChatMessage(), "fromString", String.class); + } + + private Object handle; + private transient String cache; + + private WrappedChatComponent(Object handle, String cache) { + this.handle = handle; + this.cache = cache; + } + + /** + * Construct a new chat component wrapper around the given NMS object. + * @param handle - the NMS object. + * @return The wrapper. + */ + public static WrappedChatComponent fromHandle(Object handle) { + if (handle == null) + throw new IllegalArgumentException("handle cannot be NULL."); + if (!COMPONENT.isAssignableFrom(handle.getClass())) + throw new IllegalArgumentException("handle (" + handle + ") is not a " + COMPONENT); + return new WrappedChatComponent(handle, null); + } + + /** + * Construct a new chat component wrapper from the given JSON string. + * @param json - the json. + * @return The chat component wrapper. + */ + public static WrappedChatComponent fromJson(String json) { + return new WrappedChatComponent(DESERIALIZE_COMPONENT.invoke(null, json), json); + } + + /** + * Construct an array of chat components from a standard Minecraft message. + *

+ * This uses {@link ChatColor} for formating. + * @param message - the message. + * @return The equivalent chat components. + */ + public static WrappedChatComponent[] fromChatMessage(String message) { + Object[] components = (Object[]) CONSTRUCT_COMPONENT.invoke(null, message); + WrappedChatComponent[] result = new WrappedChatComponent[components.length]; + + for (int i = 0; i < components.length; i++) { + result[i] = fromHandle(components[i]); + } + return result; + } + + /** + * Retrieve a copy of this component as a JSON string. + *

+ * Note that any modifications to this JSON string will not update the current component. + * @return The JSON representation of this object. + */ + public String getJson() { + if (cache == null) { + cache = (String) SERIALIZE_COMPONENT.invoke(null, handle); + } + return cache; + } + + /** + * Set the content of this component using a JSON object. + * @param obj - the JSON that represents the new component. + */ + public void setJson(String obj) { + this.handle = DESERIALIZE_COMPONENT.invoke(null, obj); + this.cache = obj; + } + + /** + * Retrieve the underlying IChatBaseComponent instance. + * @return The underlying instance. + */ + public Object getHandle() { + return handle; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj instanceof WrappedChatComponent) { + return ((WrappedChatComponent) obj).handle.equals(handle); + } + return false; + } + + @Override + public int hashCode() { + return handle.hashCode(); + } +} diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java index 51fa812a..2f67d4c7 100644 --- a/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java @@ -32,6 +32,7 @@ import org.apache.commons.lang.builder.ToStringStyle; // Will have to be updated for every version though import org.bukkit.craftbukkit.v1_7_R1.inventory.CraftItemFactory; +import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.WorldType; import org.bukkit.inventory.ItemStack; @@ -53,6 +54,7 @@ import com.comphenix.protocol.utility.MinecraftMethods; import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.wrappers.BukkitConverters; import com.comphenix.protocol.wrappers.ChunkPosition; +import com.comphenix.protocol.wrappers.WrappedChatComponent; import com.comphenix.protocol.wrappers.WrappedDataWatcher; import com.comphenix.protocol.wrappers.WrappedGameProfile; import com.comphenix.protocol.wrappers.WrappedWatchableObject; @@ -336,6 +338,16 @@ public class PacketContainerTest { assertEquals(profile, spawnEntity.getGameProfiles().read(0)); } + @Test + public void testChatComponents() { + PacketContainer chatPacket = new PacketContainer(PacketType.Play.Server.CHAT); + chatPacket.getChatComponents().write(0, + WrappedChatComponent.fromChatMessage("You shall not " + ChatColor.ITALIC + "pass!")[0]); + + assertEquals("{\"extra\":[\"You shall not \",{\"italic\":true,\"text\":\"pass!\"}],\"text\":\"\"}", + chatPacket.getChatComponents().read(0).getJson()); + } + @Test public void testSerialization() { PacketContainer chat = new PacketContainer(PacketType.Play.Client.CHAT); diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/utility/MinecraftReflectionTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/utility/MinecraftReflectionTest.java index b2473478..5192e6bb 100644 --- a/ProtocolLib/src/test/java/com/comphenix/protocol/utility/MinecraftReflectionTest.java +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/utility/MinecraftReflectionTest.java @@ -2,6 +2,8 @@ package com.comphenix.protocol.utility; import static org.junit.Assert.*; +import net.minecraft.server.v1_7_R1.ChatSerializer; +import net.minecraft.server.v1_7_R1.IChatBaseComponent; import net.minecraft.server.v1_7_R1.NBTCompressedStreamTools; import org.junit.AfterClass; @@ -32,4 +34,14 @@ public class MinecraftReflectionTest { public void testNbtStreamTools() { assertEquals(NBTCompressedStreamTools.class, MinecraftReflection.getNbtCompressedStreamToolsClass()); } + + @Test + public void testChatComponent() { + assertEquals(IChatBaseComponent.class, MinecraftReflection.getIChatBaseComponent()); + } + + @Test + public void testChatSerializer() { + assertEquals(ChatSerializer.class, MinecraftReflection.getChatSerializer()); + } }