NMS on different versions (without reflection)

Discussion in 'Wiki Discussion' started by jflory7, May 8, 2015.

  1. jflory7

    jflory7 Retired Moderator
    Retired Benefactor

    NMS on different versions (without reflection)
    I have seen a lot of people make plugins that are version dependent which do things such as send a title, or actionbar message. Many people that do this use NMS (net.minecraft.server) code to utilize these features as it is the only way to do so. As NMS is version specific, I have seen quite a few plugins that are version dependent and when an update comes out, they no longer work with previous versions.

    A while back I was looking into the best way to offer multiple version compatibility depending on the server version my plugin was running on and ended up using Reflection.
    While I thought this was a good thing (as my plugin was now backwards compatible), @DarkSeraphim had told me in a thread where I posted a method using reflection that it was more intensive on the server to use reflection and I should use an interface which would allow me to only check what compatibility I need to offer when my plugin is loaded.

    Using an interface allows my NMS code to be contained in separate classes and only the class specific to the server version is loaded and used. This eliminates the need to use reflection!

    As this guide does not eliminate the need to use reflection for everything you may encounter
    (as there are some some reasons you might NEED to use it),
    for things like sending an actionbar or title, it is great!

    In this example plugin we will be sending an actionbar announcement to players when they join the server. This plugin will work on any 1.8 version

    So lets get started:

    In your plugin, you should create a package specific to your interface and NMS classes:
    [​IMG]

    Inside of this package, we will create an interface class named Actionbar.
    This interface will hold the abstract method that our NMS classes will use to send the actual actionbar.

    Any class that implements our interface must contain the interface abstract methods.
    This is how we can store a reference to Actionbar in our main class, but assign any of our NMS classes to that reference as long as they implement the interface.

    So lets create our interface:
    Code (Java):
    package me.clip.actionbarplugin.actionbar;

    import org.bukkit.entity.Player;

    public interface Actionbar {

        public void sendActionbar(Player p, String message);
    }
    Pretty simple right? Just one line of code here.
    This is the abstract method our classes that implement Actionbar will use to call our version specific NMS methods.
    If you were using this to do something besides sending an actionbar message, you can add as many methods you need for each NMS class. Keep in mind that every class that implements this interface must contain every method you list here.

    Now that we have our interface, lets create our classes that implement it and actually send the actionbar to players using NMS!

    1.8.1 class which implements our Actionbar interface:
    Code (Java):
    package me.clip.actionbarplugin.actionbar;

    import net.minecraft.server.v1_8_R1.ChatSerializer;
    import net.minecraft.server.v1_8_R1.IChatBaseComponent;
    import net.minecraft.server.v1_8_R1.PacketPlayOutChat;
    import org.bukkit.craftbukkit.v1_8_R1.entity.CraftPlayer;

    import org.bukkit.entity.Player;

    public class Actionbar_1_8_R1 implements Actionbar {

        @Override
        public void sendActionbar(Player p, String message) {

            IChatBaseComponent icbc = ChatSerializer.a("{\"text\": \"" + message + "\"}");

            PacketPlayOutChat bar = new PacketPlayOutChat(icbc, (byte) 2);

            ((CraftPlayer) p).getHandle().playerConnection.sendPacket(bar);
        }
    }
    1.8.3 class which implements our Actionbar interface:
    Code (Java):
    package me.clip.actionbarplugin.actionbar;

    import net.minecraft.server.v1_8_R2.IChatBaseComponent;
    import net.minecraft.server.v1_8_R2.PacketPlayOutChat;
    import net.minecraft.server.v1_8_R2.IChatBaseComponent.ChatSerializer;
    import org.bukkit.craftbukkit.v1_8_R2.entity.CraftPlayer;

    import org.bukkit.entity.Player;

    public class Actionbar_1_8_R2 implements Actionbar {

        @Override
        public void sendActionbar(Player p, String message) {

            IChatBaseComponent icbc = ChatSerializer.a("{\"text\": \"" + message + "\"}");

            PacketPlayOutChat bar = new PacketPlayOutChat(icbc, (byte) 2);

            ((CraftPlayer) p).getHandle().playerConnection.sendPacket(bar);
        }
    }
    Now we have our version specific code setup! All we need to do is create our main class which will hold a reference to our class that implements our interface and assign that class to the interface reference on plugin load!

    Actionbar plugin:
    Code (Java):
    package me.clip.actionbarplugin;

    import me.clip.actionbarplugin.actionbar.Actionbar;
    import me.clip.actionbarplugin.actionbar.Actionbar_1_8_R1;
    import me.clip.actionbarplugin.actionbar.Actionbar_1_8_R2;

    import org.bukkit.Bukkit;
    import org.bukkit.event.EventHandler;
    import org.bukkit.event.Listener;
    import org.bukkit.event.player.PlayerJoinEvent;
    import org.bukkit.plugin.java.JavaPlugin;

    public class ActionbarPlugin extends JavaPlugin implements Listener {

        // our interface reference! Any class that implements Actionbar can be assigned to this reference!
        // when we need to send an actionbar, all we need to do is call actionbar.sendActionbar(player, message);
        // since the proper NMS class was assigned onEnable, we are now backwards compatible!
        private Actionbar actionbar;

        @Override
        public void onEnable() {

            if (setupActionbar()) {

                Bukkit.getPluginManager().registerEvents(this, this);

                getLogger().info("Actionbar setup was successful!");
                getLogger().info("The plugin setup process is complete!");

            } else {

                getLogger().severe("Failed to setup Actionbar!");
                getLogger().severe("Your server version is not compatible with this plugin!");

                Bukkit.getPluginManager().disablePlugin(this);
            }
        }
        // this method will setup our actionbar class and return true if the server is running a
        // version compatible with our NMS classes.
        // If the server is not compatible, it will return false!
        private boolean setupActionbar() {

            String version;

            try {

                version = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];

            } catch (ArrayIndexOutOfBoundsException whatVersionAreYouUsingException) {
                return false;
            }

            getLogger().info("Your server is running version " + version);

            if (version.equals("v1_8_R1")) {
                //server is running 1.8-1.8.1 so we need to use the 1.8 R1 NMS class
                actionbar = new Actionbar_1_8_R1();

            } else if (version.equals("v1_8_R2")) {
                //server is running 1.8.3 so we need to use the 1.8 R2 NMS class
                actionbar = new Actionbar_1_8_R2();
            }
            // This will return true if the server version was compatible with one of our NMS classes
            // because if it is, our actionbar would not be null
            return actionbar != null;
        }

        @EventHandler
        public void onJoin(PlayerJoinEvent event) {
            actionbar.sendActionbar(event.getPlayer(), "Welcome to the server!");
        }
    }
     
    In my tutorial plugin I kept everything in the main plugin class.
    If you have a large plugin and use multiple classes (class for your listeners etc)
    you should create a getter for Actionbar in your main class so your other classes can access it.

    Creating a getter is very simple and you should already know how if you are checking out this tutorial.
    Code (Java):
    public Actionbar getActionbar() {
        return actionbar;
    }
    Now all of your classes have access to send the actionbar message!

    There are many ways you can check the server version, that is really up to you. I used the bukkit Server package name but you could also use Bukkit.getBukkitVersion() and check/assign your NMS class depending on that.

    This method of handling NMS code to be backwards compatible is pretty much fool proof.
    If you initialize one of your NMS classs that does not match the server version your plugin will throw a ClassNotFoundException though.
    Be sure to only initialize the class specific to the server version running the plugin!

    So there you go! We now have a backwards compatible actionbar plugin that uses NMS and no reflection! When a new version of Spigot is released which has new NMS package names, all we need to do is add a new NMS class that implements Actionbar and use the updated NMS packages then check / assign the new version on plugin load!

    [​IMG]

    Thanks for reading. I am not really the greatest developer and I am always learning new things every day just like you!
    I hope this tutorial helps someone get an understanding on how you can use an interface to do different things such as use NMS on different server versions without using reflecton!
     
    #1 jflory7, May 8, 2015
    Last edited: May 15, 2015
    • Useful Useful x 5
    • Like Like x 4
    • Agree Agree x 2
    • Friendly Friendly x 1
  2. foncused

    Moderator Patron

    I will have to look at all this and give it a try when I have the time. Looks well written and helpful, so thanks! :)
     
    • Like Like x 1
  3. joehot200

    Supporter

    inb4 NMS classes stay the same and tutorial is obsolete.
     
  4. clip

    Benefactor

    inb4 NMS update happens tomorrow and your plugins that use NMS break!

    (just kidding)
     
  5. jflory7

    jflory7 Retired Moderator
    Retired Benefactor

    @extended_clip: Once you feel confident about your tutorial, let me know and I can move all of the posts in this thread to the Wiki Discussion thread.
     
  6. clip

    Benefactor

    Okay! Let me play with it for a few days. I am not the greatest at posting stuff like this so I am sure I will change it up here and there to be a bit more descriptive after I think about it for a while.
     
    • Like Like x 1
  7. jflory7

    jflory7 Retired Moderator
    Retired Benefactor

    Works for me - just let me know when you wiki-ify it. :)
     
  8. gigosaurus

    Supporter

    I approve of the code - it's valid and clearly shows users how they can go about supporting multiple versions without solely using reflection.

    My only dispute is your use of white space not quite following everything that is mentioned here. I'm mostly on about section 8.2.
     
    • Agree Agree x 2
  9. clip

    Benefactor

    Understandable. I know that I may not follow code conventions perfectly but I mainly left the white space for readability.
    Thanks for the approval though! I hope this is useful to someone!
     
    #9 clip, May 8, 2015
    Last edited: May 8, 2015
  10. gigosaurus

    Supporter

    As I said, I was more on about section 8.2 - your lack of whitespace :D

    EDIT: Yay you fixed it in some places. You still missed somewhere though :p
     
    #10 gigosaurus, May 8, 2015
    Last edited: May 8, 2015
  11. The code is good.
     
  12. clip

    Benefactor

    I think I got it all :p
     
  13. @extended_clip this code is great for people making a actionbar so it won't break across versions without reflection. Great job!
     
    • Friendly Friendly x 1
  14. clip

    Benefactor

    Keep in mind, this tutorial uses the actionbar as an example but it can be used for many other things!
     
    • Agree Agree x 2
  15. Yes of corse!
     
  16. nice
     
    • Like Like x 1
  17. ...I thought I was the only one doing this. Vouch!
     
    • Like Like x 1
  18. jflory7

    jflory7 Retired Moderator
    Retired Benefactor

    This is so awesome. Just seeing everyone peer review this for the wiki article. :D
     
  19. Useful indeed, though this still doesn't prevent you from updating your plugin every time versioning changes. Unfortunatly I don't think such a magical solution is possible.
    :(
     
    • Like Like x 1
  20. clip

    Benefactor

    I guess you could create dummy classes that represent your NMS classes for the next update lol.
    Code (Text):
    import net.minecraft.server.v1_8_R3.PacketPlayOutChat;
    Then check if the version compares to the "hopefully correct" next update.

    Also hope that the next version doesn't change your NMS methods.

    I wouldn't suggest this but if you are lazy I guess it could "possibly" work.
     
    • Funny Funny x 1