diff --git a/pom.xml b/pom.xml index af1b03c..9608990 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,24 @@ + + org.junit.jupiter + junit-jupiter + 5.14.0 + test + + + org.mockbukkit.mockbukkit + mockbukkit-v1.21 + 4.95.0 + test + + + org.mockito + mockito-core + 5.23.0 + test + io.papermc.paper @@ -58,6 +76,11 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + org.apache.maven.plugins maven-compiler-plugin diff --git a/src/main/java/net/tfminecraft/musicalinstruments/managers/InstrumentManager.java b/src/main/java/net/tfminecraft/musicalinstruments/managers/InstrumentManager.java index 633bb68..674e1b4 100644 --- a/src/main/java/net/tfminecraft/musicalinstruments/managers/InstrumentManager.java +++ b/src/main/java/net/tfminecraft/musicalinstruments/managers/InstrumentManager.java @@ -1,6 +1,7 @@ package net.tfminecraft.musicalinstruments.managers; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; import net.tfminecraft.musicalinstruments.InstrumentPlugin; import net.tfminecraft.musicalinstruments.items.ItemResolver; @@ -18,11 +19,13 @@ public class InstrumentManager { private final InstrumentPlugin plugin; private final ItemResolver itemResolver; private final Map templates; + private final Map cosmeticFreeTemplates; public InstrumentManager(InstrumentPlugin plugin, ItemResolver itemResolver) { this.plugin = plugin; this.itemResolver = itemResolver; this.templates = new LinkedHashMap<>(); + this.cosmeticFreeTemplates = new LinkedHashMap<>(); } // ==================================== @@ -33,6 +36,7 @@ public InstrumentManager(InstrumentPlugin plugin, ItemResolver itemResolver) { // ==================================== public void loadTemplates() { templates.clear(); + cosmeticFreeTemplates.clear(); for (String instrument : plugin.getConfig().getKeys(false)) { String configPath = plugin.getConfig().getString(instrument + ".item"); @@ -48,7 +52,9 @@ public void loadTemplates() { continue; } + ItemStack cosmeticFree = withoutCosmetics(template); templates.put(instrument, template); + cosmeticFreeTemplates.put(instrument, cosmeticFree); } catch (Exception e) { plugin.getLogger().warning("Failed to load instrument '" + instrument + "': " + e.getMessage()); } @@ -61,7 +67,7 @@ public void loadTemplates() { // Gets the instrument ID from an ItemStack. // ==================================== public String getInstrument(ItemStack item) { - if (item == null) { + if (item == null || item.getType().isAir()) { return null; } @@ -71,7 +77,30 @@ public String getInstrument(ItemStack item) { } } - return null; + // Preserve exact matches before allowing namestone/lorestone edits. + ItemStack cosmeticFree = withoutCosmetics(item); + String match = null; + for (Map.Entry entry : cosmeticFreeTemplates.entrySet()) { + if (cosmeticFree.isSimilar(entry.getValue())) { + // Name/lore-only variants cannot be distinguished after a cosmetic edit. + if (match != null) { + return null; + } + match = entry.getKey(); + } + } + return match; + } + + private static ItemStack withoutCosmetics(ItemStack item) { + ItemStack copy = item.clone(); + ItemMeta meta = copy.getItemMeta(); + if (meta != null) { + meta.displayName(null); + meta.lore(null); + copy.setItemMeta(meta); + } + return copy; } // Gets the sound key for an instrument slot and sneak state. diff --git a/src/test/java/net/tfminecraft/musicalinstruments/managers/InstrumentManagerTest.java b/src/test/java/net/tfminecraft/musicalinstruments/managers/InstrumentManagerTest.java new file mode 100644 index 0000000..2759b81 --- /dev/null +++ b/src/test/java/net/tfminecraft/musicalinstruments/managers/InstrumentManagerTest.java @@ -0,0 +1,129 @@ +package net.tfminecraft.musicalinstruments.managers; + +import net.kyori.adventure.text.Component; +import net.tfminecraft.musicalinstruments.InstrumentPlugin; +import net.tfminecraft.musicalinstruments.items.ItemResolver; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; + +import java.util.List; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +// MockBukkit 4.95 implements the legacy model setter, not the component setter. +@SuppressWarnings("deprecation") +class InstrumentManagerTest { + private static final NamespacedKey ID = new NamespacedKey("instruments", "id"); + private final YamlConfiguration config = new YamlConfiguration(); + private InstrumentManager manager; + private ItemResolver resolver; + private ItemStack lute; + + @BeforeEach + void setUp() { + MockBukkit.mock(); + InstrumentPlugin plugin = mock(InstrumentPlugin.class); + when(plugin.getConfig()).thenReturn(config); + when(plugin.getLogger()).thenReturn(Logger.getLogger("InstrumentManagerTest")); + resolver = mock(ItemResolver.class); + manager = new InstrumentManager(plugin, resolver); + lute = new ItemStack(Material.PAPER); + lute.editMeta(meta -> { + meta.displayName(Component.text("Lute")); + meta.lore(List.of(Component.text("An instrument"))); + meta.setCustomModelData(1001); + meta.getPersistentDataContainer().set(ID, PersistentDataType.STRING, "lute"); + }); + config.set("lute.item", "m.instruments.lute"); + when(resolver.resolve("m.instruments.lute")).thenReturn(lute); + manager.loadTemplates(); + } + + @AfterEach + void tearDown() { + MockBukkit.unmock(); + } + + @Test + void recognizesUnchangedInstrumentAndIgnoresStackSize() { + ItemStack item = lute.clone(); + item.setAmount(3); + assertEquals("lute", manager.getInstrument(item)); + } + + @Test + void recognizesRenamedInstrumentWithoutMutatingItems() { + ItemStack item = lute.clone(); + item.editMeta(meta -> meta.displayName(Component.text("My lute"))); + ItemStack before = item.clone(); + ItemStack templateBefore = lute.clone(); + assertFalse(item.isSimilar(lute)); + assertEquals("lute", manager.getInstrument(item)); + assertEquals(before, item); + assertEquals(templateBefore, lute); + assertEquals(templateBefore, manager.getInstrumentItem("lute")); + } + + @Test + void recognizesLorestoneAndCombinedCosmeticEdits() { + ItemStack item = lute.clone(); + item.editMeta(meta -> meta.lore(List.of(Component.text("Passed down for generations")))); + assertEquals("lute", manager.getInstrument(item)); + item.editMeta(meta -> meta.displayName(Component.text("Family heirloom"))); + assertEquals("lute", manager.getInstrument(item)); + } + + @Test + void rejectsDifferentMaterialModelAndProviderIdentity() { + assertNull(manager.getInstrument(new ItemStack(Material.PAPER))); + ItemStack wrongMaterial = lute.clone(); + wrongMaterial.setType(Material.STICK); + assertNull(manager.getInstrument(wrongMaterial)); + ItemStack wrongModel = lute.clone(); + wrongModel.editMeta(meta -> meta.setCustomModelData(1002)); + assertNull(manager.getInstrument(wrongModel)); + ItemStack wrongId = lute.clone(); + wrongId.editMeta(meta -> meta.getPersistentDataContainer().set(ID, PersistentDataType.STRING, "flute")); + assertNull(manager.getInstrument(wrongId)); + } + + @Test + void rejectsEmptyHands() { + assertNull(manager.getInstrument(null)); + assertNull(manager.getInstrument(new ItemStack(Material.AIR))); + } + + @Test + void exactMatchesWinAndAmbiguousRenamesAreRejected() { + ItemStack other = lute.clone(); + other.editMeta(meta -> meta.displayName(Component.text("Other lute"))); + config.set("other.item", "modeled-other"); + when(resolver.resolve("modeled-other")).thenReturn(other); + manager.loadTemplates(); + assertEquals("lute", manager.getInstrument(lute)); + assertEquals("other", manager.getInstrument(other)); + ItemStack renamed = other.clone(); + renamed.editMeta(meta -> meta.displayName(Component.text("Renamed"))); + assertNull(manager.getInstrument(renamed)); + } + + @Test + void reloadRemovesOldTemplates() { + config.set("lute", null); + config.set("flute.item", "v.STICK"); + when(resolver.resolve("v.STICK")).thenReturn(new ItemStack(Material.STICK)); + manager.loadTemplates(); + assertNull(manager.getInstrument(lute)); + assertEquals("flute", manager.getInstrument(new ItemStack(Material.STICK))); + assertNull(manager.getInstrumentItem("lute")); + } +}