Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1993,7 +1993,8 @@ private boolean hasRequiredTeamPresence()
* Checks if two items match, considering the ignore-metadata setting. For potion-like materials
* that are in the ignore-metadata set, compares the base potion type while ignoring other metadata.
* For non-potion materials in the ignore-metadata set, uses type-only comparison. For materials
* not in the ignore-metadata set, uses full similarity comparison.
* not in the ignore-metadata set, uses full similarity comparison, except that the anvil
* repair cost component is ignored.
*
* @param candidate candidate item from inventory
* @param required required item template
Expand All @@ -2012,10 +2013,11 @@ private static boolean itemsMatch(ItemStack candidate, ItemStack required, Set<M
return false;
}

// If metadata should not be ignored, use full similarity check
// If metadata should not be ignored, use full similarity check. The anvil repair cost is
// disregarded so that, e.g., enchanted books combined in an anvil still match (#433).
if (!ignoreMetaData.contains(required.getType()))
{
return candidate.isSimilar(required);
return Utils.isSimilarIgnoringRepairCost(candidate, required);
}

// Metadata is being ignored. For potion-like materials, still compare base potion type.
Expand Down
52 changes: 52 additions & 0 deletions src/main/java/world/bentobox/challenges/utils/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.bukkit.inventory.meta.EnchantmentStorageMeta;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.PotionMeta;
import org.bukkit.inventory.meta.Repairable;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.potion.PotionType;
import org.eclipse.jdt.annotation.Nullable;
Expand Down Expand Up @@ -52,13 +53,64 @@ else if (stack == input)
}
else
{
input = withoutRepairCost(input);
stack = withoutRepairCost(stack);

return input.getType() == stack.getType() &&
input.hasItemMeta() == stack.hasItemMeta() &&
(!input.hasItemMeta() || Bukkit.getItemFactory().equals(input.getItemMeta(), stack.getItemMeta()));
}
}


/**
* Checks if two item stacks are similar (same type and meta, ignoring amount) while disregarding
* the anvil {@code repair_cost} component. Anvils add this bookkeeping component to any item they
* produce (for example when combining two enchanted books), so a strict {@link ItemStack#isSimilar}
* would treat an anvil-made book as a different item from one obtained by other means.
* @param first First item.
* @param second Second item.
* @return {@code true} if items are similar once the repair cost is ignored, {@code false} otherwise.
*/
public static boolean isSimilarIgnoringRepairCost(@Nullable ItemStack first, @Nullable ItemStack second)
{
if (first == null || second == null)
{
return false;
}

return withoutRepairCost(first).isSimilar(withoutRepairCost(second));
}


/**
* Returns the given item without an anvil repair cost. If the item carries a repair cost, a copy
* with the repair cost cleared is returned; otherwise the original item is returned untouched.
* @param item Item to inspect.
* @return Item without a repair cost, or the same item if it had none.
*/
public static ItemStack withoutRepairCost(ItemStack item)
{
if (item == null || !item.hasItemMeta())
{
return item;
}

ItemMeta meta = item.getItemMeta();

if (meta instanceof Repairable repairable && repairable.hasRepairCost())
{
repairable.setRepairCost(0);

ItemStack copy = item.clone();
copy.setItemMeta(meta);
return copy;
}

return item;
}


/**
* Checks if a material is potion-like (potions, splash potions, lingering potions, tipped arrows).
*
Expand Down
122 changes: 122 additions & 0 deletions src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
Expand Down Expand Up @@ -34,11 +35,14 @@
import org.bukkit.NamespacedKey;
import org.bukkit.block.Biome;
import org.bukkit.block.Block;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.EnchantmentStorageMeta;
import org.bukkit.inventory.meta.PotionMeta;
import org.bukkit.inventory.meta.Repairable;
import org.bukkit.potion.PotionType;
import org.bukkit.util.BoundingBox;
import org.eclipse.jdt.annotation.NonNull;
Expand All @@ -47,6 +51,7 @@
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;

import net.md_5.bungee.api.chat.TextComponent;
import world.bentobox.bentobox.hooks.VaultHook;
Expand Down Expand Up @@ -1581,4 +1586,121 @@
// Verify items were not removed
assertEquals(10, invEmerald.getAmount(), "Items should not be removed when requirement fails");
}

// -------------------------------------------------------------------------
// Anvil repair cost handling (Issue #433)
// -------------------------------------------------------------------------

/**
* Helper method to create an enchanted book with a single stored enchantment and, optionally,
* an anvil repair cost (as produced when books are combined in an anvil).
*/
private ItemStack createEnchantedBook(Enchantment enchantment, int level, int repairCost) {
ItemStack book = new ItemStack(Material.ENCHANTED_BOOK);
EnchantmentStorageMeta meta = (EnchantmentStorageMeta) book.getItemMeta();
meta.addStoredEnchant(enchantment, level, true);
if (repairCost > 0) {
((Repairable) meta).setRepairCost(repairCost);
}
book.setItemMeta(meta);
return book;
}

