NMS on different versions (without reflection)

Oct 6, 2016
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().replace(".",  ",").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!
  • Loading...
  • Loading...