From 9ea95710d6bf33080ddf314898ff21d5acdea451 Mon Sep 17 00:00:00 2001 From: tastybento Date: Thu, 16 Jul 2026 21:33:15 -0700 Subject: [PATCH] fix: make donation limit handling consistent across all paths Follow-ups from the 2.28.0 release review of the donation limit feature: - BlockConfig: store blockLimits keys lowercased and lowercase String lookups in getLimit(), so the level calculation (which lowercases donation keys) and the donation command/panel paths (which pass custom IDs as-is) always resolve to the same limit, including for mixed-case custom block IDs. - Extract Utils.calculateDonatedPoints() as the single source of truth for donated points (current values, world overrides, limit capping); used by both tidyUp() and the donation panel info pane, so the "Currently donated" figure now matches what the level actually uses instead of the stored donation-time total. - DonationPanel: carry the resolved blockObj from the slot walk instead of re-deriving it from the donation ID via Material.matchMaterial(), which could mis-resolve colon-less custom IDs. - getReport(): cap donated lines at the current block limit so they sum to the reported donated total, and mark capped entries. - New island.donate.limit-reached-all locale key (all 17 locales): shown when everything offered is already at its limit, instead of a misleading "empty" message (panel) or a 0-point confirm prompt (inv). - Tests: cover the new paths; stub getLimit to return null in setups because Mockito returns 0 for unstubbed Integer methods, which reads as "limit of 0" and silently exercised the wrong path. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Y6f59pxCkwS8QXtJSe1nq6 --- .../calculators/IslandLevelCalculator.java | 25 +++---- .../level/commands/IslandDonateCommand.java | 5 ++ .../bentobox/level/config/BlockConfig.java | 5 +- .../bentobox/level/panels/DonationPanel.java | 18 +++-- .../java/world/bentobox/level/util/Utils.java | 32 +++++++++ src/main/resources/locales/cs.yml | 1 + src/main/resources/locales/de.yml | 1 + src/main/resources/locales/en-US.yml | 1 + src/main/resources/locales/es.yml | 1 + src/main/resources/locales/fr.yml | 1 + src/main/resources/locales/hu.yml | 1 + src/main/resources/locales/id.yml | 1 + src/main/resources/locales/ko.yml | 1 + src/main/resources/locales/lv.yml | 1 + src/main/resources/locales/nl.yml | 1 + src/main/resources/locales/pl.yml | 1 + src/main/resources/locales/pt.yml | 1 + src/main/resources/locales/ru.yml | 1 + src/main/resources/locales/tr.yml | 1 + src/main/resources/locales/uk.yml | 1 + src/main/resources/locales/vi.yml | 1 + src/main/resources/locales/zh-CN.yml | 1 + .../IslandLevelCalculatorTidyUpTest.java | 3 +- .../commands/IslandDonateCommandTest.java | 24 +++++++ .../level/config/BlockConfigLimitsTest.java | 64 +++++++++++++++++ .../util/UtilsCalculateDonatedPointsTest.java | 69 +++++++++++++++++++ 26 files changed, 237 insertions(+), 25 deletions(-) create mode 100644 src/test/java/world/bentobox/level/config/BlockConfigLimitsTest.java create mode 100644 src/test/java/world/bentobox/level/util/UtilsCalculateDonatedPointsTest.java diff --git a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java index 8cd3894f..b4aa2f23 100644 --- a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java +++ b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java @@ -62,6 +62,7 @@ import world.bentobox.level.Level; import world.bentobox.level.calculators.Results.Result; import world.bentobox.level.config.BlockConfig; +import world.bentobox.level.util.Utils; public class IslandLevelCalculator { private final UUID calcId = UUID.randomUUID(); // ID for hashing @@ -305,10 +306,15 @@ private List getReport() { donatedBlocks.entrySet().stream() .sorted(Map.Entry.comparingByValue().reversed()) .forEach(entry -> { - Integer value = Objects.requireNonNullElse(addon.getBlockConfig().getValue(island.getWorld(), entry.getKey().toLowerCase(java.util.Locale.ENGLISH)), 0); - long totalValue = (long) value * entry.getValue(); + String key = entry.getKey().toLowerCase(java.util.Locale.ENGLISH); + Integer value = Objects.requireNonNullElse(addon.getBlockConfig().getValue(island.getWorld(), key), 0); + // Cap at the current blockconfig limit so the lines sum to the donated total + Integer limit = addon.getBlockConfig().getLimit(key); + int counted = limit == null ? entry.getValue() : Math.min(entry.getValue(), limit); + long totalValue = (long) value * counted; reportLines.add(" " + Util.prettifyText(entry.getKey()) + " x " + String.format("%,d", entry.getValue()) + + (counted < entry.getValue() ? " (capped at " + String.format("%,d", counted) + ")" : "") + " = " + String.format("%,d", totalValue) + " points"); }); reportLines.add(LINE_BREAK); @@ -735,19 +741,8 @@ public void tidyUp() { // level always reflects the current configuration, even if block values changed since donation. // Also apply the current block limit: if the limit was lowered after donation, only count // up to the current limit (donated blocks over the limit are silently ignored). - Map donatedBlocksMap = addon.getManager().getDonatedBlocks(island); - long donatedPoints = donatedBlocksMap.entrySet().stream() - .mapToLong(entry -> { - String key = entry.getKey().toLowerCase(java.util.Locale.ENGLISH); - Integer value = addon.getBlockConfig().getValue(island.getWorld(), key); - int count = entry.getValue(); - Integer limit = addon.getBlockConfig().getLimit(key); - if (limit != null) { - count = Math.min(count, limit); - } - return (long) Objects.requireNonNullElse(value, 0) * count; - }) - .sum(); + long donatedPoints = Utils.calculateDonatedPoints(addon.getBlockConfig(), island.getWorld(), + addon.getManager().getDonatedBlocks(island)); results.rawBlockCount.addAndGet(donatedPoints); results.donatedPoints.set(donatedPoints); diff --git a/src/main/java/world/bentobox/level/commands/IslandDonateCommand.java b/src/main/java/world/bentobox/level/commands/IslandDonateCommand.java index 58b88425..ceb682bf 100644 --- a/src/main/java/world/bentobox/level/commands/IslandDonateCommand.java +++ b/src/main/java/world/bentobox/level/commands/IslandDonateCommand.java @@ -263,6 +263,11 @@ private boolean handleInvDonation(User user, Island island) { MATERIAL_PLACEHOLDER, Utils.prettifyObject(displayKey, user), POINTS_PLACEHOLDER, Utils.formatNumber(user, points))); } + if (totalPoints == 0 && limited) { + // Everything offered is already at its donation limit — nothing to confirm + user.sendMessage("island.donate.limit-reached-all"); + return false; + } if (limited) { // The limit-notice locale uses '|' as a lore line-break for the GUI; // translate to '\n' here so the chat confirmation prompt wraps cleanly. diff --git a/src/main/java/world/bentobox/level/config/BlockConfig.java b/src/main/java/world/bentobox/level/config/BlockConfig.java index 62350c39..7c191d80 100644 --- a/src/main/java/world/bentobox/level/config/BlockConfig.java +++ b/src/main/java/world/bentobox/level/config/BlockConfig.java @@ -57,7 +57,8 @@ public BlockConfig(Level addon, YamlConfiguration blockValuesConfig, File file) for (String key : limits.getKeys(false)) { // Convert old materials to namespaced keys key = convertKey(limits, key); - blockLimits.put(key, limits.getInt(key)); + // Store lowercased so lookups are case-insensitive, matching blockValues + blockLimits.put(key.toLowerCase(Locale.ENGLISH), limits.getInt(key)); } } // The blocks section can include blocks, spawners, and namespacedIDs @@ -199,7 +200,7 @@ public Integer getLimit(Object obj) { return blockLimits.get(et.name().toLowerCase(Locale.ENGLISH).concat(SPAWNER)); } if (obj instanceof String s) { - return blockLimits.get(s); + return blockLimits.get(s.toLowerCase(Locale.ENGLISH)); } return null; diff --git a/src/main/java/world/bentobox/level/panels/DonationPanel.java b/src/main/java/world/bentobox/level/panels/DonationPanel.java index 72baef81..1ca313ab 100644 --- a/src/main/java/world/bentobox/level/panels/DonationPanel.java +++ b/src/main/java/world/bentobox/level/panels/DonationPanel.java @@ -86,8 +86,10 @@ private DonationPanel(Level addon, World world, User user, Island island) { // Place decorative items from the template (non-button, non-border entries) layout.decorativeItems.forEach(inventory::setItem); - // Info pane - long currentDonated = addon.getManager().getDonatedPoints(island); + // Info pane — show the effective donated points (current values, limits applied), + // the same figure tidyUp() adds to the level, not the stored donation-time total. + long currentDonated = Utils.calculateDonatedPoints(addon.getBlockConfig(), world, + addon.getManager().getDonatedBlocks(island)); ItemStack info = createNamedItem(layout.infoMaterial, user.getTranslation("island.donate.gui-info", POINTS_PLACEHOLDER, Utils.formatNumber(user, currentDonated))); @@ -148,6 +150,7 @@ private DonationTotals computeDonationTotals() { DonationTotals result = new DonationTotals(); Map rawTotals = new HashMap<>(); Map values = new HashMap<>(); + Map blockObjs = new HashMap<>(); for (int slot : layout.donationSlots) { ItemStack item = inventory.getItem(slot); if (item == null || item.getType().isAir()) continue; @@ -158,14 +161,13 @@ private DonationTotals computeDonationTotals() { if (value == null || value <= 0) continue; rawTotals.merge(donationId, item.getAmount(), Integer::sum); values.putIfAbsent(donationId, value); + blockObjs.putIfAbsent(donationId, blockObj); } Map donated = addon.getManager().getDonatedBlocks(island); for (Map.Entry e : rawTotals.entrySet()) { String donationId = e.getKey(); int total = e.getValue(); - Object blockObj = donationId.contains(":") ? donationId : Material.matchMaterial(donationId); - if (blockObj == null) blockObj = donationId; - Integer limit = addon.getBlockConfig().getLimit(blockObj); + Integer limit = addon.getBlockConfig().getLimit(blockObjs.get(donationId)); int accept = total; if (limit != null) { int already = donated.getOrDefault(donationId, 0); @@ -367,8 +369,10 @@ private void handleCancel(InventoryClickEvent event, Player player) { private void handleConfirm(InventoryClickEvent event, Player player) { event.setCancelled(true); - if (computeDonationTotals().acceptedPoints <= 0) { - user.sendMessage("island.donate.empty"); + DonationTotals totals = computeDonationTotals(); + if (totals.acceptedPoints <= 0) { + // Distinguish "nothing offered" from "everything offered is at its limit" + user.sendMessage(totals.limited ? "island.donate.limit-reached-all" : "island.donate.empty"); return; } confirmed = true; diff --git a/src/main/java/world/bentobox/level/util/Utils.java b/src/main/java/world/bentobox/level/util/Utils.java index e585e2d8..4aee9431 100644 --- a/src/main/java/world/bentobox/level/util/Utils.java +++ b/src/main/java/world/bentobox/level/util/Utils.java @@ -9,9 +9,13 @@ import java.text.NumberFormat; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; import java.util.Optional; import org.bukkit.Material; +import org.bukkit.World; import org.bukkit.entity.EntityType; import org.bukkit.inventory.ItemStack; import org.bukkit.permissions.PermissionAttachmentInfo; @@ -25,6 +29,7 @@ import world.bentobox.bentobox.hooks.LangUtilsHook; import world.bentobox.bentobox.hooks.OraxenHook; import world.bentobox.level.Level; +import world.bentobox.level.config.BlockConfig; public class Utils @@ -39,6 +44,33 @@ public static String formatNumber(User user, long value) { return NumberFormat.getInstance(user.getLocale()).format(value); } + /** + * Calculate the effective point value of a donated-blocks map using current block config + * values (world-specific where set) and capping each block type at its current blockconfig + * limit. This is the single source of truth for how donated blocks contribute to the level; + * the level calculation and any display of the donated total must use it so they agree. + * + * @param blockConfig the block config to read values and limits from + * @param world the world whose value overrides apply + * @param donatedBlocks map of donation id (Material name or custom block id) to count + * @return total donated points after value lookup and limit capping + */ + public static long calculateDonatedPoints(BlockConfig blockConfig, World world, + Map donatedBlocks) { + return donatedBlocks.entrySet().stream() + .mapToLong(entry -> { + String key = entry.getKey().toLowerCase(Locale.ENGLISH); + Integer value = blockConfig.getValue(world, key); + int count = entry.getValue(); + Integer limit = blockConfig.getLimit(key); + if (limit != null) { + count = Math.min(count, limit); + } + return (long) Objects.requireNonNullElse(value, 0) * count; + }) + .sum(); + } + /** * This method sends a message to the user with appended "prefix" text before message. * @param user User who receives message. diff --git a/src/main/resources/locales/cs.yml b/src/main/resources/locales/cs.yml index af28d6f7..a40cada4 100644 --- a/src/main/resources/locales/cs.yml +++ b/src/main/resources/locales/cs.yml @@ -63,6 +63,7 @@ island: gui-info: "Donate blocks to your island|Currently donated: [points] points|Warning: donated items are|destroyed and cannot be returned!" preview: "Points to add: [points]|These items will be destroyed!" limit-reached: "Limit darování pro [material] byl dosažen." + limit-reached-all: "Všechny tyto bloky dosáhly svých limitů darování." limit-notice: "Některé bloky podléhají limitům|a nebudou darovány" hand: keyword: "ruka" diff --git a/src/main/resources/locales/de.yml b/src/main/resources/locales/de.yml index 85bef5bc..9c2e78e7 100644 --- a/src/main/resources/locales/de.yml +++ b/src/main/resources/locales/de.yml @@ -64,6 +64,7 @@ island: gui-info: "Donate blocks to your island|Currently donated: [points] points|Warning: donated items are|destroyed and cannot be returned!" preview: "Points to add: [points]|These items will be destroyed!" limit-reached: "Das Spendenlimit für [material] wurde erreicht." + limit-reached-all: "Alle diese Blöcke haben ihre Spendenlimits erreicht." limit-notice: "Einige Blöcke unterliegen Limits|und werden nicht gespendet" hand: keyword: "hand" diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index fbd7f881..8aa262d5 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -68,6 +68,7 @@ island: gui-info: "Donate blocks to your island|Currently donated: [points] points|Warning: donated items are|destroyed and cannot be returned!" preview: "Points to add: [points]|These items will be destroyed!" limit-reached: "The donation limit for [material] has been reached." + limit-reached-all: "All of these blocks have reached their donation limits." limit-notice: "Some blocks are subject to limits|and will not be donated" hand: keyword: "hand" diff --git a/src/main/resources/locales/es.yml b/src/main/resources/locales/es.yml index 51398b90..13abea51 100644 --- a/src/main/resources/locales/es.yml +++ b/src/main/resources/locales/es.yml @@ -61,6 +61,7 @@ island: gui-info: "Donate blocks to your island|Currently donated: [points] points|Warning: donated items are|destroyed and cannot be returned!" preview: "Points to add: [points]|These items will be destroyed!" limit-reached: "Se ha alcanzado el límite de donación para [material]." + limit-reached-all: "Todos estos bloques han alcanzado sus límites de donación." limit-notice: "Algunos bloques están sujetos a límites|y no serán donados" hand: keyword: "mano" diff --git a/src/main/resources/locales/fr.yml b/src/main/resources/locales/fr.yml index 189631d8..9fa89d2f 100644 --- a/src/main/resources/locales/fr.yml +++ b/src/main/resources/locales/fr.yml @@ -63,6 +63,7 @@ island: gui-info: "Donate blocks to your island|Currently donated: [points] points|Warning: donated items are|destroyed and cannot be returned!" preview: "Points to add: [points]|These items will be destroyed!" limit-reached: "La limite de don pour [material] a été atteinte." + limit-reached-all: "Tous ces blocs ont atteint leur limite de don." limit-notice: "Certains blocs sont soumis à des limites|et ne seront pas donnés" hand: keyword: "main" diff --git a/src/main/resources/locales/hu.yml b/src/main/resources/locales/hu.yml index 82f5f68d..3dd0e296 100644 --- a/src/main/resources/locales/hu.yml +++ b/src/main/resources/locales/hu.yml @@ -64,6 +64,7 @@ island: gui-info: "Donate blocks to your island|Currently donated: [points] points|Warning: donated items are|destroyed and cannot be returned!" preview: "Points to add: [points]|These items will be destroyed!" limit-reached: "A(z) [material] adományozási limitje elérve." + limit-reached-all: "Ezek a blokkok mind elérték az adományozási limitjüket." limit-notice: "Néhány blokkra korlátozás vonatkozik|és nem lesznek adományozva" hand: keyword: "kez" diff --git a/src/main/resources/locales/id.yml b/src/main/resources/locales/id.yml index 2cc89aa3..d9a1c0f8 100644 --- a/src/main/resources/locales/id.yml +++ b/src/main/resources/locales/id.yml @@ -61,6 +61,7 @@ island: gui-info: "Donate blocks to your island|Currently donated: [points] points|Warning: donated items are|destroyed and cannot be returned!" preview: "Points to add: [points]|These items will be destroyed!" limit-reached: "Batas donasi untuk [material] telah tercapai." + limit-reached-all: "Semua blok ini telah mencapai batas donasinya." limit-notice: "Beberapa blok memiliki batasan|dan tidak akan didonasikan" hand: keyword: "tangan" diff --git a/src/main/resources/locales/ko.yml b/src/main/resources/locales/ko.yml index c221e1e7..fea49830 100644 --- a/src/main/resources/locales/ko.yml +++ b/src/main/resources/locales/ko.yml @@ -64,6 +64,7 @@ island: gui-info: "Donate blocks to your island|Currently donated: [points] points|Warning: donated items are|destroyed and cannot be returned!" preview: "Points to add: [points]|These items will be destroyed!" limit-reached: "[material]의 기부 한도에 도달했습니다." + limit-reached-all: "이 블록들은 모두 기부 한도에 도달했습니다." limit-notice: "일부 블록에는 제한이 있으며|기부되지 않습니다" hand: keyword: "hand" diff --git a/src/main/resources/locales/lv.yml b/src/main/resources/locales/lv.yml index b9f4af5a..d7faa938 100644 --- a/src/main/resources/locales/lv.yml +++ b/src/main/resources/locales/lv.yml @@ -64,6 +64,7 @@ island: gui-info: "Donate blocks to your island|Currently donated: [points] points|Warning: donated items are|destroyed and cannot be returned!" preview: "Points to add: [points]|These items will be destroyed!" limit-reached: "[material] ziedošanas limits ir sasniegts." + limit-reached-all: "Visi šie bloki ir sasnieguši savus ziedošanas limitus." limit-notice: "Dažiem blokiem ir ierobežojumi|un tie netiks ziedoti" hand: keyword: "roka" diff --git a/src/main/resources/locales/nl.yml b/src/main/resources/locales/nl.yml index 0e64a8ac..775852d7 100644 --- a/src/main/resources/locales/nl.yml +++ b/src/main/resources/locales/nl.yml @@ -61,6 +61,7 @@ island: gui-info: "Donate blocks to your island|Currently donated: [points] points|Warning: donated items are|destroyed and cannot be returned!" preview: "Points to add: [points]|These items will be destroyed!" limit-reached: "De donatielimiet voor [material] is bereikt." + limit-reached-all: "Al deze blokken hebben hun donatielimiet bereikt." limit-notice: "Sommige blokken zijn onderworpen aan limieten|en worden niet gedoneerd" hand: keyword: "hand" diff --git a/src/main/resources/locales/pl.yml b/src/main/resources/locales/pl.yml index 327dc155..f74c89b7 100644 --- a/src/main/resources/locales/pl.yml +++ b/src/main/resources/locales/pl.yml @@ -61,6 +61,7 @@ island: gui-info: "Donate blocks to your island|Currently donated: [points] points|Warning: donated items are|destroyed and cannot be returned!" preview: "Points to add: [points]|These items will be destroyed!" limit-reached: "Limit darowizn dla [material] został osiągnięty." + limit-reached-all: "Wszystkie te bloki osiągnęły swoje limity darowizn." limit-notice: "Niektóre bloki podlegają limitom|i nie zostaną przekazane" hand: keyword: "reka" diff --git a/src/main/resources/locales/pt.yml b/src/main/resources/locales/pt.yml index d8494922..9b7ef9af 100644 --- a/src/main/resources/locales/pt.yml +++ b/src/main/resources/locales/pt.yml @@ -64,6 +64,7 @@ island: gui-info: "Donate blocks to your island|Currently donated: [points] points|Warning: donated items are|destroyed and cannot be returned!" preview: "Points to add: [points]|These items will be destroyed!" limit-reached: "O limite de doação para [material] foi atingido." + limit-reached-all: "Todos estes blocos atingiram os seus limites de doação." limit-notice: "Alguns blocos estão sujeitos a limites|e não serão doados" hand: keyword: "mao" diff --git a/src/main/resources/locales/ru.yml b/src/main/resources/locales/ru.yml index 27189632..82d86cd3 100644 --- a/src/main/resources/locales/ru.yml +++ b/src/main/resources/locales/ru.yml @@ -64,6 +64,7 @@ island: gui-info: "Donate blocks to your island|Currently donated: [points] points|Warning: donated items are|destroyed and cannot be returned!" preview: "Points to add: [points]|These items will be destroyed!" limit-reached: "Лимит пожертвования для [material] достигнут." + limit-reached-all: "Все эти блоки достигли своих лимитов пожертвования." limit-notice: "Некоторые блоки подлежат ограничениям|и не будут пожертвованы" hand: keyword: "рука" diff --git a/src/main/resources/locales/tr.yml b/src/main/resources/locales/tr.yml index 710fde47..0b3cb557 100644 --- a/src/main/resources/locales/tr.yml +++ b/src/main/resources/locales/tr.yml @@ -68,6 +68,7 @@ island: gui-info: "Donate blocks to your island|Currently donated: [points] points|Warning: donated items are|destroyed and cannot be returned!" preview: "Points to add: [points]|These items will be destroyed!" limit-reached: "[material] için bağış limitine ulaşıldı." + limit-reached-all: "Bu blokların tümü bağış limitlerine ulaştı." limit-notice: "Bazı bloklar limitlere tabidir|ve bağışlanmayacaktır" hand: keyword: "el" diff --git a/src/main/resources/locales/uk.yml b/src/main/resources/locales/uk.yml index 81e040df..47f89d44 100644 --- a/src/main/resources/locales/uk.yml +++ b/src/main/resources/locales/uk.yml @@ -61,6 +61,7 @@ island: gui-info: "Пожертвуйте блоки своєму острову|Пожертвовано: [points] очок|Увага: пожертвувані предмети|знищуються і не можуть бути повернуті!" preview: "Очок буде додано: [points]|Ці предмети будуть знищені!" limit-reached: "Ліміт пожертвування для [material] досягнуто." + limit-reached-all: "Усі ці блоки досягли своїх лімітів пожертвування." limit-notice: "Деякі блоки підлягають обмеженням|і не будуть пожертвувані" hand: keyword: "рука" diff --git a/src/main/resources/locales/vi.yml b/src/main/resources/locales/vi.yml index 7a296346..642a1f22 100644 --- a/src/main/resources/locales/vi.yml +++ b/src/main/resources/locales/vi.yml @@ -67,6 +67,7 @@ island: gui-info: "Donate blocks to your island|Currently donated: [points] points|Warning: donated items are|destroyed and cannot be returned!" preview: "Points to add: [points]|These items will be destroyed!" limit-reached: "Đã đạt đến giới hạn quyên góp cho [material]." + limit-reached-all: "Tất cả các khối này đã đạt đến giới hạn quyên góp." limit-notice: "Một số khối có giới hạn|và sẽ không được quyên góp" hand: keyword: "tay" diff --git a/src/main/resources/locales/zh-CN.yml b/src/main/resources/locales/zh-CN.yml index eeecc961..6d6cba63 100644 --- a/src/main/resources/locales/zh-CN.yml +++ b/src/main/resources/locales/zh-CN.yml @@ -59,6 +59,7 @@ island: gui-info: "Donate blocks to your island|Currently donated: [points] points|Warning: donated items are|destroyed and cannot be returned!" preview: "Points to add: [points]|These items will be destroyed!" limit-reached: "[material] 的捐赠限额已达到。" + limit-reached-all: "这些方块均已达到捐赠限额。" limit-notice: "某些方块受到限制|将不会被捐赠" hand: keyword: "hand" diff --git a/src/test/java/world/bentobox/level/calculators/IslandLevelCalculatorTidyUpTest.java b/src/test/java/world/bentobox/level/calculators/IslandLevelCalculatorTidyUpTest.java index 9dfcf811..02c6e683 100644 --- a/src/test/java/world/bentobox/level/calculators/IslandLevelCalculatorTidyUpTest.java +++ b/src/test/java/world/bentobox/level/calculators/IslandLevelCalculatorTidyUpTest.java @@ -1,6 +1,7 @@ package world.bentobox.level.calculators; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; @@ -182,7 +183,7 @@ void belowStart_sqrtFormula() { // The exact interval isn't the point — we just want progress + remaining to be self-consistent // and the "remaining to next" not negative. long remaining = r.getPointsToNextLevel(); - assertEquals(true, remaining > 0, "pointsToNextLevel should be positive, got " + remaining); + assertTrue(remaining > 0, "pointsToNextLevel should be positive, got " + remaining); } @Test diff --git a/src/test/java/world/bentobox/level/commands/IslandDonateCommandTest.java b/src/test/java/world/bentobox/level/commands/IslandDonateCommandTest.java index 5945420f..fabd655e 100644 --- a/src/test/java/world/bentobox/level/commands/IslandDonateCommandTest.java +++ b/src/test/java/world/bentobox/level/commands/IslandDonateCommandTest.java @@ -62,6 +62,9 @@ protected void setUp() throws Exception { super.setUp(); when(addon.getManager()).thenReturn(manager); when(addon.getBlockConfig()).thenReturn(blockConfig); + // Mockito returns 0 (not null) for unstubbed Integer-returning methods, which would + // read as "limit of 0" — the real BlockConfig returns null when no limit is configured. + when(blockConfig.getLimit(any())).thenReturn(null); when(user.getUniqueId()).thenReturn(uuid); when(user.isPlayer()).thenReturn(true); @@ -330,4 +333,25 @@ void testExecuteInvPromptNoLimitNoticeWhenUnderCap() { verify(user, never()).getTranslation("island.donate.limit-notice"); } + + @Test + void testExecuteInvRejectsWhenEverythingAtLimit() { + // 64 cobblestone in inventory, limit 100, already donated 100 -> nothing donatable, + // no zero-point confirmation prompt, immediate limit-reached-all message + ItemStack cobble = mock(ItemStack.class); + when(cobble.getType()).thenReturn(Material.COBBLESTONE); + when(cobble.getAmount()).thenReturn(64); + + when(inventory.getStorageContents()).thenReturn(new ItemStack[] { cobble }); + when(blockConfig.getValue(any(), eq(Material.COBBLESTONE))).thenReturn(1); + when(blockConfig.getLimit(Material.COBBLESTONE)).thenReturn(100); + when(manager.getDonatedBlocks(island)).thenReturn(java.util.Map.of("COBBLESTONE", 100)); + + assertFalse(cmd.execute(user, "donate", List.of("inv"))); + + verify(user).sendMessage("island.donate.limit-reached-all"); + verify(user, never()).getTranslation(eq("island.donate.inv.confirm-total"), + anyString(), anyString()); + verify(manager, never()).donateBlocks(any(), any(UUID.class), anyString(), anyInt(), anyLong()); + } } diff --git a/src/test/java/world/bentobox/level/config/BlockConfigLimitsTest.java b/src/test/java/world/bentobox/level/config/BlockConfigLimitsTest.java new file mode 100644 index 00000000..6594874f --- /dev/null +++ b/src/test/java/world/bentobox/level/config/BlockConfigLimitsTest.java @@ -0,0 +1,64 @@ +package world.bentobox.level.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.File; +import java.nio.file.Files; + +import org.bukkit.Material; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import world.bentobox.level.CommonTestSetup; + +/** + * Verifies that {@link BlockConfig#getLimit(Object)} lookups are + * case-insensitive and agree regardless of how the caller identifies the + * block (Material, exact-case custom ID, or lowercased key) — the level + * calculation lowercases keys while the donation paths pass IDs as-is, so + * both must resolve to the same limit. + */ +class BlockConfigLimitsTest extends CommonTestSetup { + + private BlockConfig blockConfig; + + @BeforeEach + @Override + protected void setUp() throws Exception { + super.setUp(); + YamlConfiguration config = new YamlConfiguration(); + config.set("limits.COBBLESTONE", 100); // old-style material key + config.set("limits.MyCrystal", 5); // mixed-case custom block ID + config.set("limits.oraxen:my_gem", 7); // namespaced custom block ID + File file = Files.createTempFile("blockconfig", ".yml").toFile(); + file.deleteOnExit(); + blockConfig = new BlockConfig(addon, config, file); + } + + @Test + @DisplayName("Material lookup finds a limit declared with an old-style uppercase key") + void materialLookup() { + assertEquals(100, blockConfig.getLimit(Material.COBBLESTONE)); + } + + @Test + @DisplayName("String lookups find limits regardless of case") + void stringLookupCaseInsensitive() { + assertEquals(100, blockConfig.getLimit("cobblestone")); + assertEquals(100, blockConfig.getLimit("COBBLESTONE")); + assertEquals(5, blockConfig.getLimit("MyCrystal")); + assertEquals(5, blockConfig.getLimit("mycrystal")); + assertEquals(7, blockConfig.getLimit("oraxen:my_gem")); + assertEquals(7, blockConfig.getLimit("ORAXEN:MY_GEM")); + } + + @Test + @DisplayName("Unknown keys have no limit") + void unknownKey() { + assertNull(blockConfig.getLimit("diamond_block")); + assertNull(blockConfig.getLimit(Material.DIAMOND_BLOCK)); + } +} diff --git a/src/test/java/world/bentobox/level/util/UtilsCalculateDonatedPointsTest.java b/src/test/java/world/bentobox/level/util/UtilsCalculateDonatedPointsTest.java new file mode 100644 index 00000000..8aaeff6a --- /dev/null +++ b/src/test/java/world/bentobox/level/util/UtilsCalculateDonatedPointsTest.java @@ -0,0 +1,69 @@ +package world.bentobox.level.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Map; + +import org.bukkit.World; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import world.bentobox.level.config.BlockConfig; + +/** + * Unit tests for {@link Utils#calculateDonatedPoints(BlockConfig, World, Map)} — + * the single source of truth for how donated blocks contribute to the level. + */ +class UtilsCalculateDonatedPointsTest { + + private BlockConfig blockConfig; + private World world; + + @BeforeEach + void setUp() { + blockConfig = mock(BlockConfig.class); + world = mock(World.class); + // Mockito returns 0 (not null) for unstubbed Integer-returning methods, which would + // read as "limit of 0" — the real BlockConfig returns null when no limit is configured. + when(blockConfig.getLimit(any())).thenReturn(null); + } + + @Test + @DisplayName("Uses current block values and lowercases the donation key") + void usesCurrentValues() { + when(blockConfig.getValue(any(), eq("iron_block"))).thenReturn(3); + assertEquals(1500L, Utils.calculateDonatedPoints(blockConfig, world, Map.of("IRON_BLOCK", 500))); + } + + @Test + @DisplayName("Caps each block type at its current limit") + void capsAtLimit() { + when(blockConfig.getValue(any(), eq("iron_block"))).thenReturn(3); + when(blockConfig.getLimit("iron_block")).thenReturn(200); + assertEquals(600L, Utils.calculateDonatedPoints(blockConfig, world, Map.of("IRON_BLOCK", 500))); + } + + @Test + @DisplayName("Blocks with no configured value contribute nothing") + void noValueMeansZero() { + when(blockConfig.getValue(any(), anyString())).thenReturn(null); + assertEquals(0L, Utils.calculateDonatedPoints(blockConfig, world, Map.of("IRON_BLOCK", 500))); + } + + @Test + @DisplayName("Sums across multiple donated block types") + void sumsAcrossTypes() { + when(blockConfig.getValue(any(), eq("iron_block"))).thenReturn(3); + when(blockConfig.getValue(any(), eq("oraxen:my_gem"))).thenReturn(10); + when(blockConfig.getLimit("oraxen:my_gem")).thenReturn(2); + // 100 * 3 + min(5, 2) * 10 = 320 + assertEquals(320L, Utils.calculateDonatedPoints(blockConfig, world, + Map.of("IRON_BLOCK", 100, "oraxen:my_gem", 5))); + } +}