hyperdefined - flower doggy 🌺🐾

Home - Blog - Projects - Fursona

Bukkit Hack: Find Out Who Opens Newly Spawned Chest

Bukkit Hack: Find Out Who Opens Newly Spawned Chest

24 June 2023 - 2 minutes

How to use LootGenerateEvent to find out who opened a newly spawned chest.

When writing ToolStats, I wanted to figure out how to tell who opens a newly spawned chest. These are chests that spawn with structures. This is a little hack I’ve come up with.

You can use the LootGenerateEvent to first handle this process. This event is called when any loot is generated by the server. Loot in chests is not there when the chunk first loads, they spawn when the chest opens. For what I wanted to accomplish, this was perfect. When a player opens a newly spawned chest for the first time, I want to add NBT data to certain items and edit the lore.

My first initial method of doing this simply checked who had the chest opened a tick after this event was called. This was janky, however.

@EventHandler
    public void onGenerateLoot(LootGenerateEvent event) {
        InventoryHolder inventoryHolder = event.getInventoryHolder();
        if (inventoryHolder == null) {
            return;
        }
        Inventory chest = inventoryHolder.getInventory();
        // run task later since if it runs on the same tick it breaks idk
        Bukkit.getScheduler().runTaskLater(toolStats, () -> {
            // see what player current has the chest open
            Player player = (Player) chest.getViewers().get(0);
            // do a classic for loop so we keep track of chest index of item
            for (int i = 0; i < chest.getContents().length; i++) {
                ItemStack itemStack = chest.getItem(i);
                // ignore air
                if (itemStack == null || itemStack.getType() == Material.AIR) {
                    continue;
                }
                // if it's an item we want, apply the lore
                String name = itemStack.getType().toString().toLowerCase(Locale.ROOT);
                for (String x : validItems) {
                    if (name.contains(x)) {
                        chest.setItem(i, addLore(itemStack, player));
                    }
                }
            }

        },1);
    }

I remember sometime after I added this, someone reported to me an exception that was thrown. I did not save this message, but I remember it had something to do with the chest not having anyone looking into it. The plugin assumed a player was supposed to be there always, but this failed since I didn’t account for this. So instead, I had to be more creative.

While looking into what I can access from this event, I found out I can get the location of where this loot generates. With this, I could see who was around the chest and get the player from there. However, more than 1 player can be standing next to the chest. I can’t get the closet player since it could be wrong. Instead, I had a better idea.

Whenever a player opens a chest, I save both the block and player to a Map. Afterwards, it gets removed 1 second later. When the LootGenerateEvent event is called, it checks this Map and compares the distance between the loot location and the chest. If they are under 1 block, that means it’s the right location! I tested this, and it produces the same difference in distance each time. Here is the trick now:

@EventHandler(priority = EventPriority.HIGHEST)
    public void onGenerateLoot(LootGenerateEvent event) {
        InventoryHolder inventoryHolder = event.getInventoryHolder();
        if (inventoryHolder == null) {
            return;
        }
        Location lootLocation = event.getLootContext().getLocation();
        Inventory chestInv = inventoryHolder.getInventory();

        if (inventoryHolder instanceof Chest) {
            Block openedChest = null;
            // look at the current list of opened chest and get the distance
            // between the lootcontext location and chest location
            // if the distance is less than 1, it's the same chest
            for (Block chest : toolStats.playerInteract.openedChests.keySet()) {
                Location chestLocation = chest.getLocation();
                // make sure it's in the same world, as a player can open a chest when loot generates
                // in another world
                if (chest.getWorld() == lootLocation.getWorld()) {
                    double distance = lootLocation.distance(chestLocation);
                    // see if it's the same block
                    if (distance <= 1.0) {
                        openedChest = chest;
                    }
                }
            }
            // ignore if the chest is not in the same location
            if (openedChest == null) {
                return;
            }

            // run task later since if it runs on the same tick it breaks idk
            Block finalOpenedChest = openedChest;
            Bukkit.getScheduler().runTaskLater(toolStats, () -> {
                Player player = toolStats.playerInteract.openedChests.get(finalOpenedChest);
                // do a classic for loop so we keep track of chest index of item
                for (int i = 0; i < chestInv.getContents().length; i++) {
                    ItemStack itemStack = chestInv.getItem(i);
                    // ignore air
                    if (itemStack == null || itemStack.getType() == Material.AIR) {
                        continue;
                    }
                    // if it's an item we want, apply the lore
                    if (ItemChecker.isValidItem(itemStack.getType())) {
                        ItemStack newItem = addLore(itemStack, player);
                        if (newItem != null) {
                            chestInv.setItem(i, newItem);
                        }
                    }
                }

            }, 1);
        }
        // more code below
    }

This trick works, and I am really happy with the idea. I’m sure there is some way to break this with another player, but it works well for the time being.