From 929fddbea3b4f4f0ff38c6085585cc59a7af9a5e Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 11 Jul 2026 11:19:49 -0700 Subject: [PATCH 1/4] Add custom block support via BentoBox hooks (ItemsAdder, CraftEngine, Oraxen, Nexo) Generator block entries are now stored as string block IDs instead of Material: vanilla material names (COBBLESTONE) or provider-prefixed custom block IDs (itemsadder:namespace:id, craftengine:namespace:id, oraxen:id, nexo:id). Existing databases load unchanged because GSON already serialized Material values as their names. All third-party plugin access is routed through BentoBox core hooks via the new CustomBlocks utility, so the addon never depends on the plugins directly. ItemsAdder and CraftEngine blocks generate out of the box; Oraxen and Nexo IDs are accepted but degrade gracefully to the vanilla block until core hooks support placing them (BentoBoxWorld/BentoBox#3020, BentoBoxWorld/BentoBox#3025). - MagicGenerator validates picked IDs against the hooks and reports failures via /why - Listeners set vanilla blocks immediately; custom blocks are placed via their hook one tick later, since hook APIs set blocks directly - Panels render custom blocks with the provider item's texture and display name; the admin edit panel gains an Add Custom Block button with chat input validated against the hook registry - generatorTemplate.yml accepts quoted prefixed keys; unregistered custom blocks import with a warning so templates stay portable - BentoBox dependency bumped to 3.18.1 (CraftEngineHook needs 3.15.0+) Also fixes a pre-existing admin panel bug where editing a treasure's chance wrote the block list into the deprecated treasureChanceMap instead of updating treasureItemChanceMap. Addresses #103 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Q63R7wAXARZD2Rpg3oun6m --- pom.xml | 2 +- .../database/objects/GeneratorTierObject.java | 51 +-- .../listeners/GeneratorListener.java | 74 +++- .../listeners/MagicGeneratorListener.java | 15 +- .../listeners/VanillaGeneratorListener.java | 47 ++- .../managers/StoneGeneratorImportManager.java | 92 +++-- .../panels/CommonPanel.java | 9 +- .../panels/admin/GeneratorEditPanel.java | 101 +++++- .../panels/player/GeneratorViewPanel.java | 9 +- .../tasks/MagicGenerator.java | 71 ++-- .../utils/CustomBlocks.java | 334 ++++++++++++++++++ src/main/resources/generatorTemplate.yml | 7 + src/main/resources/locales/en-US.yml | 12 + .../tasks/MagicGeneratorTest.java | 66 +++- .../utils/CustomBlocksTest.java | 126 +++++++ 15 files changed, 864 insertions(+), 152 deletions(-) create mode 100644 src/main/java/world/bentobox/magiccobblestonegenerator/utils/CustomBlocks.java create mode 100644 src/test/java/world/bentobox/magiccobblestonegenerator/utils/CustomBlocksTest.java diff --git a/pom.xml b/pom.xml index 8545c43..3f8e4cd 100644 --- a/pom.xml +++ b/pom.xml @@ -35,7 +35,7 @@ 21 1.21.11-R0.1-SNAPSHOT - 3.14.0-SNAPSHOT + 3.18.1 2.5.0 1.4.0 1.18.0 diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/database/objects/GeneratorTierObject.java b/src/main/java/world/bentobox/magiccobblestonegenerator/database/objects/GeneratorTierObject.java index 3c2ec40..16fc92a 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/database/objects/GeneratorTierObject.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/database/objects/GeneratorTierObject.java @@ -346,9 +346,11 @@ public void setActivateOnUnlock(boolean activateOnUnlock) * Returns the blockChanceMap of this object. * * @return a {@code TreeMap} where the keys are {@code Double} values representing chances, - * and the values are {@code Material} objects. + * and the values are block IDs: vanilla {@code Material} names (e.g. {@code COBBLESTONE}) + * or provider-prefixed custom block IDs (e.g. {@code itemsadder:namespace:id}) — see + * {@link world.bentobox.magiccobblestonegenerator.utils.CustomBlocks}. */ - public TreeMap getBlockChanceMap() + public TreeMap getBlockChanceMap() { return blockChanceMap; } @@ -359,7 +361,7 @@ public TreeMap getBlockChanceMap() * * @param blockChanceMap new value for this object. */ - public void setBlockChanceMap(TreeMap blockChanceMap) + public void setBlockChanceMap(TreeMap blockChanceMap) { this.blockChanceMap = blockChanceMap; } @@ -813,10 +815,11 @@ public boolean includes(GeneratorType type) // Rewards section /** - * Map that stores different blocks and their chance for generating. + * Map that stores different blocks and their chance for generating. Values are vanilla material + * names or provider-prefixed custom block IDs. */ @Expose - private TreeMap blockChanceMap = new TreeMap<>(); + private TreeMap blockChanceMap = new TreeMap<>(); /** * Map that stores different extra treasures and their change for being dropped. @@ -856,10 +859,10 @@ public boolean includes(GeneratorType type) private int maxHeight = 320; /** - * Map that stores min and max heights for each material in the generator. + * Map that stores min and max heights for each block ID in the generator. */ @Expose - private TreeMap materialHeightMap = new TreeMap<>(); + private TreeMap materialHeightMap = new TreeMap<>(); /** * Maximum number of blocks this generator tier is allowed to generate during a single exhaustion period. @@ -872,22 +875,22 @@ public boolean includes(GeneratorType type) * Field to store block height ranges */ @Expose - private Map blockHeightRanges = new HashMap<>(); + private Map blockHeightRanges = new HashMap<>(); /** * Gets the height ranges for each block type - * @return Map of Material to height range array [min, max] + * @return Map of block ID to height range array [min, max] */ - public Map getBlockHeightRanges() + public Map getBlockHeightRanges() { return blockHeightRanges; } /** * Sets the height ranges for each block type - * @param blockHeightRanges Map of Material to height range array [min, max] + * @param blockHeightRanges Map of block ID to height range array [min, max] */ - public void setBlockHeightRanges(Map blockHeightRanges) + public void setBlockHeightRanges(Map blockHeightRanges) { this.blockHeightRanges = blockHeightRanges; } @@ -937,48 +940,48 @@ public void setMaxHeight(int maxHeight) /** - * Gets the map of material-specific height ranges. + * Gets the map of block-specific height ranges, keyed by block ID. * * @return the material height map */ - public TreeMap getMaterialHeightMap() + public TreeMap getMaterialHeightMap() { return this.materialHeightMap; } /** - * Sets the map of material-specific height ranges. + * Sets the map of block-specific height ranges, keyed by block ID. * * @param materialHeightMap the material height map */ - public void setMaterialHeightMap(TreeMap materialHeightMap) + public void setMaterialHeightMap(TreeMap materialHeightMap) { this.materialHeightMap = materialHeightMap; } /** - * Gets the height range for a specific material. + * Gets the height range for a specific block ID. * - * @param material the material + * @param blockId the block ID * @return an array where index 0 is minHeight and index 1 is maxHeight, or null if not set */ - public int[] getMaterialHeightRange(Material material) + public int[] getMaterialHeightRange(String blockId) { - return this.materialHeightMap.get(material); + return this.materialHeightMap.get(blockId); } /** - * Sets the height range for a specific material. + * Sets the height range for a specific block ID. * - * @param material the material + * @param blockId the block ID * @param minHeight the minimum height * @param maxHeight the maximum height */ - public void setMaterialHeightRange(Material material, int minHeight, int maxHeight) + public void setMaterialHeightRange(String blockId, int minHeight, int maxHeight) { - this.materialHeightMap.put(material, new int[]{minHeight, maxHeight}); + this.materialHeightMap.put(blockId, new int[]{minHeight, maxHeight}); } } diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/listeners/GeneratorListener.java b/src/main/java/world/bentobox/magiccobblestonegenerator/listeners/GeneratorListener.java index a1923ce..ae36796 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/listeners/GeneratorListener.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/listeners/GeneratorListener.java @@ -26,6 +26,8 @@ import world.bentobox.magiccobblestonegenerator.StoneGeneratorAddon; import world.bentobox.magiccobblestonegenerator.database.objects.GeneratorDataObject; import world.bentobox.magiccobblestonegenerator.database.objects.GeneratorTierObject; +import world.bentobox.magiccobblestonegenerator.utils.CustomBlocks; +import world.bentobox.magiccobblestonegenerator.utils.Why; /** @@ -151,13 +153,67 @@ protected void playEffects(Block block) /** - * This method returns material of new block if generator manages to replace cobblestone to a new magic block. + * This method applies a block ID picked by the generator to the given block. Vanilla IDs are set + * immediately; custom (hook-provided) IDs first set a vanilla placeholder and are replaced by the + * hook placement one tick later, because hook APIs set blocks directly rather than through a state. + * + * @param blockId Block ID picked by the generator (vanilla material name or provider-prefixed custom ID). + * @param block Block that must be replaced. + * @param placeholder Vanilla material to leave in place while a custom placement is pending. + * @return {@code true} if a replacement was applied or scheduled. + */ + protected boolean applyBlockId(String blockId, Block block, Material placeholder) + { + Material material = CustomBlocks.matchVanilla(blockId); + + if (material != null) + { + if (!material.isBlock()) + { + return false; + } + + block.setType(material); + return true; + } + + if (CustomBlocks.isCustom(blockId)) + { + block.setType(placeholder); + this.scheduleCustomBlockPlacement(blockId, block.getLocation()); + return true; + } + + return false; + } + + + /** + * This method schedules placement of a custom block via its BentoBox hook on the next tick. + * + * @param blockId Provider-prefixed custom block ID. + * @param location Location where the block must be placed. + */ + protected void scheduleCustomBlockPlacement(String blockId, Location location) + { + Bukkit.getScheduler().runTask(this.addon.getPlugin(), () -> + { + if (!CustomBlocks.place(this.addon, blockId, location)) + { + Why.report(location, "Custom block " + blockId + " could not be placed."); + } + }); + } + + + /** + * This method returns block ID of new block if generator manages to replace cobblestone to a new magic block. * * @param island Island on which block is processed. * @param location Block location that need to be replaced. - * @return Material of replaced block or null, if block was not replaced. + * @return Block ID of replaced block or null, if block was not replaced. */ - protected @Nullable Material generateCobblestoneReplacement(@Nullable Island island, Location location) + protected @Nullable String generateCobblestoneReplacement(@Nullable Island island, Location location) { GeneratorTierObject generatorTier = this.addon.getAddonManager().getGeneratorTier( island, @@ -175,13 +231,13 @@ protected void playEffects(Block block) /** - * This method returns material of new block if generator manages to replace stone to a new magic block. + * This method returns block ID of new block if generator manages to replace stone to a new magic block. * * @param island Island on which block is processed. * @param location Block that need to be replaced. - * @return Material of replaced block or null, if block was not replaced. + * @return Block ID of replaced block or null, if block was not replaced. */ - protected @Nullable Material generateStoneReplacement(@Nullable Island island, Location location) + protected @Nullable String generateStoneReplacement(@Nullable Island island, Location location) { GeneratorTierObject generatorTier = this.addon.getAddonManager().getGeneratorTier( island, @@ -199,13 +255,13 @@ protected void playEffects(Block block) /** - * This method returns material of new block if generator manages to replace basalt to a new magic block. + * This method returns block ID of new block if generator manages to replace basalt to a new magic block. * * @param island Island on which block is processed. * @param location Block that need to be replaced. - * @return Material of replaced block or null, if block was not replaced. + * @return Block ID of replaced block or null, if block was not replaced. */ - protected @Nullable Material generateBasaltReplacement(@Nullable Island island, Location location) + protected @Nullable String generateBasaltReplacement(@Nullable Island island, Location location) { GeneratorTierObject generatorTier = this.addon.getAddonManager().getGeneratorTier( island, diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/listeners/MagicGeneratorListener.java b/src/main/java/world/bentobox/magiccobblestonegenerator/listeners/MagicGeneratorListener.java index 3da672e..264e7d3 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/listeners/MagicGeneratorListener.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/listeners/MagicGeneratorListener.java @@ -123,11 +123,10 @@ public void onBlockFromToEvent(BlockFromToEvent event) { // Return from here at any case. Even if could not manage to replace stone. - Material material = this.generateStoneReplacement(island, eventToBlock.getLocation()); + String blockId = this.generateStoneReplacement(island, eventToBlock.getLocation()); - if (material != null) + if (blockId != null && this.applyBlockId(blockId, eventToBlock, Material.STONE)) { - eventToBlock.setType(material); // sound when lava transforms to cobble this.playEffects(eventToBlock); event.setCancelled(true); @@ -146,12 +145,11 @@ public void onBlockFromToEvent(BlockFromToEvent event) if (liquid.equals(Material.LAVA) && this.canLavaGenerateCobblestone(eventToBlock, event.getFace())) { - Material material = this.generateCobblestoneReplacement(island, eventToBlock.getLocation()); + String blockId = this.generateCobblestoneReplacement(island, eventToBlock.getLocation()); // Lava is generating cobblestone into eventToBlock place - if (material != null) + if (blockId != null && this.applyBlockId(blockId, eventToBlock, Material.COBBLESTONE)) { - eventToBlock.setType(material); // sound when lava transforms to cobble this.playEffects(eventToBlock); event.setCancelled(true); @@ -168,11 +166,10 @@ else if (liquid.equals(Material.WATER)) if (replacedBlock != null) { - Material material = this.generateStoneReplacement(island, replacedBlock.getLocation()); + String blockId = this.generateStoneReplacement(island, replacedBlock.getLocation()); - if (material != null) + if (blockId != null && this.applyBlockId(blockId, replacedBlock, Material.STONE)) { - replacedBlock.setType(material); // sound when lava transforms to cobble this.playEffects(eventToBlock); } diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/listeners/VanillaGeneratorListener.java b/src/main/java/world/bentobox/magiccobblestonegenerator/listeners/VanillaGeneratorListener.java index 9874848..6893050 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/listeners/VanillaGeneratorListener.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/listeners/VanillaGeneratorListener.java @@ -17,6 +17,7 @@ import world.bentobox.bentobox.database.objects.Island; import world.bentobox.magiccobblestonegenerator.StoneGeneratorAddon; +import world.bentobox.magiccobblestonegenerator.utils.CustomBlocks; import world.bentobox.magiccobblestonegenerator.utils.Why; @@ -97,33 +98,47 @@ public void onBlockFormEvent(BlockFormEvent event) if (event.getNewState().getType() == Material.COBBLESTONE) { - Material material = this.generateCobblestoneReplacement(island, eventSourceBlock.getLocation()); - - if (material != null && material.isBlock()) - { - // Replace new state with a proper material. - event.getNewState().setType(material); - } + this.applyToState(event, this.generateCobblestoneReplacement(island, eventSourceBlock.getLocation())); } else if (event.getNewState().getType() == Material.STONE) { - Material material = this.generateStoneReplacement(island, eventSourceBlock.getLocation()); - - if (material != null && material.isBlock()) - { - // Replace new state with a proper material. - event.getNewState().setType(material); - } + this.applyToState(event, this.generateStoneReplacement(island, eventSourceBlock.getLocation())); } else if (event.getNewState().getType() == Material.BASALT) { - Material material = this.generateBasaltReplacement(island, eventSourceBlock.getLocation()); + this.applyToState(event, this.generateBasaltReplacement(island, eventSourceBlock.getLocation())); + } + } + - if (material != null && material.isBlock()) + /** + * This method applies a picked block ID to the forming block. Vanilla materials replace the new + * block state directly. Custom blocks let the vanilla block form as normal and are placed over it + * via their BentoBox hook one tick later, because hook APIs set blocks directly. + * + * @param event Block form event that is being processed. + * @param blockId Picked block ID or null if no replacement happens. + */ + private void applyToState(BlockFormEvent event, String blockId) + { + if (blockId == null) + { + return; + } + + Material material = CustomBlocks.matchVanilla(blockId); + + if (material != null) + { + if (material.isBlock()) { // Replace new state with a proper material. event.getNewState().setType(material); } } + else if (CustomBlocks.isCustom(blockId)) + { + this.scheduleCustomBlockPlacement(blockId, event.getBlock().getLocation()); + } } } diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/managers/StoneGeneratorImportManager.java b/src/main/java/world/bentobox/magiccobblestonegenerator/managers/StoneGeneratorImportManager.java index 222bf10..a8cd814 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/managers/StoneGeneratorImportManager.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/managers/StoneGeneratorImportManager.java @@ -48,6 +48,7 @@ import world.bentobox.magiccobblestonegenerator.database.objects.GeneratorBundleObject; import world.bentobox.magiccobblestonegenerator.database.objects.GeneratorTierObject; import world.bentobox.magiccobblestonegenerator.utils.Constants; +import world.bentobox.magiccobblestonegenerator.utils.CustomBlocks; import world.bentobox.magiccobblestonegenerator.utils.Utils; /** @@ -315,38 +316,36 @@ private void populateRequirements(GeneratorTierObject generatorTier, Configurati */ private void populateMaterials(GeneratorTierObject generatorTier, ConfigurationSection materials) { if (materials != null) { - TreeMap blockChances = new TreeMap<>(); - TreeMap materialHeightMap = new TreeMap<>(); + TreeMap blockChances = new TreeMap<>(); + TreeMap materialHeightMap = new TreeMap<>(); for (String materialKey : materials.getKeys(false)) { - try { - Material material = Material.valueOf(materialKey.toUpperCase()); - - // Support for both formats - if (materials.isConfigurationSection(materialKey)) { - // New format with chance and height_range - ConfigurationSection materialSection = materials.getConfigurationSection(materialKey); - double chance = materialSection.getDouble("chance", 0); - double lastEntry = blockChances.isEmpty() ? 0D : blockChances.lastKey(); - blockChances.put(lastEntry + chance, material); - - // Get height range if specified - ConfigurationSection heightRange = materialSection.getConfigurationSection("height_range"); - if (heightRange != null) { - int minHeight = heightRange.getInt("min", 0); - int maxHeight = heightRange.getInt("max", 256); - materialHeightMap.put(material, new int[]{minHeight, maxHeight}); - } - } else { - // Old format where the value is directly the probability - double chance = materials.getDouble(materialKey, 0); - double lastEntry = blockChances.isEmpty() ? 0D : blockChances.lastKey(); - blockChances.put(lastEntry + chance, material); + String blockId = this.parseBlockId(generatorTier, materialKey); + + if (blockId == null) { + continue; + } + + // Support for both formats + if (materials.isConfigurationSection(materialKey)) { + // New format with chance and height_range + ConfigurationSection materialSection = materials.getConfigurationSection(materialKey); + double chance = materialSection.getDouble("chance", 0); + double lastEntry = blockChances.isEmpty() ? 0D : blockChances.lastKey(); + blockChances.put(lastEntry + chance, blockId); + + // Get height range if specified + ConfigurationSection heightRange = materialSection.getConfigurationSection("height_range"); + if (heightRange != null) { + int minHeight = heightRange.getInt("min", 0); + int maxHeight = heightRange.getInt("max", 256); + materialHeightMap.put(blockId, new int[]{minHeight, maxHeight}); } - } catch (Exception e) { - this.addon.logWarning( - "Unknown material (" + materialKey + ") in generatorTemplate.yml blocks section for tier " - + generatorTier.getUniqueId() + ". Skipping..."); + } else { + // Old format where the value is directly the probability + double chance = materials.getDouble(materialKey, 0); + double lastEntry = blockChances.isEmpty() ? 0D : blockChances.lastKey(); + blockChances.put(lastEntry + chance, blockId); } } @@ -357,6 +356,41 @@ private void populateMaterials(GeneratorTierObject generatorTier, ConfigurationS } } + /** + * This method parses a template block key into a block ID: either a vanilla material name or a + * provider-prefixed custom block ID (e.g. itemsadder:namespace:id). Unknown vanilla materials are + * rejected; custom blocks that are not currently registered are imported with a warning, so + * templates keep working when the provider plugin is installed later. + * + * @param generatorTier tier that is being populated, for log context. + * @param materialKey raw key from the template file. + * @return normalized block ID or null if the key cannot be used. + */ + private String parseBlockId(GeneratorTierObject generatorTier, String materialKey) { + if (CustomBlocks.isCustom(materialKey)) { + // Custom block IDs are case-sensitive; keep them as written. + if (!CustomBlocks.isRegistered(this.addon, materialKey)) { + this.addon.logWarning("Custom block (" + materialKey + + ") in generatorTemplate.yml blocks section for tier " + generatorTier.getUniqueId() + + " is not registered on this server. It will not generate until its plugin and" + + " BentoBox hook are available."); + } + + return materialKey; + } + + Material material = Material.matchMaterial(materialKey); + + if (material == null) { + this.addon.logWarning( + "Unknown material (" + materialKey + ") in generatorTemplate.yml blocks section for tier " + + generatorTier.getUniqueId() + ". Skipping..."); + return null; + } + + return material.name(); + } + /** * This method populates generatorTier object with treasures from given config * section. diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/panels/CommonPanel.java b/src/main/java/world/bentobox/magiccobblestonegenerator/panels/CommonPanel.java index 0f77088..aa6f80d 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/panels/CommonPanel.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/panels/CommonPanel.java @@ -29,6 +29,7 @@ import world.bentobox.magiccobblestonegenerator.database.objects.GeneratorTierObject; import world.bentobox.magiccobblestonegenerator.managers.StoneGeneratorManager; import world.bentobox.magiccobblestonegenerator.utils.Constants; +import world.bentobox.magiccobblestonegenerator.utils.CustomBlocks; import world.bentobox.magiccobblestonegenerator.utils.Utils; @@ -387,7 +388,7 @@ protected String generateHeightRangeDescription(GeneratorTierObject generator) */ private String generateBlockListDescription(GeneratorTierObject generator) { - TreeMap blockChanceMap = generator.getBlockChanceMap(); + TreeMap blockChanceMap = generator.getBlockChanceMap(); if (blockChanceMap.isEmpty()) { @@ -408,12 +409,12 @@ private String generateBlockListDescription(GeneratorTierObject generator) Double maxValue = blockChanceMap.lastKey(); Double previousValue = 0.0; - List> materialChanceList = + List> materialChanceList = blockChanceMap.entrySet().stream(). sorted(Map.Entry.comparingByKey()). collect(Collectors.toList()); - for (Map.Entry entry : materialChanceList) + for (Map.Entry entry : materialChanceList) { Double value = (entry.getKey() - previousValue) / maxValue * 100.0; @@ -429,7 +430,7 @@ private String generateBlockListDescription(GeneratorTierObject generator) } blocks.append(this.user.getTranslation(reference + "value", - Constants.BLOCK, Utils.prettifyObject(this.user, entry.getValue()), + Constants.BLOCK, CustomBlocks.getDisplayName(this.addon, this.user, entry.getValue()), TextVariables.NUMBER, String.valueOf(value), Constants.TENS, this.tensFormat.format(value), Constants.HUNDREDS, this.hundredsFormat.format(value), diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/panels/admin/GeneratorEditPanel.java b/src/main/java/world/bentobox/magiccobblestonegenerator/panels/admin/GeneratorEditPanel.java index 7153881..d9a601b 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/panels/admin/GeneratorEditPanel.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/panels/admin/GeneratorEditPanel.java @@ -32,6 +32,7 @@ import world.bentobox.magiccobblestonegenerator.panels.utils.MultiGeneratorSelector; import world.bentobox.magiccobblestonegenerator.panels.utils.SingleBlockSelector; import world.bentobox.magiccobblestonegenerator.utils.Constants; +import world.bentobox.magiccobblestonegenerator.utils.CustomBlocks; import world.bentobox.magiccobblestonegenerator.utils.Pair; import world.bentobox.magiccobblestonegenerator.utils.Utils; @@ -103,6 +104,7 @@ public void build() this.populateBlocks(panelBuilder); } panelBuilder.item(39, this.createButton(Action.ADD_MATERIAL)); + panelBuilder.item(40, this.createButton(Action.ADD_CUSTOM_BLOCK)); panelBuilder.item(41, this.createButton(Action.REMOVE_MATERIAL)); } @@ -265,7 +267,7 @@ else if (this.pageIndex > (this.materialChanceList.size() / MAX_ELEMENTS)) if (!panelBuilder.slotOccupied(index)) { // Get entry from list. - Pair materialEntry = this.materialChanceList.get(materialIndex++); + Pair materialEntry = this.materialChanceList.get(materialIndex++); // Add to panel panelBuilder.item(index, this.createMaterialButton(materialEntry, maxValue)); } @@ -1182,7 +1184,7 @@ private PanelItem createButton(Action button) if (this.activeTab == Tab.BLOCKS) { this.materialChanceList.add( - new Pair<>(material, + new Pair<>(material.name(), number.doubleValue())); this.generatorTier.setBlockChanceMap( @@ -1219,6 +1221,69 @@ else if (this.activeTab == Tab.TREASURES) return true; }; } + case ADD_CUSTOM_BLOCK -> { + description.add(this.user.getTranslationOrNothing(reference + ".description")); + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-add")); + + icon = Material.COMMAND_BLOCK; + clickHandler = (panel, user1, clickType, slot) -> + { + Consumer blockIdConsumer = blockId -> + { + if (blockId == null || blockId.isBlank()) + { + this.build(); + return; + } + + String trimmedId = blockId.trim(); + + if (!CustomBlocks.isCustom(trimmedId)) + { + Utils.sendMessage(this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "custom-block-invalid-prefix", + Constants.BLOCK, trimmedId)); + this.build(); + return; + } + + if (!CustomBlocks.isRegistered(this.addon, trimmedId)) + { + Utils.sendMessage(this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "custom-block-not-found", + Constants.BLOCK, trimmedId)); + this.build(); + return; + } + + Consumer numberConsumer = number -> + { + if (number != null) + { + this.materialChanceList.add(new Pair<>(trimmedId, number.doubleValue())); + this.generatorTier.setBlockChanceMap(Utils.pairList2TreeMap(this.materialChanceList)); + this.save(); + } + + this.build(); + }; + + ConversationUtils.createNumericInput(numberConsumer, + this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), + 0.0, + Long.MAX_VALUE); + }; + + ConversationUtils.createStringInput(blockIdConsumer, + this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-custom-block"), + null); + + return true; + }; + } case REMOVE_MATERIAL -> { icon = this.selectedMaterial.isEmpty() && this.selectedTreasures.isEmpty() ? Material.BARRIER : Material.LAVA_BUCKET; @@ -1256,7 +1321,7 @@ else if (this.activeTab == Tab.TREASURES) if (!this.selectedMaterial.isEmpty()) { this.selectedMaterial.forEach(pair -> description.add(this.user.getTranslation(reference + ".list-value", - Constants.VALUE, Utils.prettifyObject(this.user, pair.getKey()), + Constants.VALUE, CustomBlocks.getDisplayName(this.addon, this.user, pair.getKey()), Constants.NUMBER, String.valueOf(pair.getValue())))); } @@ -1294,11 +1359,11 @@ else if (this.activeTab == Tab.TREASURES) * @param maxValue Displays maximal value for map. * @return PanelItem for generator tier. */ - private PanelItem createMaterialButton(Pair blockChanceEntry, Double maxValue) + private PanelItem createMaterialButton(Pair blockChanceEntry, Double maxValue) { // Normalize value Double value = blockChanceEntry.getValue() / maxValue * 100.0; - Material material = blockChanceEntry.getKey(); + String material = blockChanceEntry.getKey(); List description = new ArrayList<>(); description.add(this.user.getTranslation(Constants.BUTTON + "block-icon.description", @@ -1383,9 +1448,9 @@ else if (clickType.isLeftClick()) return new PanelItemBuilder(). name(this.user.getTranslation(Constants.BUTTON + "block-icon.name", - Constants.BLOCK, Utils.prettifyObject(this.user, blockChanceEntry.getKey()))). + Constants.BLOCK, CustomBlocks.getDisplayName(this.addon, this.user, blockChanceEntry.getKey()))). description(description). - icon(blockChanceEntry.getKey()). + icon(CustomBlocks.getIcon(this.addon, blockChanceEntry.getKey())). clickHandler(clickHandler). glow(glow). build(); @@ -1467,7 +1532,7 @@ private PanelItem createTreasureButton(Pair treasureChanceEnt if (newValue != null) { treasureChanceEntry.setValue(newValue.doubleValue()); - this.generatorTier.setTreasureChanceMap(Utils.pairList2TreeMap(this.materialChanceList)); + this.generatorTier.setTreasureItemChanceMap(Utils.pairList2TreeMap(this.treasureChanceList)); this.save(); } @@ -1693,11 +1758,11 @@ public void setup() /** - * Configure height range for a specific material - * - * @param material The material to configure height range for + * Configure height range for a specific block ID + * + * @param material The block ID to configure height range for */ - private void configureHeightRangeForMaterial(Material material) + private void configureHeightRangeForMaterial(String material) { // Get current height range if it exists int[] currentRange = this.generatorTier.getMaterialHeightRange(material); @@ -1707,8 +1772,8 @@ private void configureHeightRangeForMaterial(Material material) // Create a new panel to configure height range PanelBuilder panelBuilder = new PanelBuilder(). user(this.user). - name(this.user.getTranslation(Constants.TITLE + "height-range-config", - Constants.BLOCK, Utils.prettifyObject(this.user, material))). + name(this.user.getTranslation(Constants.TITLE + "height-range-config", + Constants.BLOCK, CustomBlocks.getDisplayName(this.addon, this.user, material))). size(27); PanelUtils.fillBorder(panelBuilder, Material.MAGENTA_STAINED_GLASS_PANE); // Add min height button @@ -1842,6 +1907,10 @@ private enum Action * Allows to add a new material to the block list. */ ADD_MATERIAL, + /** + * Allows to add a custom block (ItemsAdder/CraftEngine/Oraxen/Nexo) to the block list by ID. + */ + ADD_CUSTOM_BLOCK, /** * Allows to remove selected materials from block list */ @@ -1983,7 +2052,7 @@ private enum Button /** * This set is used to detect and delete selected blocks. */ - private final Set> selectedMaterial; + private final Set> selectedMaterial; /** * This set is used to detect and delete selected blocks. @@ -2013,7 +2082,7 @@ private enum Button /** * This list contains elements of tree map that we can edit with a panel. */ - private List> materialChanceList; + private List> materialChanceList; /** * This list contains elements of tree map that we can edit with a panel. diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/panels/player/GeneratorViewPanel.java b/src/main/java/world/bentobox/magiccobblestonegenerator/panels/player/GeneratorViewPanel.java index aa9d0d8..6739efe 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/panels/player/GeneratorViewPanel.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/panels/player/GeneratorViewPanel.java @@ -31,6 +31,7 @@ import world.bentobox.magiccobblestonegenerator.database.objects.GeneratorTierObject; import world.bentobox.magiccobblestonegenerator.panels.CommonPanel; import world.bentobox.magiccobblestonegenerator.utils.Constants; +import world.bentobox.magiccobblestonegenerator.utils.CustomBlocks; import world.bentobox.magiccobblestonegenerator.utils.Utils; @@ -530,7 +531,7 @@ private PanelItem createMaterialButton(ItemTemplateRecord template, TemplatedPan * @return PanelItem for generator tier. */ private PanelItem createMaterialButton(ItemTemplateRecord template, - Map.Entry blockChanceEntry, + Map.Entry blockChanceEntry, Double previousValue, Double maxValue) { @@ -542,13 +543,13 @@ private PanelItem createMaterialButton(ItemTemplateRecord template, } else { - builder.icon(blockChanceEntry.getValue()); + builder.icon(CustomBlocks.getIcon(this.addon, blockChanceEntry.getValue())); } if (template.title() != null) { builder.name(this.user.getTranslation(this.world, template.title(), - Constants.BLOCK, Utils.prettifyObject(this.user, blockChanceEntry.getValue()))); + Constants.BLOCK, CustomBlocks.getDisplayName(this.addon, this.user, blockChanceEntry.getValue()))); } if (template.description() != null) @@ -1363,7 +1364,7 @@ private enum Button /** * This variable stores chance for every block to be spawned. */ - private List> materialChanceList; + private List> materialChanceList; /** * This variable stores chance for every treasure to be spawned. diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/tasks/MagicGenerator.java b/src/main/java/world/bentobox/magiccobblestonegenerator/tasks/MagicGenerator.java index 0978bfd..7481640 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/tasks/MagicGenerator.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/tasks/MagicGenerator.java @@ -6,7 +6,6 @@ import org.bukkit.Bukkit; import org.bukkit.Location; -import org.bukkit.Material; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.Nullable; @@ -14,6 +13,7 @@ import world.bentobox.magiccobblestonegenerator.StoneGeneratorAddon; import world.bentobox.magiccobblestonegenerator.database.objects.GeneratorTierObject; import world.bentobox.magiccobblestonegenerator.events.GeneratorTreasureDropEvent; +import world.bentobox.magiccobblestonegenerator.utils.CustomBlocks; import world.bentobox.magiccobblestonegenerator.utils.Why; @@ -38,9 +38,9 @@ public MagicGenerator(StoneGeneratorAddon addon) * * @param generatorTier Object that contains all possible chances. * @param location Location of the block that need to be replaced. - * @return the replaced material or null. + * @return the picked block ID (vanilla material name or provider-prefixed custom block ID) or null. */ - public @Nullable Material processBlockReplacement(@Nullable GeneratorTierObject generatorTier, Location location) + public @Nullable String processBlockReplacement(@Nullable GeneratorTierObject generatorTier, Location location) { return this.processBlockReplacement(generatorTier, location, null); } @@ -53,9 +53,9 @@ public MagicGenerator(StoneGeneratorAddon addon) * @param location Location of the block that need to be replaced. * @param island Island on which the block is processed, or null if unknown. Used to provide context for the * {@link GeneratorTreasureDropEvent}. - * @return the replaced material or null. + * @return the picked block ID (vanilla material name or provider-prefixed custom block ID) or null. */ - public @Nullable Material processBlockReplacement(@Nullable GeneratorTierObject generatorTier, Location location, + public @Nullable String processBlockReplacement(@Nullable GeneratorTierObject generatorTier, Location location, @Nullable Island island) { if (generatorTier == null) @@ -74,7 +74,7 @@ public MagicGenerator(StoneGeneratorAddon addon) return null; } - TreeMap chanceMap = generatorTier.getBlockChanceMap(); + TreeMap chanceMap = generatorTier.getBlockChanceMap(); if (chanceMap.isEmpty()) { @@ -84,9 +84,9 @@ public MagicGenerator(StoneGeneratorAddon addon) return null; } - Material newMaterial = this.getMaterialFromMap(chanceMap); + String newBlockId = this.getMaterialFromMap(chanceMap); - if (newMaterial == null) + if (newBlockId == null) { Why.report(location, "Cannot parse material from ChanceMap in " + generatorTier.getUniqueId()); @@ -95,23 +95,23 @@ public MagicGenerator(StoneGeneratorAddon addon) } // Check if this material has specific height restrictions - int[] materialHeightRange = generatorTier.getMaterialHeightRange(newMaterial); + int[] materialHeightRange = generatorTier.getMaterialHeightRange(newBlockId); if (materialHeightRange != null) { int materialMinHeight = materialHeightRange[0]; int materialMaxHeight = materialHeightRange[1]; - + if (blockY < materialMinHeight || blockY > materialMaxHeight) { - Why.report(location, "Material " + newMaterial + " outside its specific height range: " + blockY + + Why.report(location, "Material " + newBlockId + " outside its specific height range: " + blockY + " (min: " + materialMinHeight + ", max: " + materialMaxHeight + ")"); - + // Try to find another material that can be generated at this height - Material alternativeMaterial = findMaterialForHeight(generatorTier, blockY); + String alternativeMaterial = findMaterialForHeight(generatorTier, blockY); if (alternativeMaterial != null) { Why.report(location, "Using alternative material " + alternativeMaterial + " for height " + blockY); - newMaterial = alternativeMaterial; + newBlockId = alternativeMaterial; } else { @@ -121,7 +121,22 @@ public MagicGenerator(StoneGeneratorAddon addon) } } - Why.report(location, "Replace with " + newMaterial + " by " + generatorTier.getUniqueId()); + if (CustomBlocks.isCustom(newBlockId)) + { + if (!CustomBlocks.canPlace(this.addon, newBlockId)) + { + Why.report(location, "Custom block " + newBlockId + " cannot be placed (plugin missing, ID not " + + "registered, or BentoBox hook lacks placement support) in " + generatorTier.getUniqueId()); + return null; + } + } + else if (CustomBlocks.matchVanilla(newBlockId) == null) + { + Why.report(location, "Unknown material " + newBlockId + " in " + generatorTier.getUniqueId()); + return null; + } + + Why.report(location, "Replace with " + newBlockId + " by " + generatorTier.getUniqueId()); if (generatorTier.getMaxTreasureAmount() > 0 && generatorTier.getTreasureChance() > 0 && @@ -175,37 +190,37 @@ public MagicGenerator(StoneGeneratorAddon addon) } } - return newMaterial; + return newBlockId; } /** - * Finds a material from the generator's block chance map that can be generated at the specified height. + * Finds a block ID from the generator's block chance map that can be generated at the specified height. * * @param generatorTier The generator tier object containing the material configurations * @param blockY The Y coordinate to check against - * @return A material that can be generated at the specified height, or null if none found + * @return A block ID that can be generated at the specified height, or null if none found */ - private Material findMaterialForHeight(GeneratorTierObject generatorTier, int blockY) + private String findMaterialForHeight(GeneratorTierObject generatorTier, int blockY) { - TreeMap chanceMap = generatorTier.getBlockChanceMap(); - - for (Material material : chanceMap.values()) + TreeMap chanceMap = generatorTier.getBlockChanceMap(); + + for (String blockId : chanceMap.values()) { - int[] heightRange = generatorTier.getMaterialHeightRange(material); - + int[] heightRange = generatorTier.getMaterialHeightRange(blockId); + // If this material has no specific height range, it can be generated anywhere within the generator's global range if (heightRange == null) { - return material; + return blockId; } - + // Check if the block Y is within this material's height range if (blockY >= heightRange[0] && blockY <= heightRange[1]) { - return material; + return blockId; } } - + return null; } 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")); + } +} From b46a41dfa87d83c060990d2d5954de3f098605c8 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 11 Jul 2026 11:29:00 -0700 Subject: [PATCH 2/4] Target BentoBox 3.17.0: 3.18.x artifacts are built for Java 25 CI failed with "class file has wrong version 69.0, should be 65.0" on every BentoBox class: the published 3.18.1 jar is compiled for Java 25, which the Java 21 toolchain used by this project (and its CI) cannot read. 3.17.0 is the newest Java 21-compatible release and contains all hook APIs used by the custom block support (CraftEngineHook.getItemStack landed in 3.16.0). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Q63R7wAXARZD2Rpg3oun6m --- pom.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3f8e4cd..7696998 100644 --- a/pom.xml +++ b/pom.xml @@ -35,7 +35,9 @@ 21 1.21.11-R0.1-SNAPSHOT - 3.18.1 + + 3.17.0 2.5.0 1.4.0 1.18.0 From 7f7077939d63fa996f7bc395eb7c1a504c5183a4 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 11 Jul 2026 11:44:32 -0700 Subject: [PATCH 3/4] Address SonarCloud issues on PR #165 - S1319: expose NavigableMap instead of TreeMap in GeneratorTierObject block-chance and material-height getters/setters (defensive copy on set) - S1192: extract duplicated template-context literal into a constant in StoneGeneratorImportManager - S3776/S6541: split MagicGenerator.processBlockReplacement into selectBlockId/isPlaceable/processTreasureDrop to cut cognitive complexity and Brain Method metrics - S3776: extract click-handler and min/max height buttons in GeneratorEditPanel to reduce cognitive complexity Updated call sites (MagicGenerator, CommonPanel) to the interface type. Full test suite: 165 tests, 0 failures. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Q63R7wAXARZD2Rpg3oun6m --- .../database/objects/GeneratorTierObject.java | 15 +- .../managers/StoneGeneratorImportManager.java | 8 +- .../panels/CommonPanel.java | 3 +- .../panels/admin/GeneratorEditPanel.java | 163 ++++++++----- .../tasks/MagicGenerator.java | 214 +++++++++++------- 5 files changed, 250 insertions(+), 153 deletions(-) diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/database/objects/GeneratorTierObject.java b/src/main/java/world/bentobox/magiccobblestonegenerator/database/objects/GeneratorTierObject.java index 16fc92a..bbdd111 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/database/objects/GeneratorTierObject.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/database/objects/GeneratorTierObject.java @@ -13,6 +13,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.NavigableMap; import java.util.Set; import java.util.TreeMap; @@ -345,12 +346,12 @@ public void setActivateOnUnlock(boolean activateOnUnlock) /** * Returns the blockChanceMap of this object. * - * @return a {@code TreeMap} where the keys are {@code Double} values representing chances, + * @return a {@code SortedMap} where the keys are {@code Double} values representing chances, * and the values are block IDs: vanilla {@code Material} names (e.g. {@code COBBLESTONE}) * or provider-prefixed custom block IDs (e.g. {@code itemsadder:namespace:id}) — see * {@link world.bentobox.magiccobblestonegenerator.utils.CustomBlocks}. */ - public TreeMap getBlockChanceMap() + public NavigableMap getBlockChanceMap() { return blockChanceMap; } @@ -361,9 +362,9 @@ public TreeMap getBlockChanceMap() * * @param blockChanceMap new value for this object. */ - public void setBlockChanceMap(TreeMap blockChanceMap) + public void setBlockChanceMap(NavigableMap blockChanceMap) { - this.blockChanceMap = blockChanceMap; + this.blockChanceMap = new TreeMap<>(blockChanceMap); } @@ -944,7 +945,7 @@ public void setMaxHeight(int maxHeight) * * @return the material height map */ - public TreeMap getMaterialHeightMap() + public NavigableMap getMaterialHeightMap() { return this.materialHeightMap; } @@ -955,9 +956,9 @@ public TreeMap getMaterialHeightMap() * * @param materialHeightMap the material height map */ - public void setMaterialHeightMap(TreeMap materialHeightMap) + public void setMaterialHeightMap(NavigableMap materialHeightMap) { - this.materialHeightMap = materialHeightMap; + this.materialHeightMap = new TreeMap<>(materialHeightMap); } diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/managers/StoneGeneratorImportManager.java b/src/main/java/world/bentobox/magiccobblestonegenerator/managers/StoneGeneratorImportManager.java index a8cd814..d23a20c 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/managers/StoneGeneratorImportManager.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/managers/StoneGeneratorImportManager.java @@ -371,7 +371,7 @@ private String parseBlockId(GeneratorTierObject generatorTier, String materialKe // Custom block IDs are case-sensitive; keep them as written. if (!CustomBlocks.isRegistered(this.addon, materialKey)) { this.addon.logWarning("Custom block (" + materialKey - + ") in generatorTemplate.yml blocks section for tier " + generatorTier.getUniqueId() + + TIER_TEMPLATE_CONTEXT + generatorTier.getUniqueId() + " is not registered on this server. It will not generate until its plugin and" + " BentoBox hook are available."); } @@ -383,7 +383,7 @@ private String parseBlockId(GeneratorTierObject generatorTier, String materialKe if (material == null) { this.addon.logWarning( - "Unknown material (" + materialKey + ") in generatorTemplate.yml blocks section for tier " + "Unknown material (" + materialKey + TIER_TEMPLATE_CONTEXT + generatorTier.getUniqueId() + ". Skipping..."); return null; } @@ -416,7 +416,7 @@ private void populateTreasures(GeneratorTierObject generatorTier, ConfigurationS blockChances.put(lastEntry + materials.getDouble(materialKey, 0), new ItemStack(material)); } catch (Exception e) { this.addon.logWarning("Unknown material (" + materialKey - + ") in generatorTemplate.yml blocks section for tier " + generatorTier.getUniqueId() + + TIER_TEMPLATE_CONTEXT + generatorTier.getUniqueId() + ". Skipping..."); } } @@ -856,4 +856,6 @@ public void setAuthor(String author) { private static final String DESCRIPTION = "description"; + private static final String TIER_TEMPLATE_CONTEXT = ") in generatorTemplate.yml blocks section for tier "; + } diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/panels/CommonPanel.java b/src/main/java/world/bentobox/magiccobblestonegenerator/panels/CommonPanel.java index aa6f80d..0077770 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/panels/CommonPanel.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/panels/CommonPanel.java @@ -7,6 +7,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.NavigableMap; import java.util.Objects; import java.util.TreeMap; import java.util.stream.Collectors; @@ -388,7 +389,7 @@ protected String generateHeightRangeDescription(GeneratorTierObject generator) */ private String generateBlockListDescription(GeneratorTierObject generator) { - TreeMap blockChanceMap = generator.getBlockChanceMap(); + NavigableMap blockChanceMap = generator.getBlockChanceMap(); if (blockChanceMap.isEmpty()) { diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/panels/admin/GeneratorEditPanel.java b/src/main/java/world/bentobox/magiccobblestonegenerator/panels/admin/GeneratorEditPanel.java index d9a601b..bd0e31a 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/panels/admin/GeneratorEditPanel.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/panels/admin/GeneratorEditPanel.java @@ -1408,7 +1408,29 @@ private PanelItem createMaterialButton(Pair blockChanceEntry, Do description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-deselect")); } - PanelItem.ClickHandler clickHandler = (panel, user1, clickType, slot) -> { + return new PanelItemBuilder(). + name(this.user.getTranslation(Constants.BUTTON + "block-icon.name", + Constants.BLOCK, CustomBlocks.getDisplayName(this.addon, this.user, blockChanceEntry.getKey()))). + description(description). + icon(CustomBlocks.getIcon(this.addon, blockChanceEntry.getKey())). + clickHandler(this.createMaterialButtonClickHandler(blockChanceEntry, material)). + glow(glow). + build(); + } + + + /** + * Creates the click handler for a material button: right-click selects/deselects, shift-left-click + * configures the height range and left-click edits the chance value. + * + * @param blockChanceEntry blockChanceEntry that the button represents. + * @param material the block ID that the button represents. + * @return the click handler for the button. + */ + private PanelItem.ClickHandler createMaterialButtonClickHandler(Pair blockChanceEntry, + String material) + { + return (panel, user1, clickType, slot) -> { if (clickType.isRightClick()) { if (!this.selectedMaterial.remove(blockChanceEntry)) @@ -1445,15 +1467,6 @@ else if (clickType.isLeftClick()) return true; }; - - return new PanelItemBuilder(). - name(this.user.getTranslation(Constants.BUTTON + "block-icon.name", - Constants.BLOCK, CustomBlocks.getDisplayName(this.addon, this.user, blockChanceEntry.getKey()))). - description(description). - icon(CustomBlocks.getIcon(this.addon, blockChanceEntry.getKey())). - clickHandler(clickHandler). - glow(glow). - build(); } @@ -1777,111 +1790,139 @@ private void configureHeightRangeForMaterial(String material) size(27); PanelUtils.fillBorder(panelBuilder, Material.MAGENTA_STAINED_GLASS_PANE); // Add min height button - panelBuilder.item(20, new PanelItemBuilder(). + panelBuilder.item(20, this.createMinHeightButton(material, currentMinHeight, currentMaxHeight)); + + // Add max height button + panelBuilder.item(24, this.createMaxHeightButton(material, currentMinHeight, currentMaxHeight)); + + // Add clear height range button + panelBuilder.item(22, new PanelItemBuilder(). + name(this.user.getTranslation(Constants.BUTTON + "clear-height-range.name")). + description(this.user.getTranslation(Constants.BUTTON + "clear-height-range.description")). + icon(Material.BARRIER). + clickHandler((panel, user, clickType, slot) -> { + // Remove the height range for this material + this.generatorTier.getMaterialHeightMap().remove(material); + this.save(); + + // Return to the main panel + this.configureHeightRangeForMaterial(material); + + return true; + }). + build()); + + // Add return button + panelBuilder.item(40, new PanelItemBuilder(). + name(this.user.getTranslation(Constants.BUTTON + "return.name")). + description(this.user.getTranslation(Constants.BUTTON + "return.description")). + icon(Material.OAK_DOOR). + clickHandler((panel, user, clickType, slot) -> { + this.build(); + return true; + }). + build()); + + panelBuilder.build(); + } + + + /** + * Creates the button that prompts for and stores the minimum height of a material's height range. + * + * @param material the block ID whose height range is being configured. + * @param currentMinHeight the currently configured minimum height, shown in the description. + * @param currentMaxHeight the currently configured maximum height, used to validate the new minimum. + * @return the configured min-height PanelItem. + */ + private PanelItem createMinHeightButton(String material, int currentMinHeight, int currentMaxHeight) + { + return new PanelItemBuilder(). name(this.user.getTranslation(Constants.BUTTON + "min-height.name")). - description(this.user.getTranslation(Constants.BUTTON + "min-height.description", + description(this.user.getTranslation(Constants.BUTTON + "min-height.description", Constants.MIN_HEIGHT, String.valueOf(currentMinHeight))). icon(Material.BEDROCK). clickHandler((panel, user, clickType, slot) -> { Consumer numberConsumer = number -> { if (number != null) { int newMinHeight = number.intValue(); - + // Ensure min height is not greater than max height if (newMinHeight > currentMaxHeight) { - this.user.sendMessage("admin.errors.min-height-greater-than-max", + this.user.sendMessage("admin.errors.min-height-greater-than-max", Constants.MIN, String.valueOf(newMinHeight), Constants.MAX, String.valueOf(currentMaxHeight)); return; - + } - + // Set the new height range this.generatorTier.setMaterialHeightRange(material, newMinHeight, currentMaxHeight); this.save(); } - + // Return to the main panel this.configureHeightRangeForMaterial(material); }; - + ConversationUtils.createNumericInput(numberConsumer, this.user, this.user.getTranslation(Constants.CONVERSATIONS + "input-min-height"), Integer.MIN_VALUE, 320); - + return true; }). - build()); - - // Add max height button - panelBuilder.item(24, new PanelItemBuilder(). + build(); + } + + + /** + * Creates the button that prompts for and stores the maximum height of a material's height range. + * + * @param material the block ID whose height range is being configured. + * @param currentMinHeight the currently configured minimum height, used to validate the new maximum. + * @param currentMaxHeight the currently configured maximum height, shown in the description. + * @return the configured max-height PanelItem. + */ + private PanelItem createMaxHeightButton(String material, int currentMinHeight, int currentMaxHeight) + { + return new PanelItemBuilder(). name(this.user.getTranslation(Constants.BUTTON + "max-height.name")). - description(this.user.getTranslation(Constants.BUTTON + "max-height.description", + description(this.user.getTranslation(Constants.BUTTON + "max-height.description", Constants.MAX_HEIGHT, String.valueOf(currentMaxHeight))). icon(Material.GRASS_BLOCK). clickHandler((panel, user, clickType, slot) -> { Consumer numberConsumer = number -> { if (number != null) { int newMaxHeight = number.intValue(); - + // Ensure max height is not less than min height if (newMaxHeight < currentMinHeight) { - this.user.sendMessage("admin.errors.max-height-less-than-min", + this.user.sendMessage("admin.errors.max-height-less-than-min", Constants.MIN, String.valueOf(currentMinHeight), Constants.MAX, String.valueOf(newMaxHeight)); return; } - + // Set the new height range this.generatorTier.setMaterialHeightRange(material, currentMinHeight, newMaxHeight); this.save(); } - + // Return to the main panel this.configureHeightRangeForMaterial(material); }; - + ConversationUtils.createNumericInput(numberConsumer, this.user, this.user.getTranslation(Constants.CONVERSATIONS + "input-max-height"), Integer.MIN_VALUE, 320); - - return true; - }). - build()); - - // Add clear height range button - panelBuilder.item(22, new PanelItemBuilder(). - name(this.user.getTranslation(Constants.BUTTON + "clear-height-range.name")). - description(this.user.getTranslation(Constants.BUTTON + "clear-height-range.description")). - icon(Material.BARRIER). - clickHandler((panel, user, clickType, slot) -> { - // Remove the height range for this material - this.generatorTier.getMaterialHeightMap().remove(material); - this.save(); - - // Return to the main panel - this.configureHeightRangeForMaterial(material); - - return true; - }). - build()); - - // Add return button - panelBuilder.item(40, new PanelItemBuilder(). - name(this.user.getTranslation(Constants.BUTTON + "return.name")). - description(this.user.getTranslation(Constants.BUTTON + "return.description")). - icon(Material.OAK_DOOR). - clickHandler((panel, user, clickType, slot) -> { - this.build(); + return true; }). - build()); - - panelBuilder.build(); + build(); } diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/tasks/MagicGenerator.java b/src/main/java/world/bentobox/magiccobblestonegenerator/tasks/MagicGenerator.java index 7481640..bc18e16 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/tasks/MagicGenerator.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/tasks/MagicGenerator.java @@ -1,8 +1,8 @@ package world.bentobox.magiccobblestonegenerator.tasks; +import java.util.NavigableMap; import java.util.Random; -import java.util.TreeMap; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -69,17 +69,48 @@ public MagicGenerator(StoneGeneratorAddon addon) int blockY = location.getBlockY(); if (blockY < generatorTier.getMinHeight() || blockY > generatorTier.getMaxHeight()) { - Why.report(location, "Block outside global height range: " + blockY + + Why.report(location, "Block outside global height range: " + blockY + " (min: " + generatorTier.getMinHeight() + ", max: " + generatorTier.getMaxHeight() + ")"); return null; } - TreeMap chanceMap = generatorTier.getBlockChanceMap(); + String newBlockId = this.selectBlockId(generatorTier, location, blockY); + + if (newBlockId == null) + { + // No suitable block could be selected; the block is left untouched. + return null; + } + + if (!this.isPlaceable(generatorTier, location, newBlockId)) + { + return null; + } + + Why.report(location, "Replace with " + newBlockId + " by " + generatorTier.getUniqueId()); + + this.processTreasureDrop(generatorTier, location, island); + + return newBlockId; + } + + + /** + * Picks a block ID from the generator's chance map and validates it against the material-specific + * height range, falling back to an alternative material when the picked one cannot spawn at this height. + * + * @param generatorTier Object that contains all possible chances. + * @param location Location of the block that need to be replaced (used for reporting). + * @param blockY The Y coordinate of the block being replaced. + * @return the block ID to place, or null if nothing suitable was found. + */ + private @Nullable String selectBlockId(GeneratorTierObject generatorTier, Location location, int blockY) + { + NavigableMap chanceMap = generatorTier.getBlockChanceMap(); if (chanceMap.isEmpty()) { Why.report(location, "Missing Block Chances in " + generatorTier.getUniqueId()); - // Check if any block has a chance to spawn return null; } @@ -89,110 +120,131 @@ public MagicGenerator(StoneGeneratorAddon addon) if (newBlockId == null) { Why.report(location, "Cannot parse material from ChanceMap in " + generatorTier.getUniqueId()); - // Check if a material was found return null; } // Check if this material has specific height restrictions int[] materialHeightRange = generatorTier.getMaterialHeightRange(newBlockId); - if (materialHeightRange != null) + + if (materialHeightRange == null || + (blockY >= materialHeightRange[0] && blockY <= materialHeightRange[1])) { - int materialMinHeight = materialHeightRange[0]; - int materialMaxHeight = materialHeightRange[1]; + return newBlockId; + } - if (blockY < materialMinHeight || blockY > materialMaxHeight) - { - Why.report(location, "Material " + newBlockId + " outside its specific height range: " + blockY + - " (min: " + materialMinHeight + ", max: " + materialMaxHeight + ")"); - - // Try to find another material that can be generated at this height - String alternativeMaterial = findMaterialForHeight(generatorTier, blockY); - if (alternativeMaterial != null) - { - Why.report(location, "Using alternative material " + alternativeMaterial + " for height " + blockY); - newBlockId = alternativeMaterial; - } - else - { - // If no suitable material found, don't replace the block - return null; - } - } + Why.report(location, "Material " + newBlockId + " outside its specific height range: " + blockY + + " (min: " + materialHeightRange[0] + ", max: " + materialHeightRange[1] + ")"); + + // Try to find another material that can be generated at this height + String alternativeMaterial = findMaterialForHeight(generatorTier, blockY); + + if (alternativeMaterial != null) + { + Why.report(location, "Using alternative material " + alternativeMaterial + " for height " + blockY); } + // If no suitable material was found, alternativeMaterial is null and the block is left untouched. + return alternativeMaterial; + } + + + /** + * Validates that the chosen block ID can actually be placed: custom blocks must be placeable via a + * hook, vanilla blocks must match a known material. + * + * @param generatorTier Object that owns the block (used for reporting). + * @param location Location of the block that need to be replaced (used for reporting). + * @param newBlockId The block ID that has been selected. + * @return true if the block can be placed, false otherwise. + */ + private boolean isPlaceable(GeneratorTierObject generatorTier, Location location, String newBlockId) + { if (CustomBlocks.isCustom(newBlockId)) { if (!CustomBlocks.canPlace(this.addon, newBlockId)) { Why.report(location, "Custom block " + newBlockId + " cannot be placed (plugin missing, ID not " + "registered, or BentoBox hook lacks placement support) in " + generatorTier.getUniqueId()); - return null; + return false; } } else if (CustomBlocks.matchVanilla(newBlockId) == null) { Why.report(location, "Unknown material " + newBlockId + " in " + generatorTier.getUniqueId()); - return null; + return false; } - Why.report(location, "Replace with " + newBlockId + " by " + generatorTier.getUniqueId()); + return true; + } - if (generatorTier.getMaxTreasureAmount() > 0 && - generatorTier.getTreasureChance() > 0 && - !generatorTier.getTreasureItemChanceMap().isEmpty()) + + /** + * Rolls for a treasure drop and, when successful, fires a cancellable {@link GeneratorTreasureDropEvent} + * before dropping the item naturally at the resulting location. + * + * @param generatorTier Object that contains the treasure chances. + * @param location Location of the block that has been replaced. + * @param island Island on which the block is processed, or null if unknown. + */ + private void processTreasureDrop(GeneratorTierObject generatorTier, Location location, @Nullable Island island) + { + if (generatorTier.getMaxTreasureAmount() <= 0 || + generatorTier.getTreasureChance() <= 0 || + generatorTier.getTreasureItemChanceMap().isEmpty()) { - // Random check on getting treasure. - if (this.random.nextDouble() <= generatorTier.getTreasureChance()) - { - // Use the same variables for treasures. - TreeMap treasureMap = generatorTier.getTreasureItemChanceMap(); - ItemStack itemStack = this.getMaterialFromMap(treasureMap); - - // Double check, in general it should always be a material. - if (itemStack != null) - { - ItemStack drop = itemStack.clone(); - drop.setAmount(this.random.nextInt(generatorTier.getMaxTreasureAmount() + 1) + 1); - - // Fire a cancellable event so other plugins can intercept, modify or veto the treasure drop. - GeneratorTreasureDropEvent treasureEvent = new GeneratorTreasureDropEvent(generatorTier, - island == null ? null : island.getUniqueId(), - location, - drop); - Bukkit.getPluginManager().callEvent(treasureEvent); - - if (!treasureEvent.isCancelled() && treasureEvent.getItemStack() != null) - { - ItemStack finalDrop = treasureEvent.getItemStack(); - Location dropLocation = treasureEvent.getLocation(); - - // A listener may have nulled the location (or its world); guard against it so we do - // not throw and break block generation. - if (dropLocation == null || dropLocation.getWorld() == null) - { - Why.report(location, "Treasure drop skipped: listener supplied an invalid drop location for " + - generatorTier.getUniqueId()); - } - else - { - Why.report(location, "Dropping treasure " + finalDrop + " by " + generatorTier.getUniqueId()); - - // drop item naturally in the location of the block - dropLocation.getWorld().dropItemNaturally(dropLocation, finalDrop); - } - } - else - { - Why.report(location, "Treasure drop cancelled by " + generatorTier.getUniqueId()); - } - } - } + return; } - return newBlockId; + // Random check on getting treasure. + if (this.random.nextDouble() > generatorTier.getTreasureChance()) + { + return; + } + + ItemStack itemStack = this.getMaterialFromMap(generatorTier.getTreasureItemChanceMap()); + + // Double check, in general it should always be a material. + if (itemStack == null) + { + return; + } + + ItemStack drop = itemStack.clone(); + drop.setAmount(this.random.nextInt(generatorTier.getMaxTreasureAmount() + 1) + 1); + + // Fire a cancellable event so other plugins can intercept, modify or veto the treasure drop. + GeneratorTreasureDropEvent treasureEvent = new GeneratorTreasureDropEvent(generatorTier, + island == null ? null : island.getUniqueId(), + location, + drop); + Bukkit.getPluginManager().callEvent(treasureEvent); + + if (treasureEvent.isCancelled() || treasureEvent.getItemStack() == null) + { + Why.report(location, "Treasure drop cancelled by " + generatorTier.getUniqueId()); + return; + } + + ItemStack finalDrop = treasureEvent.getItemStack(); + Location dropLocation = treasureEvent.getLocation(); + + // A listener may have nulled the location (or its world); guard against it so we do + // not throw and break block generation. + if (dropLocation == null || dropLocation.getWorld() == null) + { + Why.report(location, "Treasure drop skipped: listener supplied an invalid drop location for " + + generatorTier.getUniqueId()); + return; + } + + Why.report(location, "Dropping treasure " + finalDrop + " by " + generatorTier.getUniqueId()); + + // drop item naturally in the location of the block + dropLocation.getWorld().dropItemNaturally(dropLocation, finalDrop); } + /** * Finds a block ID from the generator's block chance map that can be generated at the specified height. * @@ -202,7 +254,7 @@ else if (CustomBlocks.matchVanilla(newBlockId) == null) */ private String findMaterialForHeight(GeneratorTierObject generatorTier, int blockY) { - TreeMap chanceMap = generatorTier.getBlockChanceMap(); + NavigableMap chanceMap = generatorTier.getBlockChanceMap(); for (String blockId : chanceMap.values()) { @@ -231,7 +283,7 @@ private String findMaterialForHeight(GeneratorTierObject generatorTier, int bloc * @param chanceMap Map that contains all objects with their chance to drop. * @return from map or null. */ - private T getMaterialFromMap(TreeMap chanceMap) + private T getMaterialFromMap(NavigableMap chanceMap) { if (chanceMap.isEmpty()) { From 71c9eff03cb7843cfe3116be6fe4f5f487613cbd Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 11 Jul 2026 11:47:52 -0700 Subject: [PATCH 4/4] Update build.version from 2.9.0 to 2.10.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7696998..52a9212 100644 --- a/pom.xml +++ b/pom.xml @@ -52,7 +52,7 @@ ${build.version}-SNAPSHOT - 2.9.0 + 2.10.0 -LOCAL BentoBoxWorld_MagicCobblestoneGenerator