diff --git a/pom.xml b/pom.xml index 8d7473b..c18e98c 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ v1.21-SNAPSHOT 1.21.11-R0.1-SNAPSHOT - 3.15.0-SNAPSHOT + 3.15.1-SNAPSHOT 1.12.0 @@ -69,7 +69,7 @@ -LOCAL - 2.26.0 + 2.27.0 BentoBoxWorld_Level bentobox-world https://sonarcloud.io diff --git a/src/main/java/world/bentobox/level/Level.java b/src/main/java/world/bentobox/level/Level.java index da58a37..82f6702 100644 --- a/src/main/java/world/bentobox/level/Level.java +++ b/src/main/java/world/bentobox/level/Level.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; import org.bukkit.Bukkit; @@ -11,16 +12,22 @@ import org.bukkit.World; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; +import com.nexomc.nexo.api.NexoItems; + import world.bentobox.bentobox.api.addons.Addon; import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.configuration.Config; import world.bentobox.bentobox.api.flags.Flag; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.hooks.CraftEngineHook; +import world.bentobox.bentobox.hooks.ItemsAdderHook; +import world.bentobox.bentobox.hooks.OraxenHook; import world.bentobox.bentobox.managers.RanksManager; import world.bentobox.bentobox.util.Util; import world.bentobox.level.calculators.Pipeliner; @@ -268,7 +275,11 @@ private void registerCommands(GameModeAddon gm) { new IslandLevelCommand(this, playerCmd); new IslandTopCommand(this, playerCmd); new IslandValueCommand(this, playerCmd); - new IslandDetailCommand(this, playerCmd); + // In donations-only mode, there are no scanned blocks to break down, + // so the detail command is not registered. + if (!getSettings().isDonationsOnly()) { + new IslandDetailCommand(this, playerCmd); + } new IslandDonateCommand(this, playerCmd); }); } @@ -508,6 +519,52 @@ public boolean isItemsAdder() { return !getSettings().isDisableItemsAdder() && getPlugin().getHooks().getHook("ItemsAdder").isPresent(); } + /** + * Returns the custom-block plugin ID for an ItemStack, checking Oraxen, Nexo, + * ItemsAdder, and CraftEngine in that order. Returns {@code null} when the item + * is not recognized as a custom block by any supported plugin. + * + * @param item the ItemStack to check (may be null) + * @return a namespaced custom-block ID such as {@code "oraxen:my_block"}, + * {@code "nexo:my_block"}, an ItemsAdder ID, or a CraftEngine ID + * (e.g. {@code "default:my_block"}), or {@code null} + */ + @Nullable + public String getCustomBlockId(ItemStack item) { + if (item == null || item.getType().isAir()) { + return null; + } + // Check Oraxen + if (getPlugin().getHooks().getHook("Oraxen").isPresent()) { + String id = OraxenHook.getIdByItem(item); + if (id != null) { + return "oraxen:" + id; + } + } + // Check Nexo + if (isNexo()) { + String id = NexoItems.idFromItem(item); + if (id != null) { + return "nexo:" + id; + } + } + // Check ItemsAdder + if (isItemsAdder()) { + Optional id = ItemsAdderHook.getNamespacedId(item); + if (id.isPresent()) { + return id.get(); + } + } + // Check CraftEngine — IDs are already namespaced (e.g. "mynamespace:my_block") + if (isCraftEngine()) { + String id = CraftEngineHook.getItemId(item); + if (id != null) { + return id; + } + } + return null; + } + /** * @return true if the Nexo plugin is enabled and not disabled in config */ diff --git a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java index 146540d..dc1ddcc 100644 --- a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java +++ b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java @@ -133,9 +133,14 @@ public IslandLevelCalculator(Level addon, Island island, CompletableFuture getReport() { if (addon.getSettings().isZeroNewIslandLevels()) { reportLines.add("Initial island level = " + (0L - addon.getManager().getInitialLevel(island))); }*/ - if (addon.getSettings().isZeroNewIslandLevels()) { + if (addon.getSettings().isZeroNewIslandLevels() && !addon.getSettings().isDonationsOnly()) { reportLines.add("Initial island count = " + (0L - addon.getManager().getInitialCount(island))); } reportLines.add("Previous level = " + addon.getManager().getIslandLevel(island.getWorld(), island.getOwner())); @@ -750,13 +755,36 @@ public void tidyUp() { } this.results.level.set(calculateLevel(blockAndDeathPoints)); - // Calculate how many points are required to get to the next level - long nextLevel = this.results.level.get(); - long blocks = blockAndDeathPoints; - while (nextLevel < this.results.level.get() + 1 && blocks - blockAndDeathPoints < MAX_AMOUNT) { - nextLevel = calculateLevel(++blocks); + // Binary search for points to next level (first point count that exceeds currentLevel) + long currentLevel = this.results.level.get(); + long lo = blockAndDeathPoints + 1; + long hi = blockAndDeathPoints + MAX_AMOUNT; + while (lo < hi) { + long mid = lo + (hi - lo) / 2; + if (calculateLevel(mid) > currentLevel) { + hi = mid; + } else { + lo = mid + 1; + } + } + this.results.pointsToNextLevel.set(lo - blockAndDeathPoints); + + // Binary search for points accumulated within the current level. + // Floor at initialCount when zeroing new islands to avoid negative/NaN in non-linear formulas. + // In donations-only mode, the initial count is ignored (see calculateLevel). + long minBlocks = addon.getSettings().isZeroNewIslandLevels() && !addon.getSettings().isDonationsOnly() + ? results.initialCount.get() : 0; + lo = Math.max(minBlocks, blockAndDeathPoints - MAX_AMOUNT); + hi = blockAndDeathPoints; + while (lo < hi) { + long mid = lo + (hi - lo) / 2; + if (calculateLevel(mid) >= currentLevel) { + hi = mid; + } else { + lo = mid + 1; + } } - this.results.pointsToNextLevel.set(blocks - blockAndDeathPoints); + this.results.pointsFromCurrentLevel.set(blockAndDeathPoints - lo); // Report results.report = getReport(); @@ -773,6 +801,19 @@ boolean isNotZeroIsland() { } public void scanIsland(Pipeliner pipeliner) { + // In donations-only mode, skip the chunk scan for regular level calcs: + // tidyUp() will add the donated points and compute the level from those + // alone. Zero-island scans (run on island create/reset when + // zero-new-island-levels is true) still run the full scan so the + // initial-count handicap is recorded — this lets an admin later + // disable donations-only without losing the handicap that would + // otherwise need to be subtracted from the scanned block total. + if (addon.getSettings().isDonationsOnly() && !zeroIsland) { + pipeliner.getInProcessQueue().remove(this); + this.tidyUp(); + this.getR().complete(getResults()); + return; + } // Scan the next chunk scanNextChunk().thenAccept(result -> { if (!Bukkit.isPrimaryThread()) { diff --git a/src/main/java/world/bentobox/level/calculators/Results.java b/src/main/java/world/bentobox/level/calculators/Results.java index 01c0c0a..5deaae6 100644 --- a/src/main/java/world/bentobox/level/calculators/Results.java +++ b/src/main/java/world/bentobox/level/calculators/Results.java @@ -48,6 +48,14 @@ public enum Result { AtomicLong level = new AtomicLong(0); AtomicInteger deathHandicap = new AtomicInteger(0); AtomicLong pointsToNextLevel = new AtomicLong(0); + /** + * Points already accumulated within the current level (i.e. how far past the + * start-of-current-level threshold the island has progressed). Combined with + * {@link #pointsToNextLevel} this gives the actual size of the current level + * interval — which is what should be shown to players when the configured + * level formula is non-linear. + */ + AtomicLong pointsFromCurrentLevel = new AtomicLong(0); //AtomicLong initialLevel = new AtomicLong(0); AtomicLong initialCount = new AtomicLong(0); /** @@ -115,6 +123,21 @@ public void setPointsToNextLevel(long points) { pointsToNextLevel.set(points); } + /** + * @return the points already accumulated within the current level + */ + public long getPointsFromCurrentLevel() { + return pointsFromCurrentLevel.get(); + } + + /** + * Set the points already accumulated within the current level + * @param points + */ + public void setPointsFromCurrentLevel(long points) { + pointsFromCurrentLevel.set(points); + } + /** * @return the totalPoints */ diff --git a/src/main/java/world/bentobox/level/commands/IslandDonateCommand.java b/src/main/java/world/bentobox/level/commands/IslandDonateCommand.java index fa76d70..80af368 100644 --- a/src/main/java/world/bentobox/level/commands/IslandDonateCommand.java +++ b/src/main/java/world/bentobox/level/commands/IslandDonateCommand.java @@ -1,10 +1,13 @@ package world.bentobox.level.commands; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.commands.ConfirmableCommand; @@ -17,13 +20,17 @@ import world.bentobox.level.util.Utils; /** - * Command: /island donate [hand [amount]] - * Opens a donation GUI or donates blocks from hand. + * Command: /island donate [hand [amount]] [inv] + * Opens a donation GUI, or donates blocks from the player's hand, or + * donates every donatable block from the player's inventory. * * @author tastybento */ public class IslandDonateCommand extends ConfirmableCommand { + private static final String MATERIAL_PLACEHOLDER = "[material]"; + private static final String POINTS_PLACEHOLDER = "[points]"; + private final Level addon; public IslandDonateCommand(Level addon, CompositeCommand parent) { @@ -65,6 +72,11 @@ public boolean execute(User user, String label, List args) { return handleHandDonation(user, island, args); } + // Handle "inv" subcommand (accepts English "inv" or the localized keyword) + if (!args.isEmpty() && isInvKeyword(user, args.get(0))) { + return handleInvDonation(user, island); + } + // No args - open GUI DonationPanel.openPanel(addon, getWorld(), user, island); return true; @@ -75,13 +87,20 @@ public boolean execute(User user, String label, List args) { */ private boolean handleHandDonation(User user, Island island, List args) { ItemStack hand = user.getPlayer().getInventory().getItemInMainHand(); - if (hand.getType().isAir() || !hand.getType().isBlock()) { + + // Check for a custom block (Oraxen, Nexo, ItemsAdder) first + final String customId = addon.getCustomBlockId(hand); + + // If not a custom block, require a vanilla block + if (customId == null && (hand.getType().isAir() || !hand.getType().isBlock())) { user.sendMessage("island.donate.hand.not-block"); return false; } final Material material = hand.getType(); - final Integer blockValue = addon.getBlockConfig().getValue(getWorld(), material); + final Integer blockValue = customId != null + ? addon.getBlockConfig().getValue(getWorld(), customId) + : addon.getBlockConfig().getValue(getWorld(), material); if (blockValue == null || blockValue <= 0) { user.sendMessage("island.donate.no-value"); return false; @@ -109,18 +128,25 @@ private boolean handleHandDonation(User user, Island island, List args) final long previewPoints = (long) previewAmount * blockValue; final int finalRequested = requested; + Object displayKey = customId != null ? customId : material; String prompt = user.getTranslation("island.donate.hand.confirm-prompt", TextVariables.NUMBER, String.valueOf(previewAmount), - "[material]", Utils.prettifyObject(material, user), - "[points]", Utils.formatNumber(user, previewPoints)); + MATERIAL_PLACEHOLDER, Utils.prettifyObject(displayKey, user), + POINTS_PLACEHOLDER, Utils.formatNumber(user, previewPoints)); - askConfirmation(user, prompt, () -> performHandDonation(user, island, material, blockValue, finalRequested)); + askConfirmation(user, prompt, () -> performHandDonation(user, island, material, customId, blockValue, finalRequested)); return true; } - private void performHandDonation(User user, Island island, Material material, int blockValue, int requested) { + private void performHandDonation(User user, Island island, Material material, String customId, int blockValue, int requested) { ItemStack currentHand = user.getPlayer().getInventory().getItemInMainHand(); - if (currentHand.getType() != material || currentHand.getAmount() == 0) { + // Verify the item in hand is still the same + if (customId != null) { + if (!customId.equals(addon.getCustomBlockId(currentHand)) || currentHand.getAmount() == 0) { + user.sendMessage("island.donate.hand.not-block"); + return; + } + } else if (currentHand.getType() != material || currentHand.getAmount() == 0) { user.sendMessage("island.donate.hand.not-block"); return; } @@ -133,23 +159,139 @@ private void performHandDonation(User user, Island island, Material material, in currentHand.setAmount(currentHand.getAmount() - amount); } - addon.getManager().donateBlocks(island, user.getUniqueId(), material.name(), amount, points); + String donationId = customId != null ? customId : material.name(); + addon.getManager().donateBlocks(island, user.getUniqueId(), donationId, amount, points); addon.getManager().recalculateAfterDonation(island); + Object displayKey = customId != null ? customId : material; user.sendMessage("island.donate.hand.success", TextVariables.NUMBER, String.valueOf(amount), - "[material]", Utils.prettifyObject(material, user), - "[points]", Utils.formatNumber(user, points)); + MATERIAL_PLACEHOLDER, Utils.prettifyObject(displayKey, user), + POINTS_PLACEHOLDER, Utils.formatNumber(user, points)); + } + + /** + * Handle the /island donate inv subcommand. Scans the player's inventory for + * blocks with a positive donation value, shows a per-material breakdown plus + * the total, and asks for confirmation. Items with no value or that aren't + * donatable blocks remain in the inventory. + */ + private boolean handleInvDonation(User user, Island island) { + Map totals = collectDonatableTotals(user.getPlayer().getInventory()); + + if (totals.isEmpty()) { + user.sendMessage("island.donate.empty"); + return false; + } + + long totalPoints = 0L; + StringBuilder prompt = new StringBuilder( + user.getTranslation("island.donate.inv.confirm-header")); + for (Map.Entry e : totals.entrySet()) { + // Vanilla keys are stored as Material.name() (e.g. "STONE"), but blockValues uses + // the lowercase namespaced key (e.g. "stone"). Resolving to a Material first lets + // getValue() derive the correct lowercase key via material.getKey().getKey(). + // Custom-block keys (e.g. "oraxen:my_block") do not match any Material, so they + // are passed through as Strings and match blockValues directly. + Material mat = Material.matchMaterial(e.getKey()); + Object displayKey = mat != null ? mat : e.getKey(); + Integer rawValue = addon.getBlockConfig().getValue(getWorld(), displayKey); + if (rawValue == null) continue; + long points = (long) rawValue * e.getValue(); + totalPoints += points; + prompt.append('\n').append(user.getTranslation("island.donate.inv.confirm-line", + TextVariables.NUMBER, String.valueOf(e.getValue()), + MATERIAL_PLACEHOLDER, Utils.prettifyObject(displayKey, user), + POINTS_PLACEHOLDER, Utils.formatNumber(user, points))); + } + prompt.append('\n').append(user.getTranslation("island.donate.inv.confirm-total", + POINTS_PLACEHOLDER, Utils.formatNumber(user, totalPoints))); + + askConfirmation(user, prompt.toString(), () -> performInvDonation(user, island)); + return true; + } + + private void performInvDonation(User user, Island island) { + PlayerInventory pInv = user.getPlayer().getInventory(); + ItemStack[] contents = pInv.getStorageContents(); + Map donated = new HashMap<>(); + long totalPoints = 0L; + + for (int i = 0; i < contents.length; i++) { + ItemStack item = contents[i]; + Integer value = donationValue(item); + if (value == null) { + continue; + } + int amount = item.getAmount(); + long points = (long) value * amount; + String customId = addon.getCustomBlockId(item); + String donationId = customId != null ? customId : item.getType().name(); + donated.merge(donationId, amount, Integer::sum); + totalPoints += points; + addon.getManager().donateBlocks(island, user.getUniqueId(), donationId, amount, points); + contents[i] = null; + } + pInv.setStorageContents(contents); + + if (donated.isEmpty()) { + user.sendMessage("island.donate.empty"); + return; + } + int totalBlocks = donated.values().stream().mapToInt(Integer::intValue).sum(); + user.sendMessage("island.donate.success", + POINTS_PLACEHOLDER, Utils.formatNumber(user, totalPoints), + TextVariables.NUMBER, String.valueOf(totalBlocks)); + addon.getManager().recalculateAfterDonation(island); + } + + private Map collectDonatableTotals(PlayerInventory pInv) { + Map totals = new HashMap<>(); + for (ItemStack item : pInv.getStorageContents()) { + if (donationValue(item) != null) { + String customId = addon.getCustomBlockId(item); + String key = customId != null ? customId : item.getType().name(); + totals.merge(key, item.getAmount(), Integer::sum); + } + } + return totals; + } + + /** + * @return the per-block donation value if the item is a donatable block with a + * positive configured value, or null otherwise + */ + private Integer donationValue(ItemStack item) { + if (item == null || item.getType().isAir()) { + return null; + } + // Check custom block plugins first (Oraxen, Nexo, ItemsAdder) + String customId = addon.getCustomBlockId(item); + if (customId != null) { + Integer value = addon.getBlockConfig().getValue(getWorld(), customId); + return (value != null && value > 0) ? value : null; + } + // Fall back to vanilla block check + if (!item.getType().isBlock()) { + return null; + } + Integer value = addon.getBlockConfig().getValue(getWorld(), item.getType()); + return (value != null && value > 0) ? value : null; } @Override public Optional> tabComplete(User user, String alias, List args) { + // BentoBox includes the command label as args.get(0); the user-typed args start at index 1. String lastArg = !args.isEmpty() ? args.get(args.size() - 1) : ""; String handKeyword = user.getTranslation("island.donate.hand.keyword"); - if (args.size() <= 1) { - return Optional.of(Util.tabLimit(List.of(handKeyword), lastArg)); + String invKeyword = user.getTranslation("island.donate.inv.keyword"); + + // First user-arg slot: suggest "hand" and "inv". + if (args.size() <= 2) { + return Optional.of(Util.tabLimit(List.of(handKeyword, invKeyword), lastArg)); } - if (args.size() == 2 && isHandKeyword(user, args.get(0)) && user.isPlayer()) { + // Second user-arg slot after "hand": suggest the held count. + if (args.size() == 3 && isHandKeyword(user, args.get(1)) && user.isPlayer()) { int held = user.getPlayer().getInventory().getItemInMainHand().getAmount(); if (held > 0) { return Optional.of(Util.tabLimit(List.of(String.valueOf(held)), lastArg)); @@ -162,4 +304,9 @@ private boolean isHandKeyword(User user, String arg) { String localized = user.getTranslation("island.donate.hand.keyword"); return "hand".equalsIgnoreCase(arg) || localized.equalsIgnoreCase(arg); } + + private boolean isInvKeyword(User user, String arg) { + String localized = user.getTranslation("island.donate.inv.keyword"); + return "inv".equalsIgnoreCase(arg) || localized.equalsIgnoreCase(arg); + } } diff --git a/src/main/java/world/bentobox/level/commands/IslandLevelCommand.java b/src/main/java/world/bentobox/level/commands/IslandLevelCommand.java index 240df2e..1d4e0af 100644 --- a/src/main/java/world/bentobox/level/commands/IslandLevelCommand.java +++ b/src/main/java/world/bentobox/level/commands/IslandLevelCommand.java @@ -112,10 +112,14 @@ private void showResult(User user, UUID playerUUID, Island island, long oldLevel } // Send player how many points are required to reach next island level if (results.getPointsToNextLevel() >= 0) { + long interval = results.getPointsFromCurrentLevel() + results.getPointsToNextLevel(); + if (interval <= 0) { + interval = this.addon.getSettings().getLevelCost(); + } user.sendMessage("island.level.required-points-to-next-level", "[points]", Utils.formatNumber(user, results.getPointsToNextLevel()), - "[progress]", Utils.formatNumber(user, this.addon.getSettings().getLevelCost() - results.getPointsToNextLevel()), - "[levelcost]", Utils.formatNumber(user, this.addon.getSettings().getLevelCost()) + "[progress]", Utils.formatNumber(user, results.getPointsFromCurrentLevel()), + "[levelcost]", Utils.formatNumber(user, interval) ); } // Tell other team members diff --git a/src/main/java/world/bentobox/level/commands/IslandValueCommand.java b/src/main/java/world/bentobox/level/commands/IslandValueCommand.java index 92141ff..098d0f2 100644 --- a/src/main/java/world/bentobox/level/commands/IslandValueCommand.java +++ b/src/main/java/world/bentobox/level/commands/IslandValueCommand.java @@ -11,12 +11,9 @@ import org.bukkit.inventory.PlayerInventory; import org.eclipse.jdt.annotation.NonNull; -import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; -import world.bentobox.bentobox.hooks.ItemsAdderHook; -import world.bentobox.bentobox.hooks.OraxenHook; import world.bentobox.bentobox.util.Util; import world.bentobox.level.Level; import world.bentobox.level.objects.IslandLevels; @@ -80,21 +77,11 @@ private void executeHandCommand(User user) { return; } - // Oraxen - if (BentoBox.getInstance().getHooks().getHook("Oraxen").isPresent()) { - String id = OraxenHook.getIdByItem(mainHandItem); - if (id != null) { - printValue(user, "oraxen:" + id); - return; - } - } - // ItemsAdder - if (addon.isItemsAdder()) { - Optional id = ItemsAdderHook.getNamespacedId(mainHandItem); - if (id.isPresent()) { - printValue(user, id.get()); - return; - } + // Check custom block plugins first (Oraxen, Nexo, ItemsAdder) + String customId = addon.getCustomBlockId(mainHandItem); + if (customId != null) { + printValue(user, customId); + return; } printValue(user, mainHandItem.getType()); @@ -120,14 +107,18 @@ private void printValue(User user, Object material) if (value != null) { + String displayName = (material instanceof String id) + ? Utils.getCustomBlockDisplayName(Utils.getCustomBlockItemStack(addon, id), id, user) + : Utils.prettifyObject(material, user); + Utils.sendMessage(user, user.getTranslation(this.getWorld(), "level.conversations.value", "[value]", - String.valueOf(value), MATERIAL, Utils.prettifyObject(material, user))); + String.valueOf(value), MATERIAL, displayName)); double underWater = this.addon.getSettings().getUnderWaterMultiplier(); if (underWater > 1.0) { Utils.sendMessage(user, user.getTranslation(this.getWorld(), "level.conversations.success-underwater", - "[value]", (underWater * value) + ""), MATERIAL, Utils.prettifyObject(material, user)); + "[value]", (underWater * value) + ""), MATERIAL, displayName); } // Show how many have been placed and how many are allowed diff --git a/src/main/java/world/bentobox/level/config/ConfigSettings.java b/src/main/java/world/bentobox/level/config/ConfigSettings.java index f7879a3..ef47003 100644 --- a/src/main/java/world/bentobox/level/config/ConfigSettings.java +++ b/src/main/java/world/bentobox/level/config/ConfigSettings.java @@ -54,6 +54,17 @@ public class ConfigSettings implements ConfigObject { @ConfigEntry(path = "zero-new-island-levels") private boolean zeroNewIslandLevels = true; + @ConfigComment("") + @ConfigComment("Donations-only mode") + @ConfigComment("If true, the island block scan is skipped entirely and the island level") + @ConfigComment("is computed only from blocks donated via /island donate. This removes the") + @ConfigComment("per-recalculation CPU cost of scanning the island.") + @ConfigComment("The /island detail command is not registered in this mode, since there") + @ConfigComment("are no scanned blocks to break down. /island level still works and reports") + @ConfigComment("the level based on donated blocks.") + @ConfigEntry(path = "donations-only") + private boolean donationsOnly = false; + @ConfigComment("") @ConfigComment("Calculate island level on login") @@ -509,4 +520,19 @@ public boolean isDisableItemsAdder() { public void setDisableItemsAdder(boolean disableItemsAdder) { this.disableItemsAdder = disableItemsAdder; } + + /** + * @return true if donations-only mode is enabled (block scan skipped, level + * computed from donations only, /island detail disabled) + */ + public boolean isDonationsOnly() { + return donationsOnly; + } + + /** + * @param donationsOnly the donationsOnly to set + */ + public void setDonationsOnly(boolean donationsOnly) { + this.donationsOnly = donationsOnly; + } } diff --git a/src/main/java/world/bentobox/level/panels/DetailsPanel.java b/src/main/java/world/bentobox/level/panels/DetailsPanel.java index d19769b..3bd84fd 100644 --- a/src/main/java/world/bentobox/level/panels/DetailsPanel.java +++ b/src/main/java/world/bentobox/level/panels/DetailsPanel.java @@ -22,7 +22,6 @@ import world.bentobox.bentobox.api.panels.reader.ItemTemplateRecord; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.bentobox.hooks.ItemsAdderHook; import world.bentobox.level.Level; import world.bentobox.level.objects.IslandLevels; import world.bentobox.level.util.Utils; @@ -738,19 +737,20 @@ private BlockDataRec getBlockData(Object key) { Objects.requireNonNullElse(this.addon.getBlockConfig().getLimit(e), 0), Utils.prettifyObject(key, this.user), this.user.getTranslation(this.world, "level.gui.buttons.spawner.block-name")); - } else if (key instanceof String s && addon.isItemsAdder()) { - Optional opt = ItemsAdderHook.getItemStack(s); - ItemStack icon = opt.orElse(new ItemStack(Material.PAPER)); - String disp = opt.filter(is -> is.getItemMeta().hasDisplayName()) - .map(is -> is.getItemMeta().getDisplayName()).orElse(Utils.prettifyObject(key, this.user)); - return new BlockDataRec(icon, this.user.getTranslationOrNothing(ref + "id", "[id]", s), + } else if (key instanceof String s) { + Optional optItem = Utils.getCustomBlockItemStack(addon, s); + ItemStack icon = optItem.orElse(new ItemStack(Material.PAPER)); + String disp = Utils.getCustomBlockDisplayName(optItem, s, this.user); + + return new BlockDataRec(icon, + this.user.getTranslationOrNothing(ref + "id", "[id]", s), this.addon.getBlockConfig().getBlockValues().getOrDefault(s, 0), - Objects.requireNonNullElse(this.addon.getBlockConfig().getLimit(s), 0), disp, ""); + Objects.requireNonNullElse(this.addon.getBlockConfig().getLimit(s), 0), + disp, ""); } return new BlockDataRec(new ItemStack(Material.PAPER), "", 0, 0, Utils.prettifyObject(key, this.user), ""); } - // --------------------------------------------------------------------- // Section: Other Methods // --------------------------------------------------------------------- diff --git a/src/main/java/world/bentobox/level/panels/DonationPanel.java b/src/main/java/world/bentobox/level/panels/DonationPanel.java index ccb5ae7..760611e 100644 --- a/src/main/java/world/bentobox/level/panels/DonationPanel.java +++ b/src/main/java/world/bentobox/level/panels/DonationPanel.java @@ -137,7 +137,10 @@ private long calculateDonationValue() { for (int slot : layout.donationSlots) { ItemStack item = inventory.getItem(slot); if (item != null && !item.getType().isAir()) { - Integer value = addon.getBlockConfig().getValue(world, item.getType()); + String customId = addon.getCustomBlockId(item); + Integer value = customId != null + ? addon.getBlockConfig().getValue(world, customId) + : addon.getBlockConfig().getValue(world, item.getType()); if (value != null && value > 0) { total += (long) value * item.getAmount(); } @@ -176,15 +179,24 @@ private void processDonation() { for (int slot : layout.donationSlots) { ItemStack item = inventory.getItem(slot); if (item != null && !item.getType().isAir()) { - Material mat = item.getType(); - Integer value = addon.getBlockConfig().getValue(world, mat); + String customId = addon.getCustomBlockId(item); + String donationId; + Integer value; + if (customId != null) { + value = addon.getBlockConfig().getValue(world, customId); + donationId = customId; + } else { + Material mat = item.getType(); + value = addon.getBlockConfig().getValue(world, mat); + donationId = mat.name(); + } if (value != null && value > 0) { int count = item.getAmount(); long points = (long) value * count; - donations.merge(mat.name(), count, Integer::sum); + donations.merge(donationId, count, Integer::sum); totalPoints += points; // Record each material type as a separate donation log entry - addon.getManager().donateBlocks(island, user.getUniqueId(), mat.name(), count, points); + addon.getManager().donateBlocks(island, user.getUniqueId(), donationId, count, points); // Clear the slot - items are consumed inventory.setItem(slot, null); } else { @@ -337,7 +349,17 @@ private void handleDonationSlotClick(int slot, Player player) { } private boolean isValidDonationItem(ItemStack item) { - if (item == null || item.getType().isAir() || !item.getType().isBlock()) { + if (item == null || item.getType().isAir()) { + return false; + } + // Check custom block plugins first (Oraxen, Nexo, ItemsAdder) + String customId = addon.getCustomBlockId(item); + if (customId != null) { + Integer value = addon.getBlockConfig().getValue(world, customId); + return value != null && value > 0; + } + // Fall back to vanilla block check + if (!item.getType().isBlock()) { return false; } Integer value = addon.getBlockConfig().getValue(world, item.getType()); diff --git a/src/main/java/world/bentobox/level/panels/TopLevelPanel.java b/src/main/java/world/bentobox/level/panels/TopLevelPanel.java index 81910b8..3dfa68c 100644 --- a/src/main/java/world/bentobox/level/panels/TopLevelPanel.java +++ b/src/main/java/world/bentobox/level/panels/TopLevelPanel.java @@ -180,7 +180,11 @@ private PanelItem createIslandIcon(ItemTemplateRecord template, IslandTopRecord || !this.addon.getVisitHook().getAddonManager().preprocessTeleportation(this.user, island, true); } case "VIEW" -> { - return island.getOwner() == null + // In donations-only mode there is no detail panel to view, so this + // action is filtered out — that also drops the "click to view" + // tooltip from the rendered button. + return this.addon.getSettings().isDonationsOnly() + || island.getOwner() == null || !island.getMemberSet(RanksManager.MEMBER_RANK).contains(this.user.getUniqueId()); } default -> { diff --git a/src/main/java/world/bentobox/level/panels/ValuePanel.java b/src/main/java/world/bentobox/level/panels/ValuePanel.java index 70a0bf1..7a7695f 100644 --- a/src/main/java/world/bentobox/level/panels/ValuePanel.java +++ b/src/main/java/world/bentobox/level/panels/ValuePanel.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.Optional; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -28,6 +29,7 @@ import world.bentobox.bentobox.api.panels.builders.TemplatedPanelBuilder; import world.bentobox.bentobox.api.panels.reader.ItemTemplateRecord; import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.hooks.CraftEngineHook; import world.bentobox.bentobox.hooks.ItemsAdderHook; import world.bentobox.bentobox.util.Util; import world.bentobox.level.Level; @@ -667,6 +669,16 @@ private PanelItem createMaterialButton(ItemTemplateRecord template, TemplatedPan return this.createMaterialButton(template, this.elementList.get(index)); } + private static String stripCustomPrefix(String key) { + if (key.startsWith("oraxen:")) { + return key.substring(7); + } + if (key.startsWith("nexo:")) { + return key.substring(5); + } + return key; + } + private Material getIcon(String key) { // Filter out some names key = key.replaceAll("wall_", ""); @@ -735,6 +747,14 @@ private Material getIcon(String key) { if (key.startsWith("oraxen:") && BentoBox.getInstance().getHooks().getHook("Oraxen").isPresent()) { return Material.PAPER; } + // Try Nexo + if (key.startsWith("nexo:") && addon.isNexo()) { + return Material.PAPER; + } + // Try CraftEngine — IDs are already namespaced (e.g. "mynamespace:my_block") + if (addon.isCraftEngine() && CraftEngineHook.exists(key)) { + return Material.PAPER; + } return null; } @@ -764,16 +784,21 @@ private PanelItem createMaterialButton(ItemTemplateRecord template, BlockRecord ? this.user.getTranslationOrNothing(baseKey + "limit", TextVariables.NUMBER, String.valueOf(blockLimit)) : ""; - // Determine icon and display material text - Material icon = getIcon(key); - builder.icon((icon == null || icon == Material.AIR) ? Material.PAPER : icon); - if (key.startsWith("oraxen:")) { - key = key.substring(7); - } - String displayMaterial = (icon == null) ? Util.prettifyText(key) : Utils.prettifyObject(key, user); - // Special handling for spawn eggs - if (icon != null && icon.name().endsWith("_SPAWN_EGG")) { - displayMaterial = Util.prettifyText(key); + // Prefer the actual custom-block ItemStack (real texture + display name) over a fallback Material icon. + Optional customStack = Utils.getCustomBlockItemStack(addon, key); + String displayMaterial; + if (customStack.isPresent()) { + builder.icon(customStack.get().clone()); + displayMaterial = Utils.getCustomBlockDisplayName(customStack, stripCustomPrefix(key), user); + } else { + Material icon = getIcon(key); + builder.icon((icon == null || icon == Material.AIR) ? Material.PAPER : icon); + String stripped = stripCustomPrefix(key); + displayMaterial = (icon == null) ? Util.prettifyText(stripped) : Utils.prettifyObject(stripped, user); + // Special handling for spawn eggs + if (icon != null && icon.name().endsWith("_SPAWN_EGG")) { + displayMaterial = Util.prettifyText(stripped); + } } // Set button title if available diff --git a/src/main/java/world/bentobox/level/util/Utils.java b/src/main/java/world/bentobox/level/util/Utils.java index b807cb9..e585e2d 100644 --- a/src/main/java/world/bentobox/level/util/Utils.java +++ b/src/main/java/world/bentobox/level/util/Utils.java @@ -9,13 +9,22 @@ import java.text.NumberFormat; import java.util.List; +import java.util.Optional; import org.bukkit.Material; import org.bukkit.entity.EntityType; +import org.bukkit.inventory.ItemStack; import org.bukkit.permissions.PermissionAttachmentInfo; +import com.nexomc.nexo.api.NexoItems; + +import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.hooks.CraftEngineHook; +import world.bentobox.bentobox.hooks.ItemsAdderHook; import world.bentobox.bentobox.hooks.LangUtilsHook; +import world.bentobox.bentobox.hooks.OraxenHook; +import world.bentobox.level.Level; public class Utils @@ -216,6 +225,61 @@ public static String prettifyObject(Object object, User user) { return ""; } + /** + * Returns the best available ItemStack for a custom-block string ID. + * Checks Oraxen, Nexo, ItemsAdder, and CraftEngine in order; returns empty when none matches. + * + * @param addon the Level addon + * @param id the custom block ID (e.g. "oraxen:my_block", "nexo:my_block", an ItemsAdder ID, + * or a CraftEngine ID such as "default:my_block") + * @return an Optional containing the representative ItemStack, or empty + */ + public static Optional getCustomBlockItemStack(Level addon, String id) { + if (id == null) { + return Optional.empty(); + } + if (id.startsWith("oraxen:") && BentoBox.getInstance().getHooks().getHook("Oraxen").isPresent()) { + return OraxenHook.getOptionalItemById(id.substring(7)).map(itemBuilder -> itemBuilder.build()); + } + if (id.startsWith("nexo:") && addon.isNexo()) { + com.nexomc.nexo.items.ItemBuilder nexoBuilder = NexoItems.itemFromId(id.substring(5)); + return nexoBuilder != null ? Optional.of(nexoBuilder.build()) : Optional.empty(); + } + if (addon.isItemsAdder() && ItemsAdderHook.isInRegistry(id)) { + return ItemsAdderHook.getItemStack(id); + } + if (addon.isCraftEngine()) { + return CraftEngineHook.getItemStack(id); + } + return Optional.empty(); + } + + /** + * Returns the display name from an ItemStack's meta when present, otherwise falls back to + * {@link #prettifyObject(Object, User)} on the original key. + *

