Remove all reflection for sending fancy messages

- Reflection removed
- JSON is now fully escaped instead of lenient escaping
- Sending is now done with /tellraw
This commit is contained in:
Thijs Wiefferink 2016-08-01 21:31:14 +02:00
parent 6493bc4f78
commit e957b4fc73
5 changed files with 18 additions and 440 deletions

View File

@ -1,108 +0,0 @@
package me.wiefferink.areashop.messages;
import org.apache.commons.lang.Validate;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collection;
/**
* Class made by glan3b (Github: https://github.com/glen3b) for the Fanciful project (Github: https://github.com/mkremins/fanciful)
*/
/**
* Represents a wrapper around an array class of an arbitrary reference type,
* which properly implements "value" hash code and equality functions.
* <p>
* This class is intended for use as a key to a map.
* </p>
* @param <E> The type of elements in the array.
* @author Glen Husman
* @see Arrays
*/
public final class ArrayWrapper<E> {
/**
* Creates an array wrapper with some elements.
* @param elements The elements of the array.
*/
public ArrayWrapper(E... elements) {
setArray(elements);
}
private E[] _array;
/**
* Retrieves a reference to the wrapped array instance.
* @return The array wrapped by this instance.
*/
public E[] getArray() {
return _array;
}
/**
* Set this wrapper to wrap a new array instance.
* @param array The new wrapped array.
*/
public void setArray(E[] array) {
Validate.notNull(array, "The array must not be null.");
_array = array;
}
/**
* Determines if this object has a value equivalent to another object.
* @see Arrays#equals(Object[], Object[])
*/
@SuppressWarnings("rawtypes")
@Override
public boolean equals(Object other) {
if(!(other instanceof ArrayWrapper)) {
return false;
}
return Arrays.equals(_array, ((ArrayWrapper)other)._array);
}
/**
* Gets the hash code represented by this objects value.
* @return This object's hash code.
* @see Arrays#hashCode(Object[])
*/
@Override
public int hashCode() {
return Arrays.hashCode(_array);
}
/**
* Converts an iterable element collection to an array of elements.
* The iteration order of the specified object will be used as the array element order.
* @param list The iterable of objects which will be converted to an array.
* @param c The type of the elements of the array.
* @param <T> The type
* @return An array of elements in the specified iterable.
*/
@SuppressWarnings("unchecked")
public static <T> T[] toArray(Iterable<? extends T> list, Class<T> c) {
int size = -1;
if(list instanceof Collection<?>) {
@SuppressWarnings("rawtypes")
Collection coll = (Collection)list;
size = coll.size();
}
if(size < 0) {
size = 0;
// Ugly hack: Count it ourselves
for(@SuppressWarnings("unused") T element : list) {
size++;
}
}
T[] result = (T[])Array.newInstance(c, size);
int i = 0;
for(T element : list) { // Assumes iteration order is consistent
result[i++] = element; // Assign array element at index THEN increment counter
}
return result;
}
}

View File