@Test
void testInventoryChallengeEnchantedBookFromAnvilMatches() {
// Required: Unbreaking III book with no repair cost (as configured by an admin)
InventoryRequirements req = new InventoryRequirements();
ItemStack requiredBook = createEnchantedBook(Enchantment.UNBREAKING, 3, 0);
req.setRequiredItems(Collections.singletonList(requiredBook));
req.setTakeItems(true);
challenge.setRequirements(req);

// Player holds an Unbreaking III book that was combined in an anvil (has repair_cost)
ItemStack anvilBook = createEnchantedBook(Enchantment.UNBREAKING, 3, 1);
assertFalse(anvilBook.isSimilar(requiredBook), "Sanity: strict isSimilar must differ on repair cost");
when(inv.getContents()).thenReturn(new ItemStack[] { anvilBook });
when(player.getInventory()).thenReturn(inv);

assertTrue(TryToComplete.complete(addon, user, challenge, world, topLabel, permissionPrefix));
verify(user, never()).getTranslation(any(World.class), eq("challenges.errors.not-enough-items"), any(), any());
assertEquals(0, anvilBook.getAmount(), "The anvil-made book should have been taken");
}

@Test
void testInventoryChallengeEnchantedBookRequiredHasRepairCost() {
// Required item itself was created from an anvil-made book; player holds a clean one
InventoryRequirements req = new InventoryRequirements();
ItemStack requiredBook = createEnchantedBook(Enchantment.UNBREAKING, 3, 2);
req.setRequiredItems(Collections.singletonList(requiredBook));
challenge.setRequirements(req);

ItemStack cleanBook = createEnchantedBook(Enchantment.UNBREAKING, 3, 0);
when(inv.getContents()).thenReturn(new ItemStack[] { cleanBook });
when(player.getInventory()).thenReturn(inv);

assertTrue(TryToComplete.complete(addon, user, challenge, world, topLabel, permissionPrefix));
}

@Test
void testInventoryChallengeEnchantedBookWrongLevelStillFails() {
// Ignoring repair cost must not loosen the enchantment check itself
InventoryRequirements req = new InventoryRequirements();
ItemStack requiredBook = createEnchantedBook(Enchantment.UNBREAKING, 3, 0);
req.setRequiredItems(Collections.singletonList(requiredBook));
challenge.setRequirements(req);

ItemStack lowerBook = createEnchantedBook(Enchantment.UNBREAKING, 2, 1);
when(inv.getContents()).thenReturn(new ItemStack[] { lowerBook });
when(player.getInventory()).thenReturn(inv);
// The "missing items" message prettifies the enchanted book, which needs vararg translations
Mockito.doAnswer(invocation -> invocation.getArgument(0, String.class))
.when(user).getTranslationOrNothing(anyString(), Mockito.any(String[].class));

assertFalse(TryToComplete.complete(addon, user, challenge, world, topLabel, permissionPrefix));
verify(user).getTranslation(any(World.class), eq("challenges.errors.not-enough-items"), eq("[items]"),
any());
}

@Test
void testUtilsIsSimilarIgnoringRepairCost() {
ItemStack clean = createEnchantedBook(Enchantment.UNBREAKING, 3, 0);
ItemStack anvil = createEnchantedBook(Enchantment.UNBREAKING, 3, 5);
ItemStack other = createEnchantedBook(Enchantment.MENDING, 1, 5);

assertTrue(Utils.isSimilarIgnoringRepairCost(clean, anvil));
assertTrue(Utils.isSimilarIgnoringRepairCost(anvil, clean));
assertTrue(Utils.isSimilarIgnoringRepairCost(anvil, anvil.clone()));
assertFalse(Utils.isSimilarIgnoringRepairCost(anvil, other));
assertFalse(Utils.isSimilarIgnoringRepairCost(null, clean));
assertFalse(Utils.isSimilarIgnoringRepairCost(clean, null));

// The original item must not be modified
assertEquals(5, ((Repairable) anvil.getItemMeta()).getRepairCost());
}

@Test
void testUtilsWithoutRepairCost() {
ItemStack clean = createEnchantedBook(Enchantment.UNBREAKING, 3, 0);
ItemStack anvil = createEnchantedBook(Enchantment.UNBREAKING, 3, 5);
ItemStack plain = new ItemStack(Material.DIRT);

// Items without a repair cost are returned as-is
assertTrue(clean == Utils.withoutRepairCost(clean));

Check warning on line 1688 in src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use assertSame instead.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_Challenges&issues=AaBy6W9XwSzpTW4LRwcB&open=AaBy6W9XwSzpTW4LRwcB&pullRequest=434
assertTrue(plain == Utils.withoutRepairCost(plain));

Check warning on line 1689 in src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use assertSame instead.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_Challenges&issues=AaBy6W9XwSzpTW4LRwcC&open=AaBy6W9XwSzpTW4LRwcC&pullRequest=434
assertNull(Utils.withoutRepairCost(null));

ItemStack stripped = Utils.withoutRepairCost(anvil);
assertFalse(((Repairable) stripped.getItemMeta()).hasRepairCost());
assertTrue(stripped.isSimilar(clean));
assertEquals(5, ((Repairable) anvil.getItemMeta()).getRepairCost(), "Original must be untouched");
}

@Test
void testGroupEqualItemsMergesAnvilBooks() {
ItemStack clean = createEnchantedBook(Enchantment.UNBREAKING, 3, 0);
ItemStack anvil = createEnchantedBook(Enchantment.UNBREAKING, 3, 3);
List<ItemStack> grouped = Utils.groupEqualItems(Arrays.asList(clean, anvil), Set.of());
assertEquals(1, grouped.size());
assertEquals(2, grouped.get(0).getAmount());
}
}
Loading