[TUT] Item tooltips with the ChatComponent API

Discussion in 'Spigot Plugin Development' started by SainttX, May 14, 2015.

  1. Hi all, today we're going to be creating and sending text messages to players that have an item when you hover the text. I'm creating this tutorial because up until now I've only know how to create basic text messages through the ChatComponent API and item tooltips weren't very intuitive (so I resorted to 3rd party libraries like fanciful).

    This tutorial will explain and demonstrate (with code examples) what we need to do, and how to do it. The final result will look like this:
    [​IMG]

    The tutorial uses a ReflectionUtil class that you can find here: https://gist.github.com/sainttx/34fdd8fa7657024414ba

    Let's begin.

    So essentially our checklist to complete the above task is as follows (in no particular order):
    • We required a structured JSON chat message to send to the client
      • This JSON message needs a hoverEvent with show_item action
      • If you're interested about the message structure, take a peak here: http://wiki.vg/Chat
    • We required an item to be used in the hoverEvent of the message
    • We require the item to be in a valid JSON string representation of itself
    We required an item
    We'll begin by actually getting the ItemStack that we want to use with as the hover event. You should all be familiar with ItemStack creation and ItemMeta, here's the helper method we're using to get our item:
    Code (Java):
    /*
    * Build our demonstration item
    */

    private ItemStack getExampleItemStack() {
        ItemStack itemStack = new ItemStack(Material.DIAMOND_SWORD);
        ItemMeta meta = itemStack.getItemMeta();
        List<String> lore = new ArrayList<String>();

        // Let's give this diamond sword a cool name and some lore
        meta.setDisplayName(ChatColor.YELLOW + "Super Sword");
        lore.add(ChatColor.RED + "This is a great sword!");
        meta.setLore(lore);

        // ...and some powerful enchantments
        meta.addEnchant(Enchantment.DAMAGE_ALL, 5, true);
        meta.addEnchant(Enchantment.FIRE_ASPECT, 2, true);

        itemStack.setItemMeta(meta);
        return itemStack;
    }
    This is just a regular diamond sword with a display name, a string of lore, and some enchantment. Nothing spectacular or overly complicated.

    We require the item to be in valid JSON
    How am I supposed to parse the item into a valid JSON String? The most reliable way to serialize the item into JSON is through the use of NMS. Here is a breakdown of the steps we must take to successfully do this:
    1. Convert the org.bukkit.inventory.ItemStack object into a net.minecraft.server.ItemStack object
    2. Use the net.minecraft.server.ItemStack#save(NBTTagCompound) method on an empty NBTTagCompound object to save the items data into the object
    3. Convert the NBTTagCompound into a String using Object#toString. This String is our valid JSON representation of the ItemStack
    First I'm going to show you how to accomplish this through the use of raw NMS, and then afterward we can break down the process and do it through the use of Reflection. I highly doubt that the method/class names will change, so the use of Reflection will likely give you long time use where you won't have to update your NMS pathways.

    Convert the org.bukkit.inventory.ItemStack object into a net.minecraft.server.ItemStack object
    This is done through the use of static method CraftItemStack#asNMSCopy(ItemStack), essentially:
    Code (Java):
    net.minecraft.server.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
    and through the use of Reflection:
    Code (Java):
    // ItemStack methods to get a net.minecraft.server.ItemStack object for serialization
    Class<?> craftItemStackClazz = ReflectionUtil.getOBCClass("inventory.CraftItemStack");
    Method asNMSCopyMethod = ReflectionUtil.getMethod(craftItemStackClazz, "asNMSCopy", ItemStack.class);
    Object nmsItemStackObj = asNMSCopyMethod.invoke(null, itemStack);
    Use the net.minecraft.server.ItemStack#save(NBTTagCompound) method on an empty NBTTagCompound object to save the items data into the object
    So to do this, all we need to do is create a new NBTTagCompound object and invoke the save method on it. Simple.
    Code (Java):
    net.minecraft.server.NBTTagCompound compound = new NBTTagCompound();
    compound = nmsItemStack.save(compound);
    A little less simple with Reflection.
    Code (Java):
    // NMS Method to serialize a net.minecraft.server.ItemStack to a valid Json string
    Class<?> nmsItemStackClazz = ReflectionUtil.getNMSClass("ItemStack");
    Class<?> nbtTagCompoundClazz = ReflectionUtil.getNMSClass("NBTTagCompound");
    Method saveNmsItemStackMethod = ReflectionUtil.getMethod(nmsItemStackClazz, "save", nbtTagCompoundClazz);

    Object nmsNbtTagCompoundObj; // This will just be an empty NBTTagCompound instance to invoke the saveNms method
    Object itemAsJsonObject; // This is the net.minecraft.server.ItemStack after being put through saveNmsItem method
    nmsNbtTagCompoundObj = nbtTagCompoundClazz.newInstance(); // Create the instance
    itemAsJsonObject = saveNmsItemStackMethod.invoke(nmsItemStackObj, nmsNbtTagCompoundObj);
    ... but not really that bad.

    Convert the NBTTagCompound into a String using Object#toString. This String is our valid JSON representation of the ItemStack
    This shouldn't really be a section, but all we do is take our net.minecraft.server.NBTTagCompound compound/Object itemAsJsonObject objects from step 2 and run them toString() on them.
    Code (Java):
    String json = compound.toString(); // standard object
    String json = itemAsJsonObject.toString(); // reflection object
    We'll put both of these into helper methods so that we can reuse the code easily for different items. Here are the results:
    Code (Java):
    /**
    * Converts an {@link org.bukkit.inventory.ItemStack} to a Json string
    * for sending with {@link net.md_5.bungee.api.chat.BaseComponent}'s.
    *
    * @param itemStack the item to convert
    * @return the Json string representation of the item
    */

    public String convertItemStackToJsonRegular(ItemStack itemStack) {
        // First we convert the item stack into an NMS itemstack
        net.minecraft.server.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
        net.minecraft.server.NBTTagCompound compound = new NBTTagCompound();
        compound = nmsItemStack.save(compound);

        return compound.toString();
    }
    Code (Java):
    /**
    * Converts an {@link org.bukkit.inventory.ItemStack} to a Json string
    * for sending with {@link net.md_5.bungee.api.chat.BaseComponent}'s.
    *
    * @param itemStack the item to convert
    * @return the Json string representation of the item
    */

    public String convertItemStackToJson(ItemStack itemStack) {
        // ItemStack methods to get a net.minecraft.server.ItemStack object for serialization
        Class<?> craftItemStackClazz = ReflectionUtil.getOBCClass("inventory.CraftItemStack");
        Method asNMSCopyMethod = ReflectionUtil.getMethod(craftItemStackClazz, "asNMSCopy", ItemStack.class);

        // NMS Method to serialize a net.minecraft.server.ItemStack to a valid Json string
        Class<?> nmsItemStackClazz = ReflectionUtil.getNMSClass("ItemStack");
        Class<?> nbtTagCompoundClazz = ReflectionUtil.getNMSClass("NBTTagCompound");
        Method saveNmsItemStackMethod = ReflectionUtil.getMethod(nmsItemStackClazz, "save", nbtTagCompoundClazz);

        Object nmsNbtTagCompoundObj; // This will just be an empty NBTTagCompound instance to invoke the saveNms method
        Object nmsItemStackObj; // This is the net.minecraft.server.ItemStack object received from the asNMSCopy method
        Object itemAsJsonObject; // This is the net.minecraft.server.ItemStack after being put through saveNmsItem method

        try {
            nmsNbtTagCompoundObj = nbtTagCompoundClazz.newInstance();
            nmsItemStackObj = asNMSCopyMethod.invoke(null, itemStack);
            itemAsJsonObject = saveNmsItemStackMethod.invoke(nmsItemStackObj, nmsNbtTagCompoundObj);
        } catch (Throwable t) {
            Bukkit.getLogger().log(Level.SEVERE, "failed to serialize itemstack to nms item", t);
            return null;
        }

        // Return a string representation of the serialized object
        return itemAsJsonObject.toString();
    }
    (the first is our standard, the second reflection)

    We required a structured JSON chat message
    Great! We have our valid Json representation of the ItemStack! Now all that's left is creating the chat message. Fortunately, the ChatComponent API takes care of all of this for us and all we have to do is figure out how to create the HoverEvents and TextComponents to send.

    A breakdown of what we do with the ChatComponent API:
    1. Create a TextComponent with the Json representation of the item
    2. Put the TextComponent object into a BaseComponent array (because HoverEvent's constructor requires the array for some reason)
    3. Create a new TextComponent for our actual text that the player will see
    4. Set the hover event for the newly created TextComponent
    5. Send the TextComponent to the player
    And in code, our helper method looks like this:
    Code (Java):
    /**
    * Sends a message to a player with an item as it's tooltip
    *
    * @param player      the player
    * @param message  the message to send
    * @param item        the item to display in the tooltip
    */

    public void sendItemTooltipMessage(Player player, String message, ItemStack item) {
        String itemJson = convertItemStackToJson(item);

        // Prepare a BaseComponent array with the itemJson as a text component
        BaseComponent[] hoverEventComponents = new BaseComponent[]{
                new TextComponent(itemJson) // The only element of the hover events basecomponents is the item json
        };

        // Create the hover event
        HoverEvent event = new HoverEvent(HoverEvent.Action.SHOW_ITEM, hoverEventComponents);

        /* And now we create the text component (this is the actual text that the player sees)
         * and set it's hover event to the item event */

        TextComponent component = new TextComponent(message);
        component.setHoverEvent(event);

        // Finally, send the message to the player
        player.spigot().sendMessage(component);
    }
    So now we're basically done. We can use this in commands, util methods, or whatever you want. Here's a final example of usage in a command so that we actually see what we've done in-game:
    Code (Java):
    @Override
    public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
        if (!(sender instanceof Player)) {
            sender.sendMessage("Only players can view item tooltips!");
        } else {
            Player player = (Player) sender;
            ItemStack tooltipItem = getExampleItemStack();
            this.sendItemTooltipMessage(player, ChatColor.GREEN + "Hover over me to see a diamond sword!", tooltipItem);
        }
       
        return true;
    }
     
    • Useful Useful x 11
    • Winner Winner x 3
    • Informative Informative x 2
  2. first
     
    • Like Like x 2
  3. second. Nice tutorial and should help a lot of people with this stuff.
     
    #3 TheLightMC2, May 14, 2015
    Last edited: May 14, 2015
    • Like Like x 1
  4. clip

    Benefactor

    Third! Great tutorial man!
     
    • Like Like x 1
  5. So... Why not just build the JSON ourselves? Without using reflection or nms, we can still get about everything but the amount of nbt tags.
     
    • Agree Agree x 1
  6. Is there really much benefit in doing so? Sure, if you want you can and at that point you can just replace the second part of the tutorial (serializing the ItemStack) with your own JSON method. However I really don't see the point in going through the trouble to parse the ItemStack yourself when the methods referenced (CraftItemStack#asNMSCopy, net.minecraft.server.ItemStack#save(NBTTagCompound)) have been around for a fairly long time, are unlikely to change, and do everything for you including NBT tags and attributes.
     
  7. Bumping because this thread is cool.
    And there are too many premium resources with this ezpz method.
     
  8. lol I was trying to do this long time ago but the problem is I try doing player.spigot().whatever
    but it never works .spigot() doesn't work for me, I imported spigot and everything required but still, I don't know whats the problem, can you help me
     
  9. Use spigot?
     
  10. its not about using spigot, its the whole plugin not compiling because of this error
     
    • Optimistic Optimistic x 1
  11. This seems to be broken for 1.9 clients, they cant see the item properly.
     
  12. Hello, great tutorial!
    How can i get only item name as string?
     
  13. How can you add raw text(without hover event) right after a hover event text??
    The raw text will still have the hover if I use
    hover_text.addExtra(raw_text)

    Code (Text):
    net.minecraft.server.v1_12_R1.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(item);
                            NBTTagCompound compound = new NBTTagCompound();
                            nmsItemStack.save(compound);
                            String json = compound.toString();
                            BaseComponent[] hoverEventComponents = new BaseComponent[]{
                                    new TextComponent(json) // The only element of the hover events basecomponents is the item json
                            };
                            HoverEvent hover_event = new HoverEvent(HoverEvent.Action.SHOW_ITEM, hoverEventComponents);
                            TextComponent component = new TextComponent("[" + ChatColor.GRAY + name + ChatColor.RESET + " x " + ChatColor.YELLOW + item.getAmount() + ChatColor.RESET + "]");
                            component.setHoverEvent(hover_event);


                            TextComponent component2 = new TextComponent();
                            component2.setText("raw text here");
                            component2.setHoverEvent(null);

                            component.addExtra(component2);

                            for (Player recipients : event.getRecipients()) {
                                recipients.spigot().sendMessage(component);
                            }
    ps. sorry by not using reflection, will correct it after I did the text connection
     
    #13 marklai1998, Jan 16, 2018
    Last edited: Jan 16, 2018
  14. 12th. This is cool I could use this for something.
     
    • Friendly Friendly x 1