@ -88,10 +88,11 @@ public class FancyMessageFormat {
LinkedList<InteractiveMessagePart> message = parse(lines);
StringBuilder sb = new StringBuilder();
sb.append("[");
if(message.size() == 1) {
sb.append(message.getFirst().toJSON());
} else if(message.size() > 0) {
sb.append("{text=\"\",extra:[");
sb.append("{\"text\":\"\",\"extra\":[");
for(InteractiveMessagePart messagePart : message) {
sb.append(messagePart.toJSON());
sb.append(',');
@ -99,6 +100,7 @@ public class FancyMessageFormat {
sb.deleteCharAt(sb.length()-1);
sb.append("]}");
}
sb.append("]");
return sb.toString();
}
@ -556,13 +558,13 @@ public class FancyMessageFormat {
String toJSON() {
StringBuilder sb = new StringBuilder();
sb.append('{');
sb.append("text:").append(quoteStringJson(text));
sb.append("\"text\":").append(quoteStringJson(text));
if(color != null && color != Color.WHITE) {
sb.append(",color:").append(color.jsonValue);
sb.append(",\"color\":\"").append(color.jsonValue).append("\"");
}
for(FormatType formatting : formatTypes) {
sb.append(',');
sb.append(formatting.jsonKey).append(':');
sb.append(",\"");
sb.append(formatting.jsonKey).append("\":");
sb.append("true");
}
sb.append('}');
@ -619,7 +621,7 @@ public class FancyMessageFormat {
sb.deleteCharAt(sb.length()-1);
} else {
sb.append('{');
sb.append("text=\"\",extra:[");
sb.append("\"text\":\"\",\"extra\":[");
for(TextMessagePart textPart : content) {
sb.append(textPart.toJSON());
sb.append(',');
@ -629,16 +631,16 @@ public class FancyMessageFormat {
}
if(clickType != null) {
sb.append(',');
sb.append("clickEvent:{");
sb.append("action:").append(clickType.getJsonKey()).append(',');
sb.append("value:").append(quoteStringJson(clickContent));
sb.append("\"clickEvent\":{");
sb.append("\"action\":\"").append(clickType.getJsonKey()).append("\",");
sb.append("\"value\":").append(quoteStringJson(clickContent));
sb.append('}');
}
if(hoverType != null) {
sb.append(',');
sb.append("hoverEvent:{");
sb.append("action:").append(hoverType.getJsonKey()).append(',');
sb.append("value:");
sb.append("\"hoverEvent\":{");
sb.append("\"action\":\"").append(hoverType.getJsonKey()).append("\",");
sb.append("\"value\":");
if(hoverContent.size() == 1) {
TextMessagePart hoverPart = hoverContent.getFirst();
if(hoverPart.hasFormatting()) {

View File

@ -1,100 +0,0 @@
package me.wiefferink.areashop.messages;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.lang.reflect.*;
import java.util.logging.Level;
/**
* Methods written by the Fanciful project (Github: https://github.com/mkremins/fanciful)
*/
public class FancyMessageSender {
private static Constructor<?> nmsPacketPlayOutChatConstructor;
public static boolean sendJSON(Player player, String jsonString) {
try {
Object handle = Reflection.getHandle(player);
Object connection = Reflection.getField(handle.getClass(), "playerConnection").get(handle);
Reflection.getMethod(connection.getClass(), "sendPacket", Reflection.getNMSClass("Packet")).invoke(connection, createChatPacket(jsonString));
return true;
} catch(IllegalArgumentException e) {
Bukkit.getLogger().log(Level.WARNING, "Argument could not be passed.", e);
} catch(IllegalAccessException e) {
Bukkit.getLogger().log(Level.WARNING, "Could not access method.", e);
} catch(InstantiationException e) {
Bukkit.getLogger().log(Level.WARNING, "Underlying class is abstract.", e);
} catch(InvocationTargetException e) {
Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", e);
} catch(NoSuchMethodException e) {
Bukkit.getLogger().log(Level.WARNING, "Could not find method.", e);
} catch(ClassNotFoundException e) {
Bukkit.getLogger().log(Level.WARNING, "Could not find class.", e);
}
return false;
}
// The ChatSerializer's instance of Gson
private static Object nmsChatSerializerGsonInstance;
private static Method fromJsonMethod;
private static Object createChatPacket(String json) throws IllegalArgumentException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, ClassNotFoundException {
if(nmsChatSerializerGsonInstance == null) {
// Find the field and its value, completely bypassing obfuscation
Class<?> chatSerializerClazz;
String version = Reflection.getVersion();
if(version == null || version.length() < 2) {
throw new RuntimeException("Could not get NMS version, found: "+version);
}
version = version.substring(1, version.length()-1); // Strip v and the . at the end
String[] parts = version.split("_");
if(parts.length < 3) {
throw new RuntimeException("Not enough parts in the version, found: "+version);
}
int majorVersion = Integer.parseInt(parts[0]);
int minorVersion = Integer.parseInt(parts[1]);
int revision = Integer.parseInt(parts[2].substring(1));
if((majorVersion <= 1 && minorVersion < 8) || (majorVersion == 1 && minorVersion == 8 && revision == 1)) {
chatSerializerClazz = Reflection.getNMSClass("ChatSerializer");
} else {
chatSerializerClazz = Reflection.getNMSClass("IChatBaseComponent$ChatSerializer");
}
if(chatSerializerClazz == null) {
throw new ClassNotFoundException("Can't find the ChatSerializer class");
}
for(Field declaredField : chatSerializerClazz.getDeclaredFields()) {
if(Modifier.isFinal(declaredField.getModifiers()) && Modifier.isStatic(declaredField.getModifiers()) && declaredField.getType().getName().endsWith("Gson")) {
// We've found our field
declaredField.setAccessible(true);
nmsChatSerializerGsonInstance = declaredField.get(null);
fromJsonMethod = nmsChatSerializerGsonInstance.getClass().getMethod("fromJson", String.class, Class.class);
break;
}
}
}
// Since the method is so simple, and all the obfuscated methods have the same name, it's easier to reimplement 'IChatBaseComponent a(String)' than to reflectively call it
// Of course, the implementation may change, but fuzzy matches might break with signature changes
Object serializedChatComponent = fromJsonMethod.invoke(nmsChatSerializerGsonInstance, json, Reflection.getNMSClass("IChatBaseComponent"));
if(nmsPacketPlayOutChatConstructor == null) {
try {
nmsPacketPlayOutChatConstructor = Reflection.getNMSClass("PacketPlayOutChat").getDeclaredConstructor(Reflection.getNMSClass("IChatBaseComponent"));
nmsPacketPlayOutChatConstructor.setAccessible(true);
} catch(NoSuchMethodException e) {
Bukkit.getLogger().log(Level.SEVERE, "Could not find Minecraft method or constructor.", e);
} catch(SecurityException e) {
Bukkit.getLogger().log(Level.WARNING, "Could not access constructor.", e);
}
}
return nmsPacketPlayOutChatConstructor.newInstance(serializedChatComponent);
}
}

View File

@ -3,6 +3,8 @@ package me.wiefferink.areashop.messages;
import me.wiefferink.areashop.AreaShop;
import me.wiefferink.areashop.Utils;
import me.wiefferink.areashop.regions.GeneralRegion;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@ -182,12 +184,13 @@ public class Message {
AreaShop.getInstance().getLogger().severe("Message with key "+key+" could not be send, results in a JSON string that is too big to send to the client, start of the message: "+Utils.getMessageStart(this, 100));
return this;
}
boolean result = FancyMessageSender.sendJSON((Player)target, jsonString);
boolean result = Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "tellraw "+((Player)target).getName()+" "+jsonString);
sendPlain = !result;
fancyWorks = result;
} catch(Exception e) {
fancyWorks = false;
AreaShop.getInstance().getLogger().warning("Sending fancy message did not work, falling back to plain messages. Message key: "+key);
AreaShop.debug(ExceptionUtils.getStackTrace(e));
}
}
if(sendPlain) { // Fancy messages disabled or broken

View File

@ -1,219 +0,0 @@
package me.wiefferink.areashop.messages;
import org.bukkit.Bukkit;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* Class made by glan3b (Github: https://github.com/glen3b) for the Fanciful project (Github: https://github.com/mkremins/fanciful)
*/
/**
* A class containing static utility methods and caches which are intended as reflective conveniences.
* Unless otherwise noted, upon failure methods will return {@code null}.
*/
public final class Reflection {
private static String _versionString;
private Reflection() {
}
/**
* Gets the version string from the package name of the CraftBukkit server implementation.
* This is needed to bypass the JAR package name changing on each update.
* @return The version string of the OBC and NMS packages, <em>including the trailing dot</em>.
*/
public synchronized static String getVersion() {
if(_versionString == null) {
if(Bukkit.getServer() == null) {
// The server hasn't started, static initializer call?
return null;
}
String name = Bukkit.getServer().getClass().getPackage().getName();
_versionString = name.substring(name.lastIndexOf('.')+1)+".";
}
return _versionString;
}
/**
* Stores loaded classes from the {@code net.minecraft.server} package.
*/
private static final Map<String, Class<?>> _loadedNMSClasses = new HashMap<String, Class<?>>();
/**
* Stores loaded classes from the {@code org.bukkit.craftbukkit} package (and subpackages).
*/
private static final Map<String, Class<?>> _loadedOBCClasses = new HashMap<String, Class<?>>();
/**
* Gets a {@link Class} object representing a type contained within the {@code net.minecraft.server} versioned package.
* The class instances returned by this method are cached, such that no lookup will be done twice (unless multiple threads are accessing this method simultaneously).
* @param className The name of the class, excluding the package, within NMS.
* @return The class instance representing the specified NMS class, or {@code null} if it could not be loaded.
*/
public synchronized static Class<?> getNMSClass(String className) {
if(_loadedNMSClasses.containsKey(className)) {
return _loadedNMSClasses.get(className);
}
String fullName = "net.minecraft.server."+getVersion()+className;
Class<?> clazz = null;
try {
clazz = Class.forName(fullName);
} catch(Exception e) {
e.printStackTrace();
_loadedNMSClasses.put(className, null);
return null;
}
_loadedNMSClasses.put(className, clazz);
return clazz;
}
/**
* Gets a {@link Class} object representing a type contained within the {@code org.bukkit.craftbukkit} versioned package.
* The class instances returned by this method are cached, such that no lookup will be done twice (unless multiple threads are accessing this method simultaneously).
* @param className The name of the class, excluding the package, within OBC. This name may contain a subpackage name, such as {@code inventory.CraftItemStack}.
* @return The class instance representing the specified OBC class, or {@code null} if it could not be loaded.
*/
public synchronized static Class<?> getOBCClass(String className) {
if(_loadedOBCClasses.containsKey(className)) {
return _loadedOBCClasses.get(className);
}
String fullName = "org.bukkit.craftbukkit."+getVersion()+className;
Class<?> clazz = null;
try {
clazz = Class.forName(fullName);
} catch(Exception e) {
e.printStackTrace();
_loadedOBCClasses.put(className, null);
return null;
}
_loadedOBCClasses.put(className, clazz);
return clazz;
}
/**
* Attempts to get the NMS handle of a CraftBukkit object.
* <p>
* The only match currently attempted by this method is a retrieval by using a parameterless {@code getHandle()} method implemented by the runtime type of the specified object.
* </p>
* @param obj The object for which to retrieve an NMS handle.
* @return The NMS handle of the specified object, or {@code null} if it could not be retrieved using {@code getHandle()}.
*/
public synchronized static Object getHandle(Object obj) {
try {
return getMethod(obj.getClass(), "getHandle").invoke(obj);
} catch(Exception e) {
e.printStackTrace();
return null;
}
}
private static final Map<Class<?>, Map<String, Field>> _loadedFields = new HashMap<Class<?>, Map<String, Field>>();
/**
* Retrieves a {@link Field} instance declared by the specified class with the specified name.
* Java access modifiers are ignored during this retrieval. No guarantee is made as to whether the field
* returned will be an instance or static field.
* <p>
* A global caching mechanism within this class is used to store fields. Combined with synchronization, this guarantees that
* no field will be reflectively looked up twice.
* </p>
* <p>
* If a field is deemed suitable for return, {@link Field#setAccessible(boolean) setAccessible} will be invoked with an argument of {@code true} before it is returned.
* This ensures that callers do not have to check or worry about Java access modifiers when dealing with the returned instance.
* </p>
* @param clazz The class which contains the field to retrieve.
* @param name The declared name of the field in the class.
* @return A field object with the specified name declared by the specified class.
* @see Class#getDeclaredField(String)
*/
public synchronized static Field getField(Class<?> clazz, String name) {
Map<String, Field> loaded;
if(!_loadedFields.containsKey(clazz)) {
loaded = new HashMap<String, Field>();
_loadedFields.put(clazz, loaded);
} else {
loaded = _loadedFields.get(clazz);
}
if(loaded.containsKey(name)) {
// If the field is loaded (or cached as not existing), return the relevant value, which might be null
return loaded.get(name);
}
try {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
loaded.put(name, field);
return field;
} catch(Exception e) {
// Error loading
e.printStackTrace();
// Cache field as not existing
loaded.put(name, null);
return null;
}
}
/**
* Contains loaded methods in a cache.
* The map maps [types to maps of [method names to maps of [parameter types to method instances]]].
*/
private static final Map<Class<?>, Map<String, Map<ArrayWrapper<Class<?>>, Method>>> _loadedMethods = new HashMap<Class<?>, Map<String, Map<ArrayWrapper<Class<?>>, Method>>>();
/**
* Retrieves a {@link Method} instance declared by the specified class with the specified name and argument types.
* Java access modifiers are ignored during this retrieval. No guarantee is made as to whether the field
* returned will be an instance or static field.
* <p>
* A global caching mechanism within this class is used to store method. Combined with synchronization, this guarantees that
* no method will be reflectively looked up twice.
* </p>
* <p>
* If a method is deemed suitable for return, {@link Method#setAccessible(boolean) setAccessible} will be invoked with an argument of {@code true} before it is returned.
* This ensures that callers do not have to check or worry about Java access modifiers when dealing with the returned instance.
* </p>
* <p>
* This method does <em>not</em> search superclasses of the specified type for methods with the specified signature.
* Callers wishing this behavior should use {@link Class#getDeclaredMethod(String, Class...)}.
* @param clazz The class which contains the method to retrieve.
* @param name The declared name of the method in the class.
* @param args The formal argument types of the method.
* @return A method object with the specified name declared by the specified class.
*/
public synchronized static Method getMethod(Class<?> clazz, String name,
Class<?>... args) {
if(!_loadedMethods.containsKey(clazz)) {
_loadedMethods.put(clazz, new HashMap<String, Map<ArrayWrapper<Class<?>>, Method>>());
}
Map<String, Map<ArrayWrapper<Class<?>>, Method>> loadedMethodNames = _loadedMethods.get(clazz);
if(!loadedMethodNames.containsKey(name)) {
loadedMethodNames.put(name, new HashMap<ArrayWrapper<Class<?>>, Method>());
}
Map<ArrayWrapper<Class<?>>, Method> loadedSignatures = loadedMethodNames.get(name);
ArrayWrapper<Class<?>> wrappedArg = new ArrayWrapper<Class<?>>(args);
if(loadedSignatures.containsKey(wrappedArg)) {
return loadedSignatures.get(wrappedArg);
}
for(Method m : clazz.getMethods()) {
if(m.getName().equals(name) && Arrays.equals(args, m.getParameterTypes())) {
m.setAccessible(true);
loadedSignatures.put(wrappedArg, m);
return m;
}
}
loadedSignatures.put(wrappedArg, null);
return null;
}
}