diff --git a/.gradle/8.8/executionHistory/executionHistory.lock b/.gradle/8.8/executionHistory/executionHistory.lock index 3d7bdde..6e7a580 100644 Binary files a/.gradle/8.8/executionHistory/executionHistory.lock and b/.gradle/8.8/executionHistory/executionHistory.lock differ diff --git a/.gradle/8.8/fileHashes/fileHashes.lock b/.gradle/8.8/fileHashes/fileHashes.lock index 2022636..610ab4f 100644 Binary files a/.gradle/8.8/fileHashes/fileHashes.lock and b/.gradle/8.8/fileHashes/fileHashes.lock differ diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock index a0e16bd..b603c89 100644 Binary files a/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/.idea/modules/ArmorSMP.main.iml b/.idea/modules/ArmorSMP.main.iml index 746f1fd..bbeeb3e 100644 --- a/.idea/modules/ArmorSMP.main.iml +++ b/.idea/modules/ArmorSMP.main.iml @@ -11,8 +11,4 @@ - - - - \ No newline at end of file diff --git a/build/tmp/compileJava/previous-compilation-data.bin b/build/tmp/compileJava/previous-compilation-data.bin index 32aaaa5..7975c6b 100644 Binary files a/build/tmp/compileJava/previous-compilation-data.bin and b/build/tmp/compileJava/previous-compilation-data.bin differ diff --git a/src/main/java/me/trouper/armorsmp/data/Unique.java b/src/main/java/me/trouper/armorsmp/data/Unique.java index 7afd006..0ac2ff0 100644 --- a/src/main/java/me/trouper/armorsmp/data/Unique.java +++ b/src/main/java/me/trouper/armorsmp/data/Unique.java @@ -44,7 +44,8 @@ public enum Unique { DRAGON_EGG(ItemBuilder.create() .material(Material.NETHERITE_CHESTPLATE) .lore(Text.legacyColor("&bAbilities:")) - .lore(Text.legacyColor("&3| &7Dragon's Breath")) + .lore(Text.legacyColor("&3| &7Bad Breath")) + .lore(Text.legacyColor(" &1- &7Shoot Dragon's Breath for 7 Seconds")) .lore(Text.legacyColor("&3| &7Resistence I")) .lore(Text.legacyColor("&3| &7Strength I")) .enchant(Enchantment.BINDING_CURSE,1) @@ -78,7 +79,8 @@ public enum Unique { LEGGINGS(ItemBuilder.create() .material(Material.NETHERITE_LEGGINGS) .lore(Text.legacyColor("&bAbilities:")) - .lore(Text.legacyColor("&3| &7Knockback Shield")) + .lore(Text.legacyColor("&3| &7Back-Up")) + .lore(Text.legacyColor(" &1- &7Create a shield around you knocking players away")) .lore(Text.legacyColor("&3| &7Resistence I")) .enchant(Enchantment.BINDING_CURSE,1) .enchant(Enchantment.PROTECTION,4) @@ -90,8 +92,6 @@ public enum Unique { "Netherite Leggings", (p) -> { p.addPotionEffect(new PotionEffect(PotionEffectType.RESISTANCE,21,0,true,false,false)); }, (p) -> { - // TODO: Test Shield - Stream toKnockBack = p.getNearbyEntities(10,10,10).stream().filter(target -> { if (!(target instanceof Player v)) return false; boolean tooClose = target.getLocation().distance(p.getLocation()) < 10; @@ -140,6 +140,7 @@ public enum Unique { .material(Material.NETHERITE_BOOTS) .lore(Text.legacyColor("&bAbilities:")) .lore(Text.legacyColor("&3| &7Dash")) + .lore(Text.legacyColor(" &1- &7Launch yourself forwards")) .lore(Text.legacyColor("&3| &7Speed 1")) .enchant(Enchantment.BINDING_CURSE,1) .enchant(Enchantment.PROTECTION,4) @@ -175,6 +176,9 @@ public enum Unique { }, 50), SWORD(ItemBuilder.create() .material(Material.NETHERITE_SWORD) + .lore(Text.legacyColor("&bAbilities:")) + .lore(Text.legacyColor("&3| &7Go Wild")) + .lore(Text.legacyColor(" &1- &7Strength 2 for 7 seconds")) .enchant(Enchantment.UNBREAKING,3) .enchant(Enchantment.MENDING,1) .enchant(Enchantment.SHARPNESS,5) @@ -182,10 +186,13 @@ public enum Unique { .build(), "Netherite Sword", (p) -> { }, (p) -> { - p.addPotionEffect(new PotionEffect(PotionEffectType.STRENGTH,140,1,true,false,false)); + p.addPotionEffect(new PotionEffect(PotionEffectType.STRENGTH,20*7,1,true,false,false)); }, 50), AXE(ItemBuilder.create() .material(Material.NETHERITE_AXE) + .lore(Text.legacyColor("&bAbilities:")) + .lore(Text.legacyColor("&3| &7Rampage")) + .lore(Text.legacyColor(" &1- &7Haste 6 for 5 seconds")) .enchant(Enchantment.UNBREAKING,3) .enchant(Enchantment.MENDING,1) .enchant(Enchantment.SHARPNESS,5) @@ -194,7 +201,7 @@ public enum Unique { .build(), "Netherite Axe", (p) -> { }, (p) -> { - p.addPotionEffect(new PotionEffect(PotionEffectType.HASTE,140,5,true,false,false)); + p.addPotionEffect(new PotionEffect(PotionEffectType.HASTE,20*5,5,true,false,false)); }, 50); private final ItemStack inGame; @@ -217,10 +224,12 @@ public enum Unique { public static boolean isUnique(ItemStack i) { if (i == null) return false; + Verbose.send("Checking if item is unique %s",i.getType()); for (Unique value : values()) { if (ItemUtils.isSimilar(value.getInGameItem(),i)) return true; if (i.getType().equals(Material.DRAGON_EGG)) return true; } + Verbose.send("Item did not match any unique material..."); return false; } diff --git a/src/main/java/me/trouper/armorsmp/server/Manager.java b/src/main/java/me/trouper/armorsmp/server/Manager.java index 6d0f95c..8059967 100644 --- a/src/main/java/me/trouper/armorsmp/server/Manager.java +++ b/src/main/java/me/trouper/armorsmp/server/Manager.java @@ -19,6 +19,7 @@ public class Manager { public TierBackend tiers; public Broadcaster broadcaster; public UniquesBackend uniques; + public ArmorUpgrade upgrade; public Manager() { io = new IO(); @@ -33,7 +34,8 @@ public class Manager { //uniques = new UniquesBackend(); broadcaster = new Broadcaster(); uniques = new UniquesBackend(); - + upgrade = new ArmorUpgrade(); + registerCommands(); registerEvents(); registerCrafting(); @@ -66,9 +68,8 @@ public class Manager { private void registerCrafting() { ArmorSMP.getInstance().getLogger().info("Registering Crafts"); - ArmorUpgrade armorUpgrade = new ArmorUpgrade(); - armorUpgrade.removeRecipe(); - armorUpgrade.addRecipe(); + upgrade.removeRecipe(); + upgrade.addRecipe(); } } diff --git a/src/main/java/me/trouper/armorsmp/server/commands/AbilityCommand.java b/src/main/java/me/trouper/armorsmp/server/commands/AbilityCommand.java index 8c20cf5..5337a87 100644 --- a/src/main/java/me/trouper/armorsmp/server/commands/AbilityCommand.java +++ b/src/main/java/me/trouper/armorsmp/server/commands/AbilityCommand.java @@ -19,7 +19,7 @@ import java.util.UUID; @CommandRegistry(value = "ability", printStackTrace = true, playersOnly = true) public class AbilityCommand implements CustomCommand { - private final Cooldown> abilityCooldown = new Cooldown<>(); + private final Cooldown abilityCooldown = new Cooldown<>(); @Override public void dispatchCommand(CommandSender sender, Command command, String label, Args args) { @@ -33,12 +33,12 @@ public class AbilityCommand implements CustomCommand { Text.sendMessage(false, Text.Pallet.WARNING, sender, "You do not own the {0}.",piece.getCanonical()); return; } - if (abilityCooldown.isOnCooldown(Pair.of(piece,p.getUniqueId()))) { - Text.sendMessage(false, Text.Pallet.WARNING, sender, "The ability for your {0} is on cooldown for {1} seconds.",piece.getCanonical(),abilityCooldown.getCooldownSec(Pair.of(piece,p.getUniqueId()))); + if (abilityCooldown.isOnCooldown(getCooldownString(p.getUniqueId(),piece))) { + Text.sendMessage(false, Text.Pallet.WARNING, sender, "The ability for your {0} is on cooldown for {1} seconds.",piece.getCanonical(),abilityCooldown.getCooldownSec(getCooldownString(p.getUniqueId(),piece))); return; } piece.getAbility().accept(p); - abilityCooldown.addCooldown(Pair.of(piece,p.getUniqueId()),piece.getAbilityCooldownSeconds() * 1000L); + abilityCooldown.addCooldown(getCooldownString(p.getUniqueId(),piece),piece.getAbilityCooldownSeconds() * 1000L); Text.sendMessage(false, Text.Pallet.SUCCESS,sender,"Successfully used your {0}'s ability!",piece.getCanonical()); } @@ -46,4 +46,8 @@ public class AbilityCommand implements CustomCommand { public void dispatchCompletions(CommandSender commandSender, Command command, String label, CompletionBuilder b) { b.then(b.argEnum(Unique.class)); } + + public String getCooldownString(UUID who, Unique what) { + return "%s:%s".formatted(who,what); + } } diff --git a/src/main/java/me/trouper/armorsmp/server/commands/AdminCommand.java b/src/main/java/me/trouper/armorsmp/server/commands/AdminCommand.java index 8eb6e8c..d1a4721 100644 --- a/src/main/java/me/trouper/armorsmp/server/commands/AdminCommand.java +++ b/src/main/java/me/trouper/armorsmp/server/commands/AdminCommand.java @@ -9,7 +9,6 @@ import me.trouper.armorsmp.ArmorSMP; import me.trouper.armorsmp.data.ArmorTier; import me.trouper.armorsmp.data.io.Config; import me.trouper.armorsmp.data.Unique; -import me.trouper.armorsmp.server.crafting.ArmorUpgrade; import me.trouper.armorsmp.utils.Text; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; @@ -51,9 +50,15 @@ public class AdminCommand implements CustomCommand { b.arg("toggle") ) .then( - b.arg("include","exclude") + b.arg("exclude") .then( b.arg("Class.method"))) + .then( + b.arg("include") + .then( + b.arg(ArmorSMP.getInstance().getManager().io.config.debuggerExclusions) + ) + ) ).then( b.arg("change") .then( @@ -181,7 +186,7 @@ public class AdminCommand implements CustomCommand { return; } - target.getInventory().addItem(ArmorUpgrade.ARMOR_UGPRADE); + target.getInventory().addItem(ArmorSMP.getInstance().getManager().upgrade.getItem()); Text.sendMessage(false, Text.Pallet.SUCCESS, sender, "Given and Upgrader to {0}", target.getName()); } diff --git a/src/main/java/me/trouper/armorsmp/server/crafting/ArmorUpgrade.java b/src/main/java/me/trouper/armorsmp/server/crafting/ArmorUpgrade.java index 52ee0a0..309f5b8 100644 --- a/src/main/java/me/trouper/armorsmp/server/crafting/ArmorUpgrade.java +++ b/src/main/java/me/trouper/armorsmp/server/crafting/ArmorUpgrade.java @@ -2,23 +2,28 @@ package me.trouper.armorsmp.server.crafting; import io.github.itzispyder.pdk.plugin.builders.ItemBuilder; import me.trouper.armorsmp.ArmorSMP; +import me.trouper.armorsmp.utils.Text; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ShapedRecipe; public class ArmorUpgrade { - public static final ItemStack ARMOR_UGPRADE = ItemBuilder.create() - .material(Material.NETHER_STAR) - .name("§b§lArmor Upgrader") - .lore("§9| §3Right click to upgrade your gear") - .count(1) - .customModelData(1) - .build(); + private ItemStack armorUpgrade; + public final NamespacedKey key; + public final ShapedRecipe recipe; - public static final NamespacedKey KEY = new NamespacedKey(ArmorSMP.getInstance(), "armor_upgrade_recipe"); - - public static ShapedRecipe recipe = new ShapedRecipe(KEY, ARMOR_UGPRADE); + public ArmorUpgrade() { + this.key = new NamespacedKey(ArmorSMP.getInstance(), "armor_upgrade_recipe"); + this.armorUpgrade = ItemBuilder.create() + .material(Material.NETHER_STAR) + .name(Text.legacyColor("&b&lArmor Upgrade")) + .lore(Text.legacyColor("&9| &3Right click to upgrade your gear")) + .count(1) + .customModelData(1) + .build(); + this.recipe = new ShapedRecipe(key, armorUpgrade); + } public void addRecipe() { recipe.shape("ABC", "DEF", "GHI"); @@ -39,6 +44,26 @@ public class ArmorUpgrade { } public void removeRecipe() { - ArmorSMP.getInstance().getServer().removeRecipe(KEY); + ArmorSMP.getInstance().getServer().removeRecipe(key); + } + + public ItemStack getItem() { + if (!armorUpgrade.getType().equals(Material.NETHER_STAR)) { + armorUpgrade = ItemBuilder.create() + .material(Material.NETHER_STAR) + .name(Text.legacyColor("&b&lArmor Upgrade")) + .lore(Text.legacyColor("&9| &3Right click to upgrade your gear")) + .count(1) + .customModelData(1) + .build(); + return ItemBuilder.create() + .material(Material.NETHER_STAR) + .name(Text.legacyColor("&b&lArmor Upgrade")) + .lore(Text.legacyColor("&9| &3Right click to upgrade your gear")) + .count(1) + .customModelData(1) + .build(); + } + return armorUpgrade; } } diff --git a/src/main/java/me/trouper/armorsmp/server/events/DeathEvents.java b/src/main/java/me/trouper/armorsmp/server/events/DeathEvents.java index d5b91ec..406cc60 100644 --- a/src/main/java/me/trouper/armorsmp/server/events/DeathEvents.java +++ b/src/main/java/me/trouper/armorsmp/server/events/DeathEvents.java @@ -3,15 +3,17 @@ package me.trouper.armorsmp.server.events; import io.github.itzispyder.pdk.events.CustomListener; import me.trouper.armorsmp.ArmorSMP; import me.trouper.armorsmp.data.ArmorTier; -import me.trouper.armorsmp.server.crafting.ArmorUpgrade; +import me.trouper.armorsmp.data.Unique; import me.trouper.armorsmp.utils.Text; import me.trouper.armorsmp.utils.Verbose; import me.trouper.armorsmp.utils.ItemUtils; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.player.PlayerRespawnEvent; +import org.bukkit.inventory.ItemStack; public class DeathEvents implements CustomListener { @@ -21,6 +23,9 @@ public class DeathEvents implements CustomListener { final ArmorTier tier = ArmorSMP.getInstance().getManager().tiers.getTier(p); Verbose.send("Handling death event for %s, their tier is %s",p.getName(),tier); e.getDrops().removeIf(ItemUtils::notDroppable); + if (ArmorSMP.getInstance().getManager().uniques.isOwner(p,Unique.DRAGON_EGG)) { + e.getDrops().add(new ItemStack(Material.DRAGON_EGG)); + } if (tier.equals(ArmorTier.NONE)) { Verbose.send("Tier was none"); @@ -29,7 +34,7 @@ public class DeathEvents implements CustomListener { } if (ArmorSMP.getInstance().getManager().tiers.downTier(p)) { Verbose.send("They have been down-tiered"); - e.getDrops().add(ArmorUpgrade.ARMOR_UGPRADE); + e.getDrops().add(ArmorSMP.getInstance().getManager().upgrade.getItem()); e.deathMessage(Text.getMessage(false, Text.Pallet.INFO,"{0} has died, and dropped an {1}!", LegacyComponentSerializer.legacyAmpersand().serialize(p.name()),"Armor Upgrader")); } ArmorSMP.getInstance().getManager().uniques.dropAllUniques(p); @@ -38,5 +43,6 @@ public class DeathEvents implements CustomListener { @EventHandler public void onRespawn(PlayerRespawnEvent e) { ArmorSMP.getInstance().getManager().tiers.queueUpdate(e.getPlayer(),false); + ArmorSMP.getInstance().getManager().uniques.queueUpdate(e.getPlayer()); } } diff --git a/src/main/java/me/trouper/armorsmp/server/events/UpgradeRedeemEvent.java b/src/main/java/me/trouper/armorsmp/server/events/UpgradeRedeemEvent.java index 234124e..d2411d5 100644 --- a/src/main/java/me/trouper/armorsmp/server/events/UpgradeRedeemEvent.java +++ b/src/main/java/me/trouper/armorsmp/server/events/UpgradeRedeemEvent.java @@ -3,7 +3,7 @@ package me.trouper.armorsmp.server.events; import io.github.itzispyder.pdk.events.CustomListener; import me.trouper.armorsmp.ArmorSMP; import me.trouper.armorsmp.data.ArmorTier; -import me.trouper.armorsmp.server.crafting.ArmorUpgrade; +import me.trouper.armorsmp.utils.ItemUtils; import me.trouper.armorsmp.utils.Text; import me.trouper.armorsmp.utils.Verbose; import org.bukkit.entity.Player; @@ -15,21 +15,24 @@ public class UpgradeRedeemEvent implements CustomListener { @EventHandler public void onClick(PlayerInteractEvent e) { + Verbose.send("Detected player clicking."); Player p = e.getPlayer(); - ItemStack holding = e.getItem(); + ItemStack holding = e.getPlayer().getInventory().getItemInMainHand(); - if (holding == null || holding.isEmpty()) return; - Verbose.send("Detected Interact Event Holding: %s", holding.getType()); - if (!holding.getType().equals(ArmorUpgrade.ARMOR_UGPRADE.getType())) return; - if (!holding.hasItemMeta()) return; - if (holding.getItemMeta().getCustomModelData() != ArmorUpgrade.ARMOR_UGPRADE.getItemMeta().getCustomModelData()) return; + if (holding.isEmpty()) return; + if (!ItemUtils.isSimilar(holding,ArmorSMP.getInstance().getManager().upgrade.getItem())) { + Verbose.send("Player was not holding an Armor Upgrader"); + return; + } final ArmorTier tier = ArmorSMP.getInstance().getManager().tiers.getTier(p); if (ArmorSMP.getInstance().getManager().tiers.upTier(p)) { - holding.setAmount(holding.getAmount() - 1); + holding.setAmount(holding.getAmount() - 1); + Verbose.send("Successfully updated player"); Text.sendMessage(true, Text.Pallet.INFO,p,"Successfully redeemed armor upgrade! Tier {0} -> Tier {1}",tier,ArmorSMP.getInstance().getManager().tiers.getTier(p)); } else { + Verbose.send("Failed to update player (tier manager refused)"); Text.sendMessage(true, Text.Pallet.ERROR,p,"Unable to upgrade. You are already at the maximum Armor Tier!"); } } diff --git a/src/main/java/me/trouper/armorsmp/utils/ItemUtils.java b/src/main/java/me/trouper/armorsmp/utils/ItemUtils.java index be1fb19..65bc8f7 100644 --- a/src/main/java/me/trouper/armorsmp/utils/ItemUtils.java +++ b/src/main/java/me/trouper/armorsmp/utils/ItemUtils.java @@ -1,22 +1,37 @@ package me.trouper.armorsmp.utils; +import com.google.common.collect.Multimap; import me.trouper.armorsmp.data.Unique; import org.bukkit.Material; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeModifier; +import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import java.util.ArrayList; import java.util.List; +import java.util.Map; public class ItemUtils { public static boolean notDroppable(ItemStack i) { - return isArmor(i) && !isUnique(i) && !isDragonEggEquivalent(i); + boolean armor = isArmor(i); + boolean unique = isUnique(i); + boolean isEgg = isDragonEggEquivalent(i); + Verbose.send(""" + Item Type: %s + Unique: %b + Armor: %b + Egg Equivalent: %b + And: %b + """,i.getType().name(),unique,armor,isEgg,(!unique && armor)); + return (!unique && armor) || isEgg; } private static boolean isDragonEggEquivalent(ItemStack i) { Material m = i.getType(); - return m.equals(Unique.DRAGON_EGG); + return m.equals(Unique.DRAGON_EGG.getInGameItem().getType()); } public static boolean isArmor(ItemStack i) { @@ -62,16 +77,91 @@ public class ItemUtils { } public static boolean isSimilar(ItemStack item1, ItemStack item2) { - if (item1 == null || item2 == null) return false; - if (item1.getType() != item2.getType()) return false; - if (item1.hasItemMeta() != item2.hasItemMeta()) return false; + if (item1 == null || item2 == null) { + Verbose.send("One of the items is null: item1: %s, item2: %s", item1, item2); + return false; + } + // Check material/type + Material type1 = item1.getType(); + Material type2 = item2.getType(); + boolean typeEqual = type1 == type2; + Verbose.send("Checking Material: item1 type: %s, item2 type: %s, equal: %s", type1, type2, typeEqual); + if (!typeEqual) return false; + + // Check existence of ItemMeta + boolean hasMeta1 = item1.hasItemMeta(); + boolean hasMeta2 = item2.hasItemMeta(); + boolean metaExistEqual = (hasMeta1 == hasMeta2); + Verbose.send("Checking ItemMeta existence: item1 has meta: %s, item2 has meta: %s, equal: %s", hasMeta1, hasMeta2, metaExistEqual); + if (!metaExistEqual) return false; + + // If neither item has meta, they are similar (nothing else to compare) + if (!hasMeta1 && !hasMeta2) { + return true; + } + + // Both items have meta ItemMeta meta1 = item1.getItemMeta(); ItemMeta meta2 = item2.getItemMeta(); - return meta1 == null || meta2 == null || meta1.equals(meta2); + // Custom Name Check with null handling + String name1 = meta1.hasDisplayName() ? meta1.getDisplayName() : null; + String name2 = meta2.hasDisplayName() ? meta2.getDisplayName() : null; + if (name1 == null ^ name2 == null) { // one is null and the other is not + Verbose.send("Custom Name mismatch: item1 name: %s, item2 name: %s", name1, name2); + return false; + } + boolean nameEqual = (name1 == null || name1.equals(name2)); + Verbose.send("Checking Custom Name: item1: %s, item2: %s, equal: %s", name1, name2, nameEqual); + if (!nameEqual) return false; + + // Lore Check with null handling + List lore1 = meta1.hasLore() ? meta1.getLore() : null; + List lore2 = meta2.hasLore() ? meta2.getLore() : null; + if (lore1 == null ^ lore2 == null) { + Verbose.send("Lore mismatch: item1 lore: %s, item2 lore: %s", lore1, lore2); + return false; + } + boolean loreEqual = (lore1 == null || lore1.equals(lore2)); + Verbose.send("Checking Lore: item1: %s, item2: %s, equal: %s", lore1, lore2, loreEqual); + if (!loreEqual) return false; + + // Custom Model Data Check: Using -1 as the default if not set + int cmd1 = meta1.hasCustomModelData() ? meta1.getCustomModelData() : -1; + int cmd2 = meta2.hasCustomModelData() ? meta2.getCustomModelData() : -1; + boolean cmdEqual = (cmd1 == cmd2); + Verbose.send("Checking Custom Model Data: item1: %d, item2: %d, equal: %s", cmd1, cmd2, cmdEqual); + if (!cmdEqual) return false; + + // Enchantments Check with null handling (should not be null by API, but checking for safety) + Map enchants1 = meta1.getEnchants(); + Map enchants2 = meta2.getEnchants(); + if (enchants1 == null ^ enchants2 == null) { + Verbose.send("Enchantments mismatch: item1 enchants: %s, item2 enchants: %s", enchants1, enchants2); + return false; + } + boolean enchantsEqual = (enchants1 == null || enchants1.equals(enchants2)); + Verbose.send("Checking Enchantments: item1: %s, item2: %s, equal: %s", enchants1, enchants2, enchantsEqual); + if (!enchantsEqual) return false; + + // Attribute Modifiers Check with null handling + Multimap modifiers1 = meta1.getAttributeModifiers(); + Multimap modifiers2 = meta2.getAttributeModifiers(); + if (modifiers1 == null ^ modifiers2 == null) { + Verbose.send("Attribute Modifiers mismatch: item1 modifiers: %s, item2 modifiers: %s", modifiers1, modifiers2); + return false; + } + boolean modifiersEqual = (modifiers1 == null || modifiers1.equals(modifiers2)); + Verbose.send("Checking Attribute Modifiers: item1: %s, item2: %s, equal: %s", modifiers1, modifiers2, modifiersEqual); + if (!modifiersEqual) return false; + + // All checks passed + return true; } + + /** @excludes unique enchants */ public static void transferEnchants(ItemStack oldItem, ItemStack newItem) { if (oldItem != null && !isUnique(oldItem)) {