From c71f673bc46514719b7d405ae96dc648b7fb7a91 Mon Sep 17 00:00:00 2001 From: Jules Date: Fri, 21 Jun 2024 17:43:35 -0700 Subject: [PATCH] Improved `soulbound.can-drop` option to support moving items around in chests, dispensers... --- .../api/item/template/MMOItemTemplate.java | 22 +-- .../net/Indyuce/mmoitems/util/MMOUtils.java | 5 + .../net/Indyuce/mmoitems/MMOItemsBukkit.java | 4 + .../mmoitems/listener/ItemListener.java | 8 - .../mmoitems/listener/PlayerListener.java | 5 - .../listener/option/SoulboundNoDrop.java | 154 ++++++++++++++++++ MMOItems-Dist/src/main/resources/config.yml | 9 +- 7 files changed, 177 insertions(+), 30 deletions(-) create mode 100644 MMOItems-Dist/src/main/java/net/Indyuce/mmoitems/listener/option/SoulboundNoDrop.java diff --git a/MMOItems-API/src/main/java/net/Indyuce/mmoitems/api/item/template/MMOItemTemplate.java b/MMOItems-API/src/main/java/net/Indyuce/mmoitems/api/item/template/MMOItemTemplate.java index 94892246..22e7a195 100644 --- a/MMOItems-API/src/main/java/net/Indyuce/mmoitems/api/item/template/MMOItemTemplate.java +++ b/MMOItems-API/src/main/java/net/Indyuce/mmoitems/api/item/template/MMOItemTemplate.java @@ -192,22 +192,16 @@ public class MMOItemTemplate implements ItemReference, PreloadedObject { return options.contains(option); } - public MMOItemBuilder newBuilder(@Nullable Player player) { - if (player != null) { - return newBuilder(PlayerData.get(player).getRPG()); - } - return newBuilder((RPGPlayer) null); - } - public MMOItemBuilder newBuilder() { return newBuilder((RPGPlayer) null); } + public MMOItemBuilder newBuilder(@Nullable Player player) { + return newBuilder(player == null ? null : PlayerData.get(player).getRPG()); + } + public MMOItemBuilder newBuilder(@Nullable PlayerData player) { - if (player != null) { - return newBuilder(player.getRPG()); - } - return newBuilder((RPGPlayer) null); + return newBuilder(player == null ? null : player.getRPG()); } /** @@ -233,12 +227,11 @@ public class MMOItemTemplate implements ItemReference, PreloadedObject { * @param forDisplay Should it take modifiers into account * @return Item builder with random level and tier? */ + @NotNull public MMOItemBuilder newBuilder(@Nullable RPGPlayer player, boolean forDisplay) { // No player ~ default settings - if (player == null) { - return newBuilder(0, null); - } + if (player == null) return newBuilder(0, null); // Read from player int itemLevel = hasOption(TemplateOption.LEVEL_ITEM) ? MMOItems.plugin.getTemplates().rollLevel(player.getLevel()) : 0; @@ -251,6 +244,7 @@ public class MMOItemTemplate implements ItemReference, PreloadedObject { * @param itemTier The desired item tier, can be null * @return Item builder with specific item level and tier */ + @NotNull public MMOItemBuilder newBuilder(int itemLevel, @Nullable ItemTier itemTier) { return new MMOItemBuilder(this, itemLevel, itemTier); } diff --git a/MMOItems-API/src/main/java/net/Indyuce/mmoitems/util/MMOUtils.java b/MMOItems-API/src/main/java/net/Indyuce/mmoitems/util/MMOUtils.java index e6b68020..1bdd8e1c 100644 --- a/MMOItems-API/src/main/java/net/Indyuce/mmoitems/util/MMOUtils.java +++ b/MMOItems-API/src/main/java/net/Indyuce/mmoitems/util/MMOUtils.java @@ -36,6 +36,11 @@ public class MMOUtils { return particle.getDataType() == Particle.DustOptions.class; } + /** + * Optimized Soulbound check based on the fact that the + * compressed item Soulbound data contains only one UUID, + * the target player's UUID, sparing one Json parse pass. + */ public static boolean isSoulboundTo(@NotNull NBTItem item, @NotNull Player player) { final @Nullable String foundNbt = item.getString("MMOITEMS_SOULBOUND"); return foundNbt != null && foundNbt.contains(player.getUniqueId().toString()); diff --git a/MMOItems-Dist/src/main/java/net/Indyuce/mmoitems/MMOItemsBukkit.java b/MMOItems-Dist/src/main/java/net/Indyuce/mmoitems/MMOItemsBukkit.java index df955431..79fd6c33 100644 --- a/MMOItems-Dist/src/main/java/net/Indyuce/mmoitems/MMOItemsBukkit.java +++ b/MMOItems-Dist/src/main/java/net/Indyuce/mmoitems/MMOItemsBukkit.java @@ -6,6 +6,7 @@ import net.Indyuce.mmoitems.comp.PhatLootsHook; import net.Indyuce.mmoitems.gui.listener.GuiListener; import net.Indyuce.mmoitems.listener.*; import net.Indyuce.mmoitems.listener.option.DroppedItems; +import net.Indyuce.mmoitems.listener.option.SoulboundNoDrop; import org.bukkit.Bukkit; public class MMOItemsBukkit { @@ -33,6 +34,9 @@ public class MMOItemsBukkit { if (plugin.getLanguage().disableRemovedItems) Bukkit.getPluginManager().registerEvents(new DisabledItemsListener(plugin), plugin); + if (!plugin.getConfig().getBoolean("soulbound.can-drop")) + Bukkit.getPluginManager().registerEvents(new SoulboundNoDrop(), plugin); + // Profile support if (MythicLib.plugin.hasProfiles()) Bukkit.getPluginManager().registerEvents(new ProfileSupportListener(), plugin); diff --git a/MMOItems-Dist/src/main/java/net/Indyuce/mmoitems/listener/ItemListener.java b/MMOItems-Dist/src/main/java/net/Indyuce/mmoitems/listener/ItemListener.java index 12a959a3..802f8003 100644 --- a/MMOItems-Dist/src/main/java/net/Indyuce/mmoitems/listener/ItemListener.java +++ b/MMOItems-Dist/src/main/java/net/Indyuce/mmoitems/listener/ItemListener.java @@ -21,7 +21,6 @@ import org.bukkit.event.inventory.CraftItemEvent; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.inventory.PrepareItemCraftEvent; -import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.inventory.CraftingInventory; import org.bukkit.inventory.ItemStack; @@ -137,13 +136,6 @@ public class ItemListener implements Listener { if (newItem != null) event.setCurrentItem(newItem); } - @EventHandler(ignoreCancelled = true) - private void dropItem(PlayerDropItemEvent event) { - NBTItem nbt = NBTItem.get(event.getItemDrop().getItemStack()); - if (!MMOItems.plugin.getConfig().getBoolean("soulbound.can-drop") && nbt.hasTag("MMOITEMS_SOULBOUND")) - event.setCancelled(true); - } - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void playerJoin(PlayerJoinEvent event) { if (!MythicLib.plugin.hasProfiles()) updateInventory(event.getPlayer()); diff --git a/MMOItems-Dist/src/main/java/net/Indyuce/mmoitems/listener/PlayerListener.java b/MMOItems-Dist/src/main/java/net/Indyuce/mmoitems/listener/PlayerListener.java index 7883b937..2ca3b5ce 100644 --- a/MMOItems-Dist/src/main/java/net/Indyuce/mmoitems/listener/PlayerListener.java +++ b/MMOItems-Dist/src/main/java/net/Indyuce/mmoitems/listener/PlayerListener.java @@ -81,11 +81,6 @@ public class PlayerListener implements Listener { final ItemStack item = iterator.next(); final NBTItem nbt = NBTItem.get(item); - /* - * Not a perfect check but it's very sufficient and so we avoid - * using a JsonParser followed by map checkups in the SoulboundData - * constructor - */ if (nbt.getBoolean("MMOITEMS_DISABLE_DEATH_DROP") || (MMOItems.plugin.getLanguage().keepSoulboundOnDeath && MMOUtils.isSoulboundTo(nbt, player))) { iterator.remove(); soulboundInfo.registerItem(item); diff --git a/MMOItems-Dist/src/main/java/net/Indyuce/mmoitems/listener/option/SoulboundNoDrop.java b/MMOItems-Dist/src/main/java/net/Indyuce/mmoitems/listener/option/SoulboundNoDrop.java new file mode 100644 index 00000000..7ddcebfd --- /dev/null +++ b/MMOItems-Dist/src/main/java/net/Indyuce/mmoitems/listener/option/SoulboundNoDrop.java @@ -0,0 +1,154 @@ +package net.Indyuce.mmoitems.listener.option; + +import io.lumine.mythic.lib.api.item.NBTItem; +import net.Indyuce.mmoitems.util.MMOUtils; +import org.bukkit.entity.HumanEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.event.player.PlayerDropItemEvent; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +/** + * Useful Resources: + * - ... + * - ... + * + * @author Jules + */ +public class SoulboundNoDrop implements Listener { + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void cannotDrop(PlayerDropItemEvent event) { + if (isBound(event.getItemDrop().getItemStack(), event.getPlayer())) event.setCancelled(true); + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void cannotDragAround(InventoryDragEvent event) { + + if (event.getView().getType() == InventoryType.CRAFTING) return; + + // This easily allows to check if the item was dragged in or out of the player's inventory + final int topInventorySize = event.getView().getTopInventory().getContents().length; + for (Map.Entry entry : event.getNewItems().entrySet()) + if (entry.getKey() < topInventorySize && isBound(entry.getValue(), event.getWhoClicked())) { + event.setCancelled(true); + return; + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void cannotMoveAround(InventoryClickEvent event) { + + // Can only move around in + if (event.getView().getType() == InventoryType.CRAFTING) return; + + try { + + // Depends on click and inventory type. + final boolean result = isSafe(event); + if (!result) event.setCancelled(true); + } catch (RuntimeException exception) { + + // Safe check... + if (isBound(event.getCurrentItem(), event.getWhoClicked()) || isBound(event.getCursor(), event.getWhoClicked())) + event.setCancelled(true); + } + } + + private boolean isSafe(@NotNull InventoryClickEvent event) { + switch (event.getAction()) { + + /* + * Pickups + */ + case NOTHING: // 'Nothing happens' is safe enough + case PICKUP_ALL: // Can pickup any item + case PICKUP_SOME: + case PICKUP_HALF: + case PICKUP_ONE: + case COLLECT_TO_CURSOR: // Considered a pickup + case CLONE_STACK: // (Creative) Clones currentItem into cursor. Considered a pickup + case HOTBAR_MOVE_AND_READD: // Some is given to the player, but not the target inventory, hence safe! + return true; + + /* + * Drop cursor. Check cursor + */ + case DROP_ONE_CURSOR: // Check cursor (dropped) + case DROP_ALL_CURSOR: + return !isBound(event.getCursor(), event.getWhoClicked()); + + /* + * Drop current item. Check current item + */ + case DROP_ALL_SLOT: // Check current item (dropped) + case DROP_ONE_SLOT: + return !isBound(event.getCurrentItem(), event.getWhoClicked()); + + /* + * Places. Check cursor only if place is in remove inventory + */ + case SWAP_WITH_CURSOR: + case PLACE_ALL: + case PLACE_SOME: + case PLACE_ONE: { + + // Can place any item in player's inventory + if (event.getClickedInventory().getType() == InventoryType.PLAYER) return true; + + // Only accepted if the item is not soulbound + return !isBound(event.getCursor(), event.getWhoClicked()); + } + + /* + * Swap with hotbar. Check hotbar item only if + * swap is done with remote inventory + */ + case HOTBAR_SWAP: { + + // Can place any item in player's inventory + if (event.getClickedInventory().getType() == InventoryType.PLAYER) return true; + + // Check hotbar + final ItemStack hotbarItem = event.getWhoClicked().getInventory().getItem(event.getHotbarButton()); + return !isBound(hotbarItem, event.getWhoClicked()); + } + + /* + * Shift click item move. Check current item only if + * being placed in remove inventory + */ + case MOVE_TO_OTHER_INVENTORY: { + + // Can move anything to player's inventory + if (event.getClickedInventory().getType() != InventoryType.PLAYER) return true; + + // Check current item + return !isBound(event.getCurrentItem(), event.getWhoClicked()); + } + + /* + * For anything else, check both current item and cursor for safeguard. + * Maybe caused by 1.20.6+ inventory actions and other plugins. + */ + case UNKNOWN: + default: + throw new RuntimeException("Not implemented"); + } + } + + private boolean isBound(@Nullable ItemStack item, @NotNull HumanEntity player) { + return item != null && item.hasItemMeta() && MMOUtils.isSoulboundTo(NBTItem.get(item), (Player) player); + } +} + + diff --git a/MMOItems-Dist/src/main/resources/config.yml b/MMOItems-Dist/src/main/resources/config.yml index 9d02792c..6b639d59 100644 --- a/MMOItems-Dist/src/main/resources/config.yml +++ b/MMOItems-Dist/src/main/resources/config.yml @@ -125,12 +125,15 @@ soulbound: base: 1 per-lvl: 1 - # Whether or not soulbound items should be + # Whether soulbound items should be # kept when a player dies. keep-on-death: true - # Whether or not soulbound item can be - # dropped by the player + # [Experimental feature] + # When toggled off, players cannot drop or take + # Soulbound items away from their inventory. + # Requires `keep-on-death` enabled. + # Changes apply on server restart. can-drop: true # Enable, disable, and customize the weapon effects here.