Resource [Reflection] Create and edit bossbars with this library.

Discussion in 'Spigot Plugin Development' started by Marido, Jul 13, 2018.

  1. Hello everyone.
    Recently I made a bossbar library made with Reflection which uses an EntityWither to send out bossbar packets.
    Here is the library class you can put in your project to use it and it works on every server version from 1.8 - 1.12.
    Code (Text):
    package nl.marido.witherbar.handlers;

    import java.lang.reflect.Constructor;
    import java.lang.reflect.Method;
    import java.util.HashMap;
    import java.util.Map.Entry;
    import java.util.UUID;

    import org.bukkit.Bukkit;
    import org.bukkit.Location;
    import org.bukkit.entity.Player;
    import org.bukkit.plugin.java.JavaPlugin;
    import org.bukkit.scheduler.BukkitRunnable;
    import org.bukkit.util.Vector;

    public class Witherbar extends BukkitRunnable {

        private static String title;
        private static String version = Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(",")[3] + ".";
        private static HashMap<UUID, Object> withers = new HashMap<>();
        private static Class<?> craftworldclass, entitywitherclass, packetentinitylivingout, craftplayerclass;
        private static Class<?> packetclass, packetconnection, packetplayoutdestroy, packetplayoutmeta, packetplayteleport;
        private static Constructor<?> witherconstructor, entitylivingconstructor, packetplayteleportconstructor, packetplaymetaconstructor;
        private static Method craftworldgethandle, craftplayergethandle, packetconnectsend, getentitywitherid, withersetlocationmethod;

        static {
            try {
                craftworldclass = getObcClass("CraftWorld");
                entitywitherclass = getNmsClass("EntityWither");
                packetentinitylivingout = getNmsClass("PacketPlayOutSpawnEntityLiving");
                craftplayerclass = getObcClass("entity.CraftPlayer");
                packetclass = getNmsClass("Packet");
                packetconnection = getNmsClass("PlayerConnection");
                packetplayoutdestroy = getNmsClass("PacketPlayOutEntityDestroy");
                packetplayoutmeta = getNmsClass("PacketPlayOutEntityMetadata");
                packetplayteleport = getNmsClass("PacketPlayOutEntityTeleport");
                witherconstructor = entitywitherclass.getConstructor(getNmsClass("World"));
                entitylivingconstructor = packetentinitylivingout.getConstructor(getNmsClass("EntityLiving"));
                packetplaymetaconstructor = packetplayoutmeta.getConstructor(int.class, getNmsClass("DataWatcher"), boolean.class);
                packetplayteleportconstructor = packetplayteleport.getConstructor(getNmsClass("Entity"));
                craftworldgethandle = craftworldclass.getMethod("getHandle");
                craftplayergethandle = craftplayerclass.getMethod("getHandle");
                packetconnectsend = packetconnection.getMethod("sendPacket", packetclass);
                getentitywitherid = entitywitherclass.getMethod("getId");
                withersetlocationmethod = entitywitherclass.getMethod("setLocation", double.class, double.class, double.class, float.class, float.class);
            } catch (Exception error) {
                error.printStackTrace();
            }
        }

        public Witherbar(String title) {
            Witherbar.title = title;
            JavaPlugin fixed = nl.marido.witherbar.Witherbar.getInstance();
            runTaskTimer(fixed, 0, 0);
        }

        public static void addPlayer(Player player) {
            try {
                Object craftworld = craftworldclass.cast(player.getWorld());
                Object worldserver = craftworldgethandle.invoke(craftworld);
                Object wither = witherconstructor.newInstance(worldserver);
                Location location = getWitherLocation(player.getLocation());
                wither.getClass().getMethod("setCustomName", String.class).invoke(wither, title);
                wither.getClass().getMethod("setInvisible", boolean.class).invoke(wither, true);
                wither.getClass().getMethod("setLocation", double.class, double.class, double.class, float.class, float.class).invoke(wither, location.getX(), location.getY(), location.getZ(), 0, 0);
                Object packet = entitylivingconstructor.newInstance(wither);
                Object craftplayer = craftplayerclass.cast(player);
                Object entityplayer = craftplayergethandle.invoke(craftplayer);
                Object playerconnection = entityplayer.getClass().getField("playerConnection").get(entityplayer);
                packetconnectsend.invoke(packetconnection.cast(playerconnection), packet);
                withers.put(player.getUniqueId(), wither);
            } catch (Exception error) {
                error.printStackTrace();
            }
        }

        public static void removePlayer(Player player) {
            try {
                if (withers.containsKey(player.getUniqueId())) {
                    Object packet = packetplayoutdestroy.getConstructor(getNmsClass("EntityLiving")).newInstance(withers.get(player.getUniqueId()));
                    withers.remove(player.getUniqueId());
                    packetconnection = getNmsClass("PlayerConnection");
                    packetclass = getNmsClass("Packet");
                    craftplayerclass = getObcClass("entity.CraftPlayer");
                    Object craftplayer = craftplayerclass.cast(player);
                    Object entityplayer = craftplayergethandle.invoke(craftplayer);
                    Object playerconnection = entityplayer.getClass().getField("playerConnection").get(entityplayer);
                    packetconnectsend.invoke(packetconnection.cast(playerconnection), packet);
                }
            } catch (Exception error) {
                error.printStackTrace();
            }
        }

        public static void setTitle(String title) {
            try {
                Witherbar.title = title;
                for (Entry<UUID, Object> entry : withers.entrySet()) {
                    Object wither = entry.getValue();
                    entitywitherclass = getNmsClass("EntityWither");
                    wither.getClass().getMethod("setCustomName", String.class).invoke(wither, title);
                    Object packet = packetplaymetaconstructor.newInstance((getentitywitherid.invoke(wither)), entitywitherclass.getMethod("getDataWatcher").invoke(wither), true);
                    craftplayerclass = getObcClass("entity.CraftPlayer");
                    assert Bukkit.getPlayer(entry.getKey()) != null;
                    Object craftplayer = craftplayerclass.cast(Bukkit.getPlayer(entry.getKey()));
                    Object entityplayer = craftplayergethandle.invoke(craftplayer);
                    packetclass = getNmsClass("Packet");
                    packetconnection = getNmsClass("PlayerConnection");
                    Object playerconnection = entityplayer.getClass().getField("playerConnection").get(entityplayer);
                    packetconnectsend.invoke(packetconnection.cast(playerconnection), packet);
                }
            } catch (Exception error) {
                error.printStackTrace();
            }
        }

        public static void setProgress(float progress) {
            if (progress <= 0) {
                progress = (float) 0.001;
            }
            try {
                for (Entry<UUID, Object> entry : withers.entrySet()) {
                    Object wither = entry.getValue();
                    entitywitherclass = getNmsClass("EntityWither");
                    wither.getClass().getMethod("setHealth", float.class).invoke(wither, progress * (float) entitywitherclass.getMethod("getMaxHealth").invoke(wither));
                    Object packet = packetplayoutmeta.getConstructor(int.class, getNmsClass("DataWatcher"), boolean.class).newInstance((entitywitherclass.getMethod("getId").invoke(wither)), entitywitherclass.getMethod("getDataWatcher").invoke(wither), true);
                    craftplayerclass = getObcClass("entity.CraftPlayer");
                    assert Bukkit.getPlayer(entry.getKey()) != null;
                    Object craftplayer = craftplayerclass.cast(Bukkit.getPlayer(entry.getKey()));
                    Object entityplayer = craftplayergethandle.invoke(craftplayer);
                    packetclass = getNmsClass("Packet");
                    packetconnection = getNmsClass("PlayerConnection");
                    Object playerconnection = entityplayer.getClass().getField("playerConnection").get(entityplayer);
                    packetconnectsend.invoke(packetconnection.cast(playerconnection), packet);
                }
            } catch (Exception error) {
                error.printStackTrace();
            }
        }

        public static boolean hasPlayer(Player player) {
            return withers.containsKey(player.getUniqueId());
        }

        public void run() {
            for (Entry<UUID, Object> entry : withers.entrySet()) {
                if (Bukkit.getPlayer(entry.getKey()) != null) {
                    try {
                        Object wither = entry.getValue();
                        assert Bukkit.getPlayer(entry.getKey()) != null;
                        Location location = getWitherLocation(Bukkit.getPlayer(entry.getKey()).getEyeLocation());
                        entitywitherclass = getNmsClass("EntityWither");
                        withersetlocationmethod.invoke(wither, location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
                        Object packet = packetplayteleportconstructor.newInstance(wither);
                        craftplayerclass = getObcClass("entity.CraftPlayer");
                        assert Bukkit.getPlayer(entry.getKey()) != null;
                        Object craftplayer = craftplayerclass.cast(Bukkit.getPlayer(entry.getKey()));
                        Object entityplayer = craftplayergethandle.invoke(craftplayer);
                        packetclass = getNmsClass("Packet");
                        packetconnection = getNmsClass("PlayerConnection");
                        Object playerconnection = entityplayer.getClass().getField("playerConnection").get(entityplayer);
                        packetconnectsend.invoke(packetconnection.cast(playerconnection), packet);
                    } catch (Exception error) {
                        error.printStackTrace();
                    }
                } else {
                    cancel();
                }
            }
        }

        public static Location getWitherLocation(Location location) {
            return location.add(location.getDirection().normalize().multiply(20).add(new Vector(0, 5, 0)));
        }

        public static Class<?> getNmsClass(String classname) {
            String fullname = "net.minecraft.server." + version + classname;
            Class<?> realclass = null;
            try {
                realclass = Class.forName(fullname);
            } catch (Exception error) {
                error.printStackTrace();
            }
            return realclass;
        }

        public static Class<?> getObcClass(String classname) {
            String fullname = "org.bukkit.craftbukkit." + version + classname;
            Class<?> realclass = null;
            try {
                realclass = Class.forName(fullname);
            } catch (Exception error) {
                error.printStackTrace();
            }
            return realclass;
        }

    }

    Spigot: https://www.spigotmc.org/resources/58080/ (compiled version to use as a dependency).
    GitHub: https://github.com/CoderMarido/Witherbar (source-code and a useful usage README).
    Feel free to fork the repository or make a pull request or let me know if something could be improved.

    Note that some plugin authors just want to make their resource compatible with 1.8 - 1.12 and have to use this library instead of using the new built-in methods if you are on 1.9 or higher which will not be compatible with lower versions. Use the built-in methods if you are on 1.9 - 1.12.

    Have fun.
    - Marido
     
    #1 Marido, Jul 13, 2018
    Last edited: Jul 13, 2018
    • Winner Winner x 1
    • Optimistic Optimistic x 1
  2. You should cache your reflection by retrieving any constructors, methods, fields, etc. you'll need in the static{} initializer.

    Otherwise the code is currently very inefficient
     
  3. I found out that yesterday, will probably improve it today and update it. Thanks for the suggestion.

    Updated it with cached classes, methods and constructors and some issue fixes.
    https://github.com/CoderMarido/Witherbar/commit/a6296f1ce081dedb4b8386c540dfa08c166a7c19
     
    #3 Marido, Jul 13, 2018
    Last edited: Jul 13, 2018
    • Friendly Friendly x 1
  4. MiniDigger

    Supporter

  5. Not meant to offend you but do you even use your eyes to read?
     
    • Funny Funny x 1
  6. Also, do not forget that developers have to write in 1.8.8 to make it compitable with 1.8 - 1.12. A resource written in 1.12 will most likely be not compitable with 1.8.8 or some lower versions.
     
  7. He said fallbacl to that api if possible, so if server version over 1.9 then it should just use bossbar api.
     
  8. Well, I am not sure if you could implement the 'methods' in the 1.8.8 jar (even though if you add the 1.9 Spigot API).
     
  9. Benz56

    Moderator Supporter

    That is incorrect.
    The 1.13 API won’t be backwards compatible but 1.12 definitely is.
     
  10. Not as easy as in 1.8, you probably gotta avoid all things and method that the Spigot 1.8 does not provide (I think).
    Never tried it (just a few times maybe), so I can not confirm anything.
     
  11. MiniDigger

    Supporter

    yes I did use them, thats why I suggested write an abstraction layer and use the spigot api when possible as its 100% more performant than this reflection mess
     
    • Winner Winner x 1
  12. Benz56

    Moderator Supporter

    Well I can indeed confirm that 90% of methods using the 1.12 API are compatible with 1.8. You simply try running your plugin in 1.8 and check if any errors are thrown.
     
  13. If you want something that won't break across different versions of MC, the correct way to do this would be to just have an abstract system that you can implement in different ways for each version, in which case there is no need to use reflection for it in 1.12. Then at startup you just determine which version the servers running and load the implementation of the system for the server version. Basically what @MiniDigger said in his reply.

    (A system I'm working with now uses dependency injection to assign implementations of systems to specific MC version(s) by annotation, and automatically loads the correct implementation at runtime).
     
  14. Got it, so, for now, you could choose between using Reflection (indeed a bit messy) or method implementations based on the server running version to get the Spigot API methods from a version.
     
  15. The Bukkit API is designed to be forwards compatible when possible, but not backwards compatible. Fewer changes might cause backwards compatibility to some extend, but that's not the goal.

    However, you're still missing one thing: Developers might want to make use of newer features and simply exclude them for earlier versions. They'd need to compile against the latest version then, not against 1.8.8. And even if you don't need new features, you could still compile against the latest version if the forwards compatibility didn't break yet.