Resource [Guide] Update Inv Title Without Closing/Opening Inv

Discussion in 'Spigot Plugin Development' started by PlagueisTheWise, Jun 30, 2020 at 2:34 PM.

  1. Hey guys, so bare with me, as this is my first guide / tutorial and not too sure what to call it exactly.

    Recently needed to update the inventory title but couldn't for the life of me find a guide to do so. After extensive searching I finally figured it out.

    It does use NMS (which some people don't like), and I am working on figuring out the reflections, so if anyone as any tips, they'd be appreciated.

    Pretty much just run this method when a player has an inventory open, and the title will change for them. It is useful if say you want to have a countdown in the title. Just a note that if you check the inventory title through event.getView().getTItle() it will return the original unedited title

    Code (Java):
    public void update(Player p, String title){
            EntityPlayer ep = ((CraftPlayer)p).getHandle();
            IChatBaseComponent invTitle = new ChatMessage(title);
            PacketPlayOutOpenWindow packet = new PacketPlayOutOpenWindow(ep.activeContainer.windowId, Containers.GENERIC_9X6, invTitle);
            ep.playerConnection.sendPacket(packet);      
            ep.updateInventory(ep.activeContainer);
        }
     
    • Useful Useful x 3
    • Like Like x 1
    • Informative Informative x 1
  2. Awesome! I figured it was possible with Packets too lazy to dig into it myself :)

    As for the reflection there are a few utils out there where you pass a classname and get the NMS object back of the current version. Here is the one I use in my plugins. Happy to discuss how its used but simply lets you pass in the name of the class as a string and get back the current running version of it during runtime. Includes caching of the classes since reflection is not cheap normally.

    public static Class<?> getNMSClass(String name) - is the method you're looking for since there is other uses in there!
     
  3. Consider creating a Spigot PR for this. This could be part of InventoryView (and implemented in CraftInventoryView).
     
  4. Sorry, I'm a little new to this all, what is a PR?
     
  5. PullRequest also known as MergeRequest.
     
  6. Ah yep, that makes more sense than "Public Relations". WIll have a look into it
     
    • Funny Funny x 1
  7. What is that?
     
  8. Here is a link that could help you contribute: https://www.spigotmc.org/wiki/guide-contributing-to-spigot/
    It would be really nice if you add this, in InventoryView (as said above), the method should probably be called setTitle.
    Thanks for sharing!
    When you fork a repository and you wish upstream to pull your changes, you request them to do so, hence a pull request.
     
  9. I tryed the code but when importing the classes I couldn't find the import for the Containers.GENERIC_9X6. Is this a class that you have created?
     
  10. What version of MC did you use? Did you use Spigot or Spigot-API?
     
  11. 1.13.2 and imported the spigot server as a jar
     
  12. This resource is probably for latest.
     
  13. It is in 1.14 but not in 1.13. Is there any way to make this work in olther versions (mainly 1.13)?
     
  14. I can help you with the reflections
    Code (Java):


    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.lang.reflect.Field;

    import org.bukkit.entity.Player;
    import org.bukkit.inventory.InventoryView;

    public class InventoryUtil {

        /**
        * Updates the name of the opend inventory of the player without closing the
        * inventory and opening a new one
        *
        * @param player
        *            The player to update the opend inventory
        * @param newTitle
        *            The new title of the inventory
        */

        public static void updateInventoryName(Player player, String newTitle) {
            // EntityPlayer ep = ((CraftPlayer) player).getHandle();
            // IChatBaseComponent invTitle = new ChatMessage(title);
            // PacketPlayOutOpenWindow packet = new PacketPlayOutOpenWindow(ep.activeContainer.windowId, Containers.GENERIC_9X6, invTitle);
            // ep.playerConnection.sendPacket(packet);
            // ep.updateInventory(ep.activeContainer);

            try {
                // Start of EntityPlayer ep = ((CraftPlayer) player).getHandle();
                Class<?> craftPlayerClass = ReflectionUtils.getCraftClass("entity.CraftPlayer");
                Object craftPlayerObject = craftPlayerClass.cast(player);
                Method getHandleMethod = craftPlayerClass.getMethod("getHandle");
                Object entityPlayerObject = getHandleMethod.invoke(craftPlayerObject);
                // End of EntityPlayer ep = ((CraftPlayer) player).getHandle();

                // Start of IChatBaseComponent invTitle = new ChatMessage(title);
                Class<?> chatMessageClass = ReflectionUtils.getNMSClass("ChatMessage");
                Constructor<?> chatMessageConstructor = chatMessageClass.getConstructor(String.class, Object[].class);
                Object invTitleObject = chatMessageConstructor.newInstance(newTitle, new Object[] {});
                // End of IChatBaseComponent invTitle = new ChatMessage(title);

                // Start of PacketPlayOutOpenWindow packet = new PacketPlayOutOpenWindow(ep.activeContainer.windowId, Containers.GENERIC_9X6, invTitle);
                Class<?> iChatBaseComponentClass = ReflectionUtils.getNMSClass("IChatBaseComponent");
                Class<?> packetPlayOutOpenWindowClass = ReflectionUtils.getNMSClass("PacketPlayOutOpenWindow");
                Class<?> containersClass = ReflectionUtils.getNMSClass("Containers");
                Constructor<?> packetPlayOutOpenWindowConstructor = packetPlayOutOpenWindowClass
                        .getConstructor(int.class, containersClass/*1.13 and below this is a string*/, iChatBaseComponentClass);
                Class<?> entityPlayerClass = ReflectionUtils.getNMSClass("EntityPlayer");
                Field activeContainerField = entityPlayerClass.getField("activeContainer");
                Object activeContainerObject = activeContainerField.get(entityPlayerObject);
                Class<?> containerClass = ReflectionUtils.getNMSClass("Container");
                Field windowIdField = containerClass.getField("windowId");
                Object windowIdObject = windowIdField.get(activeContainerObject);
                Object packetPlayOutOpenWindowObject = packetPlayOutOpenWindowConstructor.newInstance(windowIdObject,
                        ContainerType.getContainerType(activeContainerObject).getObject(), invTitleObject);
                // End of PacketPlayOutOpenWindow packet = new PacketPlayOutOpenWindow(ep.activeContainer.windowId, Containers.GENERIC_9X6, invTitle);

                // Start of ep.playerConnection.sendPacket(packet);
                ReflectionUtils.sendPacket(player, packetPlayOutOpenWindowObject);
                // End of ep.playerConnection.sendPacket(packet);

                // Start of ep.updateInventory(ep.activeContainer);
                Method updateInventoryMethod = entityPlayerClass.getMethod("updateInventory", containerClass);
                updateInventoryMethod.invoke(entityPlayerObject, activeContainerObject);
                // End of ep.updateInventory(ep.activeContainer);
            } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException |
                    InvocationTargetException | InstantiationException | NoSuchFieldException e) {
                // So many errors :(
                e.printStackTrace();
            }
        }

        public static enum ContainerType {
            GENERIC_9X1(14, "GENERIC_9X1", "CHEST"),
            GENERIC_9X2(14, "GENERIC_9X2", "CHEST"),
            GENERIC_9X3(14, "GENERIC_9X3", "CHEST"),
            GENERIC_9X4(14, "GENERIC_9X4", "CHEST"),
            GENERIC_9X5(14, "GENERIC_9X5", "CHEST"),
            GENERIC_9X6(14, "GENERIC_9X6", "CHEST"),
            GENERIC_3X3(14, "GENERIC_3X3", "DISPENSER", "DROPPER"),
            ANVIL(14, "ANVIL", "ANVIL"),
            BEACON(14, "BEACON", "BEACON"),
            BLAST_FURNACE(14, "BLAST_FURNACE", "BLAST_FURNACE"),
            BREWING_STAND(14, "BREWING_STAND", "BREWING"),
            CRAFTING(14, "CRAFTING", "CRAFTING"),
            ENCHANTMENT(14, "ENCHANTMENT", "ENCHANTING"),
            FURNACE(14, "FURNACE", "FURNACE"),
            GRINDSTONE(14, "GRINDSTONE", "GRINDSTONE"),
            HOPPER(14, "HOPPER", "HOPPER"),
            LECTERN(14, "LECTERN", "LECTERN"),
            LOOM(14, "LOOM", "LOOM"),
            MERCHANT(14, "MERCHANT", "MERCHANT"),
            SHULKER_BOX(14, "SHULKER_BOX", "SHULKER_BOX"),
            SMOKER(14, "SMOKER", "SMOKER"),
            CARTOGRAPHY(14, "CARTOGRAPHY", "CARTOGRAPHY"),
            STONECUTTER(14, "STONECUTTER", "STONECUTTER"),
            SMITHING(16, "SMITHING", "SMITHING");

            private final static Class<?> CONTAINER_CLASS = ReflectionUtils.getNMSClass("Containers");

            private int version;
            private String fieldName;
            private String[] bukkitIntTypeName;

            private ContainerType(int version, String fieldName, String... invType) {
                this.version = version;
                this.fieldName = fieldName;
                this.bukkitIntTypeName = invType;
            }

            public static ContainerType getContainerType(Object containerObject) {
                InventoryView view = null;
                try {
                    Method getBukkitViewMethod = ReflectionUtils.getNMSClass("Container").getMethod("getBukkitView");
                    Object containerObjectBukkitViewObject = getBukkitViewMethod.invoke(containerObject);
                    if (!(containerObjectBukkitViewObject instanceof InventoryView))
                        return null;
                    view = (InventoryView) containerObjectBukkitViewObject;
                } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException |
                        InvocationTargetException e) {
                    // So many errors :(
                    e.printStackTrace();
                }

                // Comapre with string and not the InventoryType because all inventory types
                // do not exist in all versions
                String type = view.getTopInventory().getType().name();

                if (type.equals("CHEST"))
                    return ContainerType.valueOf("GENERIC_9X" + view.getTopInventory().getSize() / 9);

                for (ContainerType containerType : values())
                    for (String bukkitIntTypeName : containerType.bukkitIntTypeName)
                        if (bukkitIntTypeName.equals(type))
                            return containerType;

                return null;
            }

            /**
            * @return The instance of the containers class
            */

            public Object getObject() {
                try {
                    Field f = CONTAINER_CLASS.getField(getFieldName());
                    return f.get(null);
                } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
                    e.printStackTrace();
                }
                return null;
            }

            public int getVersion() {
                return version;
            }

            public String getFieldName() {
                return fieldName;
            }
        }

    }
     
    It uses ReflectionsUtil
    Edit: The code didn't work, now it does, or at least in 1.14. I'm shure it won't work in 1.13 and below (yet) because the constructor of the packet changed
     
    #14 Nemo_64, Jun 30, 2020 at 7:15 PM
    Last edited: Jul 1, 2020 at 10:20 AM
    • Useful Useful x 1
  15. It's a nice resource, but keep in mind that using Containers.GENERIC_9X6 the inventory will be updated as a chest of 54 slots (9X6), something undesirable if we have a chest of 27 slots (9X3). To solve that you can make a simple switch that will return the indicated Container depending on the number of slots, something like this:

    Code (Java):
        public Containers<?> getContainerBySlots(int slots) {
            switch (slots / 9) {
                case 1:
                    return Containers.GENERIC_9X1;
                case 2:
                    return Containers.GENERIC_9X2;
                case 3:
                    return Containers.GENERIC_3X3;
                case 4:
                    return Containers.GENERIC_9X4;
                case 5:
                    return Containers.GENERIC_9X5;
                case 6:
                    return Containers.GENERIC_9X6;
            }
            // Maybe it's not a chest?
            return null;
        }
    Then you use it like this: (make sure the inventory is a chest first)
    Code (Text):
    PacketPlayOutOpenWindow packet = new PacketPlayOutOpenWindow(ep.activeContainer.windowId, getContainerBySlots(entityPlayer.activeContainer.getBukkitView().getTopInventory().getSize()), invTitle);
    Yes, in 1.13 instead of using a Container you've to use "minecraft:chest" (if I'm not mistaken :unsure:).
     
    • Agree Agree x 1
  16. with others it would be minecraft:anvil etc?
    I'd like to turn this into a fully functional class with reflection
     
  17. Yes, it should. Anyway if I'm not mistaken the title of the anvil can only be changed in 1.14+. But the others I think is possible.
    That would be great :)
     
  18. When originally figuring this out, I believe the constructor changed in 1.14, where they removed one of the input values (my mind has gone blank on me, can't think of what it is properly called). Ill have a look and try and figure it out for 1.13
     
  19. Oh yes, I knew I had forgotten something, That is actually a pretty important thing. Thanks for adding this
     
    • Friendly Friendly x 1
  20. I checked the code, 1.13 the constructor is with a string, in 1.14 with the containers
    1.13:
    public PacketPlayOutOpenWindow(int paramInt, String paramString, IChatBaseComponent paramIChatBaseComponent)

    1.14:
    public PacketPlayOutOpenWindow(int var0, Containers<?> var1, IChatBaseComponent var2)
     
    #20 Nemo_64, Jul 1, 2020 at 10:07 AM
    Last edited: Jul 1, 2020 at 10:22 AM