From 752807fa5fe12aa0e8fef66191531287ce629e33 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Sat, 1 Feb 2014 16:38:24 +0100 Subject: [PATCH] Split up TinyProtocol and its built-in reflection system. --- .../comphenix/tinyprotocol/ExamplePlugin.java | 18 +- .../comphenix/tinyprotocol/Reflection.java | 294 +++++++++++++++ .../comphenix/tinyprotocol/TinyProtocol.java | 342 +++++------------- 3 files changed, 400 insertions(+), 254 deletions(-) create mode 100644 Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/Reflection.java diff --git a/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/ExamplePlugin.java b/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/ExamplePlugin.java index dc67b2ce..f0bd5c31 100644 --- a/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/ExamplePlugin.java +++ b/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/ExamplePlugin.java @@ -4,20 +4,20 @@ import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; -import com.comphenix.tinyprotocol.TinyProtocol.FieldAccessor; +import com.comphenix.tinyprotocol.Reflection.FieldAccessor; public class ExamplePlugin extends JavaPlugin { // Chat packets - private FieldAccessor CHAT_MESSAGE = TinyProtocol.getField( - "PacketPlayInChat", String.class, 0); + private FieldAccessor CHAT_MESSAGE = Reflection.getField( + "{nms}.PacketPlayInChat", String.class, 0); // Explosion packet - private Class particleClass = TinyProtocol.getMinecraftClass("PacketPlayOutWorldParticles"); - private FieldAccessor particleName = TinyProtocol.getField(particleClass, String.class, 0); - private FieldAccessor particleX = TinyProtocol.getField(particleClass, float.class, 0); - private FieldAccessor particleY = TinyProtocol.getField(particleClass, float.class, 1); - private FieldAccessor particleZ = TinyProtocol.getField(particleClass, float.class, 2); - private FieldAccessor particleCount = TinyProtocol.getField(particleClass, int.class, 0); + private Class particleClass = Reflection.getClass("{nms}.PacketPlayOutWorldParticles"); + private FieldAccessor particleName = Reflection.getField(particleClass, String.class, 0); + private FieldAccessor particleX = Reflection.getField(particleClass, float.class, 0); + private FieldAccessor particleY = Reflection.getField(particleClass, float.class, 1); + private FieldAccessor particleZ = Reflection.getField(particleClass, float.class, 2); + private FieldAccessor particleCount = Reflection.getField(particleClass, int.class, 0); private TinyProtocol protocol; diff --git a/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/Reflection.java b/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/Reflection.java new file mode 100644 index 00000000..d91c6571 --- /dev/null +++ b/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/Reflection.java @@ -0,0 +1,294 @@ +package com.comphenix.tinyprotocol; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.bukkit.Bukkit; + +/** + * An utility class that simplifies reflection in Bukkit plugins. + * @author Kristian + */ +public final class Reflection { + /** + * An interface for invoking a specific method. + */ + public interface MethodInvoker { + /** + * Invoke a method on a specific target object. + * @param target - the target object, or NULL for a static method. + * @param arguments - the arguments to pass to the method. + * @return The return value, or NULL if is void. + */ + public Object invoke(Object target, Object... arguments); + } + + /** + * An interface for retrieving the field content. + * @param - field type. + */ + public interface FieldAccessor { + /** + * Retrieve the content of a field. + * @param target - the target object, or NULL for a static field. + * @return The value of the field. + */ + public T get(Object target); + + /** + * Set the content of a field. + * @param target - the target object, or NULL for a static field. + * @param value - the new value of the field. + */ + public void set(Object target, Object value); + + /** + * Determine if the given object has this field. + * @param target - the object to test. + * @return TRUE if it does, FALSE otherwise. + */ + public boolean hasField(Object target); + } + + // Deduce the net.minecraft.server.v* package + private static String OBC_PREFIX = Bukkit.getServer().getClass().getPackage().getName(); + private static String NMS_PREFIX = OBC_PREFIX.replace("org.bukkit.craftbukkit", "net.minecraft.server"); + private static String VERSION = OBC_PREFIX.replace("org.bukkit.craftbukkit", "").replace(".", ""); + + // Variable replacement + private static Pattern MATCH_VARIABLE = Pattern.compile("\\{([^\\}]+)\\}"); + + private Reflection() { + // Seal class + } + + /** + * Retrieve a field accessor for a specific field type and name. + * @param target - the target type. + * @param name - the name of the field, or NULL to ignore. + * @param fieldType - a compatible field type. + * @return The field accessor. + */ + public static FieldAccessor getField(Class target, String name, Class fieldType) { + return getField(target, name, fieldType, 0); + } + + /** + * Retrieve a field accessor for a specific field type and name. + * @param className - lookup name of the class, see {@link #getClass(String)}. + * @param name - the name of the field, or NULL to ignore. + * @param fieldType - a compatible field type. + * @return The field accessor. + */ + public static FieldAccessor getField(String className, String name, Class fieldType) { + return getField(getClass(className), name, fieldType, 0); + } + + /** + * Retrieve a field accessor for a specific field type and name. + * @param target - the target type. + * @param fieldType - a compatible field type. + * @param index - the number of compatible fields to skip. + * @return The field accessor. + */ + public static FieldAccessor getField(Class target, Class fieldType, int index) { + return getField(target, null, fieldType, index); + } + + /** + * Retrieve a field accessor for a specific field type and name. + * @param className - lookup name of the class, see {@link #getClass(String)}. + * @param fieldType - a compatible field type. + * @param index - the number of compatible fields to skip. + * @return The field accessor. + */ + public static FieldAccessor getField(String className, Class fieldType, int index) { + return getField(getClass(className), fieldType, index); + } + + // Common method + private static FieldAccessor getField(Class target, String name, Class fieldType, int index) { + for (final Field field : target.getDeclaredFields()) { + if ((name == null || field.getName().equals(name)) && + fieldType.isAssignableFrom(field.getType()) && index-- <= 0) { + field.setAccessible(true); + + // A function for retrieving a specific field value + return new FieldAccessor() { + @SuppressWarnings("unchecked") + @Override + public T get(Object target) { + try { + return (T) field.get(target); + } catch (IllegalAccessException e) { + throw new RuntimeException("Cannot access reflection.", e); + } + } + + @Override + public void set(Object target, Object value) { + try { + field.set(target, value); + } catch (IllegalAccessException e) { + throw new RuntimeException("Cannot access reflection.", e); + } + } + + @Override + public boolean hasField(Object target) { + // target instanceof DeclaringClass + return field.getDeclaringClass().isAssignableFrom(target.getClass()); + } + }; + } + } + + // Search in parent classes + if (target.getSuperclass() != null) + return getField(target.getSuperclass(), name, fieldType, index); + throw new IllegalArgumentException("Cannot find field with type " + fieldType); + } + + /** + * Search for the first publically and privately defined method of the given name and parameter count. + * @param className - lookup name of the class, see {@link #getClass(String)}. + * @param methodName - the method name, or NULL to skip. + * @param params - the expected parameters. + * @return An object that invokes this specific method. + * @throws IllegalStateException If we cannot find this method. + */ + public static MethodInvoker getMethod(String className, String methodName, Class... params) { + return getMethod(getClass(className), methodName, params); + } + + /** + * Search for the first publically and privately defined method of the given name and parameter count. + * @param clazz - a class to start with. + * @param methodName - the method name, or NULL to skip. + * @param params - the expected parameters. + * @return An object that invokes this specific method. + * @throws IllegalStateException If we cannot find this method. + */ + public static MethodInvoker getMethod(Class clazz, String methodName, Class... params) { + for (final Method method : clazz.getDeclaredMethods()) { + if ((methodName == null || method.getName().equals(methodName)) && + Arrays.equals(method.getParameterTypes(), params)) { + + method.setAccessible(true); + return new MethodInvoker() { + @Override + public Object invoke(Object target, Object... arguments) { + try { + return method.invoke(target, arguments); + } catch (Exception e) { + throw new RuntimeException("Cannot invoke method " + method, e); + } + } + }; + } + } + // Search in every superclass + if (clazz.getSuperclass() != null) + return getMethod(clazz.getSuperclass(), methodName, params); + throw new IllegalStateException(String.format( + "Unable to find method %s (%s).", methodName, Arrays.asList(params))); + } + + /** + * Retrieve a class from its full name. + *

+ * Strings enclosed with curly brackets - such as {TEXT} - will be replaced according + * to the following table: + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
VariableContent
{nms}Actual package name of net.minecraft.server.VERSION
{obc}Actual pacakge name of org.bukkit.craftbukkit.VERSION
{version}The current Minecraft package VERSION, if any.
+ * @param lookupName - the class name with variables. + * @return The looked up class. + * @throws IllegalArgumentException If a variable or class could not be found. + */ + public static Class getClass(String lookupName) { + return getCanonicalClass(expandVariables(lookupName)); + } + + /** + * Retrieve a class in the net.minecraft.server.VERSION.* package. + * @param name - the name of the class, excluding the package. + * @throws IllegalArgumentException If the class doesn't exist. + */ + public static Class getMinecraftClass(String name) { + return getCanonicalClass(NMS_PREFIX + "." + name); + } + + /** + * Retrieve a class in the org.bukkit.craftbukkit.VERSION.* package. + * @param name - the name of the class, excluding the package. + * @throws IllegalArgumentException If the class doesn't exist. + */ + public static Class getCraftBukkitClass(String name) { + return getCanonicalClass(OBC_PREFIX + "." + name); + } + + /** + * Retrieve a class by its canonical name. + * @param canonicalName - the canonical name. + * @return The class. + */ + private static Class getCanonicalClass(String canonicalName) { + try { + return Class.forName(canonicalName); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Cannot find " + canonicalName, e); + } + } + + /** + * Expand variables such as "{nms}" and "{obc}" to their corresponding packages. + * @param name - the full name of the class. + * @return The expanded string. + */ + private static String expandVariables(String name) { + StringBuffer output = new StringBuffer(); + Matcher matcher = MATCH_VARIABLE.matcher(name); + + while (matcher.find()) { + String variable = matcher.group(1); + String replacement = ""; + + // Expand all detected variables + if ("nms".equalsIgnoreCase(variable)) + replacement = NMS_PREFIX; + else if ("obc".equalsIgnoreCase(variable)) + replacement = OBC_PREFIX; + else if ("version".equalsIgnoreCase(variable)) + replacement = VERSION; + else + throw new IllegalArgumentException("Unknown variable: " + variable); + + // Assume the expanded variables are all packages, and append a dot + if (replacement.length() > 0 && matcher.end() < name.length() && name.charAt(matcher.end()) != '.') + replacement += "."; + matcher.appendReplacement(output, replacement); + } + matcher.appendTail(output); + return output.toString(); + } +} diff --git a/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/TinyProtocol.java b/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/TinyProtocol.java index ef28c4fa..12e4bc8c 100644 --- a/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/TinyProtocol.java +++ b/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/TinyProtocol.java @@ -1,8 +1,5 @@ package com.comphenix.tinyprotocol; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.Arrays; import java.util.Map; import java.util.logging.Level; @@ -12,44 +9,108 @@ import net.minecraft.util.io.netty.channel.ChannelDuplexHandler; import net.minecraft.util.io.netty.channel.ChannelHandlerContext; import net.minecraft.util.io.netty.channel.ChannelPromise; -import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; +import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.server.PluginDisableEvent; import org.bukkit.plugin.Plugin; +import com.comphenix.tinyprotocol.Reflection.FieldAccessor; +import com.comphenix.tinyprotocol.Reflection.MethodInvoker; import com.google.common.collect.MapMaker; -public abstract class TinyProtocol implements Listener { - // Deduce the net.minecraft.server.v* package - private static String OBC_PREFIX = Bukkit.getServer().getClass().getPackage().getName(); - private static String NMS_PREFIX = OBC_PREFIX.replace("org.bukkit.craftbukkit", "net.minecraft.server"); - +/** + * Represents a very tiny alternative to ProtocolLib in 1.7.2. + *

+ * Note that it does not support intercepting packets sent during login (such as OUT_SERVER_PING). + * @author Kristian + */ +public abstract class TinyProtocol { // Used in order to lookup a channel - private MethodInvoker getPlayerHandle = getMethod(getCraftBukkitClass("entity.CraftPlayer"), "getHandle"); - private FieldAccessor getConnection = getField(getMinecraftClass("EntityPlayer"), "playerConnection", Object.class); - private FieldAccessor getManager = getField(getMinecraftClass("PlayerConnection"), "networkManager", Object.class); - private FieldAccessor getChannel = getField(getMinecraftClass("NetworkManager"), Channel.class, 0); + private MethodInvoker getPlayerHandle = Reflection.getMethod("{obc}.entity.CraftPlayer", "getHandle"); + private FieldAccessor getConnection = Reflection.getField("{nms}.EntityPlayer", "playerConnection", Object.class); + private FieldAccessor getManager = Reflection.getField("{nms}.PlayerConnection", "networkManager", Object.class); + private FieldAccessor getChannel = Reflection.getField("{nms}.NetworkManager", Channel.class, 0); // Speedup channel lookup private Map channelLookup = new MapMaker().weakKeys().makeMap(); + private Listener listener; - private boolean closed; - private Plugin plugin; + protected boolean closed; + protected Plugin plugin; public TinyProtocol(Plugin plugin) { this.plugin = plugin; - this.plugin.getServer().getPluginManager().registerEvents(this, plugin); + this.plugin.getServer().getPluginManager().registerEvents( + listener = createListener(), plugin); // Prepare existing players for (Player player : plugin.getServer().getOnlinePlayers()) { injectPlayer(player); } } + + /** + * Invoked when the server is starting to send a packet to a player. + *

+ * Note that this is not executed on the main thread. + * @param reciever - the receiving player. + * @param packet - the packet being sent. + * @return The packet to send instead, or NULL to cancel the transmission. + */ + public Object onPacketOutAsync(Player reciever, Object packet) { + return packet; + } + + /** + * Invoked when the server has received a packet from a given player. + * @param sender - the player that sent the packet. + * @param packet - the packet being sent. + * @return The packet to recieve instead, or NULL to cancel. + */ + public Object onPacketInAsync(Player sender, Object packet) { + return packet; + } + /** + * Send a packet to a particular player. + *

+ * Note that {@link #onPacketOutAsync(Player, Object)} will be invoked with this packet. + * @param player - the destination player. + * @param packet - the packet to send. + */ + public void sendPacket(Player player, Object packet) { + getChannel(player).pipeline().writeAndFlush(packet); + } + + /** + * Pretend that a given packet has been received from a player. + *

+ * Note that {@link #onPacketInAsync(Player, Object)} will be invoked with this packet. + * @param player - the player that sent the packet. + * @param packet - the packet that will be received by the server. + */ + public void receivePacket(Player player, Object packet) { + getChannel(player).pipeline().context("encoder").fireChannelRead(packet); + } + + /** + * Retrieve the name of the channel injector, default implementation is "tiny-" + plugin name. + *

+ * Override this if you have multiple instances of TinyProtocol, and return a unique string per instance. + * @return A unique channel handler name. + */ + protected String getHandlerName() { + return "tiny-" + plugin.getName(); + } + + /** + * Add a custom channel handler to the given player's channel pipeline, allowing us to intercept sent and received packets. + * @param player - the player to inject. + */ private void injectPlayer(final Player player) { // Inject our packet interceptor getChannel(player).pipeline().addBefore("packet_handler", getHandlerName(), new ChannelDuplexHandler() { @@ -80,42 +141,6 @@ public abstract class TinyProtocol implements Listener { } }); } - - private String getHandlerName() { - return "tiny-" + plugin.getName(); - } - - @EventHandler(priority = EventPriority.LOWEST) - public final void onPlayerJoin(PlayerJoinEvent e) { - if (closed) - return; - injectPlayer(e.getPlayer()); - } - - @EventHandler - public final void onPluginDisable(PluginDisableEvent e) { - if (e.getPlugin().equals(plugin)) { - close(); - } - } - - /** - * Send a packet to a particular player. - * @param player - the destination player. - * @param packet - the packet to send. - */ - public void sendPacket(Player player, Object packet) { - getChannel(player).pipeline().writeAndFlush(packet); - } - - /** - * Pretend that a given packet has been received from a player. - * @param player - the player that sent the packet. - * @param packet - the packet that will be received by the server. - */ - public void receivePacket(Player player, Object packet) { - getChannel(player).pipeline().context("encoder").fireChannelRead(packet); - } /** * Retrieve the Netty channel associated with a player. This is cached. @@ -136,25 +161,24 @@ public abstract class TinyProtocol implements Listener { } /** - * Invoked when the server is starting to send a packet to a player. - *

- * Note that this is not executed on the main thread. - * @param reciever - the receiving player. - * @param packet - the packet being sent. - * @return The packet to send instead, or NULL to cancel the transmission. + * Create the Bukkit listener. */ - public Object onPacketOutAsync(Player reciever, Object packet) { - return packet; - } - - /** - * Invoked when the server has received a packet from a given player. - * @param sender - the player that sent the packet. - * @param packet - the packet being sent. - * @return The packet to recieve instead, or NULL to cancel. - */ - public Object onPacketInAsync(Player sender, Object packet) { - return packet; + private Listener createListener() { + return new Listener() { + @EventHandler(priority = EventPriority.LOWEST) + public final void onPlayerJoin(PlayerJoinEvent e) { + if (closed) + return; + injectPlayer(e.getPlayer()); + } + + @EventHandler + public final void onPluginDisable(PluginDisableEvent e) { + if (e.getPlugin().equals(plugin)) { + close(); + } + } + }; } /** @@ -168,181 +192,9 @@ public abstract class TinyProtocol implements Listener { for (Player player : plugin.getServer().getOnlinePlayers()) { getChannel(player).pipeline().remove(getHandlerName()); } + + // Clean up Bukkit + HandlerList.unregisterAll(listener); } } - - /** - * Retrieve a field accessor for a specific field type and name. - * @param target - the target type. - * @param name - the name of the field, or NULL to ignore. - * @param fieldType - a compatible field type. - * @return The field accessor. - */ - public static FieldAccessor getField(Class target, String name, Class fieldType) { - return getField(target, name, fieldType, 0); - } - - /** - * Retrieve a field accessor for a specific field type and name. - * @param target - the target type. - * @param fieldType - a compatible field type. - * @param index - the number of compatible fields to skip. - * @return The field accessor. - */ - public static FieldAccessor getField(Class target, Class fieldType, int index) { - return getField(target, null, fieldType, index); - } - - /** - * Retrieve a field accessor for a specific field type and name. - * @param nmsTargetClass - the net.minecraft.server class name. - * @param fieldType - a compatible field type. - * @param index - the number of compatible fields to skip. - * @return The field accessor. - */ - public static FieldAccessor getField(String nmsTargetClass, Class fieldType, int index) { - return getField(getMinecraftClass(nmsTargetClass), fieldType, index); - } - - // Common method - private static FieldAccessor getField(Class target, String name, Class fieldType, int index) { - for (final Field field : target.getDeclaredFields()) { - if ((name == null || field.getName().equals(name)) && - fieldType.isAssignableFrom(field.getType()) && index-- <= 0) { - field.setAccessible(true); - - // A function for retrieving a specific field value - return new FieldAccessor() { - @SuppressWarnings("unchecked") - @Override - public T get(Object target) { - try { - return (T) field.get(target); - } catch (IllegalAccessException e) { - throw new RuntimeException("Cannot access reflection.", e); - } - } - - @Override - public void set(Object target, Object value) { - try { - field.set(target, value); - } catch (IllegalAccessException e) { - throw new RuntimeException("Cannot access reflection.", e); - } - } - - @Override - public boolean hasField(Object target) { - // target instanceof DeclaringClass - return field.getDeclaringClass().isAssignableFrom(target.getClass()); - } - }; - } - } - - // Search in parent classes - if (target.getSuperclass() != null) - return getField(target.getSuperclass(), name, fieldType, index); - throw new IllegalArgumentException("Cannot find field with type " + fieldType); - } - - /** - * Search for the first publically and privately defined method of the given name and parameter count. - * @param clazz - a class to start with. - * @param methodName - the method name, or NULL to skip. - * @param params - the expected parameters. - * @return An object that invokes this specific method. - * @throws IllegalStateException If we cannot find this method. - */ - public static MethodInvoker getMethod(Class clazz, String methodName, Class... params) { - for (final Method method : clazz.getDeclaredMethods()) { - if ((methodName == null || method.getName().equals(methodName)) && - Arrays.equals(method.getParameterTypes(), params)) { - - method.setAccessible(true); - return new MethodInvoker() { - @Override - public Object invoke(Object target, Object... arguments) { - try { - return method.invoke(target, arguments); - } catch (Exception e) { - throw new RuntimeException("Cannot invoke method " + method, e); - } - } - }; - } - } - // Search in every superclass - if (clazz.getSuperclass() != null) - return getMethod(clazz.getSuperclass(), methodName, params); - throw new IllegalStateException(String.format( - "Unable to find method %s (%s).", methodName, Arrays.asList(params))); - } - - /** - * Retrieve a class in the net.minecraft.server.VERSION.* package. - * @param name - the name of the class, excluding the package. - * @throws IllegalArgumentException If the class doesn't exist. - */ - public static Class getMinecraftClass(String name) { - try { - return Class.forName(NMS_PREFIX + "." + name); - } catch (ClassNotFoundException e) { - throw new IllegalArgumentException("Cannot find nms." + name, e); - } - } - - /** - * Retrieve a class in the org.bukkit.craftbukkit.VERSION.* package. - * @param name - the name of the class, excluding the package. - * @throws IllegalArgumentException If the class doesn't exist. - */ - public static Class getCraftBukkitClass(String name) { - try { - return Class.forName(OBC_PREFIX + "." + name); - } catch (ClassNotFoundException e) { - throw new IllegalArgumentException("Cannot find obc." + name, e); - } - } - - /** - * An interface for invoking a specific method. - */ - public interface MethodInvoker { - /** - * Invoke a method on a specific target object. - * @param target - the target object, or NULL for a static method. - * @param arguments - the arguments to pass to the method. - * @return The return value, or NULL if is void. - */ - public Object invoke(Object target, Object... arguments); - } - - /** - * An interface for retrieving the field content. - * @param - field type. - */ - public interface FieldAccessor { - /** - * Retrieve the content of a field. - * @param target - the target object, or NULL for a static field. - * @return The value of the field. - */ - public T get(Object target); - - /** - * Set the content of a field. - * @param target - the target object, or NULL for a static field. - * @param value - the new value of the field. - */ - public void set(Object target, Object value); - - /** - * Determine if the given object has this field. - * @param target - the object to test. - * @return TRUE if it does, FALSE otherwise. - */ - public boolean hasField(Object target); - } }