Tutorial Basic reflection tutorial

Discussion in 'Resources' started by Skionz, Dec 18, 2014.

Thread Status:
Not open for further replies.
  1. Offline

    Skionz

    NOTE: To understand this tutorial you will have to understand basic Java not just Bukkit. You will need to know what a method, class, and constructor is, and basic Java vocabulary.
    This tutorial will go over how to use reflection to grab NMS classes. In this tutorial we will actually be creating a simple particle API with reflection. Please note that reflection is very slow compared to instantiating.
    Reasons not to use reflection (open)

    Directly quote from Oracle's websites

    You might be wondering what is reflection. Well reflection lets you examine and modify classes, methods, fields, and such at runtime.

    To begin you are going to need these methods
    Code:
    private Class<?> getNMSClass(String nmsClassString) throws ClassNotFoundException {
        String version = Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(",")[3] + ".";
        String name = "net.minecraft.server." + version + nmsClassString;
        Class<?> nmsClass = Class.forName(name);
        return nmsClass;
    }
    private Object getConnection(Player player) throws SecurityException, NoSuchMethodException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
        Method getHandle = player.getClass().getMethod("getHandle");
        Object nmsPlayer = getHandle.invoke(player);
        Field conField = nmsPlayer.getClass().getField("playerConnection");
        Object con = conField.get(nmsPlayer);
        return con;
    }
    These are not my methods, and I am not sure where I originally got them from, but they are simple enough to understand and I didn't feel like reinventing the wheel. The 'getConnection' method returns the player's PlayerConnection instance. What this does is get the method 'getHandle' from the Player's class, invokes the method and it returns an Object. Then it gets the 'playerConnection' field in the EntityPlayer class. Now that is has the field in the actually class it gets the object specific value which is assigned to that field. In this case it is an instance of the PlayerConnection class. As for the other method, if you can't tell already it parses a String to get the NMS package then uses concatenation to add the version and Classes name to the package. Finally it returns the Class with that given name.

    Now onto the actual tutorial. Create a class called 'ParticleAPI' or something similar and add those methods to it. Now we are going to create a new method called 'sendParticles' with the same parameters as the PacketPlayOutWorldParticles constructor plus a player to whom we wish to send this packet too.
    Code:
    public void sendParticles(Player player, String particle, float x, float y, float z, float xOffset, float yOffset, float zOffset, float data, int amount) {
    }
    Now we are going to want to get the PacketPlayOutWorldParticles class like this
    Code:
    Class<?> packetClass = this.getNMSClass("PacketPlayOutWorldParticles");
    This basically gets the 'PacketPlayOutWorldParticles' using the method above.
    Since some classes/most have multiples constructors we have to specify which one we want before creating a new instance of the class like this
    Code:
    Class<?> packetClass = this.getNMSClass("PacketPlayOutWorldParticles");
    Constructor<?> packetConstructor = packetClass.getConstructor(String.class, float.class, float.class, float.class, float.class, float.class, float.class, float.class, int.class);
    Now you are probably wondering what is up with all of the 'String.class' and 'float.class.' These are the parameters for the constructor that we will be using.

    Since we now have obtained the constructor we want, we can now create a new PacketPlayOutWorldParticles object with that constructor by using the 'newInstance' method as so:
    Code:
    Object packet = packetConstructor.newInstance(particle, x, y, z, xOffset, yOffset, zOffset, data, amount);
    This will create a PacketPlayOutWorldParticles object using the parameters from our method. Since we have the actual packet now all we have to do is send it. This can be accomplished like this:
    Code:
    Method sendPacket = getNMSClass("PlayerConnection").getMethod("sendPacket", this.getNMSClass("Packet"));
    sendPacket.invoke(this.getConnection(player), packet);
    Which gets the 'PlayerConnection' class and gets the 'sendPacket' method inside it. The Class#getMethod() method takes an indefinite number of parameters. The first is the name of the method as a String and the rest are that methods parameters. As you can see, the only parameter the sendPacket method takes is a Packet object.
    After we have got the method we invoke it using Method#invoke(). First we get the players connection using the method and explanation above. This will invoke the method on the players connection. For the second parameter we use our PacketPlayOutWorldParticles object which we constructed above.
    And that is it! We are finished and here is the final class
    Code:
    public class ParticleAPI {
        private Class<?> getNMSClass(String nmsClassString) throws ClassNotFoundException {
            String version = Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(",")[3] + ".";
            String name = "net.minecraft.server." + version + nmsClassString;
            Class<?> nmsClass = Class.forName(name);
            return nmsClass;
        }
        private Object getConnection(Player player) throws SecurityException, NoSuchMethodException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
            Method getHandle = player.getClass().getMethod("getHandle");
            Object nmsPlayer = getHandle.invoke(player);
            Field conField = nmsPlayer.getClass().getField("playerConnection");
            Object con = conField.get(nmsPlayer);
            return con;
        }
        public void sendParticles(Player player, String particle, float x, float y, float z, float xOffset, float yOffset, float zOffset, float data, int amount) throws ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchFieldException {
            Class<?> packetClass = this.getNMSClass("PacketPlayOutWorldParticles");
            Constructor<?> packetConstructor = packetClass.getConstructor(String.class, float.class, float.class, float.class, float.class, float.class, float.class, float.class, int.class);
            Object packet = packetConstructor.newInstance(particle, x, y, z, xOffset, yOffset, zOffset, data, amount);
            Method sendPacket = getNMSClass("PlayerConnection").getMethod("sendPacket", this.getNMSClass("Packet"));
            sendPacket.invoke(this.getConnection(player), packet);
        }
    }
    We can easily use this class by creating a new instance of it and invoking the sendParticles method. Make sure to catch your exceptions :D
    Note that Spigot changed the PacketPlayOutWorldParticles constructor in 1.8 to use an Enum. This is an easy fix and I am sure you can figure it out with a quick search.

    Useful Resources:
    http://docs.oracle.com/javase/tutorial/reflect/
    http://en.wikipedia.org/wiki/Reflection_(computer_programming)
    http://docs.oracle.com/javase/7/docs/api/java/lang/reflect/Method.html
    http://docs.oracle.com/javase/7/docs/api/java/lang/Class.html
    https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Constructor.html
    http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html

    I probably made a few mistakes while writing this so feel free to point them out. I really didn't feel like reading this over...
     
  2. Invisible

    nverdier

    Last edited by a moderator: Sep 7, 2019
  3. @Skionz "Basic reflection tutorial" or in other words "how to break CraftBukkit's security measures".

    Seriously, I hate that this reflection technique is passed around as a kind of magical fix for a problem that doesn't exist. The version packaged CraftBukkit & NMS classes were accidentally put in by the Bukkit team. It didn't make their life any easier by having the version in the packages, so did you not think that they might have put it in for a reason? The truth is, it is in there for a reason.

    If you want to support multiple versions of CraftBukkit when using NMS, then this is the correct approach. If you want to not have to update your plugin everytime CraftBukkit updates, then don't use NMS. Using this sort of approach puts server owners at risk for your own convenience. It really is shameful, and I hope such approaches will eventually disappear.
     
    Avygeil likes this.
  4. Offline

    Skionz

    @AdamQpzm I did think there was a reason behind it, but I had never seen that thread until now. It looks like this was originally implemented because the official API was supposed to come out with new features and Mojang had to make many changes, but it never came out so is there really a need to avoid reflection? However, the method you linked too looks interesting and fairly organized. It would make the plugin larger, but it also supports changes to the constructor with different versions. You could do that with reflection, but it would be very disorganized and hard to read. I will probably add a link to that in the post later. I do have a question though. What risks could there be? I guess crashes are definitely a possibility, but most server owners use a test server to check if everything works before updating.
     
  5. @Skionz That's not the reason it was added, it was added as a security measure against unchecked plugins. Had nothing to do with the official API. Because of the volatile nature of the internals, a method you think does one thing is liable to be changed to something completely different. With this security measure, it forces developers to update the plugins each time it updates, encouraging them to check whether the method is still the same.

    Crashes aren't the only risk. Mishandled NMS can even be so harmful as to corrupt worlds. I would disagree that "most" server owners use a test server, but even under the assumption that they do, you never know what might set off a problem. It is entirely possible to test a plugin, but then a problem be caused that was raised during testing.
     
    Skionz likes this.
  6. Offline

    ChocolateChimp

    I think the idea of "Version independence" is false. Even using reflection, you have to specify the class's name. When Mojang renames a class BOOM! "Version independence" no longer works. Not only that, but reflection doesn't make any packet tasks easier, it makes them harder. It's so much easier to use the classic way! Even if the version changes, or maybe the class name changes, it's just a simple import fix and a quick rename. Not to mention, "Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications.". Most likely you're not gonna call it infrequently, and are gonna use it quite often. This still was a great introduction to NMS, and even though I'm not gonna use this way, I still have to thank-you for your tutorial @Skionz, you did a great job at explaining the concepts, aswell as the pros and cons. Thanks! :D
     
    Skionz likes this.
  7. Offline

    xTrollxDudex

    Unlikely. NMS doesn't change names so it would work with CraftBukkit, at least as far as I can see.

    This is actually harder for applications which use a lot of NMS imports. Additionally, people like avoiding maintenance overhead for some strange reason.

    Credit oracle docs :)

    Some smart people will be able to avoid this
     
Thread Status:
Not open for further replies.

Share This Page