Added the ability to read and modify server-side chat messages.

This introduces the new WrappedChatComponent class, which can be 
accessed through PacketContainer.getChatComponents().
This commit is contained in:
Kristian S. Stangeland 2013-12-08 23:45:35 +01:00
parent 1aaf272878
commit 154d73ae51
7 changed files with 278 additions and 5 deletions

View File

@ -66,6 +66,7 @@ import com.comphenix.protocol.utility.StreamSerializer;
import com.comphenix.protocol.wrappers.BukkitConverters; import com.comphenix.protocol.wrappers.BukkitConverters;
import com.comphenix.protocol.wrappers.ChunkPosition; import com.comphenix.protocol.wrappers.ChunkPosition;
import com.comphenix.protocol.wrappers.WrappedAttribute; import com.comphenix.protocol.wrappers.WrappedAttribute;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.comphenix.protocol.wrappers.WrappedDataWatcher; import com.comphenix.protocol.wrappers.WrappedDataWatcher;
import com.comphenix.protocol.wrappers.WrappedGameProfile; import com.comphenix.protocol.wrappers.WrappedGameProfile;
import com.comphenix.protocol.wrappers.WrappedWatchableObject; 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.
* <p> * <p>
* This modifier will automatically marshall between WrappedGameProfile and the * This modifier will automatically marshall between WrappedGameProfile and the
* internal Minecraft GameProfile. * internal Minecraft GameProfile.
@ -490,6 +491,19 @@ public class PacketContainer implements Serializable {
GameProfile.class, BukkitConverters.getWrappedGameProfileConverter()); GameProfile.class, BukkitConverters.getWrappedGameProfileConverter());
} }
/**
* Retrieves a read/write structure for chat components in Minecraft 1.7.2.
* <p>
* This modifier will automatically marshall between WrappedChatComponent and the
* internal Minecraft GameProfile.
* @return A modifier for GameProfile fields.
*/
public StructureModifier<WrappedChatComponent> getChatComponents() {
// Convert to and from the Bukkit wrapper
return structureModifier.<WrappedChatComponent>withType(
MinecraftReflection.getIChatBaseComponent(), BukkitConverters.getWrappedChatComponentConverter());
}
/** /**
* Retrieves the ID of this packet. * Retrieves the ID of this packet.
* <p> * <p>

View File

@ -6,6 +6,7 @@ import java.util.List;
import com.comphenix.protocol.reflect.compiler.EmptyClassVisitor; import com.comphenix.protocol.reflect.compiler.EmptyClassVisitor;
import com.comphenix.protocol.reflect.compiler.EmptyMethodVisitor; import com.comphenix.protocol.reflect.compiler.EmptyMethodVisitor;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import net.sf.cglib.asm.ClassReader; import net.sf.cglib.asm.ClassReader;
@ -28,10 +29,19 @@ public class ClassAnalyser {
this.signature = signature; this.signature = signature;
} }
public String getOwnerClass() { public String getOwnerName() {
return ownerClass; 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() { public String getMethodName() {
return methodName; return methodName;
} }
@ -57,7 +67,18 @@ public class ClassAnalyser {
* @throws IOException Cannot access the parent class. * @throws IOException Cannot access the parent class.
*/ */
public List<AsmMethod> getMethodCalls(Method method) throws IOException { public List<AsmMethod> 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<AsmMethod> getMethodCalls(Class<?> clazz, Method method) throws IOException {
final ClassReader reader = new ClassReader(clazz.getCanonicalName());
final List<AsmMethod> output = Lists.newArrayList(); final List<AsmMethod> output = Lists.newArrayList();
// The method we are looking for // The method we are looking for

View File

@ -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. * Retrieve the WorldServer (NMS) class.
* @return The WorldServer 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<AsmMethod> 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. * Determine if this Minecraft version is using Netty.
* <p> * <p>
@ -1300,7 +1354,7 @@ public class MinecraftReflection {
try { try {
// Now -- we inspect all the method calls within that method, and use the first foreign Minecraft class // Now -- we inspect all the method calls within that method, and use the first foreign Minecraft class
for (AsmMethod method : ClassAnalyser.getDefault().getMethodCalls(writeNbt)) { 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)) { if (!packetSerializer.equals(owner) && isMinecraftClass(owner)) {
return setMinecraftClass("NBTCompressedStreamTools", owner); return setMinecraftClass("NBTCompressedStreamTools", owner);

View File

@ -257,6 +257,29 @@ public class BukkitConverters {
}; };
} }
/**
* Retrieve a converter for wrapped chat components.
* @return Wrapped chat componetns.
*/
public static EquivalentConverter<WrappedChatComponent> getWrappedChatComponentConverter() {
return new IgnoreNullConverter<WrappedChatComponent>() {
@Override
protected Object getGenericValue(Class<?> genericType, WrappedChatComponent specific) {
return specific.getHandle();
}
@Override
protected WrappedChatComponent getSpecificValue(Object generic) {
return WrappedChatComponent.fromHandle(generic);
}
@Override
public Class<WrappedChatComponent> getSpecificType() {
return WrappedChatComponent.class;
}
};
}
/** /**
* Retrieve a converter for wrapped attribute snapshots. * Retrieve a converter for wrapped attribute snapshots.
* @return Wrapped attribute snapshot converter. * @return Wrapped attribute snapshot converter.
@ -601,6 +624,12 @@ public class BukkitConverters {
put(WrappedWatchableObject.class, (EquivalentConverter) getWatchableObjectConverter()). put(WrappedWatchableObject.class, (EquivalentConverter) getWatchableObjectConverter()).
put(PotionEffect.class, (EquivalentConverter) getPotionEffectConverter()); 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) if (hasWorldType)
builder.put(WorldType.class, (EquivalentConverter) getWorldTypeConverter()); builder.put(WorldType.class, (EquivalentConverter) getWorldTypeConverter());
if (hasAttributeSnapshot) if (hasAttributeSnapshot)
@ -631,6 +660,12 @@ public class BukkitConverters {
builder.put(MinecraftReflection.getWorldTypeClass(), (EquivalentConverter) getWorldTypeConverter()); builder.put(MinecraftReflection.getWorldTypeClass(), (EquivalentConverter) getWorldTypeConverter());
if (hasAttributeSnapshot) if (hasAttributeSnapshot)
builder.put(MinecraftReflection.getAttributeSnapshotClass(), (EquivalentConverter) getWrappedAttributeConverter()); 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(); genericConverters = builder.build();
} }
return genericConverters; return genericConverters;

View File

@ -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.
* <p>
* 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.
* <p>
* 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();
}
}

View File

@ -32,6 +32,7 @@ import org.apache.commons.lang.builder.ToStringStyle;
// Will have to be updated for every version though // Will have to be updated for every version though
import org.bukkit.craftbukkit.v1_7_R1.inventory.CraftItemFactory; import org.bukkit.craftbukkit.v1_7_R1.inventory.CraftItemFactory;
import org.bukkit.ChatColor;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.WorldType; import org.bukkit.WorldType;
import org.bukkit.inventory.ItemStack; 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.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.BukkitConverters; import com.comphenix.protocol.wrappers.BukkitConverters;
import com.comphenix.protocol.wrappers.ChunkPosition; import com.comphenix.protocol.wrappers.ChunkPosition;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.comphenix.protocol.wrappers.WrappedDataWatcher; import com.comphenix.protocol.wrappers.WrappedDataWatcher;
import com.comphenix.protocol.wrappers.WrappedGameProfile; import com.comphenix.protocol.wrappers.WrappedGameProfile;
import com.comphenix.protocol.wrappers.WrappedWatchableObject; import com.comphenix.protocol.wrappers.WrappedWatchableObject;
@ -336,6 +338,16 @@ public class PacketContainerTest {
assertEquals(profile, spawnEntity.getGameProfiles().read(0)); 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 @Test
public void testSerialization() { public void testSerialization() {
PacketContainer chat = new PacketContainer(PacketType.Play.Client.CHAT); PacketContainer chat = new PacketContainer(PacketType.Play.Client.CHAT);

View File

@ -2,6 +2,8 @@ package com.comphenix.protocol.utility;
import static org.junit.Assert.*; 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 net.minecraft.server.v1_7_R1.NBTCompressedStreamTools;
import org.junit.AfterClass; import org.junit.AfterClass;
@ -32,4 +34,14 @@ public class MinecraftReflectionTest {
public void testNbtStreamTools() { public void testNbtStreamTools() {
assertEquals(NBTCompressedStreamTools.class, MinecraftReflection.getNbtCompressedStreamToolsClass()); assertEquals(NBTCompressedStreamTools.class, MinecraftReflection.getNbtCompressedStreamToolsClass());
} }
@Test
public void testChatComponent() {
assertEquals(IChatBaseComponent.class, MinecraftReflection.getIChatBaseComponent());
}
@Test
public void testChatSerializer() {
assertEquals(ChatSerializer.class, MinecraftReflection.getChatSerializer());
}
} }