chanceMap)
{
if (chanceMap.isEmpty())
{
diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/utils/CustomBlocks.java b/src/main/java/world/bentobox/magiccobblestonegenerator/utils/CustomBlocks.java
new file mode 100644
index 0000000..483c4c1
--- /dev/null
+++ b/src/main/java/world/bentobox/magiccobblestonegenerator/utils/CustomBlocks.java
@@ -0,0 +1,334 @@
+package world.bentobox.magiccobblestonegenerator.utils;
+
+
+import java.util.Optional;
+
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.Nullable;
+
+import world.bentobox.bentobox.api.user.User;
+import world.bentobox.bentobox.hooks.CraftEngineHook;
+import world.bentobox.bentobox.hooks.ItemsAdderHook;
+import world.bentobox.bentobox.hooks.OraxenHook;
+import world.bentobox.magiccobblestonegenerator.StoneGeneratorAddon;
+
+
+/**
+ * Helper for custom blocks provided by third-party plugins (ItemsAdder, CraftEngine, Oraxen, Nexo).
+ *
+ * Generator block entries are stored as plain strings. A vanilla entry is a {@link Material} name
+ * (e.g. {@code COBBLESTONE}), while a custom entry is prefixed with the provider, e.g.
+ * {@code itemsadder:iasurvival:ruby_ore}, {@code craftengine:default:topaz_ore} or {@code oraxen:ruby_ore}.
+ *
+ * All plugin access is routed through BentoBox core hooks, so this addon never depends on the
+ * third-party plugins directly. Providers whose hook does not support placement yet (Oraxen, Nexo)
+ * are recognised but will not generate until BentoBox core adds support.
+ */
+public final class CustomBlocks
+{
+ private CustomBlocks()
+ {
+ }
+
+
+ /**
+ * Custom block providers supported by this addon via BentoBox hooks.
+ */
+ public enum Provider
+ {
+ ITEMS_ADDER("itemsadder", "ItemsAdder", true),
+ CRAFT_ENGINE("craftengine", "CraftEngine", true),
+ ORAXEN("oraxen", "Oraxen", false),
+ NEXO("nexo", "Nexo", false);
+
+
+ Provider(String prefix, String pluginName, boolean placementSupported)
+ {
+ this.prefix = prefix;
+ this.pluginName = pluginName;
+ this.placementSupported = placementSupported;
+ }
+
+
+ /**
+ * @return prefix used in block IDs, e.g. {@code itemsadder}.
+ */
+ public String getPrefix()
+ {
+ return this.prefix;
+ }
+
+
+ /**
+ * @return plugin name used to look up the BentoBox hook.
+ */
+ public String getPluginName()
+ {
+ return this.pluginName;
+ }
+
+
+ /**
+ * @return {@code true} if the BentoBox core hook for this provider can place blocks.
+ */
+ public boolean isPlacementSupported()
+ {
+ return this.placementSupported;
+ }
+
+
+ private final String prefix;
+
+ private final String pluginName;
+
+ private final boolean placementSupported;
+ }
+
+
+ /**
+ * Returns the custom block provider for the given block ID, or {@code null} if the ID does not
+ * carry a known provider prefix (i.e. it is a vanilla material name).
+ *
+ * @param blockId block ID to parse.
+ * @return provider or {@code null}.
+ */
+ public static @Nullable Provider getProvider(@Nullable String blockId)
+ {
+ if (blockId == null)
+ {
+ return null;
+ }
+
+ int separator = blockId.indexOf(':');
+
+ if (separator <= 0)
+ {
+ return null;
+ }
+
+ String prefix = blockId.substring(0, separator).toLowerCase();
+
+ for (Provider provider : Provider.values())
+ {
+ if (provider.getPrefix().equals(prefix))
+ {
+ return provider;
+ }
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Returns if given block ID references a custom block from a known provider.
+ *
+ * @param blockId block ID to check.
+ * @return {@code true} if the ID has a known provider prefix.
+ */
+ public static boolean isCustom(@Nullable String blockId)
+ {
+ return getProvider(blockId) != null;
+ }
+
+
+ /**
+ * Strips the provider prefix from a custom block ID, returning the plugin-native ID.
+ *
+ * @param blockId provider-prefixed block ID.
+ * @return the ID without the provider prefix, or the input unchanged if it has none.
+ */
+ public static String getCustomId(String blockId)
+ {
+ return getProvider(blockId) == null ? blockId : blockId.substring(blockId.indexOf(':') + 1);
+ }
+
+
+ /**
+ * Parses a vanilla material from the given block ID.
+ *
+ * @param blockId block ID to parse.
+ * @return the matching {@link Material}, or {@code null} if the ID is a custom block or unknown.
+ */
+ public static @Nullable Material matchVanilla(@Nullable String blockId)
+ {
+ if (blockId == null || isCustom(blockId))
+ {
+ return null;
+ }
+
+ return Material.matchMaterial(blockId);
+ }
+
+
+ /**
+ * Returns if the BentoBox hook for the given provider is present (i.e. the plugin is installed).
+ *
+ * @param addon addon instance used to reach the hook manager.
+ * @param provider custom block provider.
+ * @return {@code true} if the hook is registered.
+ */
+ public static boolean isHooked(StoneGeneratorAddon addon, Provider provider)
+ {
+ return addon.getPlugin().getHooks().getHook(provider.getPluginName()).isPresent();
+ }
+
+
+ /**
+ * Returns if the given custom block ID is registered by its provider plugin on this server.
+ *
+ * @param addon addon instance used to reach the hook manager.
+ * @param blockId provider-prefixed block ID.
+ * @return {@code true} if the provider plugin is hooked and knows the ID.
+ */
+ public static boolean isRegistered(StoneGeneratorAddon addon, String blockId)
+ {
+ Provider provider = getProvider(blockId);
+
+ if (provider == null || !isHooked(addon, provider))
+ {
+ return false;
+ }
+
+ String customId = getCustomId(blockId);
+
+ return switch (provider)
+ {
+ case ITEMS_ADDER -> ItemsAdderHook.isInRegistry(customId);
+ case CRAFT_ENGINE -> CraftEngineHook.exists(customId);
+ case ORAXEN -> OraxenHook.exists(customId);
+ // No BentoBox core hook for Nexo yet.
+ case NEXO -> false;
+ };
+ }
+
+
+ /**
+ * Returns if the given custom block ID can actually be placed on this server: the provider
+ * plugin is hooked, the ID is registered, and the BentoBox hook supports block placement.
+ *
+ * @param addon addon instance used to reach the hook manager.
+ * @param blockId provider-prefixed block ID.
+ * @return {@code true} if {@link #place(StoneGeneratorAddon, String, Location)} can succeed.
+ */
+ public static boolean canPlace(StoneGeneratorAddon addon, String blockId)
+ {
+ Provider provider = getProvider(blockId);
+
+ return provider != null &&
+ provider.isPlacementSupported() &&
+ isRegistered(addon, blockId);
+ }
+
+
+ /**
+ * Places the given custom block through its BentoBox hook.
+ *
+ * @param addon addon instance used to reach the hook manager.
+ * @param blockId provider-prefixed block ID.
+ * @param location target location.
+ * @return {@code true} if the block was placed.
+ */
+ public static boolean place(StoneGeneratorAddon addon, String blockId, Location location)
+ {
+ if (!canPlace(addon, blockId))
+ {
+ return false;
+ }
+
+ String customId = getCustomId(blockId);
+
+ return switch (getProvider(blockId))
+ {
+ case ITEMS_ADDER ->
+ {
+ ItemsAdderHook.place(customId, location);
+ yield true;
+ }
+ case CRAFT_ENGINE -> CraftEngineHook.placeBlock(location, customId);
+ // canPlace already filters these out; kept for exhaustiveness.
+ case ORAXEN, NEXO -> false;
+ };
+ }
+
+
+ /**
+ * Returns an icon ItemStack for the given block ID, for use in GUI panels. Vanilla IDs map to
+ * their material; custom IDs use the provider's item (correct texture / model data) when the
+ * hook is available, otherwise a fallback icon.
+ *
+ * @param addon addon instance used to reach the hook manager.
+ * @param blockId block ID.
+ * @return icon ItemStack, never null.
+ */
+ public static ItemStack getIcon(StoneGeneratorAddon addon, String blockId)
+ {
+ Material vanilla = matchVanilla(blockId);
+
+ if (vanilla != null)
+ {
+ return new ItemStack(vanilla.isItem() ? vanilla : Material.PAPER);
+ }
+
+ Provider provider = getProvider(blockId);
+
+ if (provider != null && isHooked(addon, provider))
+ {
+ String customId = getCustomId(blockId);
+
+ Optional icon = switch (provider)
+ {
+ case ITEMS_ADDER -> ItemsAdderHook.getItemStack(customId);
+ case CRAFT_ENGINE -> CraftEngineHook.getItemStack(customId);
+ // Oraxen hook exposes icons only via Oraxen classes; Nexo has no hook yet.
+ case ORAXEN, NEXO -> Optional.empty();
+ };
+
+ if (icon.isPresent())
+ {
+ return icon.get().clone();
+ }
+ }
+
+ return new ItemStack(Material.NOTE_BLOCK);
+ }
+
+
+ /**
+ * Returns a user-facing display name for the given block ID. Vanilla IDs are translated via the
+ * locale files; custom IDs use the provider item's display name when available, otherwise the
+ * plugin-native ID.
+ *
+ * @param addon addon instance used to reach the hook manager.
+ * @param user user for translations.
+ * @param blockId block ID.
+ * @return display name, never null.
+ */
+ public static String getDisplayName(StoneGeneratorAddon addon, User user, String blockId)
+ {
+ Material vanilla = matchVanilla(blockId);
+
+ if (vanilla != null)
+ {
+ return Utils.prettifyObject(user, vanilla);
+ }
+
+ if (isCustom(blockId))
+ {
+ ItemStack icon = getIcon(addon, blockId);
+ ItemMeta meta = icon.getItemMeta();
+
+ if (meta != null && meta.hasDisplayName())
+ {
+ return meta.getDisplayName();
+ }
+
+ return getCustomId(blockId);
+ }
+
+ return blockId;
+ }
+}
diff --git a/src/main/resources/generatorTemplate.yml b/src/main/resources/generatorTemplate.yml
index 465ace5..f2d7831 100644
--- a/src/main/resources/generatorTemplate.yml
+++ b/src/main/resources/generatorTemplate.yml
@@ -29,6 +29,13 @@ tiers:
# Materials and their chances. Use actual blocks please.
# Chance supports any positive number.
# Everything in the end will be normalized.
+ # Custom blocks from ItemsAdder, CraftEngine, Oraxen or Nexo can be used by
+ # prefixing the block ID with the plugin name (quote keys that contain colons):
+ # "itemsadder:iasurvival:ruby_ore": 1
+ # "craftengine:default:topaz_ore": 1
+ # ItemsAdder and CraftEngine work out of the box (via BentoBox hooks);
+ # Oraxen and Nexo entries are accepted but generate only once BentoBox
+ # core hooks support placing their blocks.
blocks:
STONE: 10
COBBLESTONE: 86
diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml
index 2882c64..d0ccae6 100644
--- a/src/main/resources/locales/en-US.yml
+++ b/src/main/resources/locales/en-US.yml
@@ -431,6 +431,12 @@ stone-generator:
description: |-
Add new material to the
current material list.
+ add_custom_block:
+ name: "Add Custom Block"
+ description: |-
+ Add a custom block from
+ ItemsAdder, CraftEngine,
+ Oraxen or Nexo by its ID.
remove_material:
name: "Remove Materials"
description: |-
@@ -1124,6 +1130,12 @@ stone-generator:
click-text-to-activate: "You have unlocked [generator]! Click here to activate it now."
input-min-height: "Enter the minimum height for this material (between -64 and 320):"
input-max-height: "Enter the maximum height for this material (between -64 and 320):"
+ # Message that appears when admin clicks on the add custom block button.
+ input-custom-block: "Please enter the custom block ID in chat, prefixed by its plugin: 'itemsadder:namespace:id', 'craftengine:namespace:id', 'oraxen:id' or 'nexo:id'."
+ # Message that appears when the entered custom block ID has no known plugin prefix.
+ custom-block-invalid-prefix: "'[block]' is not a valid custom block ID. Use 'itemsadder:', 'craftengine:', 'oraxen:' or 'nexo:' followed by the block ID."
+ # Message that appears when the entered custom block ID is not registered on this server.
+ custom-block-not-found: "Custom block '[block]' was not found. Check that the plugin is installed and the ID is correct."
# Showcase for manual material translation
materials:
# Names should be lowercase.
diff --git a/src/test/java/world/bentobox/magiccobblestonegenerator/tasks/MagicGeneratorTest.java b/src/test/java/world/bentobox/magiccobblestonegenerator/tasks/MagicGeneratorTest.java
index 3591153..d3fba2b 100644
--- a/src/test/java/world/bentobox/magiccobblestonegenerator/tasks/MagicGeneratorTest.java
+++ b/src/test/java/world/bentobox/magiccobblestonegenerator/tasks/MagicGeneratorTest.java
@@ -1,29 +1,32 @@
package world.bentobox.magiccobblestonegenerator.tasks;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
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.Optional;
import java.util.TreeMap;
import org.bukkit.Location;
-import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
+import world.bentobox.bentobox.managers.HooksManager;
import world.bentobox.magiccobblestonegenerator.CommonTestSetup;
import world.bentobox.magiccobblestonegenerator.StoneGeneratorAddon;
import world.bentobox.magiccobblestonegenerator.database.objects.GeneratorTierObject;
import world.bentobox.magiccobblestonegenerator.events.GeneratorTreasureDropEvent;
/**
- * Tests for {@link MagicGenerator}, focused on the treasure drop event (#140).
+ * Tests for {@link MagicGenerator}, focused on the treasure drop event (#140) and custom block IDs.
*/
class MagicGeneratorTest extends CommonTestSetup {
@@ -33,6 +36,8 @@ class MagicGeneratorTest extends CommonTestSetup {
private GeneratorTierObject generatorTier;
@Mock
private ItemStack treasure;
+ @Mock
+ private HooksManager hooksManager;
private MagicGenerator generator;
@@ -42,8 +47,8 @@ public void setUp() throws Exception {
super.setUp();
// Block chance map with a single guaranteed material.
- TreeMap blockMap = new TreeMap<>();
- blockMap.put(1.0, Material.COBBLESTONE);
+ TreeMap blockMap = new TreeMap<>();
+ blockMap.put(1.0, "COBBLESTONE");
when(generatorTier.getBlockChanceMap()).thenReturn(blockMap);
when(generatorTier.getMinHeight()).thenReturn(-64);
when(generatorTier.getMaxHeight()).thenReturn(320);
@@ -67,8 +72,8 @@ public void setUp() throws Exception {
@Test
void testProcessBlockReplacementReturnsMaterial() {
- Material result = generator.processBlockReplacement(generatorTier, location, island);
- assertEquals(Material.COBBLESTONE, result);
+ String result = generator.processBlockReplacement(generatorTier, location, island);
+ assertEquals("COBBLESTONE", result);
}
@Test
@@ -88,10 +93,10 @@ void testCancelledTreasureDropEventPreventsDrop() {
return null;
}).when(pim).callEvent(any(GeneratorTreasureDropEvent.class));
- Material result = generator.processBlockReplacement(generatorTier, location, island);
+ String result = generator.processBlockReplacement(generatorTier, location, island);
// Block is still generated, but no treasure is dropped.
- assertEquals(Material.COBBLESTONE, result);
+ assertEquals("COBBLESTONE", result);
verify(world, never()).dropItemNaturally(any(Location.class), any(ItemStack.class));
}
@@ -106,17 +111,54 @@ void testListenerNullingDropLocationIsGuarded() {
return null;
}).when(pim).callEvent(any(GeneratorTreasureDropEvent.class));
- Material result = generator.processBlockReplacement(generatorTier, location, island);
+ String result = generator.processBlockReplacement(generatorTier, location, island);
- assertEquals(Material.COBBLESTONE, result);
+ assertEquals("COBBLESTONE", result);
verify(world, never()).dropItemNaturally(any(Location.class), any(ItemStack.class));
}
@Test
void testProcessBlockReplacementWithoutIslandOverload() {
// The two-argument overload delegates with a null island and still generates the block.
- Material result = generator.processBlockReplacement(generatorTier, location);
- assertEquals(Material.COBBLESTONE, result);
+ String result = generator.processBlockReplacement(generatorTier, location);
+ assertEquals("COBBLESTONE", result);
+ }
+
+ @Test
+ void testCustomBlockWithoutHookReturnsNull() {
+ // A custom block ID whose provider plugin is not hooked must not generate anything.
+ TreeMap blockMap = new TreeMap<>();
+ blockMap.put(1.0, "itemsadder:iasurvival:ruby_ore");
+ when(generatorTier.getBlockChanceMap()).thenReturn(blockMap);
+
+ when(addon.getPlugin()).thenReturn(plugin);
+ when(plugin.getHooks()).thenReturn(hooksManager);
+ when(hooksManager.getHook(anyString())).thenReturn(Optional.empty());
+
+ String result = generator.processBlockReplacement(generatorTier, location, island);
+
+ assertNull(result);
+ // No treasure may drop when the block generation failed.
+ verify(world, never()).dropItemNaturally(any(Location.class), any(ItemStack.class));
+ }
+
+ @Test
+ void testUnknownVanillaMaterialReturnsNull() {
+ TreeMap blockMap = new TreeMap<>();
+ blockMap.put(1.0, "NOT_A_REAL_MATERIAL");
+ when(generatorTier.getBlockChanceMap()).thenReturn(blockMap);
+
+ assertNull(generator.processBlockReplacement(generatorTier, location, island));
+ }
+
+ @Test
+ void testOraxenBlockReturnsNullUntilCoreSupportsPlacement() {
+ // Oraxen IDs are recognised but placement is not supported by the BentoBox hook yet.
+ TreeMap blockMap = new TreeMap<>();
+ blockMap.put(1.0, "oraxen:ruby_ore");
+ when(generatorTier.getBlockChanceMap()).thenReturn(blockMap);
+
+ assertNull(generator.processBlockReplacement(generatorTier, location, island));
}
@Test
diff --git a/src/test/java/world/bentobox/magiccobblestonegenerator/utils/CustomBlocksTest.java b/src/test/java/world/bentobox/magiccobblestonegenerator/utils/CustomBlocksTest.java
new file mode 100644
index 0000000..4756129
--- /dev/null
+++ b/src/test/java/world/bentobox/magiccobblestonegenerator/utils/CustomBlocksTest.java
@@ -0,0 +1,126 @@
+package world.bentobox.magiccobblestonegenerator.utils;
+
+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.anyString;
+import static org.mockito.Mockito.when;
+
+import java.util.Optional;
+
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+
+import world.bentobox.bentobox.managers.HooksManager;
+import world.bentobox.magiccobblestonegenerator.CommonTestSetup;
+import world.bentobox.magiccobblestonegenerator.StoneGeneratorAddon;
+
+/**
+ * Tests for {@link CustomBlocks} block ID parsing and hook dispatch.
+ */
+class CustomBlocksTest extends CommonTestSetup {
+
+ @Mock
+ private StoneGeneratorAddon addon;
+ @Mock
+ private HooksManager hooksManager;
+
+ @Override
+ @BeforeEach
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Test
+ void testGetProviderParsesKnownPrefixes() {
+ assertEquals(CustomBlocks.Provider.ITEMS_ADDER, CustomBlocks.getProvider("itemsadder:ns:ruby_ore"));
+ assertEquals(CustomBlocks.Provider.CRAFT_ENGINE, CustomBlocks.getProvider("craftengine:default:topaz"));
+ assertEquals(CustomBlocks.Provider.ORAXEN, CustomBlocks.getProvider("oraxen:ruby_ore"));
+ assertEquals(CustomBlocks.Provider.NEXO, CustomBlocks.getProvider("nexo:ruby_ore"));
+ // Prefix matching is case-insensitive.
+ assertEquals(CustomBlocks.Provider.ITEMS_ADDER, CustomBlocks.getProvider("ItemsAdder:ns:ruby_ore"));
+ }
+
+ @Test
+ void testGetProviderRejectsVanillaAndUnknown() {
+ assertNull(CustomBlocks.getProvider("COBBLESTONE"));
+ assertNull(CustomBlocks.getProvider("minecraft:cobblestone"));
+ assertNull(CustomBlocks.getProvider("someplugin:block"));
+ assertNull(CustomBlocks.getProvider(null));
+ assertNull(CustomBlocks.getProvider(":oops"));
+ }
+
+ @Test
+ void testGetCustomIdStripsOnlyProviderPrefix() {
+ assertEquals("ns:ruby_ore", CustomBlocks.getCustomId("itemsadder:ns:ruby_ore"));
+ assertEquals("ruby_ore", CustomBlocks.getCustomId("oraxen:ruby_ore"));
+ // Non-custom IDs pass through unchanged.
+ assertEquals("COBBLESTONE", CustomBlocks.getCustomId("COBBLESTONE"));
+ assertEquals("minecraft:cobblestone", CustomBlocks.getCustomId("minecraft:cobblestone"));
+ }
+
+ @Test
+ void testMatchVanilla() {
+ assertEquals(Material.COBBLESTONE, CustomBlocks.matchVanilla("COBBLESTONE"));
+ assertEquals(Material.COBBLESTONE, CustomBlocks.matchVanilla("minecraft:cobblestone"));
+ assertNull(CustomBlocks.matchVanilla("itemsadder:ns:ruby_ore"));
+ assertNull(CustomBlocks.matchVanilla("NOT_A_REAL_MATERIAL"));
+ assertNull(CustomBlocks.matchVanilla(null));
+ }
+
+ @Test
+ void testHookAbsentMeansNotRegisteredAndNoPlacement() {
+ when(addon.getPlugin()).thenReturn(plugin);
+ when(plugin.getHooks()).thenReturn(hooksManager);
+ when(hooksManager.getHook(anyString())).thenReturn(Optional.empty());
+
+ assertFalse(CustomBlocks.isRegistered(addon, "itemsadder:ns:ruby_ore"));
+ assertFalse(CustomBlocks.canPlace(addon, "itemsadder:ns:ruby_ore"));
+ assertFalse(CustomBlocks.place(addon, "itemsadder:ns:ruby_ore", location));
+ }
+
+ @Test
+ void testPlacementUnsupportedProvidersCannotPlace() {
+ // Oraxen and Nexo are rejected before any hook lookup, as core hooks cannot place them yet.
+ assertFalse(CustomBlocks.canPlace(addon, "oraxen:ruby_ore"));
+ assertFalse(CustomBlocks.canPlace(addon, "nexo:ruby_ore"));
+ assertFalse(CustomBlocks.place(addon, "oraxen:ruby_ore", location));
+ }
+
+ @Test
+ void testVanillaIdsAreNeverPlaceableAsCustom() {
+ assertFalse(CustomBlocks.canPlace(addon, "COBBLESTONE"));
+ assertFalse(CustomBlocks.isCustom("COBBLESTONE"));
+ assertTrue(CustomBlocks.isCustom("craftengine:default:topaz"));
+ }
+
+ @Test
+ void testGetIconForVanillaBlock() {
+ ItemStack icon = CustomBlocks.getIcon(addon, "COBBLESTONE");
+ assertEquals(Material.COBBLESTONE, icon.getType());
+ }
+
+ @Test
+ void testGetIconFallsBackWhenHookMissing() {
+ when(addon.getPlugin()).thenReturn(plugin);
+ when(plugin.getHooks()).thenReturn(hooksManager);
+ when(hooksManager.getHook(anyString())).thenReturn(Optional.empty());
+
+ ItemStack icon = CustomBlocks.getIcon(addon, "itemsadder:ns:ruby_ore");
+ assertEquals(Material.NOTE_BLOCK, icon.getType());
+ }
+
+ @Test
+ void testGetDisplayNameFallsBackToCustomId() {
+ when(addon.getPlugin()).thenReturn(plugin);
+ when(plugin.getHooks()).thenReturn(hooksManager);
+ when(hooksManager.getHook(anyString())).thenReturn(Optional.empty());
+
+ // Fallback icon has no display name, so the plugin-native ID is used.
+ assertEquals("ns:ruby_ore", CustomBlocks.getDisplayName(addon, null, "itemsadder:ns:ruby_ore"));
+ }
+}