1.15.2 onInventoryMoveItemEvent not working as expected

Discussion in 'Spigot Plugin Development' started by flokol120, Mar 25, 2020.

  1. I am trying to move items to another chest as soon as a hopper is moving items into a specific chest (unique by the chests location).
    hopper --> sender chest --> receiver chest, which is not connected by a hopper or anything
    | grab the items here and move them to the remote chest
    2020-03-25_14.13.29-min.png
    I also got it all working using the onInventoryCloseEvent, but this obviously does not work with hoppers (So if the user closes the chest, all contents will be sent to a remote chest).

    Now to my Problem with using the onInventoryMoveItemEvent: It does not work if I put only one item in the hopper! The item will be stuck in the hoppers destination chest. When using more than one item there will always be one item left in the chest.

    Little example:
    I am putting 4 stones into the chest above the hopper:
    2020-03-25_14.23.45-min.png Three of the items are actually getting into the "receiver" chest
    2020-03-25_14.23.56-min.png
    One item still remains in the "sender" chest 2020-03-25_14.24.07-min.png

    I also threw in some "debugging" messages which were also only called three times:

    Code (Text):

    [14:28:39] [Server thread/INFO]: STONE
    [14:28:39] [Server thread/INFO]: true
    [14:28:39] [Server thread/INFO]: true
    [14:28:39] [Server thread/INFO]: true
    [14:28:39] [Server thread/INFO]: about to move the items...
    [14:28:39] [Server thread/INFO]: 1
    [14:28:39] [Server thread/INFO]: 1
    [14:28:39] [Server thread/INFO]: sorted 1 stone to your chest
    [14:28:39] [Server thread/INFO]: removed item(s)
    [14:28:39] [Server thread/INFO]: STONE
    [14:28:39] [Server thread/INFO]: true
    [14:28:39] [Server thread/INFO]: true
    [14:28:39] [Server thread/INFO]: true
    [14:28:39] [Server thread/INFO]: about to move the items...
    [14:28:39] [Server thread/INFO]: 1
    [14:28:39] [Server thread/INFO]: 2
    [14:28:39] [Server thread/INFO]: sorted 1 stone to your chest
    [14:28:39] [Server thread/INFO]: removed item(s)
    [14:28:40] [Server thread/INFO]: STONE
    [14:28:40] [Server thread/INFO]: true
    [14:28:40] [Server thread/INFO]: true
    [14:28:40] [Server thread/INFO]: true
    [14:28:40] [Server thread/INFO]: about to move the items...
    [14:28:40] [Server thread/INFO]: 1
    [14:28:40] [Server thread/INFO]: 3
    [14:28:40] [Server thread/INFO]: sorted 1 stone to your chest
    [14:28:40] [Server thread/INFO]: removed item(s)
     
    My Code (sorry, but it is in Kotlin. Therefore it is fairly well documented, the code is also on GitHub it just not includes the onInventoryMoveItemEvent yet [https://github.com/flokol120/Spigot-Item-Chest-Sorter]):

    Listener.kt (only the relevant parts, if you need more feel free to ask):
    Code (Java):
       
    @EventHandler
    fun onInventoryCloseEvent(e: InventoryCloseEvent) {
        // runBlocking = staying on the main thread, because spigot does not seem to support proper multi-threading
        runBlocking {
            checkInventory(e.inventory, e.player)
        }
    }

    @EventHandler
    fun onInventoryMoveItemEvent(e: InventoryMoveItemEvent) {
        // runBlocking = staying on the main thread, because spigot does not seem to support proper multi-threading
        runBlocking {
            if(e.destination.type == InventoryType.CHEST && e.source.type == InventoryType.HOPPER) {
                checkInventory(e.destination, null)
            }
        }
    }

    /**
    * checks the omitted inventory for a potential transfer of items
    * @param inventory Inventory of the sender chest
    * @param player player who triggered the event
    *
    * @author Flo D?rr
    */

    private suspend fun checkInventory(inventory: Inventory, player: HumanEntity? = null) {
        if(inventory.location !== null) {
            // get the sender by the inventory location
            val sender = db.getSenderByCords(locationToCords(inventory.location!!))
            // if null chest is no sender chest
            if(sender !== null) {
                // get items in chest
                val contents: Array<ItemStack?> = inventory.contents
                if(contents.isNotEmpty()) {
                    // loop through all chest slots
                    val receivers = sender.receiver

                    if(receivers.size > 0) {
                        val airReceiver = ArrayList<HashMap<String, Any?>>()
                        val realReceiver = ArrayList<HashMap<String, Any?>>()

                        val world = player?.world ?: inventory.location!!.world!!

                        for (receiver in receivers) {
                            val leftChest = world.getBlockAt(cordsToLocation(receiver.cords.left, world)).state as Chest
                            // get right chest if cords not null
                            val rightChest = if(receiver.cords.right != null) {
                                inventory.location!!.world
                                world.getBlockAt(cordsToLocation(receiver.cords.right!!, world)).state as Chest
                            }else{
                                null
                            }
                            // get block in item frame on chest
                            val block = getItemFromItemFrameNearChest(leftChest, rightChest)
                            // check if no item frame is placed and give hint to user
                            if (block != null) {
                                val map = HashMap<String, Any?>()
                                map["leftChest"] = leftChest
                                map["block"] = block
                                if(!block.type.isAir) {
                                    realReceiver.add(map)
                                }else{
                                    airReceiver.add(map)
                                }
                            }else{
                                val message = "${ChatColor.YELLOW}There is a receiver chest which has no item frame on it. Please but an item frame on a receiver chest, containing the target item/block. You can also leave the item frame empty to accept all items which could not be sorted."
                                if(player != null) {
                                    player.sendMessage(message)
                                }else{
                                    main.server.consoleSender.sendMessage(message)
                                }
                            }
                        }

                        // left over items which cannot be sorted in a chest
                        val leftOverContent = ArrayList<ItemStack?>()

                        if(realReceiver.size > 0) {
                            for (content in contents) {
                                // get an itemstack if item cannot be sorted
                                val stack = handleItems(content, player, inventory, realReceiver)
                                if (stack != null) {
                                    // add this stack to the leftOverContent List
                                    leftOverContent.add(stack)
                                }
                            }
                        }else{
                            // add all items the leftOverContent if there are no realReceivers
                            leftOverContent.addAll(contents)
                        }
                        // if there are items which could not be sorted and there is at least one air chest (#1)
                        if(leftOverContent.size > 0 && airReceiver.size > 0) {
                            // sort items into "air chests"
                            var sendMessage = false
                            for (leftOver in leftOverContent) {
                                if(leftOver != null && !leftOver.type.isAir) {
                                    sendMessage = true
                                    handleItems(leftOver, player, inventory, airReceiver)
                                }
                            }
                            if(sendMessage) {
                                // only send a message to the player if there is more than air in the chest
                                val message = "${ChatColor.YELLOW}Found item(s) which are/is not specified. Sorting into air chest, if there is enough space..."
                                if(player != null) {
                                    player.sendMessage(message)
                                }else{
                                    main.server.consoleSender.sendMessage(message)
                                }
                            }
                        }
                    }else {
                        if(player != null) {
                            // some ugly chat message :( ...
                            val m1 = TextComponent("There are no receivers configured yet. Use the ")
                            m1.color = net.md_5.bungee.api.ChatColor.YELLOW
                            val m2 = TextComponent("/ics add receiver ")
                            m2.isItalic = true
                            m2.color = net.md_5.bungee.api.ChatColor.GRAY
                            m2.isUnderlined = true
                            m2.clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, "/ics add receiver")
                            val m3 = TextComponent("command.")
                            m3.color = net.md_5.bungee.api.ChatColor.YELLOW
                            m1.addExtra(m2)
                            m1.addExtra(m3)
                            player.spigot().sendMessage(m1)
                        }else{
                            main.server.consoleSender.sendMessage("There are no receivers configured yet")
                        }
                    }
                }
            }
        }
    }

    /**
    * handles the items to move them to their desired chest
    * @param content ItemStack in the sender chest
    * @param player player who triggered the event
    * @param inventory Inventory of the sender chest
    * @param receivers receiver HashMap
    * @return null or itemstack. If null the item was sorted successfully. If not no chest was found for this item.
    *
    * @author Flo D?rr
    */

    private fun handleItems(content: ItemStack?, player: HumanEntity?, inventory: Inventory, receivers: ArrayList<HashMap<String, Any?>>): ItemStack? {
        var notFound: ItemStack? = null
        if(content != null) {
            print(content.type.name)
            print(content.amount > 0 && !content.type.isAir)
        }
        if(content != null && content.amount > 0 && !content.type.isAir) {
            for (receiver in receivers) {
                val block = receiver["block"] as ItemStack?
                // First check if block == null or the bloc is air, if that is the case we have found an air chest
                // check if item in chest and item on frame are the same
                // check if we have an item set defined in the config.yml matching the item in the frame and in the sender chest
                print((block == null || block.type.isAir) || block.type == content.type || main.isItemInSet(content, block))
                if((block == null || block.type.isAir) || block.type == content.type || main.isItemInSet(content, block)) {
                    val leftChest = receiver["leftChest"] as Chest
                    // determine whether the chest has enough space to hold the items
                    val spaceResult = checkIfChestHasSpace(content, leftChest)
                    // if the spaceResult and the amount of items are the same, the chest is full with no space
                    // to put a single item. continue to find the next chest, if present.
                    if(spaceResult == content.amount) {
                        continue
                    }
                    // only put the amount of items in the receiver chest it can hold
                    if (spaceResult > 0) {
                        content.amount -= spaceResult
                        val message = "${ChatColor.DARK_GREEN}A chest is about to be full. ${ChatColor.YELLOW}$spaceResult${ChatColor.DARK_GREEN} items will be left over if no other chest with this item is defined."
                        if(player != null) {
                            player.sendMessage(message)
                        }else{
                            main.server.consoleSender.sendMessage(message)
                        }
                    }
                    print("about to move the items...")
                    // move the items
                    moveItem(content, leftChest, player, inventory)

                    // if there were leftover items: add leftover items to sender chest and continue to find the next chest, if present.
                    if(spaceResult > 0) {
                        content.amount = spaceResult
                        inventory.addItem(content)
                        continue
                    }else{
                        // if all items fitted in the chest: No need to search for another, breaking...
                        // also there was a chest found for the items, resetting notFound...
                        notFound = null
                        break
                    }
                }else{
                    // so far no chest was found containing this item, remember it by assigning it to notFound
                    notFound = content
                }
            }
        }
        // return notFound can be null or an itemstack
        return notFound
    }

    /**
    * handles the items to move them to their desired chest
    * @param content ItemStack in the sender chest
    * @param leftChest left part of a chest
    * @param player player who triggered the event
    * @param inventory Inventory of the sender chest
    *
    * @author Flo D?rr
    */

    private fun moveItem(content: ItemStack, leftChest: Chest, player: HumanEntity?, inventory: Inventory) {
        // got some weird bugs with the amount... defining this var actually helped :thinking:
        val amount = content.amount
        print(amount)
        // add to (left) chest. Do not use blockInventory as its only the leftChests inventory
        // inventory represents the whole potential double chest inventory
        leftChest.inventory.addItem(content)
        println(leftChest.inventory.contents[0]?.amount)

        val message = "${ChatColor.GREEN}sorted ${ChatColor.YELLOW}${amount} ${ChatColor.GREEN}${ChatColor.AQUA}${getItemName(content)} ${ChatColor.GREEN}to your chest"
        if(player != null) {
            player.sendMessage(message)
        }else{
            main.server.consoleSender.sendMessage(message)
        }
        //remove the item from the sender chest
        inventory.removeItem(content)
        print("removed item(s)")
    }
     
    ItemChestSorter.kt (Main Class, all of it):
    Code (Java):

    package com.flodoerr.item_chest_sorter

    import com.flodoerr.item_chest_sorter.json.JsonHelper
    import org.bukkit.entity.Item
    import org.bukkit.inventory.ItemStack
    import org.bukkit.plugin.java.JavaPlugin
    import org.bukkit.util.StringUtil
    import java.nio.file.Paths

    class ItemChestSorter: JavaPlugin() {

        private lateinit var db: JsonHelper

        override fun onEnable() {
            super.onEnable()

            createConfig()

            if(this.config.getBoolean("enabled")) {
                db = JsonHelper(dataFolder, server.consoleSender)

                // register commands
                getCommand(BASE_COMMAND)!!.setExecutor(Commands(db))

                // register tab completer
                getCommand(BASE_COMMAND)!!.setTabCompleter { _, _, _, args ->
                    val completions = ArrayList<String>()
                    if(args.size == 1){
                        StringUtil.copyPartialMatches(args[0], COMMANDS[0], completions)
                    }else if(args.size == 2) {
                        if(args[0].toLowerCase() == COMMANDS[0][2]) {
                            return@setTabCompleter listOf(COMMANDS[1][1])
                        }else{
                            StringUtil.copyPartialMatches(args[1], COMMANDS[1], completions)
                        }
                    }
                    return@setTabCompleter completions
                }

                //register listener
                server.pluginManager.registerEvents(Listener(db, this), this)

                server.consoleSender.sendMessage("Item-Chest-Sorter loaded.")
            }else{
                server.consoleSender.sendMessage("Item-Chest-Sorter loaded in ghost mode! Nothing will work. If this is not intended look in the config.yml.")
            }
        }

        override fun onLoad() {
            super.onLoad()
            this.reloadConfig()
            server.consoleSender.sendMessage("Item-Chest-Sorter config reloaded.")
        }

        override fun onDisable() {
            server.consoleSender.sendMessage("Item-Chest-Sorter is going to stop...")
            super.onDisable()
        }

        /**
         * creates the default config file if needed
         *
         * @author Flo D?rr
         */

        private fun createConfig() {
            val configFile = Paths.get(dataFolder.absolutePath, "config.yml").toFile()
            if(!configFile.exists()) {
                this.saveDefaultConfig()
            }
        }

        /**
         * gets the sets defined in the config.yml
         * I know that this is an ArrayList<ArrayList<String>> so suppressing the unchecked cast should be OK
         * @return 2d list of sets
         *
         * @author Flo D?rr
         */

        @Suppress("UNCHECKED_CAST")
        fun getSets(): ArrayList<ArrayList<String>> {
            val sets = this.config.getList("sets")
            return if(sets != null) {
                sets as ArrayList<ArrayList<String>>
            }else{
                ArrayList()
            }
        }

        /**
         * checks if both, the item in the chest and the item in the frame are in one set
         * @param itemInChest ItemStack in the chest
         * @param itemInItemFrame ItemStack in the item frame
         * @return true if both items are in one set
         *
         * @author Flo D?rr
         */

        fun isItemInSet(itemInChest: ItemStack, itemInItemFrame: ItemStack): Boolean {
            val sets = getSets()
            for (set in sets) {
                if(set.contains(itemInChest.type.key.key) && set.contains(itemInItemFrame.type.key.key)) {
                    return true
                }
            }
            return false
        }
    }
     
    The JsonHelper class just contains functions to read (and write) saved chests from a json file. those chests are saved with their location in the world.

    Is this an error on my end? Or is there something wrong with spigot?