diff --git a/build.gradle.kts b/build.gradle.kts index c2dd75be3..caab2525a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -46,7 +46,7 @@ paperweight.reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArt group = "world.bentobox" // From // Base properties from -val buildVersion = "3.14.2" +val buildVersion = "3.15.0" val buildNumberDefault = "-LOCAL" // Local build identifier val snapshotSuffix = "-SNAPSHOT" // Indicates development/snapshot version diff --git a/src/main/java/world/bentobox/bentobox/blueprints/Blueprint.java b/src/main/java/world/bentobox/bentobox/blueprints/Blueprint.java index 8433f2cbb..13bdf1672 100644 --- a/src/main/java/world/bentobox/bentobox/blueprints/Blueprint.java +++ b/src/main/java/world/bentobox/bentobox/blueprints/Blueprint.java @@ -6,12 +6,14 @@ import org.bukkit.Material; import org.bukkit.util.Vector; +import org.bukkit.inventory.ItemStack; import org.eclipse.jdt.annotation.NonNull; import com.google.gson.annotations.Expose; import world.bentobox.bentobox.blueprints.dataobjects.BlueprintBlock; import world.bentobox.bentobox.blueprints.dataobjects.BlueprintEntity; +import world.bentobox.bentobox.util.ItemParser; /** * Stores all details of a blueprint @@ -20,6 +22,8 @@ */ public class Blueprint { + private static final String DEFAULT_ICON = "PAPER"; + /** * Unique name for this blueprint. The filename will be this plus the blueprint suffix */ @@ -27,8 +31,13 @@ public class Blueprint { private @NonNull String name = ""; @Expose private String displayName; + /** + * Icon of the blueprint. Supports plain material names (e.g. "DIAMOND"), + * vanilla namespaced materials (e.g. "minecraft:diamond"), and custom + * item model keys (e.g. "myserver:island_tropical"). + */ @Expose - private @NonNull Material icon = Material.PAPER; + private String icon = DEFAULT_ICON; @Expose private List description; @Expose @@ -77,17 +86,52 @@ public Blueprint setDisplayName(String displayName) { return this; } /** - * @return the icon + * Returns the base Material for this blueprint's icon. + * Resolves plain names ("DIAMOND") and vanilla namespaced keys ("minecraft:diamond") + * via {@link Material#matchMaterial}. For custom item-model keys that are not + * valid vanilla materials (e.g. "myserver:island_tropical"), returns {@link Material#PAPER} + * as the base item — use {@link #getIconItemStack()} to get the full item with model data. + * @return the icon material, never null */ public @NonNull Material getIcon() { - return icon; + return ItemParser.parseIconMaterial(icon); + } + + /** + * Returns an {@link ItemStack} representing this blueprint's icon. + *
    + *
  • Plain material name (e.g. {@code "DIAMOND"}) → {@code new ItemStack(Material.DIAMOND)}
  • + *
  • Vanilla namespaced material (e.g. {@code "minecraft:diamond"}) → same as above
  • + *
  • Custom item-model key (e.g. {@code "myserver:island_tropical"}) → PAPER base item + * with the model key set via {@link ItemMeta#setItemModel}
  • + *
+ * @return ItemStack for this blueprint's icon, never null + * @since 3.0.0 + */ + public @NonNull ItemStack getIconItemStack() { + return ItemParser.parseIconItemStack(icon); } + /** - * @param icon the icon to set + * Sets the icon from a Material (backward-compatible setter). + * @param icon the icon material to set; if null, defaults to {@link Material#PAPER} * @return blueprint */ public Blueprint setIcon(Material icon) { - this.icon = icon; + this.icon = icon != null ? icon.name() : DEFAULT_ICON; + return this; + } + + /** + * Sets the icon from a string. Accepts plain material names (e.g. {@code "DIAMOND"}), + * vanilla namespaced materials (e.g. {@code "minecraft:diamond"}), and custom item-model + * keys (e.g. {@code "myserver:island_tropical"}). + * @param icon the icon string; if null, defaults to {@code "PAPER"} + * @return blueprint + * @since 3.0.0 + */ + public Blueprint setIcon(String icon) { + this.icon = icon != null ? icon : DEFAULT_ICON; return this; } /** diff --git a/src/main/java/world/bentobox/bentobox/blueprints/dataobjects/BlueprintBundle.java b/src/main/java/world/bentobox/bentobox/blueprints/dataobjects/BlueprintBundle.java index 788e5916e..7f43094d0 100644 --- a/src/main/java/world/bentobox/bentobox/blueprints/dataobjects/BlueprintBundle.java +++ b/src/main/java/world/bentobox/bentobox/blueprints/dataobjects/BlueprintBundle.java @@ -7,11 +7,13 @@ import org.bukkit.Material; import org.bukkit.World; +import org.bukkit.inventory.ItemStack; import com.google.gson.annotations.Expose; import world.bentobox.bentobox.blueprints.Blueprint; import world.bentobox.bentobox.database.objects.DataObject; +import world.bentobox.bentobox.util.ItemParser; /** * Represents a bundle of three {@link Blueprint}s. @@ -21,16 +23,20 @@ */ public class BlueprintBundle implements DataObject { + private static final String DEFAULT_ICON = "PAPER"; + /** * The unique id of this bundle */ @Expose private String uniqueId; /** - * Icon of the bundle + * Icon of the bundle. Supports plain material names (e.g. "DIAMOND"), + * vanilla namespaced materials (e.g. "minecraft:diamond"), and custom + * item model keys (e.g. "myserver:island_tropical"). */ @Expose - private Material icon = Material.PAPER; + private String icon = DEFAULT_ICON; /** * Name on the icon */ @@ -97,16 +103,49 @@ public void setUniqueId(String uniqueId) { this.uniqueId = uniqueId; } /** - * @return the icon + * Returns the base Material for this bundle's icon. + * Resolves plain names ("DIAMOND") and vanilla namespaced keys ("minecraft:diamond") + * via {@link Material#matchMaterial}. For custom item-model keys that are not + * valid vanilla materials (e.g. "myserver:island_tropical"), returns {@link Material#PAPER} + * as the base item — use {@link #getIconItemStack()} to get the full item with model data. + * @return the icon material, never null */ public Material getIcon() { - return icon; + return ItemParser.parseIconMaterial(icon); } + + /** + * Returns an {@link ItemStack} representing this bundle's icon. + *
    + *
  • Plain material name (e.g. {@code "DIAMOND"}) → {@code new ItemStack(Material.DIAMOND)}
  • + *
  • Vanilla namespaced material (e.g. {@code "minecraft:diamond"}) → same as above
  • + *
  • Custom item-model key (e.g. {@code "myserver:island_tropical"}) → PAPER base item + * with the model key set via {@link ItemMeta#setItemModel}
  • + *
+ * @return ItemStack for this bundle's icon, never null + * @since 3.0.0 + */ + public ItemStack getIconItemStack() { + return ItemParser.parseIconItemStack(icon); + } + /** - * @param icon the icon to set + * Sets the icon from a Material (backward-compatible setter). + * @param icon the icon material to set; if null, defaults to {@link Material#PAPER} */ public void setIcon(Material icon) { - this.icon = icon; + this.icon = icon != null ? icon.name() : DEFAULT_ICON; + } + + /** + * Sets the icon from a string. Accepts plain material names (e.g. {@code "DIAMOND"}), + * vanilla namespaced materials (e.g. {@code "minecraft:diamond"}), and custom item-model + * keys (e.g. {@code "myserver:island_tropical"}). + * @param icon the icon string; if null, defaults to {@code "PAPER"} + * @since 3.0.0 + */ + public void setIcon(String icon) { + this.icon = icon != null ? icon : DEFAULT_ICON; } /** * @return the displayName diff --git a/src/main/java/world/bentobox/bentobox/panels/BlueprintManagementPanel.java b/src/main/java/world/bentobox/bentobox/panels/BlueprintManagementPanel.java index 3f1d52c6b..6a81375cc 100644 --- a/src/main/java/world/bentobox/bentobox/panels/BlueprintManagementPanel.java +++ b/src/main/java/world/bentobox/bentobox/panels/BlueprintManagementPanel.java @@ -134,7 +134,7 @@ public void openPanel() { PanelItem item = new PanelItemBuilder() .name(bb.getDisplayName()) .description(t("edit"), t("rename")) - .icon(bb.getIcon()) + .icon(bb.getIconItemStack()) .clickHandler((panel, u, clickType, s) -> { u.closeInventory(); if (clickType.equals(ClickType.RIGHT)) { @@ -350,7 +350,7 @@ protected PanelItem getBundleIcon(BlueprintBundle bb) { return new PanelItemBuilder() .name(t("edit-description")) .description(bb.getDescription()) - .icon(bb.getIcon()) + .icon(bb.getIconItemStack()) .clickHandler((panel, u, clickType, slot) -> { u.closeInventory(); // Description conversation @@ -458,7 +458,7 @@ protected PanelItem getBlueprintItem(GameModeAddon addon, int pos, BlueprintBund return new PanelItemBuilder() .name(blueprint.getDisplayName() == null ? blueprint.getName() : blueprint.getDisplayName()) .description(desc) - .icon(blueprint.getIcon()) + .icon(blueprint.getIconItemStack()) .glow(selected != null && pos == selected.getKey()) .clickHandler((panel, u, clickType, slot) -> { // Handle the world squares diff --git a/src/main/java/world/bentobox/bentobox/panels/IconChanger.java b/src/main/java/world/bentobox/bentobox/panels/IconChanger.java index 809b15455..fdccef3ff 100644 --- a/src/main/java/world/bentobox/bentobox/panels/IconChanger.java +++ b/src/main/java/world/bentobox/bentobox/panels/IconChanger.java @@ -6,6 +6,7 @@ import org.bukkit.Sound; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.inventory.meta.ItemMeta; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.addons.GameModeAddon; @@ -48,15 +49,26 @@ public void onInventoryClick(User user, InventoryClickEvent event) { Entry selected = blueprintManagementPanel.getSelected(); user.getPlayer().playSound(user.getLocation(), Sound.BLOCK_METAL_HIT, 1F, 1F); if (selected == null) { - // Change the Bundle Icon - bb.setIcon(icon); + // Change the Bundle Icon — prefer item model key over plain material so that + // datapacked items (e.g. paper[item_model="myserver:island_tropical"]) are stored correctly. + ItemMeta meta = event.getCurrentItem().getItemMeta(); + if (meta != null && meta.hasItemModel()) { + bb.setIcon(meta.getItemModel().toString()); + } else { + bb.setIcon(icon); + } // Save it plugin.getBlueprintsManager().saveBlueprintBundle(addon, bb); } else { - // Change the Blueprint icon + // Change the Blueprint icon — same model-key detection as for bundles Blueprint bp = selected.getValue(); - bp.setIcon(icon); + ItemMeta bpMeta = event.getCurrentItem().getItemMeta(); + if (bpMeta != null && bpMeta.hasItemModel()) { + bp.setIcon(bpMeta.getItemModel().toString()); + } else { + bp.setIcon(icon); + } // Save it plugin.getBlueprintsManager().saveBlueprint(addon, bp); } diff --git a/src/main/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanel.java b/src/main/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanel.java index d47f01bac..953450779 100644 --- a/src/main/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanel.java +++ b/src/main/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanel.java @@ -414,7 +414,7 @@ private void applyTemplate(PanelItemBuilder builder, ItemTemplateRecord template } else { - builder.icon(bundle.getIcon()); + builder.icon(bundle.getIconItemStack()); } if (template.title() != null) diff --git a/src/main/java/world/bentobox/bentobox/util/ItemParser.java b/src/main/java/world/bentobox/bentobox/util/ItemParser.java index 6f47b9f3e..4648171f9 100644 --- a/src/main/java/world/bentobox/bentobox/util/ItemParser.java +++ b/src/main/java/world/bentobox/bentobox/util/ItemParser.java @@ -12,6 +12,7 @@ import org.bukkit.Bukkit; import org.bukkit.DyeColor; import org.bukkit.Material; +import org.bukkit.NamespacedKey; import org.bukkit.block.banner.Pattern; import org.bukkit.block.banner.PatternType; import org.bukkit.inventory.ItemStack; @@ -43,6 +44,61 @@ public class ItemParser { private static final int MAX_AMOUNT = 99; private ItemParser() {} // private constructor to hide the implicit public one. + + /** + * Resolves an icon string to a {@link Material}. + * Accepts plain material names (e.g. {@code "DIAMOND"}), vanilla namespaced keys + * (e.g. {@code "minecraft:diamond"}), and custom item-model keys + * (e.g. {@code "myserver:island_tropical"}). Custom model keys that are not + * recognised vanilla materials fall back to {@link Material#PAPER}. + * @param icon the icon string, may be null + * @return resolved Material, never null + * @since 3.0.0 + */ + public static Material parseIconMaterial(@Nullable String icon) { + if (icon == null) { + return Material.PAPER; + } + Material m = Material.matchMaterial(icon); + return m != null ? m : Material.PAPER; + } + + /** + * Resolves an icon string to an {@link ItemStack}. + *
    + *
  • Plain material name or vanilla namespaced key → {@code new ItemStack(material)}
  • + *
  • Custom item-model key (namespace:key not matching any vanilla material) → + * PAPER base item with the model applied via {@link ItemMeta#setItemModel(NamespacedKey)}
  • + *
+ * @param icon the icon string, may be null + * @return resolved ItemStack, never null + * @since 3.0.0 + */ + public static ItemStack parseIconItemStack(@Nullable String icon) { + if (icon == null) { + return new ItemStack(Material.PAPER); + } + Material m = Material.matchMaterial(icon); + if (m != null) { + return new ItemStack(m); + } + // Contains a colon but isn't a vanilla material → treat as a custom item model key + if (icon.contains(":")) { + ItemStack item = new ItemStack(Material.PAPER); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + String[] parts = icon.split(":", 2); + try { + meta.setItemModel(new NamespacedKey(parts[0], parts[1])); + item.setItemMeta(meta); + } catch (IllegalArgumentException ignored) { + // Invalid namespace/key format — return plain PAPER + } + } + return item; + } + return new ItemStack(Material.PAPER); + } /** * Parse given string to ItemStack. * @param text String value of item stack. diff --git a/src/test/java/world/bentobox/bentobox/blueprints/dataobjects/BlueprintBundleTest.java b/src/test/java/world/bentobox/bentobox/blueprints/dataobjects/BlueprintBundleTest.java new file mode 100644 index 000000000..82a05946a --- /dev/null +++ b/src/test/java/world/bentobox/bentobox/blueprints/dataobjects/BlueprintBundleTest.java @@ -0,0 +1,133 @@ +package world.bentobox.bentobox.blueprints.dataobjects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import world.bentobox.bentobox.CommonTestSetup; + +/** + * Tests for {@link BlueprintBundle} icon field parsing. + * @author tastybento + */ +class BlueprintBundleTest extends CommonTestSetup { + + private BlueprintBundle bundle; + + @BeforeEach + @Override + public void setUp() throws Exception { + super.setUp(); + bundle = new BlueprintBundle(); + } + + @AfterEach + @Override + public void tearDown() throws Exception { + super.tearDown(); + } + + /** + * Default icon should be PAPER. + */ + @Test + void testDefaultIcon() { + assertEquals(Material.PAPER, bundle.getIcon()); + } + + /** + * Default getIconItemStack should return a PAPER ItemStack. + */ + @Test + void testDefaultIconItemStack() { + ItemStack is = bundle.getIconItemStack(); + assertNotNull(is); + assertEquals(Material.PAPER, is.getType()); + } + + /** + * Plain material name (e.g. "DIAMOND") should resolve correctly. + */ + @Test + void testPlainMaterialName() { + bundle.setIcon("DIAMOND"); + assertEquals(Material.DIAMOND, bundle.getIcon()); + ItemStack is = bundle.getIconItemStack(); + assertNotNull(is); + assertEquals(Material.DIAMOND, is.getType()); + } + + /** + * Setting icon via Material enum should store correctly. + */ + @Test + void testSetIconMaterial() { + bundle.setIcon(Material.GOLD_INGOT); + assertEquals(Material.GOLD_INGOT, bundle.getIcon()); + ItemStack is = bundle.getIconItemStack(); + assertNotNull(is); + assertEquals(Material.GOLD_INGOT, is.getType()); + } + + /** + * Setting icon via Material enum with null should fall back to PAPER. + */ + @Test + void testSetIconMaterialNull() { + bundle.setIcon((Material) null); + assertEquals(Material.PAPER, bundle.getIcon()); + } + + /** + * Setting icon via String with null should fall back to PAPER. + */ + @Test + void testSetIconStringNull() { + bundle.setIcon((String) null); + assertEquals(Material.PAPER, bundle.getIcon()); + } + + /** + * Vanilla namespaced material key (e.g. "minecraft:diamond") should resolve to the correct Material. + */ + @Test + void testNamespacedVanillaMaterial() { + bundle.setIcon("minecraft:diamond"); + assertEquals(Material.DIAMOND, bundle.getIcon()); + ItemStack is = bundle.getIconItemStack(); + assertNotNull(is); + assertEquals(Material.DIAMOND, is.getType()); + } + + /** + * A custom item-model key (namespace:key that is not a vanilla material) should return + * PAPER as the base material, since the player never sees the base item. + */ + @Test + void testCustomItemModelKey() { + bundle.setIcon("myserver:island_tropical"); + // getIcon() falls back to PAPER for unrecognised model keys + assertEquals(Material.PAPER, bundle.getIcon()); + // getIconItemStack() returns a PAPER-based item + ItemStack is = bundle.getIconItemStack(); + assertNotNull(is); + assertEquals(Material.PAPER, is.getType()); + } + + /** + * An icon string without a colon that is not a valid material should fall back to PAPER. + */ + @Test + void testUnknownMaterialName() { + bundle.setIcon("NOT_A_REAL_MATERIAL"); + assertEquals(Material.PAPER, bundle.getIcon()); + ItemStack is = bundle.getIconItemStack(); + assertNotNull(is); + assertEquals(Material.PAPER, is.getType()); + } +} diff --git a/src/test/java/world/bentobox/bentobox/panels/BlueprintManagementPanelTest.java b/src/test/java/world/bentobox/bentobox/panels/BlueprintManagementPanelTest.java index 7bca07f25..8e11f9144 100644 --- a/src/test/java/world/bentobox/bentobox/panels/BlueprintManagementPanelTest.java +++ b/src/test/java/world/bentobox/bentobox/panels/BlueprintManagementPanelTest.java @@ -17,6 +17,7 @@ import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -80,6 +81,7 @@ public void setUp() throws Exception { when(bb.getUniqueId()).thenReturn("test"); when(bb.getDisplayName()).thenReturn("test"); when(bb.getIcon()).thenReturn(Material.STONE); + when(bb.getIconItemStack()).thenReturn(new ItemStack(Material.STONE)); when(bb.getDescription()).thenReturn(Collections.singletonList("A description")); when(bb.getCommands()).thenReturn(Collections.emptyList()); when(bb.getSlot()).thenReturn(5); @@ -87,12 +89,14 @@ public void setUp() throws Exception { when(bb2.getUniqueId()).thenReturn("test2"); when(bb2.getDisplayName()).thenReturn("test2"); when(bb2.getIcon()).thenReturn(Material.ACACIA_BOAT); + when(bb2.getIconItemStack()).thenReturn(new ItemStack(Material.ACACIA_BOAT)); when(bb2.getDescription()).thenReturn(Collections.singletonList("A description 2")); when(bb2.getSlot()).thenReturn(-5); // Too large slot for panel when(bb3.getUniqueId()).thenReturn("test3"); when(bb3.getDisplayName()).thenReturn("test3"); when(bb3.getIcon()).thenReturn(Material.BAKED_POTATO); + when(bb3.getIconItemStack()).thenReturn(new ItemStack(Material.BAKED_POTATO)); when(bb3.getDescription()).thenReturn(Collections.singletonList("A description 3")); when(bb3.getSlot()).thenReturn(65); @@ -167,6 +171,7 @@ void testGetBlueprintItem() { void testGetBlueprintItemWithDisplayNameAndIcon() { when(blueprint.getDisplayName()).thenReturn("Display Name"); when(blueprint.getIcon()).thenReturn(Material.BEACON); + when(blueprint.getIconItemStack()).thenReturn(new ItemStack(Material.BEACON)); PanelItem pi = bmp.getBlueprintItem(addon, 0, bb, blueprint); assertEquals("Display Name", pi.getName()); assertEquals(Material.BEACON, pi.getItem().getType()); @@ -180,6 +185,7 @@ void testGetBlueprintItemWithDisplayNameAndIcon() { void testGetBlueprintItemWithDisplayNameAndIconInWorldSlot() { when(blueprint.getDisplayName()).thenReturn("Display Name"); when(blueprint.getIcon()).thenReturn(Material.BEACON); + when(blueprint.getIconItemStack()).thenReturn(new ItemStack(Material.BEACON)); PanelItem pi = bmp.getBlueprintItem(addon, 5, bb, blueprint); assertEquals("Display Name", pi.getName()); assertEquals(Material.BEACON, pi.getItem().getType()); @@ -224,6 +230,7 @@ void testOpenPanelWithManyBundles() { when(bundle.getUniqueId()).thenReturn("bundle" + i); when(bundle.getDisplayName()).thenReturn("Bundle " + String.format("%02d", i)); when(bundle.getIcon()).thenReturn(Material.STONE); + when(bundle.getIconItemStack()).thenReturn(new ItemStack(Material.STONE)); when(bundle.getDescription()).thenReturn(Collections.singletonList("Desc")); map.put("bundle" + i, bundle); } diff --git a/src/test/java/world/bentobox/bentobox/panels/IconChangerTest.java b/src/test/java/world/bentobox/bentobox/panels/IconChangerTest.java new file mode 100644 index 000000000..5a03522b5 --- /dev/null +++ b/src/test/java/world/bentobox/bentobox/panels/IconChangerTest.java @@ -0,0 +1,206 @@ +package world.bentobox.bentobox.panels; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.AbstractMap; + +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import world.bentobox.bentobox.CommonTestSetup; +import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.blueprints.Blueprint; +import world.bentobox.bentobox.blueprints.dataobjects.BlueprintBundle; +import world.bentobox.bentobox.managers.BlueprintsManager; + +/** + * Tests for {@link IconChanger}. + */ +class IconChangerTest extends CommonTestSetup { + + @Mock + private GameModeAddon addon; + @Mock + private BlueprintManagementPanel bmp; + @Mock + private BlueprintBundle bb; + @Mock + private BlueprintsManager bpManager; + @Mock + private User user; + @Mock + private InventoryClickEvent event; + + private IconChanger iconChanger; + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + when(plugin.getBlueprintsManager()).thenReturn(bpManager); + when(bmp.getSelected()).thenReturn(null); // no blueprint selected by default + when(user.getPlayer()).thenReturn(mockPlayer); + when(user.getLocation()).thenReturn(location); + iconChanger = new IconChanger(plugin, addon, bmp, bb); + } + + @Override + @AfterEach + public void tearDown() throws Exception { + super.tearDown(); + } + + /** + * Clicking a plain item (no item model) in the player inventory sets the bundle icon by Material. + */ + @Test + void testOnInventoryClickBundlePlainMaterial() { + ItemStack item = mock(ItemStack.class); + when(item.getType()).thenReturn(Material.STONE); + ItemMeta meta = mock(ItemMeta.class); + when(meta.hasItemModel()).thenReturn(false); + when(item.getItemMeta()).thenReturn(meta); + + when(event.getCurrentItem()).thenReturn(item); + when(event.getRawSlot()).thenReturn(45); // player inventory slot + + iconChanger.onInventoryClick(user, event); + + verify(bb).setIcon(Material.STONE); + verify(bb, never()).setIcon(any(String.class)); + verify(bpManager).saveBlueprintBundle(addon, bb); + } + + /** + * Clicking an item that has a custom item model sets the bundle icon by model key string. + */ + @Test + void testOnInventoryClickBundleItemModel() { + ItemStack item = mock(ItemStack.class); + when(item.getType()).thenReturn(Material.PAPER); + ItemMeta meta = mock(ItemMeta.class); + NamespacedKey modelKey = new NamespacedKey("myserver", "island_tropical"); + when(meta.hasItemModel()).thenReturn(true); + when(meta.getItemModel()).thenReturn(modelKey); + when(item.getItemMeta()).thenReturn(meta); + + when(event.getCurrentItem()).thenReturn(item); + when(event.getRawSlot()).thenReturn(45); + + iconChanger.onInventoryClick(user, event); + + verify(bb).setIcon("myserver:island_tropical"); + verify(bb, never()).setIcon(any(Material.class)); + verify(bpManager).saveBlueprintBundle(addon, bb); + } + + /** + * Clicking a plain item when a blueprint is selected changes the blueprint icon by Material. + */ + @Test + void testOnInventoryClickBlueprintSelected() { + Blueprint bp = mock(Blueprint.class); + when(bmp.getSelected()).thenReturn(new AbstractMap.SimpleEntry<>(1, bp)); + + ItemStack item = mock(ItemStack.class); + when(item.getType()).thenReturn(Material.BEACON); + ItemMeta meta = mock(ItemMeta.class); + when(meta.hasItemModel()).thenReturn(false); + when(item.getItemMeta()).thenReturn(meta); + when(event.getCurrentItem()).thenReturn(item); + when(event.getRawSlot()).thenReturn(45); + + iconChanger.onInventoryClick(user, event); + + verify(bp).setIcon(Material.BEACON); + verify(bpManager).saveBlueprint(addon, bp); + verify(bb, never()).setIcon(any(Material.class)); + verify(bb, never()).setIcon(any(String.class)); + } + + /** + * Clicking a custom item model item when a blueprint is selected stores the model key string. + */ + @Test + void testOnInventoryClickBlueprintItemModel() { + Blueprint bp = mock(Blueprint.class); + when(bmp.getSelected()).thenReturn(new AbstractMap.SimpleEntry<>(1, bp)); + + ItemStack item = mock(ItemStack.class); + when(item.getType()).thenReturn(Material.PAPER); + ItemMeta meta = mock(ItemMeta.class); + NamespacedKey modelKey = new NamespacedKey("myserver", "island_tropical"); + when(meta.hasItemModel()).thenReturn(true); + when(meta.getItemModel()).thenReturn(modelKey); + when(item.getItemMeta()).thenReturn(meta); + when(event.getCurrentItem()).thenReturn(item); + when(event.getRawSlot()).thenReturn(45); + + iconChanger.onInventoryClick(user, event); + + verify(bp).setIcon("myserver:island_tropical"); + verify(bp, never()).setIcon(any(Material.class)); + verify(bpManager).saveBlueprint(addon, bp); + } + + /** + * Clicking inside the panel (slot ≤ 44) does nothing. + */ + @Test + void testOnInventoryClickInsidePanel() { + ItemStack item = mock(ItemStack.class); + when(item.getType()).thenReturn(Material.STONE); + when(event.getCurrentItem()).thenReturn(item); + when(event.getRawSlot()).thenReturn(10); // inside the GUI + + iconChanger.onInventoryClick(user, event); + + verify(bb, never()).setIcon(any(Material.class)); + verify(bb, never()).setIcon(any(String.class)); + verify(bpManager, never()).saveBlueprintBundle(any(), any()); + } + + /** + * Clicking an AIR slot does nothing. + */ + @Test + void testOnInventoryClickAirItem() { + ItemStack item = mock(ItemStack.class); + when(item.getType()).thenReturn(Material.AIR); + when(event.getCurrentItem()).thenReturn(item); + when(event.getRawSlot()).thenReturn(45); + + iconChanger.onInventoryClick(user, event); + + verify(bb, never()).setIcon(any(Material.class)); + verify(bb, never()).setIcon(any(String.class)); + verify(bpManager, never()).saveBlueprintBundle(any(), any()); + } + + /** + * Clicking a null item does nothing. + */ + @Test + void testOnInventoryClickNullItem() { + when(event.getCurrentItem()).thenReturn(null); + when(event.getRawSlot()).thenReturn(45); + + iconChanger.onInventoryClick(user, event); + + verify(bb, never()).setIcon(any(Material.class)); + verify(bb, never()).setIcon(any(String.class)); + verify(bpManager, never()).saveBlueprintBundle(any(), any()); + } +} diff --git a/src/test/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanelTest.java b/src/test/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanelTest.java index d79fdc053..26785bca3 100644 --- a/src/test/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanelTest.java +++ b/src/test/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanelTest.java @@ -20,6 +20,7 @@ import org.bukkit.Material; import org.bukkit.World; +import org.bukkit.inventory.ItemStack; import org.bukkit.entity.Player; import org.bukkit.plugin.PluginDescriptionFile; import org.junit.jupiter.api.AfterEach; @@ -128,6 +129,7 @@ public void setUp() throws Exception { when(bundle1.getUniqueId()).thenReturn("default"); when(bundle1.getDisplayName()).thenReturn("Default"); when(bundle1.getIcon()).thenReturn(Material.GRASS_BLOCK); + when(bundle1.getIconItemStack()).thenReturn(new ItemStack(Material.GRASS_BLOCK)); when(bundle1.getDescription()).thenReturn(Collections.singletonList("Default island")); when(bundle1.getSlot()).thenReturn(0); when(bundle1.isRequirePermission()).thenReturn(false); @@ -138,6 +140,7 @@ public void setUp() throws Exception { when(bundle2.getUniqueId()).thenReturn("nether"); when(bundle2.getDisplayName()).thenReturn("Nether"); when(bundle2.getIcon()).thenReturn(Material.NETHERRACK); + when(bundle2.getIconItemStack()).thenReturn(new ItemStack(Material.NETHERRACK)); when(bundle2.getDescription()).thenReturn(Collections.singletonList("Nether island")); when(bundle2.getSlot()).thenReturn(1); when(bundle2.isRequirePermission()).thenReturn(false); @@ -148,6 +151,7 @@ public void setUp() throws Exception { when(bundle3.getUniqueId()).thenReturn("end"); when(bundle3.getDisplayName()).thenReturn("End"); when(bundle3.getIcon()).thenReturn(Material.END_STONE); + when(bundle3.getIconItemStack()).thenReturn(new ItemStack(Material.END_STONE)); when(bundle3.getDescription()).thenReturn(Collections.singletonList("End island")); when(bundle3.getSlot()).thenReturn(2); when(bundle3.isRequirePermission()).thenReturn(false);