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 82b7da37..a8eb7f7a 100644 --- a/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/ExamplePlugin.java +++ b/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/ExamplePlugin.java @@ -12,11 +12,13 @@ import org.bukkit.plugin.java.JavaPlugin; import com.comphenix.tinyprotocol.Reflection.ConstructorInvoker; import com.comphenix.tinyprotocol.Reflection.FieldAccessor; +/** + * Represents an example plugin utilizing TinyProtocol + */ public class ExamplePlugin extends JavaPlugin { // Chat packets - private FieldAccessor CHAT_MESSAGE = Reflection.getField( - "{nms}.PacketPlayInChat", String.class, 0); - + private FieldAccessor CHAT_MESSAGE = Reflection.getField("{nms}.PacketPlayInChat", String.class, 0); + // Explosion packet private Class particleClass = Reflection.getClass("{nms}.PacketPlayOutWorldParticles"); private FieldAccessor particleName = Reflection.getField(particleClass, String.class, 0); @@ -24,7 +26,7 @@ public class ExamplePlugin extends JavaPlugin { 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); - + // Server info packet private Class serverInfoClass = Reflection.getClass("{nms}.PacketStatusOutServerInfo"); private Class serverPingClass = Reflection.getUntypedClass("{nms}.ServerPing"); @@ -32,12 +34,13 @@ public class ExamplePlugin extends JavaPlugin { private FieldAccessor serverPing = Reflection.getField(serverInfoClass, serverPingClass, 0); private FieldAccessor playerSample = Reflection.getField(serverPingClass, playerSampleClass, 0); private ConstructorInvoker playerSampleInvoker = Reflection.getConstructor(playerSampleClass, int.class, int.class); - + private TinyProtocol protocol; - + @Override public void onEnable() { protocol = new TinyProtocol(this) { + @Override public Object onPacketInAsync(Player sender, Channel channel, Object packet) { // Cancel chat packets @@ -47,24 +50,28 @@ public class ExamplePlugin extends JavaPlugin { return null; } } + if (particleName.hasField(packet)) { System.out.println("Sending particle field:" + packet); } + return super.onPacketInAsync(sender, channel, packet); } - + @Override public Object onPacketOutAsync(Player reciever, Channel channel, Object packet) { if (serverInfoClass.isInstance(packet)) { Object ping = serverPing.get(packet); playerSample.set(ping, playerSampleInvoker.invoke(1000, 0)); - + // Which is equivalent to: - //serverPing.get(packet).setPlayerSample(new ServerPingPlayerSample(1000, 0)); + // serverPing.get(packet).setPlayerSample(new ServerPingPlayerSample(1000, 0)); return packet; } + return super.onPacketOutAsync(reciever, channel, packet); } + }; } @@ -72,7 +79,7 @@ public class ExamplePlugin extends JavaPlugin { public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (sender instanceof Player) { Player player = (Player) sender; - + // Toggle injection if (protocol.hasInjected(player)) { protocol.uninjectPlayer(player); @@ -81,13 +88,15 @@ public class ExamplePlugin extends JavaPlugin { protocol.injectPlayer(player); sender.sendMessage(ChatColor.DARK_GREEN + "Player " + player + " has been injected."); } + return true; } else { sender.sendMessage(ChatColor.RED + "Can only be invoked by a player."); } + return false; } - + private void sendExplosion(Player player) { try { // Only visible for the client @@ -101,9 +110,8 @@ public class ExamplePlugin extends JavaPlugin { // Send the packet to the player protocol.sendPacket(player, explosionPacket); - } catch (Exception e) { throw new RuntimeException("Cannot send packet.", e); } } -} +} \ No newline at end of file diff --git a/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/Reflection.java b/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/Reflection.java index dd41ebe6..73bf8a97 100644 --- a/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/Reflection.java +++ b/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/Reflection.java @@ -11,75 +11,83 @@ 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 constructor. + * An interface for invoking a specific constructor. */ public interface ConstructorInvoker { /** * Invoke a constructor for a specific class. + * * @param arguments - the arguments to pass to the constructor. * @return The constructed object. */ - public Object invoke(Object... arguments); + public Object invoke(Object... arguments); } - + /** - * An interface for invoking a specific method. + * 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); + 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. @@ -88,9 +96,10 @@ public final class Reflection { 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. @@ -99,9 +108,10 @@ public final class Reflection { 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. @@ -110,9 +120,10 @@ public final class Reflection { 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. @@ -121,18 +132,18 @@ public final class Reflection { 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) { + 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 + @SuppressWarnings("unchecked") public T get(Object target) { try { return (T) field.get(target); @@ -140,7 +151,7 @@ public final class Reflection { throw new RuntimeException("Cannot access reflection.", e); } } - + @Override public void set(Object target, Object value) { try { @@ -149,7 +160,7 @@ public final class Reflection { throw new RuntimeException("Cannot access reflection.", e); } } - + @Override public boolean hasField(Object target) { // target instanceof DeclaringClass @@ -158,218 +169,233 @@ public final class Reflection { }; } } - + // 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. - */ + + /** + * 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 getTypedMethod(getClass(className), methodName, null, 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) { - return getTypedMethod(clazz, methodName, null, 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 returnType - the expected return type, or NULL to ignore. - * @param params - the expected parameters. - * @return An object that invokes this specific method. - * @throws IllegalStateException If we cannot find this method. - */ - public static MethodInvoker getTypedMethod(Class clazz, String methodName, Class returnType, Class... params) { - for (final Method method : clazz.getDeclaredMethods()) { - if ((methodName == null || method.getName().equals(methodName)) && - (returnType == null) || method.getReturnType().equals(returnType) && - Arrays.equals(method.getParameterTypes(), params)) { - method.setAccessible(true); - return new MethodInvoker() { - @Override - public Object invoke(Object target, Object... arguments) { - try { + /** + * 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) { + return getTypedMethod(clazz, methodName, null, 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 returnType - the expected return type, or NULL to ignore. + * @param params - the expected parameters. + * @return An object that invokes this specific method. + * @throws IllegalStateException If we cannot find this method. + */ + public static MethodInvoker getTypedMethod(Class clazz, String methodName, Class returnType, Class... params) { + for (final Method method : clazz.getDeclaredMethods()) { + if ((methodName == null || method.getName().equals(methodName)) && (returnType == null) || method.getReturnType().equals(returnType) && 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))); - } - - /** - * Search for the first publically and privately defined constructor of the given name and parameter count. - * @param className - lookup name of the class, see {@link #getClass(String)}. - * @param params - the expected parameters. - * @return An object that invokes this constructor. - * @throws IllegalStateException If we cannot find this method. - */ - public static ConstructorInvoker getConstructor(String className, Class... params) { - return getConstructor(getClass(className), params); - } - - /** - * Search for the first publically and privately defined constructor of the given name and parameter count. - * @param clazz - a class to start with. - * @param params - the expected parameters. - * @return An object that invokes this constructor. - * @throws IllegalStateException If we cannot find this method. - */ - public static ConstructorInvoker getConstructor(Class clazz, Class... params) { - for (final Constructor constructor : clazz.getDeclaredConstructors()) { - if (Arrays.equals(constructor.getParameterTypes(), params)) { + } - constructor.setAccessible(true); - return new ConstructorInvoker() { - @Override - public Object invoke(Object... arguments) { - try { + }; + } + } + + // 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))); + } + + /** + * Search for the first publically and privately defined constructor of the given name and parameter count. + * + * @param className - lookup name of the class, see {@link #getClass(String)}. + * @param params - the expected parameters. + * @return An object that invokes this constructor. + * @throws IllegalStateException If we cannot find this method. + */ + public static ConstructorInvoker getConstructor(String className, Class... params) { + return getConstructor(getClass(className), params); + } + + /** + * Search for the first publically and privately defined constructor of the given name and parameter count. + * + * @param clazz - a class to start with. + * @param params - the expected parameters. + * @return An object that invokes this constructor. + * @throws IllegalStateException If we cannot find this method. + */ + public static ConstructorInvoker getConstructor(Class clazz, Class... params) { + for (final Constructor constructor : clazz.getDeclaredConstructors()) { + if (Arrays.equals(constructor.getParameterTypes(), params)) { + constructor.setAccessible(true); + + return new ConstructorInvoker() { + + @Override + public Object invoke(Object... arguments) { + try { return constructor.newInstance(arguments); } catch (Exception e) { throw new RuntimeException("Cannot invoke constructor " + constructor, e); } - } - }; - } - } - throw new IllegalStateException(String.format( - "Unable to find constructor for %s (%s).", clazz, Arrays.asList(params))); - } - - /** - * Retrieve a class from its full name, without knowing its type on compile time. - *

- * This is useful when looking up fields by a NMS or OBC type. - *

- * @see {@link #getClass()} for more information. - * @param lookupName - the class name with variables. - * @return The class. - */ - public static Class getUntypedClass(String lookupName) { - @SuppressWarnings({"rawtypes", "unchecked"}) - Class clazz = (Class)(Class)getClass(lookupName); - return clazz; - } - - /** - * Retrieve a class from its full name. - *

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

+ } + + }; + } + } + + throw new IllegalStateException(String.format("Unable to find constructor for %s (%s).", clazz, Arrays.asList(params))); + } + + /** + * Retrieve a class from its full name, without knowing its type on compile time. + *

+ * This is useful when looking up fields by a NMS or OBC type. + *

+ * + * @see {@link #getClass()} for more information. + * @param lookupName - the class name with variables. + * @return The class. + */ + public static Class getUntypedClass(String lookupName) { + @SuppressWarnings({ "rawtypes", "unchecked" }) + Class clazz = (Class) getClass(lookupName); + return clazz; + } + + /** + * 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.
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)); - } - + * + * @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 { + 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, Matcher.quoteReplacement(replacement)); - } - matcher.appendTail(output); - return output.toString(); + 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, Matcher.quoteReplacement(replacement)); + } + + matcher.appendTail(output); + return output.toString(); } -} +} \ No newline at end of file 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 5bc8b8f3..302a3416 100644 --- a/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/TinyProtocol.java +++ b/Examples/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/TinyProtocol.java @@ -34,65 +34,67 @@ import com.google.common.collect.MapMaker; import com.mojang.authlib.GameProfile; /** - * Represents a very tiny alternative to ProtocolLib in 1.7.2. + * Represents a very tiny alternative to ProtocolLib. *

* It now supports intercepting packets during login and status ping (such as OUT_SERVER_PING)! + * * @author Kristian */ public abstract class TinyProtocol { private static final AtomicInteger ID = new AtomicInteger(0); - + // Used in order to lookup a channel private static final MethodInvoker getPlayerHandle = Reflection.getMethod("{obc}.entity.CraftPlayer", "getHandle"); private static final FieldAccessor getConnection = Reflection.getField("{nms}.EntityPlayer", "playerConnection", Object.class); private static final FieldAccessor getManager = Reflection.getField("{nms}.PlayerConnection", "networkManager", Object.class); private static final FieldAccessor getChannel = Reflection.getField("{nms}.NetworkManager", Channel.class, 0); - + // Looking up ServerConnection private static final Class minecraftServerClass = Reflection.getUntypedClass("{nms}.MinecraftServer"); private static final Class serverConnectionClass = Reflection.getUntypedClass("{nms}.ServerConnection"); private static final FieldAccessor getMinecraftServer = Reflection.getField("{obc}.CraftServer", minecraftServerClass, 0); private static final FieldAccessor getServerConnection = Reflection.getField(minecraftServerClass, serverConnectionClass, 0); private static final MethodInvoker getNetworkMarkers = Reflection.getTypedMethod(serverConnectionClass, null, List.class, serverConnectionClass); - + // Packets we have to intercept private static final Class PACKET_LOGIN_IN_START = Reflection.getMinecraftClass("PacketLoginInStart"); private static final FieldAccessor getGameProfile = Reflection.getField(PACKET_LOGIN_IN_START, GameProfile.class, 0); - + // Speedup channel lookup private Map channelLookup = new MapMaker().weakValues().makeMap(); private Listener listener; - + // Channels that have already been removed private Set uninjectedChannels = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap()); - + // List of network markers private List networkManagers; - + // Injected channel handlers private List serverChannels = Lists.newArrayList(); private ChannelInboundHandlerAdapter serverChannelHandler; private ChannelInitializer beginInitProtocol; private ChannelInitializer endInitProtocol; - + // Current handler name private String handlerName; - + protected volatile boolean closed; protected Plugin plugin; - + /** * Construct a new instance of TinyProtocol, and start intercepting packets for all connected clients and future clients. *

* You can construct multiple instances per plugin. + * * @param plugin - the plugin. */ public TinyProtocol(Plugin plugin) { this.plugin = plugin; - + // Compute handler name this.handlerName = getHandlerName(); - + // Prepare existing players registerBukkitEvents(); registerChannelHandler(); @@ -100,109 +102,119 @@ public abstract class TinyProtocol { } private void createServerChannelHandler() { - // Handle connected channels - endInitProtocol = new ChannelInitializer() { - @Override - protected void initChannel(Channel channel) throws Exception { - try { - // This can take a while, so we need to stop the main thread from interfering - synchronized (networkManagers) { - // Stop injecting channels - if (closed) - return; - injectChannelInternal(channel); - } - } catch (Exception e) { - plugin.getLogger().log(Level.SEVERE, "Cannot inject incomming channel " + channel, e); - } - } - }; - - // This is executed before Minecraft's channel handler - beginInitProtocol = new ChannelInitializer() { - @Override - protected void initChannel(Channel channel) throws Exception { - channel.pipeline().addLast(endInitProtocol); - } - }; - - serverChannelHandler = new ChannelInboundHandlerAdapter() { - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - Channel channel = (Channel) msg; + // Handle connected channels + endInitProtocol = new ChannelInitializer() { - // Prepare to initialize ths channel - channel.pipeline().addFirst(beginInitProtocol); - ctx.fireChannelRead(msg); - } - }; - } + @Override + protected void initChannel(Channel channel) throws Exception { + try { + // This can take a while, so we need to stop the main thread from interfering + synchronized (networkManagers) { + // Stop injecting channels + if (!closed) { + injectChannelInternal(channel); + } + } + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Cannot inject incomming channel " + channel, e); + } + } + + }; + + // This is executed before Minecraft's channel handler + beginInitProtocol = new ChannelInitializer() { + + @Override + protected void initChannel(Channel channel) throws Exception { + channel.pipeline().addLast(endInitProtocol); + } + + }; + + serverChannelHandler = new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + Channel channel = (Channel) msg; + + // Prepare to initialize ths channel + channel.pipeline().addFirst(beginInitProtocol); + ctx.fireChannelRead(msg); + } + + }; + } + /** * Register bukkit events. */ private void registerBukkitEvents() { listener = new Listener() { + @EventHandler(priority = EventPriority.LOWEST) public final void onPlayerLogin(PlayerLoginEvent e) { if (closed) return; + Channel channel = getChannel(e.getPlayer()); - + // Don't inject players that have been explicitly uninjected if (!uninjectedChannels.contains(channel)) { injectPlayer(e.getPlayer()); } } - + @EventHandler public final void onPluginDisable(PluginDisableEvent e) { if (e.getPlugin().equals(plugin)) { close(); } } + }; - - this.plugin.getServer().getPluginManager().registerEvents(listener, plugin); + + plugin.getServer().getPluginManager().registerEvents(listener, plugin); } - + @SuppressWarnings("unchecked") private void registerChannelHandler() { Object mcServer = getMinecraftServer.get(Bukkit.getServer()); Object serverConnection = getServerConnection.get(mcServer); boolean looking = true; - + // We need to synchronize against this list networkManagers = (List) getNetworkMarkers.invoke(null, serverConnection); createServerChannelHandler(); - + // Find the correct list, or implicitly throw an exception for (int i = 0; looking; i++) { List list = Reflection.getField(serverConnection.getClass(), List.class, i).get(serverConnection); - + for (Object item : list) { if (!ChannelFuture.class.isInstance(item)) break; - + // Channel future that contains the server connection - Channel serverChannel = ((ChannelFuture)item).channel(); - + Channel serverChannel = ((ChannelFuture) item).channel(); + serverChannels.add(serverChannel); serverChannel.pipeline().addFirst(serverChannelHandler); looking = false; } } } - + private void unregisterChannelHandler() { if (serverChannelHandler == null) return; - + for (Channel serverChannel : serverChannels) { final ChannelPipeline pipeline = serverChannel.pipeline(); - + // Remove channel handler serverChannel.eventLoop().execute(new Runnable() { + @Override public void run() { try { @@ -211,20 +223,22 @@ public abstract class TinyProtocol { // That's fine } } + }); } } - + private void registerPlayers(Plugin plugin) { 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, NULL for early login/status packets. * @param remoteAddress - remote address of the sending client. Never NULL. * @param packet - the packet being sent. @@ -238,6 +252,7 @@ public abstract class TinyProtocol { * Invoked when the server has received a packet from a given player. *

* Use {@link Channel#remoteAddress()} to get the remote address of the client. + * * @param sender - the player that sent the packet, NULL for early login/status packets. * @param channel - channel that received the packet. Never NULL. * @param packet - the packet being received. @@ -246,133 +261,145 @@ public abstract class TinyProtocol { public Object onPacketInAsync(Player sender, Channel channel, 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) { sendPacket(getChannel(player), packet); } - + /** * Send a packet to a particular client. *

* Note that {@link #onPacketOutAsync(Player, Object)} will be invoked with this packet. + * * @param channel - client identified by a channel. * @param packet - the packet to send. */ public void sendPacket(Channel channel, Object packet) { channel.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) { receivePacket(getChannel(player), packet); } - + /** * Pretend that a given packet has been received from a given client. *

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

* Note that this method will only be invoked once. It is no longer necessary to override this to support multiple instances. + * * @return A unique channel handler name. */ protected String getHandlerName() { return "tiny-" + plugin.getName() + "-" + ID.incrementAndGet(); } - + /** - * Add a custom channel handler to the given player's channel pipeline, - * allowing us to intercept sent and received packets. + * Add a custom channel handler to the given player's channel pipeline, allowing us to intercept sent and received packets. *

* This will automatically be called when a player has logged in. + * * @param player - the player to inject. */ public void injectPlayer(Player player) { injectChannelInternal(getChannel(player)).player = player; } - + /** * Add a custom channel handler to the given channel. + * * @param player - the channel to inject. * @return The intercepted channel, or NULL if it has already been injected. */ public void injectChannel(Channel channel) { injectChannelInternal(channel); } - + /** * Add a custom channel handler to the given channel. + * * @param player - the channel to inject. * @return The packet interceptor. */ private PacketInterceptor injectChannelInternal(Channel channel) { try { PacketInterceptor interceptor = (PacketInterceptor) channel.pipeline().get(handlerName); - + // Inject our packet interceptor if (interceptor == null) { interceptor = new PacketInterceptor(); channel.pipeline().addBefore("packet_handler", handlerName, interceptor); uninjectedChannels.remove(channel); } + return interceptor; } catch (IllegalArgumentException e) { // Try again return (PacketInterceptor) channel.pipeline().get(handlerName); } } - + /** * Retrieve the Netty channel associated with a player. This is cached. + * * @param player - the player. * @return The Netty channel. */ public Channel getChannel(Player player) { Channel channel = channelLookup.get(player.getName()); - + // Lookup channel again if (channel == null) { Object connection = getConnection.get(getPlayerHandle.invoke(player)); Object manager = getManager.get(connection); - + channelLookup.put(player.getName(), channel = getChannel.get(manager)); } + return channel; } - + /** * Uninject a specific player. + * * @param player - the injected player. */ public void uninjectPlayer(Player player) { uninjectChannel(getChannel(player)); } - + /** * Uninject a specific channel. *

* This will also disable the automatic channel injection that occurs when a player has properly logged in. + * * @param channel - the injected channel. */ public void uninjectChannel(final Channel channel) { @@ -380,75 +407,82 @@ public abstract class TinyProtocol { if (!closed) { uninjectedChannels.add(channel); } - + // See ChannelInjector in ProtocolLib, line 590 channel.eventLoop().execute(new Runnable() { + @Override public void run() { channel.pipeline().remove(handlerName); } + }); } - + /** * Determine if the given player has been injected by TinyProtocol. + * * @param player - the player. * @return TRUE if it is, FALSE otherwise. */ public boolean hasInjected(Player player) { return hasInjected(getChannel(player)); } - + /** * Determine if the given channel has been injected by TinyProtocol. + * * @param channel - the channel. * @return TRUE if it is, FALSE otherwise. */ public boolean hasInjected(Channel channel) { return channel.pipeline().get(handlerName) != null; } - + /** * Cease listening for packets. This is called automatically when your plugin is disabled. */ public final void close() { if (!closed) { closed = true; - + // Remove our handlers for (Player player : plugin.getServer().getOnlinePlayers()) { uninjectPlayer(player); } + // Clean up Bukkit HandlerList.unregisterAll(listener); unregisterChannelHandler(); } } - + /** * Channel handler that is inserted into the player's channel pipeline, allowing us to intercept sent and received packets. + * * @author Kristian */ private final class PacketInterceptor extends ChannelDuplexHandler { // Updated by the login event public volatile Player player; - + @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // Intercept channel final Channel channel = ctx.channel(); handleLoginStart(channel, msg); - + try { msg = onPacketInAsync(player, channel, msg); } catch (Exception e) { plugin.getLogger().log(Level.SEVERE, "Error in onPacketInAsync().", e); } - + if (msg != null) { super.channelRead(ctx, msg); } } + @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { try { @@ -456,12 +490,12 @@ public abstract class TinyProtocol { } catch (Exception e) { plugin.getLogger().log(Level.SEVERE, "Error in onPacketOutAsync().", e); } - + if (msg != null) { super.write(ctx, msg, promise); } } - + private void handleLoginStart(Channel channel, Object packet) { if (PACKET_LOGIN_IN_START.isInstance(packet)) { GameProfile profile = getGameProfile.get(packet); @@ -469,4 +503,4 @@ public abstract class TinyProtocol { } } } -} +} \ No newline at end of file