Resource Storing Inventories as strings in ItemLore!

Discussion in 'Spigot Plugin Development' started by ThirtyVirus, Oct 21, 2018.

  1. Hello SpigotMC people (and people googling how to do this). For use in a plugin I am developing (Uber Items) I needed to find a way to store an inventory inside of the item lore of an item. I managed to find a way to do this that is not dependent on nms and (in my experience) does not cause lag. I developed this method in version 1.13.1, and am confident that any version downgrades / upgrades will require minimal changes in code (since I don't use nms).

    If you would like to use lore to display info to the player, you may do so as normal, then simply change the second argument of getItemsFromLore and third argument of saveItemsInLore to the index of where the lore is free. I would advise that the items stored in lore be the last thing in the list, because there is no telling at what index the serialized items list will end.

    To use this code, simply:

    Code (Text):
    //Save Inventory Contents to ItemStack lore
    ItemStack[] itemsToSave = INSERT_INVENTORY_HERE.getContents();
    saveItemsInLore(SOME_ITEMSTACK, itemsToSave, 0);


    //Get Inventory contents from item lore
    ItemStack[] itemsToLoad = getItemsFromLore(SOME_ITEMSTACK, 0);
    INSERT_INVENTORY_HERE.setContents(itemsToLoad);

    The code:
    Code (Text):
        //Save list of items into the lore of a given item, starting at startLoreIndex
        public static void saveItemsInLore(ItemStack item, ItemStack[] items, int startLoreIndex){
            String dataString = itemsToString(items);
     
            ArrayList<String> loreChunks = new ArrayList<String>();
     
            for (int index = 0; index < startLoreIndex; index++) {
                loreChunks.add(item.getItemMeta().getLore().get(index));
            }
     
            while (dataString.length() > 0){
                String chunk = "";
       
                if (dataString.length() >= 510){
                    chunk = dataString.substring(0, 510);
                    dataString = dataString.substring(510);
                }
                else{
                    chunk = dataString;
                    dataString = "";
                }

                loreChunks.add(convertToInvisibleString(chunk));

            }
            loreItem(item, loreChunks);
        }
     
        //Load list of items from the lore of a given item, starting at startLoreIndex
        public static ItemStack[] getItemsFromLore(ItemStack item, int startLoreIndex){
     
            String itemString = "";
            while (startLoreIndex < item.getItemMeta().getLore().size()){
                itemString = itemString + item.getItemMeta().getLore().get(startLoreIndex);
                startLoreIndex++;
            }
     
            if (!itemString.equals("")){
                return stringToItems(convertToVisibleString(itemString));
            }
            else {
                return null;
            }
        }
     
        //Convert list of items into string
        public static String itemsToString(ItemStack[] items) {
            try {
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(bos);
                oos.writeObject(serializeItemStack(items));
                oos.flush();
                return DatatypeConverter.printBase64Binary(bos.toByteArray());
            }
            catch (Exception e) {
                //Logger.exception(e);
            }
            return "";
        }

        //Convert string to list of items
        @SuppressWarnings("unchecked")
        public static ItemStack[] stringToItems(String s) {
            try {
                ByteArrayInputStream bis = new ByteArrayInputStream(
                       DatatypeConverter.parseBase64Binary(s));
                ObjectInputStream ois = new ObjectInputStream(bis);
                return deserializeItemStack(
                       (Map<String, Object>[]) ois.readObject());
            }
            catch (Exception e) {
                //Logger.exception(e);
            }
            return new ItemStack[] {
                   new ItemStack(Material.AIR) };
        }

        //Serialize list of items
        @SuppressWarnings("unchecked")
        private static Map<String, Object>[] serializeItemStack(ItemStack[] items) {

            Map<String, Object>[] result = new Map[items.length];

            for (int i = 0; i < items.length; i++) {
                ItemStack is = items[i];
                if (is == null) {
                    result[i] = new HashMap<>();
                }
                else {
                    result[i] = is.serialize();
                    if (is.hasItemMeta()) {
                        result[i].put("meta", is.getItemMeta().serialize());
                    }
                }
            }

            return result;
        }

        //Deserialize list of items
        @SuppressWarnings("unchecked")
        private static ItemStack[] deserializeItemStack(Map<String, Object>[] map) {
            ItemStack[] items = new ItemStack[map.length];

            for (int i = 0; i < items.length; i++) {
                Map<String, Object> s = map[i];
                if (s.size() == 0) {
                    items[i] = null;
                }
                else {
                    try {
                        if (s.containsKey("meta")) {
                            Map<String, Object> im = new HashMap<>(
                                   (Map<String, Object>) s.remove("meta"));
                            im.put("==", "ItemMeta");
                            ItemStack is = ItemStack.deserialize(s);
                            is.setItemMeta((ItemMeta) ConfigurationSerialization
                                   .deserializeObject(im));
                            items[i] = is;
                        }
                        else {
                            items[i] = ItemStack.deserialize(s);
                        }
                    }
                    catch (Exception e) {
                        //Logger.exception(e);
                        items[i] = null;
                    }
                }

            }

            return items;
        }
     
        //Makes string invisible to player
        public static String convertToInvisibleString(String s) {
            String hidden = "";
            for (char c : s.toCharArray()) hidden += ChatColor.COLOR_CHAR+""+c;
            return hidden;
        }
     
        //Makes invisible string visible to player
        public static String convertToVisibleString(String s){
            String c = "";
            c = c + ChatColor.COLOR_CHAR;
            return s.replaceAll(c, "");
        }

        //Changes Item Lore
        public static ItemStack loreItem(ItemStack item, List<String> lore) {
            ItemMeta meta = item.getItemMeta();
            //lots of options for changing item meta data
            if (meta == null) return null;
            meta.setLore(lore);
            item.setItemMeta(meta);
            return item;
        }
     
    #1 ThirtyVirus, Oct 21, 2018
    Last edited: Oct 22, 2018
  2. Actually that is cool.
     
  3. Okx

    Okx

    What is "MainClass.loreItem/2"?
     
  4. Ah, I can revise the post to include that bit :D

    I found quite a few references to MainClass in there and removed them, if anything else doesn't work on drop-in let me know!
     
  5. Or just store an UUID in the lore and store the inventory somewhere else (e.g. a database).

    Also, this wastes bandwidth of users.
     
    • Agree Agree x 1
  6. Pretty sure the constant IO usage would hurt the server more? Also even with a full inventory there is no noticeable burden on the server or my client
     
  7. I don't really understand the utility of storing an inventory in the item lore, have you some examples ? :)
     
  8. There's an NMS method which basically does this already to a byte[], no need for a String. I can't take credit for finding the method, that goes to Exerosis here. I made the concept into a plugin, its in my signature and from there you can see the src.
     
  9. Of course! Introducing: the Shooty Box. This is a hand-held dispenser that shoots stuff out of it as if it was given a redstone signal. It contains a full inventory and supports everything a standard dispenser shoots and more :D
    https://twitter.com/Thirtyvirus/status/1049085629194272769
    https://twitter.com/Thirtyvirus/status/1049338482701873152
    https://twitter.com/Thirtyvirus/status/1049340987401801729
     
  10. nms scares me, also it needs updates every time the game changes. I appreciate it, but stay clear because I want the unchanged plugin to survive through many updates :l
     
  11. What about reflection?
     
  12. Why use NMS when you can do the same without ?
    @ThirtyVirus So if i understand, you are storing what the user put in your dispenser inventory, in the dispenser lore ?
     
  13. Lore strings are changed by other plugins-NBT tag would make it more flexible.
     
  14. The dispenser inventory is mostly for show (I can use any inventory I want). When the user finishes manipulating the inventory, the items are turned into a string and put into the ItemStack lore of the dispenser item in their hotbar.
     
  15. Why don't you use table or something like that instead of storing everything in the lore ?
    Use a table with Player and UUID as key and inventory content as value, when closing inventory add a random hidden UUID to the item in hand lore.
    When right/left clicking get the item in hand get the UUID and with that UUID and your Player instance you can get the content of the inventory.

    Code (Java):
    private final Table<Player, UUID, ItemStack[]> contents = HashBasedTable.create();

    @EventHandler
    public void onInventoryClose( InventoryCloseEvent e ) {
        if ( e.getInventory().equals( yourInventory ) ) {
            final UUID uuid = UUID.randomUUID();
            setItemId( e.getPlayer().getInventory().getItemInMainHand(), uuid );
            contents.put( (Player) e.getPlayer(), uuid, e.getInventory().getContents() );
        }
    }

    @EventHandler
    public void onPlayerInteract( PlayerInteractEvent e ) {
        if ( e.getItem().getType() == Material.DISPENSER ) {
            if ( e.getAction() == Action.RIGHT_CLICK_AIR ) {
                final UUID uuid = getItemId( e.getItem() );
                final ItemStack[] content = contents.get( e.getPlayer(), uuid );
                if ( content != null ) {
                    // Launch what ever is needed depending on inventory
                } else {
                    // Need reload
                }
            } else if ( e.getAction() == Action.LEFT_CLICK_AIR ) {
                // Open GUI for reloading
            }
        }
    }
    EDIT: You can also just use a Map with UUID as key and ItemStack[] as value
     
    #15 FrozenLegend, Oct 22, 2018
    Last edited: Oct 22, 2018
  16. I thought about doing something like this when I started, but realized that things get complicated fast when you have to deal with saving / loading, IO, unplanned server crashes causing dupe glitches, etc... So I went with the direct approach by literally storing the inventory IN the item xD
     
  17. Firestar311

    Supporter

    While personally, I would agree that using NBT would be much safer and harder for other people to modify, using the lore is creative and works better than what @FrozenLegend said because lore is automatically saved so he doesn't have to worry about that himself

    I commend you @ThirtyVirus for this, and it is pretty cool, mind if I use it with slight modification (NBT instead of lore)?
     
  18. Go ahead! I am interested in seeing what this would look like with NBT :p
     
  19. Firestar311

    Supporter

    Ok, I have a version dependent with NBT as it is NMS stuff, I will work on version independent later

    The only two methods that actually changed are the saveItemsInLore() and the getItemsFromLore()
    I also removed the other lore based methods
    Screenshot of the changed methods
    [​IMG]

    Here is the full class that I created (I named it something that I thought appropriate)
    This is dependent on 1.13.1 but Imports can be changed, I will have one with reflection soon though

    Code (Java):
    import net.minecraft.server.v1_13_R2.NBTTagCompound;
    import net.minecraft.server.v1_13_R2.NBTTagString;
    import org.bukkit.Material;
    import org.bukkit.configuration.serialization.ConfigurationSerialization;
    import org.bukkit.craftbukkit.v1_13_R2.inventory.CraftItemStack;
    import org.bukkit.inventory.ItemStack;
    import org.bukkit.inventory.meta.ItemMeta;

    import javax.xml.bind.DatatypeConverter;
    import java.io.*;
    import java.util.HashMap;
    import java.util.Map;

    public class InventoryStore {
        //Save list of items into a NBTTag of a given item
        public static ItemStack saveItemsInNBT(ItemStack item, ItemStack[] items){
            String dataString = itemsToString(items);
           
            net.minecraft.server.v1_13_R2.ItemStack nmsStack = CraftItemStack.asNMSCopy(item);
            NBTTagCompound tagCompound = nmsStack.getOrCreateTag();
            tagCompound.set("invdata", new NBTTagString(dataString));
            return CraftItemStack.asBukkitCopy(nmsStack);
        }
       
        //Load list of items from the NBTTag of the item
        public static ItemStack[] getItemsFromNBT(ItemStack item){
            net.minecraft.server.v1_13_R2.ItemStack nmsStack = CraftItemStack.asNMSCopy(item);
            NBTTagCompound tagCompound = nmsStack.getTag();
            if (tagCompound == null) return null;
           
            String itemString = tagCompound.getString("invdata");
           
            if (!itemString.equals("")){
                return stringToItems(itemString);
            }
            else {
                return null;
            }
        }
       
        //Convert list of items into string
        public static String itemsToString(ItemStack[] items) {
            try {
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(bos);
                oos.writeObject(serializeItemStack(items));
                oos.flush();
                return DatatypeConverter.printBase64Binary(bos.toByteArray());
            }
            catch (Exception e) {
                //Logger.exception(e);
            }
            return "";
        }
       
        //Convert string to list of items
        @SuppressWarnings("unchecked")
        public static ItemStack[] stringToItems(String s) {
            try {
                ByteArrayInputStream bis = new ByteArrayInputStream(
                        DatatypeConverter.parseBase64Binary(s));
                ObjectInputStream ois = new ObjectInputStream(bis);
                return deserializeItemStack(
                        (Map<String, Object>[]) ois.readObject());
            }
            catch (Exception e) {
                //Logger.exception(e);
            }
            return new ItemStack[] {
                    new ItemStack(Material.AIR) };
        }
       
        //Serialize list of items
        @SuppressWarnings("unchecked")
        private static Map<String, Object>[] serializeItemStack(ItemStack[] items) {
           
            Map<String, Object>[] result = new Map[items.length];
           
            for (int i = 0; i < items.length; i++) {
                ItemStack is = items[i];
                if (is == null) {
                    result[i] = new HashMap<>();
                }
                else {
                    result[i] = is.serialize();
                    if (is.hasItemMeta()) {
                        result[i].put("meta", is.getItemMeta().serialize());
                    }
                }
            }
           
            return result;
        }
       
        //Deserialize list of items
        @SuppressWarnings("unchecked")
        private static ItemStack[] deserializeItemStack(Map<String, Object>[] map) {
            ItemStack[] items = new ItemStack[map.length];
           
            for (int i = 0; i < items.length; i++) {
                Map<String, Object> s = map[i];
                if (s.size() == 0) {
                    items[i] = null;
                }
                else {
                    try {
                        if (s.containsKey("meta")) {
                            Map<String, Object> im = new HashMap<>(
                                    (Map<String, Object>) s.remove("meta"));
                            im.put("==", "ItemMeta");
                            ItemStack is = ItemStack.deserialize(s);
                            is.setItemMeta((ItemMeta) ConfigurationSerialization
                                    .deserializeObject(im));
                            items[i] = is;
                        }
                        else {
                            items[i] = ItemStack.deserialize(s);
                        }
                    }
                    catch (Exception e) {
                        //Logger.exception(e);
                        items[i] = null;
                    }
                }
               
            }
           
            return items;
        }
    }
     
    • Like Like x 1
  20. Firestar311

    Supporter

    Alright, I have finished the version independent version of this resource
    I have commented out the stuff that was previously version dependent for comparison

    EDIT: Added a demo video of this working

    Here is the Class itself
    Code (Java):

    import org.bukkit.Material;
    import org.bukkit.configuration.serialization.ConfigurationSerialization;
    import org.bukkit.inventory.ItemStack;
    import org.bukkit.inventory.meta.ItemMeta;

    import javax.xml.bind.DatatypeConverter;
    import java.io.*;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Method;
    import java.util.HashMap;
    import java.util.Map;

    public class InventoryStore {
     
        //Save list of items into a NBTTag of a given item
        public static ItemStack saveItemsInNBT(ItemStack item, ItemStack[] items) throws Exception {
            String dataString = itemsToString(items);
     
            Class<?> NMSItemStack = ReflectionUtils.getNMSClass("ItemStack");
            Class<?> craftItemStack = ReflectionUtils.getCraftClass("inventory.CraftItemStack");
            Class<?> nbtTagCompound = ReflectionUtils.getNMSClass("NBTTagCompound");
            Class<?> nbtBase = ReflectionUtils.getNMSClass("NBTBase");
            Class<?> nbtTagString = ReflectionUtils.getNMSClass("NBTTagString");
     
            Constructor<?> tagStringConstructor = nbtTagString.getConstructor(String.class);
     
            Method asNMSCopy = craftItemStack.getDeclaredMethod("asNMSCopy", ItemStack.class);
            Method asBukkitCopy = craftItemStack.getDeclaredMethod("asBukkitCopy", NMSItemStack);
            Method getOrCreateTag = NMSItemStack.getDeclaredMethod("getOrCreateTag");
            Method setTag = nbtTagCompound.getDeclaredMethod("set", String.class, nbtBase);
         
            Object rnmsStack = asNMSCopy.invoke(null, item);
            Object rtagCompound = getOrCreateTag.invoke(rnmsStack);
            setTag.invoke(rtagCompound, "invdata", tagStringConstructor.newInstance(dataString));
            return (ItemStack) asBukkitCopy.invoke(null, rnmsStack);
         
    //        net.minecraft.server.v1_13_R2.ItemStack nmsStack = CraftItemStack.asNMSCopy(item);
    //        NBTTagCompound tagCompound = nmsStack.getOrCreateTag();
    //        tagCompound.set("invdata", new NBTTagString(dataString));
    //        return CraftItemStack.asBukkitCopy(nmsStack);
        }
     
        //Load list of items from the NBTTag of the item
        public static ItemStack[] getItemsFromNBT(ItemStack item) throws Exception {
         
            Class<?> NMSItemStack = ReflectionUtils.getNMSClass("ItemStack");
            Class<?> craftItemStack = ReflectionUtils.getCraftClass("inventory.CraftItemStack");
            Class<?> nbtTagCompound = ReflectionUtils.getNMSClass("NBTTagCompound");
         
            Method getTag = NMSItemStack.getDeclaredMethod("getTag");
            Method asNMSCopy = craftItemStack.getDeclaredMethod("asNMSCopy", ItemStack.class);
            Method getString = nbtTagCompound.getDeclaredMethod("getString", String.class);
     
            Object rnmsStack = asNMSCopy.invoke(null, item);
            Object rtagCompound = getTag.invoke(rnmsStack);
         
            if (rtagCompound == null) return null;
         
            String itemString = (String) getString.invoke(rtagCompound, "invdata");
         
            //net.minecraft.server.v1_13_R2.ItemStack nmsStack = CraftItemStack.asNMSCopy(item);
            //NBTTagCompound tagCompound = nmsStack.getTag();
            //if (tagCompound == null) return null;
         
            //String itemString = tagCompound.getString("invdata");
         
            if (!itemString.equals("")){
                return stringToItems(itemString);
            }
            else {
                return null;
            }
        }
     
        //Convert list of items into string
        public static String itemsToString(ItemStack[] items) {
            try {
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(bos);
                oos.writeObject(serializeItemStack(items));
                oos.flush();
                return DatatypeConverter.printBase64Binary(bos.toByteArray());
            }
            catch (Exception e) {
                //Logger.exception(e);
            }
            return "";
        }
     
        //Convert string to list of items
        @SuppressWarnings("unchecked")
        public static ItemStack[] stringToItems(String s) {
            try {
                ByteArrayInputStream bis = new ByteArrayInputStream(
                        DatatypeConverter.parseBase64Binary(s));
                ObjectInputStream ois = new ObjectInputStream(bis);
                return deserializeItemStack(
                        (Map<String, Object>[]) ois.readObject());
            }
            catch (Exception e) {
                //Logger.exception(e);
            }
            return new ItemStack[] {
                    new ItemStack(Material.AIR) };
        }
     
        //Serialize list of items
        @SuppressWarnings("unchecked")
        private static Map<String, Object>[] serializeItemStack(ItemStack[] items) {
         
            Map<String, Object>[] result = new Map[items.length];
         
            for (int i = 0; i < items.length; i++) {
                ItemStack is = items[i];
                if (is == null) {
                    result[i] = new HashMap<>();
                }
                else {
                    result[i] = is.serialize();
                    if (is.hasItemMeta()) {
                        result[i].put("meta", is.getItemMeta().serialize());
                    }
                }
            }
         
            return result;
        }
     
        //Deserialize list of items
        @SuppressWarnings("unchecked")
        private static ItemStack[] deserializeItemStack(Map<String, Object>[] map) {
            ItemStack[] items = new ItemStack[map.length];
         
            for (int i = 0; i < items.length; i++) {
                Map<String, Object> s = map[i];
                if (s.size() == 0) {
                    items[i] = null;
                }
                else {
                    try {
                        if (s.containsKey("meta")) {
                            Map<String, Object> im = new HashMap<>(
                                    (Map<String, Object>) s.remove("meta"));
                            im.put("==", "ItemMeta");
                            ItemStack is = ItemStack.deserialize(s);
                            is.setItemMeta((ItemMeta) ConfigurationSerialization
                                    .deserializeObject(im));
                            items[i] = is;
                        }
                        else {
                            items[i] = ItemStack.deserialize(s);
                        }
                    }
                    catch (Exception e) {
                        //Logger.exception(e);
                        items[i] = null;
                    }
                }
             
            }
         
            return items;
        }
    }
    And here is the ReflectionUtils class that is used within this class
    Code (Java):

    import org.bukkit.Bukkit;
    import org.bukkit.entity.Player;

    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    import java.util.*;

    public final class ReflectionUtils {
     
        private ReflectionUtils() {
        }
     
        private static Map<String, Class<?>> cache = new HashMap<>();
     
        private static final String version = Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(",")[3];
     
        public static Class<?> getNMSClass(String nmsClassString) {
            try {
                String name = "net.minecraft.server." + version + "." + nmsClassString;
     
                if (cache.containsKey(name)) {
                    return cache.get(name);
                }
     
                Class<?> nmsClass = Class.forName(name);
                cache.put(name, nmsClass);
                return Class.forName(name);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            return null;
        }
     
        public static Class<?> getCraftClass(String craftClassString) {
            try {
                String name = "org.bukkit.craftbukkit." + version + "." + craftClassString;
             
                if (cache.containsKey(name)) {
                    return cache.get(name);
                }
             
                Class<?> craftClass = Class.forName(name);
                cache.put(name, craftClass);
                return craftClass;
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
         
            return null;
        }
     
        public static String getVersion() {
            return version;
        }
     
        public static void setField(Object target, String field, Object value) {
            try {
                Field f = target.getClass().getDeclaredField(field);
                f.setAccessible(true);
                f.set(target, value);
            } catch (Exception e) {
            }
        }
     
        public static <T> Field getField(Class<?> target, String name, Class<T> fieldType, int index) {
            for (final Field field : target.getDeclaredFields()) {
                if ((name == null || field.getName().equals(name)) && fieldType.isAssignableFrom(field.getType()) && index-- <= 0) {
                    field.setAccessible(true);
                    return field;
                }
            }
         
            // Search in parent classes
            if (target.getSuperclass() != null) return getField(target.getSuperclass(), name, fieldType, index);
            throw new IllegalArgumentException("Cannot find field with type " + fieldType);
        }
     
        public static void sendPacket(Object packet, Player player) {
            try {
                Class<?> craftPlayerClass = getCraftClass("entity.CraftPlayer");
                Class<?> entityPlayerClass = getNMSClass("EntityPlayer");
                Object craftPlayer = Objects.requireNonNull(craftPlayerClass).cast(player);
                Object handle = craftPlayerClass.getMethod("getHandle").invoke(craftPlayer);
                Object playerConnection = entityPlayerClass.getField("playerConnection").get(handle);
                Method sendPacket = playerConnection.getClass().getMethod("sendPacket", getNMSClass("Packet"));
                sendPacket.invoke(playerConnection, packet);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
     
    Here is a video that demonstrates this working properly
     

Share This Page