+ * Checks the legacy {@code display.Name} (used by Oraxen, Nexo, ItemsAdder) and then the + * modern {@code minecraft:item_name} component (used by CraftEngine and other 1.20.5+ items). + * + * @param itemStack the optional ItemStack (typically from a custom-block plugin) + * @param key the raw key used as a fallback for prettification + * @param user the user for translation lookups + * @return the human-readable display name + */ + public static String getCustomBlockDisplayName(Optional itemStack, String key, User user) { + if (itemStack.isEmpty() || itemStack.get().getItemMeta() == null) { + return prettifyObject(key, user); + } + org.bukkit.inventory.meta.ItemMeta meta = itemStack.get().getItemMeta(); + if (meta.hasDisplayName()) { + return meta.getDisplayName(); + } + if (meta.hasItemName()) { + return meta.getItemName(); + } + return prettifyObject(key, user); + } + public static String prettifyDescription(Object object, User user) { if (object instanceof String key) { String translation = user.getTranslationOrNothing(LEVEL_MATERIALS + key + DESCRIPTION); diff --git a/src/main/resources/addon.yml b/src/main/resources/addon.yml index e99df00..61e4c89 100755 --- a/src/main/resources/addon.yml +++ b/src/main/resources/addon.yml @@ -2,7 +2,7 @@ name: Level main: world.bentobox.level.Level version: ${version}${build.number} icon: DIAMOND -api-version: 3.14.0 +api-version: 3.16.0 authors: tastybento diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 11aacec..c101ad8 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -20,13 +20,22 @@ concurrent-island-calcs: 1 # If an island takes longer that this time to calculate, then the calculation will abort. # Generally, calculation should only take a few seconds, so if this ever triggers then something is not right. calculation-timeout: 5 -# +# # Zero island levels on new island or island reset # If true, Level will calculate the starter island's level and remove it from any future level calculations. # If false, the player's starter island blocks will count towards their level. # This will reduce CPU if false. zero-new-island-levels: true -# +# +# Donations-only mode +# If true, the island block scan is skipped entirely and the island level +# is computed only from blocks donated via /island donate. This removes the +# per-recalculation CPU cost of scanning the island. +# The /island detail command is not registered in this mode, since there +# are no scanned blocks to break down. /island level still works and reports +# the level based on donated blocks. +donations-only: false +# # Calculate island level on login # This silently calculates the player's island level when they login # This applies to all islands the player has on the server, e.g., BSkyBlock, AcidIsland diff --git a/src/main/resources/locales/cs.yml b/src/main/resources/locales/cs.yml index 35ec78b..7c1da5b 100644 --- a/src/main/resources/locales/cs.yml +++ b/src/main/resources/locales/cs.yml @@ -67,6 +67,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Tyto bloky budou ZNIČENY z tvého inventáře:" + confirm-line: "[number] x [material] = [points] bodů" + confirm-total: "Celkem: [points] trvalých bodů." detail: description: "zobrazit podrobnosti o blocích vašeho ostrova" top: diff --git a/src/main/resources/locales/de.yml b/src/main/resources/locales/de.yml index b05dab9..75ebec9 100644 --- a/src/main/resources/locales/de.yml +++ b/src/main/resources/locales/de.yml @@ -68,6 +68,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Diese Blöcke werden aus deinem Inventar ZERSTÖRT:" + confirm-line: "[number] x [material] = [points] Punkte" + confirm-total: "Gesamt: [points] permanente Punkte." detail: description: "zeigt Details der Blöcke deiner Insel" top: diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index ff81550..e18ba4a 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -52,7 +52,7 @@ island: in-progress: "Island level calculation is in progress..." time-out: "The level calculation took too long. Please try again later." donate: - parameters: "[hand [amount]]" + parameters: "[hand [amount]] [inv]" description: "donate blocks to permanently raise island level" must-be-on-island: "You must be on your island to donate blocks." no-permission: "You do not have permission to donate blocks on this island." @@ -72,6 +72,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "About to DESTROY these blocks from your inventory:" + confirm-line: "[number] x [material] = [points] points" + confirm-total: "Total: [points] permanent points." detail: description: "shows detail of your island blocks" top: diff --git a/src/main/resources/locales/es.yml b/src/main/resources/locales/es.yml index 0d93e3f..8571223 100644 --- a/src/main/resources/locales/es.yml +++ b/src/main/resources/locales/es.yml @@ -65,6 +65,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Estos bloques serán DESTRUIDOS de tu inventario:" + confirm-line: "[number] x [material] = [points] puntos" + confirm-total: "Total: [points] puntos permanentes." detail: description: "muestra el detalle de los bloques de tu isla" top: diff --git a/src/main/resources/locales/fr.yml b/src/main/resources/locales/fr.yml index e1265d1..d939b6b 100644 --- a/src/main/resources/locales/fr.yml +++ b/src/main/resources/locales/fr.yml @@ -67,6 +67,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Ces blocs vont être DÉTRUITS de votre inventaire :" + confirm-line: "[number] x [material] = [points] points" + confirm-total: "Total : [points] points permanents." top: description: affiche le top 10 gui-title: "Top 10" diff --git a/src/main/resources/locales/hu.yml b/src/main/resources/locales/hu.yml index 7be400a..e9113e5 100644 --- a/src/main/resources/locales/hu.yml +++ b/src/main/resources/locales/hu.yml @@ -68,6 +68,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Ezeket a blokkokat MEGSEMMISÍTI a leltáradból:" + confirm-line: "[number] x [material] = [points] pont" + confirm-total: "Összesen: [points] állandó pont." detail: description: "megmutatja a szigeted blokkjainak részleteit" top: diff --git a/src/main/resources/locales/id.yml b/src/main/resources/locales/id.yml index 539666b..ff360f0 100644 --- a/src/main/resources/locales/id.yml +++ b/src/main/resources/locales/id.yml @@ -65,6 +65,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Akan MENGHANCURKAN blok-blok ini dari inventaris Anda:" + confirm-line: "[number] x [material] = [points] poin" + confirm-total: "Total: [points] poin permanen." top: description: menunjukkan Sepuluh Besar gui-title: " Sepuluh Besar" diff --git a/src/main/resources/locales/ko.yml b/src/main/resources/locales/ko.yml index f846737..073e26c 100644 --- a/src/main/resources/locales/ko.yml +++ b/src/main/resources/locales/ko.yml @@ -68,6 +68,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "인벤토리에서 다음 블록을 파괴합니다:" + confirm-line: "[number] x [material] = [points] 점" + confirm-total: "총: [points] 영구 점수." detail: description: "섬 블록의 세부 정보를 표시합니다" top: diff --git a/src/main/resources/locales/lv.yml b/src/main/resources/locales/lv.yml index 7ec2474..a9f6feb 100644 --- a/src/main/resources/locales/lv.yml +++ b/src/main/resources/locales/lv.yml @@ -68,6 +68,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Šie bloki tiks IZNĪCINĀTI no tavas somas:" + confirm-line: "[number] x [material] = [points] punkti" + confirm-total: "Kopā: [points] pastāvīgi punkti." detail: description: "rāda tavas salas bloku detaļas" top: diff --git a/src/main/resources/locales/nl.yml b/src/main/resources/locales/nl.yml index 324b896..9ddd410 100644 --- a/src/main/resources/locales/nl.yml +++ b/src/main/resources/locales/nl.yml @@ -65,6 +65,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Deze blokken worden VERNIETIGD uit je inventaris:" + confirm-line: "[number] x [material] = [points] punten" + confirm-total: "Totaal: [points] permanente punten." top: description: Toon de Top tien gui-title: " Top tien" diff --git a/src/main/resources/locales/pl.yml b/src/main/resources/locales/pl.yml index da6ad80..67b6940 100644 --- a/src/main/resources/locales/pl.yml +++ b/src/main/resources/locales/pl.yml @@ -65,6 +65,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Te bloki zostaną ZNISZCZONE z twojego ekwipunku:" + confirm-line: "[number] x [material] = [points] punktów" + confirm-total: "Łącznie: [points] punktów stałych." top: description: pokauje Top 10 wysp gui-title: "Top 10" diff --git a/src/main/resources/locales/pt.yml b/src/main/resources/locales/pt.yml index 57fbabe..ffb289c 100644 --- a/src/main/resources/locales/pt.yml +++ b/src/main/resources/locales/pt.yml @@ -68,6 +68,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Estes blocos serão DESTRUÍDOS do seu inventário:" + confirm-line: "[number] x [material] = [points] pontos" + confirm-total: "Total: [points] pontos permanentes." detail: description: "mostra os detalhes dos blocos da sua ilha" top: diff --git a/src/main/resources/locales/ru.yml b/src/main/resources/locales/ru.yml index 2dc53da..e483b21 100644 --- a/src/main/resources/locales/ru.yml +++ b/src/main/resources/locales/ru.yml @@ -68,6 +68,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Эти блоки будут УНИЧТОЖЕНЫ из вашего инвентаря:" + confirm-line: "[number] x [material] = [points] очков" + confirm-total: "Всего: [points] постоянных очков." detail: description: показать информацию о блоках на вашем острове top: diff --git a/src/main/resources/locales/tr.yml b/src/main/resources/locales/tr.yml index 689f9b8..2f8ae2b 100644 --- a/src/main/resources/locales/tr.yml +++ b/src/main/resources/locales/tr.yml @@ -72,6 +72,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Envanterinizden bu bloklar YOK EDİLECEK:" + confirm-line: "[number] x [material] = [points] puan" + confirm-total: "Toplam: [points] kalıcı puan." detail: description: "adanın blok ayrıntılarını gösterir" top: diff --git a/src/main/resources/locales/uk.yml b/src/main/resources/locales/uk.yml index 9ac33d3..d8286b3 100644 --- a/src/main/resources/locales/uk.yml +++ b/src/main/resources/locales/uk.yml @@ -65,6 +65,11 @@ island: success: "Пожертвовано [number] x [material] за [points] постійних очок!" not-block: "Ви повинні тримати блок, який можна розмістити." confirm-prompt: "Буде ЗНИЩЕНО [number] x [material] за [points] постійних очок." + inv: + keyword: "inv" + confirm-header: "Ці блоки будуть ЗНИЩЕНІ з вашого інвентарю:" + confirm-line: "[number] x [material] = [points] очок" + confirm-total: "Всього: [points] постійних очок." top: description: показати першу десятку gui-title: "& Десятка Кращих" diff --git a/src/main/resources/locales/vi.yml b/src/main/resources/locales/vi.yml index 6d27ce2..9c19d5d 100644 --- a/src/main/resources/locales/vi.yml +++ b/src/main/resources/locales/vi.yml @@ -71,6 +71,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Sắp PHÁ HỦY những khối này từ kho đồ của bạn:" + confirm-line: "[number] x [material] = [points] điểm" + confirm-total: "Tổng: [points] điểm vĩnh viễn." detail: description: "hiển thị chi tiết các khối trên đảo của bạn" top: diff --git a/src/main/resources/locales/zh-CN.yml b/src/main/resources/locales/zh-CN.yml index 1901208..6dd71c6 100644 --- a/src/main/resources/locales/zh-CN.yml +++ b/src/main/resources/locales/zh-CN.yml @@ -63,6 +63,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "即将从你的物品栏销毁以下方块:" + confirm-line: "[number] x [material] = [points] 点" + confirm-total: "共计: [points] 永久点数。" top: description: 显示前十名 diff --git a/src/test/java/world/bentobox/level/LevelTest.java b/src/test/java/world/bentobox/level/LevelTest.java index 288fec9..6323bf6 100644 --- a/src/test/java/world/bentobox/level/LevelTest.java +++ b/src/test/java/world/bentobox/level/LevelTest.java @@ -247,4 +247,19 @@ void testGetSettings() { assertEquals(100, s.getLevelCost()); } + /** + * Donations-only mode must not register the IslandDetailCommand, so we expect + * four player commands instead of the usual five. + */ + @Test + void testAllLoadedDonationsOnlySkipsDetailCommand() { + mockedBukkit.when(() -> Bukkit.getWorld("acidisland_world")).thenReturn(null); + addon.getSettings().setDonationsOnly(true); + addon.allLoaded(); + // 4 player commands (level, top, value, donate) — no detail + verify(cmd, times(4)).getAddon(); + // Admin command count is unchanged + verify(adminCmd, times(5)).getAddon(); + } + } diff --git a/src/test/java/world/bentobox/level/calculators/ResultsTest.java b/src/test/java/world/bentobox/level/calculators/ResultsTest.java index 66cf47d..7045ff8 100644 --- a/src/test/java/world/bentobox/level/calculators/ResultsTest.java +++ b/src/test/java/world/bentobox/level/calculators/ResultsTest.java @@ -94,6 +94,19 @@ void testDefaultDeathHandicapIsZero() { assertEquals(0, results.getDeathHandicap()); } + // --- Points from current level --- + + @Test + void testSetAndGetPointsFromCurrentLevel() { + results.setPointsFromCurrentLevel(75L); + assertEquals(75L, results.getPointsFromCurrentLevel()); + } + + @Test + void testDefaultPointsFromCurrentLevelIsZero() { + assertEquals(0L, results.getPointsFromCurrentLevel()); + } + // --- Donated points --- @Test diff --git a/src/test/java/world/bentobox/level/commands/IslandDonateCommandTest.java b/src/test/java/world/bentobox/level/commands/IslandDonateCommandTest.java index 7ba90bd..40771de 100644 --- a/src/test/java/world/bentobox/level/commands/IslandDonateCommandTest.java +++ b/src/test/java/world/bentobox/level/commands/IslandDonateCommandTest.java @@ -4,14 +4,19 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.UUID; import org.bukkit.Location; @@ -65,6 +70,10 @@ protected void setUp() throws Exception { when(user.getTranslation(anyString(), anyString(), anyString())).thenAnswer(i -> i.getArgument(0, String.class)); when(user.getTranslation(anyString(), anyString(), anyString(), anyString(), anyString())).thenAnswer(i -> i.getArgument(0, String.class)); when(user.getTranslation("island.donate.hand.keyword")).thenReturn("hand"); + when(user.getTranslation("island.donate.inv.keyword")).thenReturn("inv"); + when(user.getTranslationOrNothing(anyString())).thenReturn(""); + when(user.getTranslationOrNothing(anyString(), anyString(), anyString())).thenReturn(""); + when(user.getLocale()).thenReturn(Locale.US); when(user.getLocation()).thenReturn(location); when(player.getInventory()).thenReturn(inventory); @@ -156,9 +165,87 @@ void testExecuteHandBlockNoValue() { @Test void testTabCompleteNoArgs() { - // When no args, should suggest "hand" + // When no args, should suggest "hand" and "inv" var result = cmd.tabComplete(user, "donate", Collections.emptyList()); assertTrue(result.isPresent()); assertTrue(result.get().contains("hand")); + assertTrue(result.get().contains("inv")); + } + + @Test + void testTabCompleteFirstArgFromBentoBoxFlow() { + // BentoBox passes the leaf command label as args.get(0); the partial first + // user arg sits in args.get(1). Empty string = bare "/island donate ". + var result = cmd.tabComplete(user, "donate", List.of("donate", "")); + assertTrue(result.isPresent()); + assertTrue(result.get().contains("hand")); + assertTrue(result.get().contains("inv")); + } + + @Test + void testTabCompleteSecondArgAfterHandSuggestsHeldAmount() { + ItemStack stack = mock(ItemStack.class); + when(stack.getAmount()).thenReturn(7); + when(inventory.getItemInMainHand()).thenReturn(stack); + + var result = cmd.tabComplete(user, "donate", List.of("donate", "hand", "")); + assertTrue(result.isPresent()); + assertTrue(result.get().contains("7")); + } + + @Test + void testTabCompleteAfterInvSuggestsNothing() { + var result = cmd.tabComplete(user, "donate", List.of("donate", "inv", "")); + assertTrue(result.isPresent()); + assertTrue(result.get().isEmpty()); + } + + @Test + void testExecuteInvEmptyInventory() { + when(inventory.getStorageContents()).thenReturn(new ItemStack[] { null, null, null }); + + assertFalse(cmd.execute(user, "donate", List.of("inv"))); + verify(user).sendMessage("island.donate.empty"); + verify(manager, never()).donateBlocks(any(), any(UUID.class), anyString(), anyInt(), anyLong()); + } + + @Test + void testExecuteInvNoValuableBlocks() { + // Stone with no value, sword (not a block) + ItemStack stone = mock(ItemStack.class); + when(stone.getType()).thenReturn(Material.STONE); + when(stone.getAmount()).thenReturn(5); + ItemStack sword = mock(ItemStack.class); + when(sword.getType()).thenReturn(Material.DIAMOND_SWORD); + when(inventory.getStorageContents()).thenReturn(new ItemStack[] { stone, sword }); + when(blockConfig.getValue(any(), eq(Material.STONE))).thenReturn(null); + + assertFalse(cmd.execute(user, "donate", List.of("inv"))); + verify(user).sendMessage("island.donate.empty"); + verify(manager, never()).donateBlocks(any(), any(UUID.class), anyString(), anyInt(), anyLong()); + } + + @Test + void testExecuteInvShowsConfirmationPrompt() { + ItemStack diamond = mock(ItemStack.class); + when(diamond.getType()).thenReturn(Material.DIAMOND_BLOCK); + when(diamond.getAmount()).thenReturn(2); + ItemStack gold = mock(ItemStack.class); + when(gold.getType()).thenReturn(Material.GOLD_BLOCK); + when(gold.getAmount()).thenReturn(3); + // Non-donatable item is ignored, not destroyed + ItemStack sword = mock(ItemStack.class); + when(sword.getType()).thenReturn(Material.DIAMOND_SWORD); + + when(inventory.getStorageContents()) + .thenReturn(new ItemStack[] { diamond, sword, gold }); + when(blockConfig.getValue(any(), eq(Material.DIAMOND_BLOCK))).thenReturn(100); + when(blockConfig.getValue(any(), eq(Material.GOLD_BLOCK))).thenReturn(50); + + assertTrue(cmd.execute(user, "donate", List.of("inv"))); + // The confirmation header should have been requested via getTranslation + verify(user).getTranslation("island.donate.inv.confirm-header"); + // No donation yet — only confirmation requested + verify(manager, never()).donateBlocks(any(), any(UUID.class), anyString(), anyInt(), anyLong()); } }