From 145022fd5c1ebaf61519c32f1900a56c9423c488 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 5 Apr 2026 11:39:33 +0000
Subject: [PATCH 1/9] Initial plan
From 739e22649a813d28eaf8fd96af4145db0789e4a8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 5 Apr 2026 11:41:08 +0000
Subject: [PATCH 2/9] Add Russian locale file (MiniMessage format)
Agent-Logs-Url: https://github.com/BentoBoxWorld/TopBlock/sessions/8bbe2987-7542-416b-8201-ecded7380737
Co-authored-by: tastybento <4407265+tastybento@users.noreply.github.com>
---
src/main/resources/locales/ru.yml | 66 +++++++++++++++++++++++++++++++
1 file changed, 66 insertions(+)
create mode 100644 src/main/resources/locales/ru.yml
diff --git a/src/main/resources/locales/ru.yml b/src/main/resources/locales/ru.yml
new file mode 100644
index 0000000..6ffcf76
--- /dev/null
+++ b/src/main/resources/locales/ru.yml
@@ -0,0 +1,66 @@
+# ######################################################################################## #
+# Это YML файл. Будьте осторожны при редактировании. Проверяйте свои правки #
+# в YAML валидаторе, например, на http://yaml-online-parser.appspot.com #
+# ######################################################################################## #
+
+island:
+ topblock:
+ description: показать десять лучших по AOneBlock
+ gui-title: 'Десятка лучших'
+ gui-heading: '[name]: [rank]'
+ island-level: 'Количество [count]'
+
+topblock:
+ gui:
+ titles:
+ top: 'Топ островов'
+ detail-panel: 'Остров [name]'
+ value-panel: 'Ценность блоков'
+ buttons:
+ island:
+ empty: '[name]. место'
+ name: ' [name]'
+ description: |-
+ [owner]
+ [members]
+ [place]
+ [count]
+ [lifetime]
+ # Текст, заменяющий [name], если у острова нет названия.
+ owners-island: 'Остров [player]'
+ # Текст для [owner] в описании.
+ owner: 'Владелец: [player]'
+ # Заголовок перед списком участников для [members] в описании.
+ members-title: 'Участники:'
+ # Список каждого участника под заголовком для [members] в описании.
+ member: ' - [player]'
+ # Имя неизвестного игрока.
+ unknown: неизвестно
+ # Секция для парсинга [place]
+ place: '[number]. место'
+ # Секция для парсинга [count]
+ count: 'Количество блоков: [number]'
+ # Секция для парсинга [lifetime]
+ lifetime: 'Количество за всё время: [number]'
+ # Кнопка, используемая в многостраничных GUI для возврата на предыдущую страницу.
+ previous:
+ name: 'Предыдущая страница'
+ description: 'Переключиться на страницу [number]'
+ # Кнопка, используемая в многостраничных GUI для перехода на следующую страницу.
+ next:
+ name: 'Следующая страница'
+ description: 'Переключиться на страницу [number]'
+ tips:
+ click-to-view: 'Нажмите для просмотра.'
+ click-to-previous: 'Нажмите для просмотра предыдущей страницы.'
+ click-to-next: 'Нажмите для просмотра следующей страницы.'
+ click-to-select: 'Нажмите для выбора.'
+ left-click-to-cycle-up: 'ЛКМ для перебора вверх.'
+ right-click-to-cycle-down: 'ПКМ для перебора вниз.'
+ left-click-to-change: 'ЛКМ для редактирования.'
+ right-click-to-clear: 'ПКМ для очистки.'
+ click-to-asc: 'Нажмите для сортировки по возрастанию.'
+ click-to-desc: 'Нажмите для сортировки по убыванию.'
+ click-to-warp: 'Нажмите для телепортации.'
+ click-to-visit: 'Нажмите для посещения.'
+ right-click-to-visit: 'ПКМ для посещения.'
From c2bf4375a6bb8e322dd82a3571c14331af6813d5 Mon Sep 17 00:00:00 2001
From: tastybento
Date: Sat, 25 Apr 2026 20:39:14 -0700
Subject: [PATCH 3/9] Fix top ten panel showing no head or stats
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The @EventHandler on TopBlockManager.startMonitoring was declared
private, so Bukkit silently skipped it. BentoBoxReadyEvent never
fired the refresh task, the topTen list stayed empty, and the panel
only ever rendered fallback (LIME_STAINED_GLASS_PANE) items — no
player head, no stats. Renamed to public onBentoBoxReady.
Also removed dead code surfaced while diffing against the Level
addon: the large commented-out WARP/VISIT/VIEW block in
TopLevelPanel (TopBlock has no warp/visit hooks), the unused
ConversationUtils class, and the unused helpers in Utils
(sendMessage, getNextValue, getPreviousValue, prettifyObject,
prettifyDescription).
Co-Authored-By: Claude Opus 4.7
---
.../bentobox/topblock/TopBlockManager.java | 2 +-
.../topblock/panels/TopLevelPanel.java | 453 ++++--------------
.../topblock/util/ConversationUtils.java | 120 -----
.../world/bentobox/topblock/util/Utils.java | 197 +-------
4 files changed, 108 insertions(+), 664 deletions(-)
delete mode 100644 src/main/java/world/bentobox/topblock/util/ConversationUtils.java
diff --git a/src/main/java/world/bentobox/topblock/TopBlockManager.java b/src/main/java/world/bentobox/topblock/TopBlockManager.java
index 1039a01..d951749 100644
--- a/src/main/java/world/bentobox/topblock/TopBlockManager.java
+++ b/src/main/java/world/bentobox/topblock/TopBlockManager.java
@@ -67,7 +67,7 @@ public TopBlockManager(TopBlock addon) {
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
- private void startMonitoring(BentoBoxReadyEvent e) {
+ public void onBentoBoxReady(BentoBoxReadyEvent e) {
// Load the top ten from AOneBlock every so often
Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), () -> {
// Update TopTen
diff --git a/src/main/java/world/bentobox/topblock/panels/TopLevelPanel.java b/src/main/java/world/bentobox/topblock/panels/TopLevelPanel.java
index dcaa634..1347f47 100644
--- a/src/main/java/world/bentobox/topblock/panels/TopLevelPanel.java
+++ b/src/main/java/world/bentobox/topblock/panels/TopLevelPanel.java
@@ -27,96 +27,84 @@
/**
- * This panel opens top likes panel
+ * This panel opens the top ten panel for AOneBlock.
*/
-public class TopLevelPanel
-{
- // ---------------------------------------------------------------------
- // Section: Internal Constructor
- // ---------------------------------------------------------------------
+public class TopLevelPanel {
+ private static final String REFERENCE = "topblock.gui.buttons.island.";
+ private static final String PLAYER = "[player]";
- /**
- * This is internal constructor. It is used internally in current class to avoid creating objects everywhere.
- *
- * @param addon Level object.
- * @param user User who opens Panel.
- * @param world World where gui is opened
- * @param permissionPrefix Permission Prefix
- */
- private TopLevelPanel(TopBlock addon, User user, World world, String permissionPrefix)
- {
+ private final TopBlock addon;
+ private final User user;
+ private final World world;
+ private final String iconPermission;
+ private final List topIslands;
+
+
+ private TopLevelPanel(TopBlock addon, User user, World world, String permissionPrefix) {
this.addon = addon;
this.user = user;
this.world = world;
-
this.iconPermission = permissionPrefix + "topblock.icon";
-
this.topIslands = this.addon.getManager().getTopTen(TopBlock.TEN);
}
/**
- * Build method manages current panel opening. It uses BentoBox PanelAPI that is easy to use and users can get nice
- * panels.
+ * Open the panel for a user.
*/
- public void build()
- {
- TemplatedPanelBuilder panelBuilder = new TemplatedPanelBuilder();
+ public static void openPanel(TopBlock addon, User user, World world, String permissionPrefix) {
+ new TopLevelPanel(addon, user, world, permissionPrefix).build();
+ }
+
+ private void build() {
+ TemplatedPanelBuilder panelBuilder = new TemplatedPanelBuilder();
panelBuilder.user(this.user);
panelBuilder.world(this.world);
-
panelBuilder.template("top_panel", new File(this.addon.getDataFolder(), "panels"));
-
- //panelBuilder.registerTypeBuilder("VIEW", this::createViewerButton);
panelBuilder.registerTypeBuilder("TOP", this::createPlayerButton);
-
- // Register unknown type builder.
panelBuilder.build();
}
- // ---------------------------------------------------------------------
- // Section: Methods
- // ---------------------------------------------------------------------
+ private PanelItem createPlayerButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot itemSlot) {
+ int index = (int) template.dataMap().getOrDefault("index", 0);
+
+ if (index < 1) {
+ return this.createFallback(template.fallback(), index);
+ }
+ TopTenData record = this.topIslands.size() < index ? null : this.topIslands.get(index - 1);
- /**
- * Creates fallback based on template.
- * @param template Template record for fallback button.
- * @param index Place of the fallback.
- * @return Fallback panel item.
- */
- private PanelItem createFallback(ItemTemplateRecord template, long index)
- {
- if (template == null)
- {
- return null;
+ if (record == null) {
+ return this.createFallback(template.fallback(), index);
}
+ return this.createIslandIcon(template, record, index);
+ }
+
+ private PanelItem createFallback(ItemTemplateRecord template, long index) {
+ if (template == null) {
+ return null;
+ }
PanelItemBuilder builder = new PanelItemBuilder();
- if (template.icon() != null)
- {
+ if (template.icon() != null) {
builder.icon(template.icon().clone());
}
- if (template.title() != null)
- {
+ if (template.title() != null) {
builder.name(this.user.getTranslation(this.world, template.title(),
TextVariables.NAME, String.valueOf(index)));
- }
- else
- {
+ } else {
builder.name(this.user.getTranslation(this.world, REFERENCE,
TextVariables.NAME, String.valueOf(index)));
}
- if (template.description() != null)
- {
+ if (template.description() != null) {
builder.description(this.user.getTranslation(this.world, template.description(),
TextVariables.NUMBER, String.valueOf(index)));
}
@@ -127,45 +115,10 @@ private PanelItem createFallback(ItemTemplateRecord template, long index)
}
- /**
- * This method creates player icon with warp functionality.
- *
- * @return PanelItem for PanelBuilder.
- */
- private PanelItem createPlayerButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot itemSlot)
- {
- int index = (int) template.dataMap().getOrDefault("index", 0);
+ private PanelItem createIslandIcon(ItemTemplateRecord template, TopTenData record, int index) {
+ Island island = record.island();
- if (index < 1)
- {
- return this.createFallback(template.fallback(), index);
- }
-
- TopTenData islandTopRecord = this.topIslands.size() < index ? null : this.topIslands.get(index - 1);
-
- if (islandTopRecord == null)
- {
- return this.createFallback(template.fallback(), index);
- }
-
- return this.createIslandIcon(template, islandTopRecord, index);
- }
-
-
- /**
- * This method creates button from template for given island top record.
- * @param template Icon Template.
- * @param islandTopRecord Island Top Record.
- * @param index Place Index.
- * @return PanelItem for PanelBuilder.
- */
- private PanelItem createIslandIcon(ItemTemplateRecord template, TopTenData islandTopRecord, int index)
- {
- // Get player island.
- Island island = islandTopRecord.island();
-
- if (island == null)
- {
+ if (island == null) {
return this.createFallback(template.fallback(), index);
}
@@ -173,327 +126,109 @@ private PanelItem createIslandIcon(ItemTemplateRecord template, TopTenData islan
this.populateIslandIcon(builder, template, island);
this.populateIslandTitle(builder, template, island);
- this.populateIslandDescription(builder, template, island, islandTopRecord, index);
+ this.populateIslandDescription(builder, template, island, record, index);
builder.amount(index);
- /*
- // Get only possible actions, by removing all inactive ones.
- List activeActions = new ArrayList<>(template.actions());
-
- activeActions.removeIf(action ->
- {
- switch (action.actionType().toUpperCase())
- {
- case "WARP" -> {
- return island.getOwner() == null ||
- this.addon.getWarpHook() == null ||
- !this.addon.getWarpHook().getWarpSignsManager().hasWarp(this.world, island.getOwner());
- }
- case "VISIT" -> {
- return island.getOwner() == null ||
- this.addon.getVisitHook() == null ||
- !this.addon.getVisitHook().getAddonManager().preprocessTeleportation(this.user, island);
- }
- case "VIEW" -> {
- return island.getOwner() == null ||
- !island.getMemberSet(RanksManager.MEMBER_RANK).contains(this.user.getUniqueId());
- }
- default -> {
- return false;
- }
- }
- });
-
- // Add Click handler
- builder.clickHandler((panel, user, clickType, i) ->
- {
- for (ItemTemplateRecord.ActionRecords action : activeActions)
- {
- if (clickType == action.clickType() || action.clickType() == ClickType.UNKNOWN)
- {
- switch (action.actionType().toUpperCase())
- {
- case "WARP" -> {
- this.user.closeInventory();
- this.addon.getWarpHook().getWarpSignsManager().warpPlayer(this.world, this.user, island.getOwner());
- }
- case "VISIT" -> {
- // The command call implementation solves necessity to check for all visits options,
- // like cool down, confirmation and preprocess in single go. Would it be better to write
- // all logic here?
-
- this.addon.getPlugin().getIWM().getAddon(this.world).
- flatMap(GameModeAddon::getPlayerCommand).ifPresent(command ->
- {
- String mainCommand =
- this.addon.getVisitHook().getSettings().getPlayerMainCommand();
-
- if (!mainCommand.isBlank())
- {
- this.user.closeInventory();
- this.user.performCommand(command.getTopLabel() + " " + mainCommand + " " + island.getOwner());
- }
- });
- }
- case "VIEW" -> {
- this.user.closeInventory();
- // Open Detailed GUI.
- DetailsPanel.openPanel(this.addon, this.world, this.user);
- }
- }
- }
- }
- return true;
- });
-
- // Collect tooltips.
- List tooltips = activeActions.stream().
- filter(action -> action.tooltip() != null).
- map(action -> this.user.getTranslation(this.world, action.tooltip())).
- filter(text -> !text.isBlank()).
- collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size())));
-
- // Add tooltips.
- if (!tooltips.isEmpty())
- {
- // Empty line and tooltips.
- builder.description("");
- builder.description(tooltips);
- }
- */
return builder.build();
}
- /**
- * Populate given panel item builder name with values from template and island objects.
- *
- * @param builder the builder
- * @param template the template
- * @param island the island
- */
- private void populateIslandTitle(PanelItemBuilder builder,
- ItemTemplateRecord template,
- Island island)
- {
- // Get Island Name
+ private void populateIslandTitle(PanelItemBuilder builder, ItemTemplateRecord template, Island island) {
String nameText;
- if (island.getName() == null || island.getName().isEmpty())
- {
+ if (island.getName() == null || island.getName().isEmpty()) {
nameText = this.user.getTranslation(REFERENCE + "owners-island", PLAYER,
- island.getOwner() == null ?
- this.user.getTranslation(REFERENCE + "unknown") :
- this.addon.getPlayers().getName(island.getOwner()));
- }
- else
- {
+ island.getOwner() == null
+ ? this.user.getTranslation(REFERENCE + "unknown")
+ : this.addon.getPlayers().getName(island.getOwner()));
+ } else {
nameText = island.getName();
}
- // Template specific title is always more important than custom one.
- if (template.title() != null && !template.title().isBlank())
- {
+ if (template.title() != null && !template.title().isBlank()) {
builder.name(this.user.getTranslation(this.world, template.title(),
TextVariables.NAME, nameText));
- }
- else
- {
+ } else {
builder.name(this.user.getTranslation(REFERENCE + "name", TextVariables.NAME, nameText));
}
}
- /**
- * Populate given panel item builder icon with values from template and island objects.
- *
- * @param builder the builder
- * @param template the template
- * @param island the island
- */
- private void populateIslandIcon(PanelItemBuilder builder,
- ItemTemplateRecord template,
- Island island)
- {
+ private void populateIslandIcon(PanelItemBuilder builder, ItemTemplateRecord template, Island island) {
User owner = island.getOwner() == null ? null : User.getInstance(island.getOwner());
- // Get permission or island icon
- String permissionIcon = owner == null ? null :
- Utils.getPermissionValue(owner, this.iconPermission, null);
+ String permissionIcon = owner == null ? null
+ : Utils.getPermissionValue(owner, this.iconPermission, null);
- Material material;
+ Material material = (permissionIcon != null && !permissionIcon.equals("*"))
+ ? Material.matchMaterial(permissionIcon)
+ : null;
- if (permissionIcon != null && !permissionIcon.equals("*"))
- {
- material = Material.matchMaterial(permissionIcon);
- }
- else
- {
- material = null;
- }
-
- if (material != null)
- {
- if (!material.equals(Material.PLAYER_HEAD))
- {
+ if (material != null) {
+ if (!material.equals(Material.PLAYER_HEAD)) {
builder.icon(material);
- }
- else
- {
+ } else {
builder.icon(owner.getName());
}
- }
- else if (template.icon() != null)
- {
+ } else if (template.icon() != null) {
builder.icon(template.icon().clone());
- }
- else if (owner != null)
- {
+ } else if (owner != null) {
builder.icon(owner.getName());
- }
- else
- {
+ } else {
builder.icon(Material.PLAYER_HEAD);
}
}
- /**
- * Populate given panel item builder description with values from template and island objects.
- *
- * @param builder the builder
- * @param template the template
- * @param island the island
- * @param islandTopRecord the top record object
- * @param index place index.
- */
- private void populateIslandDescription(PanelItemBuilder builder,
- ItemTemplateRecord template,
- Island island,
- TopTenData islandTopRecord,
- int index)
- {
- // Get Owner Name
+ private void populateIslandDescription(PanelItemBuilder builder, ItemTemplateRecord template,
+ Island island, TopTenData record, int index) {
+
String ownerText = this.user.getTranslation(REFERENCE + "owner", PLAYER,
- island.getOwner() == null ?
- this.user.getTranslation(REFERENCE + "unknown") :
- this.addon.getPlayers().getName(island.getOwner()));
+ island.getOwner() == null
+ ? this.user.getTranslation(REFERENCE + "unknown")
+ : this.addon.getPlayers().getName(island.getOwner()));
- // Get Members Text
String memberText;
-
- if (island.getMemberSet().size() > 1)
- {
+ if (island.getMemberSet().size() > 1) {
StringBuilder memberBuilder = new StringBuilder(
this.user.getTranslationOrNothing(REFERENCE + "members-title"));
-
- for (UUID uuid : island.getMemberSet())
- {
+ for (UUID uuid : island.getMemberSet()) {
User u = User.getInstance(uuid);
-
- if (memberBuilder.length() > 0)
- {
+ if (memberBuilder.length() > 0) {
memberBuilder.append("\n");
}
-
- memberBuilder.append(
- this.user.getTranslationOrNothing(REFERENCE + "member",
- PLAYER, u.getName()));
+ memberBuilder.append(this.user.getTranslationOrNothing(REFERENCE + "member",
+ PLAYER, u.getName()));
}
-
memberText = memberBuilder.toString();
- }
- else
- {
+ } else {
memberText = "";
}
String placeText = this.user.getTranslation(REFERENCE + "place",
TextVariables.NUMBER, String.valueOf(index));
- String levelText = this.user.getTranslation(REFERENCE + "count",
- TextVariables.NUMBER, this.addon.getManager().formatLevel((long)islandTopRecord.blockNumber()));
+ String countText = this.user.getTranslation(REFERENCE + "count",
+ TextVariables.NUMBER, this.addon.getManager().formatLevel((long) record.blockNumber()));
String lifetimeText = this.user.getTranslation(REFERENCE + "lifetime",
- TextVariables.NUMBER, this.addon.getManager().formatLevel(islandTopRecord.lifetime()));
-
- // Template specific description is always more important than custom one.
- if (template.description() != null && !template.description().isBlank())
- {
- builder.description(this.user.getTranslation(this.world, template.description(),
- "[owner]", ownerText,
- "[members]", memberText,
- "[count]", levelText,
- "[lifetime]", lifetimeText,
- "[place]", placeText).
- replaceAll("(?m)^[ \\t]*\\r?\\n", "").
- replaceAll("(? topIslands;
}
diff --git a/src/main/java/world/bentobox/topblock/util/ConversationUtils.java b/src/main/java/world/bentobox/topblock/util/ConversationUtils.java
deleted file mode 100644
index 26fba78..0000000
--- a/src/main/java/world/bentobox/topblock/util/ConversationUtils.java
+++ /dev/null
@@ -1,120 +0,0 @@
-//
-// Created by BONNe
-// Copyright - 2021
-//
-
-
-package world.bentobox.topblock.util;
-
-
-import org.bukkit.conversations.*;
-import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
-import java.util.function.Consumer;
-
-import world.bentobox.bentobox.BentoBox;
-import world.bentobox.bentobox.api.user.User;
-
-
-public class ConversationUtils
-{
- private ConversationUtils() {}
- // ---------------------------------------------------------------------
- // Section: Conversation API implementation
- // ---------------------------------------------------------------------
-
-
- /**
- * This method will close opened gui and writes question in chat. After players answers on question in chat, message
- * will trigger consumer and gui will reopen.
- *
- * @param consumer Consumer that accepts player output text.
- * @param question Message that will be displayed in chat when player triggers conversion.
- * @param user User who is targeted with current confirmation.
- */
- public static void createStringInput(Consumer consumer,
- User user,
- @NonNull String question,
- @Nullable String successMessage)
- {
- // Text input message.
- StringPrompt stringPrompt = new StringPrompt()
- {
- @Override
- public @NonNull String getPromptText(@NonNull ConversationContext context)
- {
- user.closeInventory();
- return question;
- }
-
-
- @Override
- public @NonNull Prompt acceptInput(@NonNull ConversationContext context, @Nullable String input)
- {
- consumer.accept(input);
- return ConversationUtils.endMessagePrompt(successMessage);
- }
- };
-
- new ConversationFactory(BentoBox.getInstance()).
- withPrefix(context -> user.getTranslation("level.conversations.prefix")).
- withFirstPrompt(stringPrompt).
- // On cancel conversation will be closed.
- withLocalEcho(false).
- withTimeout(90).
- withEscapeSequence(user.getTranslation("level.conversations.cancel-string")).
- // Use null value in consumer to detect if user has abandoned conversation.
- addConversationAbandonedListener(ConversationUtils.getAbandonListener(consumer, user)).
- buildConversation(user.getPlayer()).
- begin();
- }
-
-
- /**
- * This is just a simple end message prompt that displays requested message.
- *
- * @param message Message that will be displayed.
- * @return MessagePrompt that displays given message and exists from conversation.
- */
- private static MessagePrompt endMessagePrompt(@Nullable String message)
- {
- return new MessagePrompt()
- {
- @Override
- public @NonNull String getPromptText(@NonNull ConversationContext context)
- {
- return message == null ? "" : message;
- }
-
-
- @Override
- protected @Nullable Prompt getNextPrompt(@NonNull ConversationContext context)
- {
- return Prompt.END_OF_CONVERSATION;
- }
- };
- }
-
-
- /**
- * This method creates and returns abandon listener for every conversation.
- *
- * @param consumer Consumer which must return null value.
- * @param user User who was using conversation.
- * @return ConversationAbandonedListener instance.
- */
- private static ConversationAbandonedListener getAbandonListener(Consumer> consumer, User user)
- {
- return abandonedEvent ->
- {
- if (!abandonedEvent.gracefulExit())
- {
- consumer.accept(null);
- // send cancell message
- abandonedEvent.getContext().getForWhom().sendRawMessage(
- user.getTranslation("level.conversations.prefix") +
- user.getTranslation("level.conversations.cancelled"));
- }
- };
- }
-}
diff --git a/src/main/java/world/bentobox/topblock/util/Utils.java b/src/main/java/world/bentobox/topblock/util/Utils.java
index 7794f47..b8e7a2b 100644
--- a/src/main/java/world/bentobox/topblock/util/Utils.java
+++ b/src/main/java/world/bentobox/topblock/util/Utils.java
@@ -3,37 +3,19 @@
// Copyright - 2021
//
-
package world.bentobox.topblock.util;
-
-import org.bukkit.Material;
-import org.bukkit.permissions.PermissionAttachmentInfo;
import java.util.List;
import java.util.stream.Collectors;
-import world.bentobox.bentobox.api.user.User;
-import world.bentobox.bentobox.hooks.LangUtilsHook;
+import org.bukkit.permissions.PermissionAttachmentInfo;
+import world.bentobox.bentobox.api.user.User;
-public class Utils
-{
- private static final String LEVEL_MATERIALS = "level.materials.";
+public class Utils {
private Utils() {}
- /**
- * This method sends a message to the user with appended "prefix" text before message.
- * @param user User who receives message.
- * @param translationText Translation text of the message.
- * @param parameters Parameters for the translation text.
- */
- public static void sendMessage(User user, String translationText, String... parameters)
- {
- user.sendMessage(user.getTranslation( "level.conversations.prefix") +
- user.getTranslation( translationText, parameters));
- }
-
/**
* This method gets string value of given permission prefix. If user does not have given permission or it have all
@@ -44,34 +26,28 @@ public static void sendMessage(User user, String translationText, String... para
* @param defaultValue Default value that will be returned if permission not found.
* @return String value that follows permissionPrefix.
*/
- public static String getPermissionValue(User user, String permissionPrefix, String defaultValue)
- {
- if (user.isPlayer())
- {
- if (permissionPrefix.endsWith("."))
- {
+ public static String getPermissionValue(User user, String permissionPrefix, String defaultValue) {
+ if (user.isPlayer()) {
+ if (permissionPrefix.endsWith(".")) {
permissionPrefix = permissionPrefix.substring(0, permissionPrefix.length() - 1);
}
String permPrefix = permissionPrefix + ".";
- List permissions = user.getEffectivePermissions().stream().
- map(PermissionAttachmentInfo::getPermission).
- filter(permission -> permission.startsWith(permPrefix)).
- collect(Collectors.toList());
+ List permissions = user.getEffectivePermissions().stream()
+ .map(PermissionAttachmentInfo::getPermission)
+ .filter(permission -> permission.startsWith(permPrefix))
+ .collect(Collectors.toList());
- for (String permission : permissions)
- {
- if (permission.contains(permPrefix + "*"))
- {
+ for (String permission : permissions) {
+ if (permission.contains(permPrefix + "*")) {
// * means all. So continue to search more specific.
continue;
}
String[] parts = permission.split(permPrefix);
- if (parts.length > 1)
- {
+ if (parts.length > 1) {
return parts[1];
}
}
@@ -79,151 +55,4 @@ public static String getPermissionValue(User user, String permissionPrefix, Stri
return defaultValue;
}
-
-
- /**
- * This method allows to get next value from array list after given value.
- *
- * @param values Array that should be searched for given value.
- * @param currentValue Value which next element should be found.
- * @param Instance of given object.
- * @return Next value after currentValue in values array.
- */
- public static T getNextValue(T[] values, T currentValue)
- {
- for (int i = 0; i < values.length; i++)
- {
- if (values[i].equals(currentValue))
- {
- if (i + 1 == values.length)
- {
- return values[0];
- }
- else
- {
- return values[i + 1];
- }
- }
- }
-
- return currentValue;
- }
-
-
- /**
- * This method allows to get previous value from array list after given value.
- *
- * @param values Array that should be searched for given value.
- * @param currentValue Value which previous element should be found.
- * @param Instance of given object.
- * @return Previous value before currentValue in values array.
- */
- public static T getPreviousValue(T[] values, T currentValue)
- {
- for (int i = 0; i < values.length; i++)
- {
- if (values[i].equals(currentValue))
- {
- if (i > 0)
- {
- return values[i - 1];
- }
- else
- {
- return values[values.length - 1];
- }
- }
- }
-
- return currentValue;
- }
-
-
- /**
- * Prettify Material object for user.
- * @param object Object that must be pretty.
- * @param user User who will see the object.
- * @return Prettified string for Material.
- */
- public static String prettifyObject(Material object, User user)
- {
- // Nothing to translate
- if (object == null)
- {
- return "";
- }
-
- // Find addon structure with:
- // [addon]:
- // materials:
- // [material]:
- // name: [name]
- String translation = user.getTranslationOrNothing(LEVEL_MATERIALS + object.name().toLowerCase() + ".name");
-
- if (!translation.isEmpty())
- {
- // We found our translation.
- return translation;
- }
-
- // Find addon structure with:
- // [addon]:
- // materials:
- // [material]: [name]
-
- translation = user.getTranslationOrNothing(LEVEL_MATERIALS + object.name().toLowerCase());
-
- if (!translation.isEmpty())
- {
- // We found our translation.
- return translation;
- }
-
- // Find general structure with:
- // materials:
- // [material]: [name]
-
- translation = user.getTranslationOrNothing("materials." + object.name().toLowerCase());
-
- if (!translation.isEmpty())
- {
- // We found our translation.
- return translation;
- }
-
- // Use Lang Utils Hook to translate material
- return LangUtilsHook.getMaterialName(object, user);
- }
-
-
- /**
- * Prettify Material object description for user.
- * @param object Object that must be pretty.
- * @param user User who will see the object.
- * @return Prettified description string for Material.
- */
- public static String prettifyDescription(Material object, User user)
- {
- // Nothing to translate
- if (object == null)
- {
- return "";
- }
-
- // Find addon structure with:
- // [addon]:
- // materials:
- // [material]:
- // description: [text]
- String translation = user.getTranslationOrNothing(LEVEL_MATERIALS + object.name().toLowerCase() + ".description");
-
- if (!translation.isEmpty())
- {
- // We found our translation.
- return translation;
- }
-
- // No text to return.
- return "";
- }
}
From 622eb32d213302f54026e5532949213deaee392b Mon Sep 17 00:00:00 2001
From: tastybento
Date: Sat, 25 Apr 2026 20:39:26 -0700
Subject: [PATCH 4/9] Modernise to Java 21, Paper 1.21.11, BentoBox 3.14.0
- Java 17 -> 21
- Spigot -> Paper 1.21.11-R0.1-SNAPSHOT
- BentoBox 2.7.1-SNAPSHOT -> 3.14.0-SNAPSHOT
- AOneBlock 1.12.3-SNAPSHOT -> 1.18.0
- Drop PowerMock; add JUnit 5 + MockBukkit + Mockito 5 deps
- Maven plugins refreshed (compiler 3.15.0 with fork=true,
surefire 3.5.2, shade 3.6.0, jar 3.4.2, javadoc 3.11.2,
source 3.3.1, install/deploy 3.1.3, jacoco 0.8.12)
- Add papermc, codemc-snapshots and jitpack repos; drop spigot repo
- Bump build.version to 2.0.0
- TopBlockPladdon: cache the Addon instance instead of building a
fresh one on every getAddon() call
- Fix typo in plugin.yml api-version (`"1.21""` -> `"1.21"`)
Co-Authored-By: Claude Opus 4.7
---
pom.xml | 158 ++++++++++--------
.../bentobox/topblock/TopBlockPladdon.java | 8 +-
src/main/resources/plugin.yml | 2 +-
3 files changed, 96 insertions(+), 72 deletions(-)
diff --git a/pom.xml b/pom.xml
index 1e61b93..40b67a5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -50,14 +50,16 @@
UTF-8
UTF-8
- 17
+ 21
- 2.0.9
+ 5.10.2
+ 5.11.0
+ v1.21-SNAPSHOT
- 1.21.3-R0.1-SNAPSHOT
- 2.7.1-SNAPSHOT
+ 1.21.11-R0.1-SNAPSHOT
+ 3.14.0-SNAPSHOT
- 1.12.3-SNAPSHOT
+ 1.18.0
1.1.0
@@ -65,7 +67,7 @@
-LOCAL
- 1.1.0
+ 2.0.0
BentoBoxWorld_TopBlock
bentobox-world
https://sonarcloud.io
@@ -75,7 +77,7 @@
-
ci
@@ -89,13 +91,13 @@
-
-
-
-
master
@@ -122,8 +124,8 @@
- spigot-repo
- https://hub.spigotmc.org/nexus/content/repositories/snapshots
+ papermc
+ https://repo.papermc.io/repository/maven-public/
bentoboxworld
@@ -134,38 +136,23 @@
https://repo.codemc.org/repository/maven-public/
- codemc-public
- https://repo.codemc.org/repository/maven-public/
+ codemc
+ https://repo.codemc.org/repository/maven-snapshots/
+
+
+ jitpack.io
+ https://jitpack.io
-
+
- org.spigotmc
- spigot-api
- ${spigot.version}
+ io.papermc.paper
+ paper-api
+ ${paper.version}
provided
-
-
- org.mockito
- mockito-core
- 3.11.1
- test
-
-
- org.powermock
- powermock-module-junit4
- ${powermock.version}
- test
-
-
- org.powermock
- powermock-api-mockito2
- ${powermock.version}
- test
-
world.bentobox
bentobox
@@ -184,24 +171,57 @@
${panelutils.version}
-
org.eclipse.jdt
org.eclipse.jdt.annotation
2.2.600
+
+
+ com.github.MockBukkit
+ MockBukkit
+ ${mock-bukkit.version}
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit.version}
+ test
+
+
+
+ org.mockito
+ mockito-core
+ ${mockito.version}
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ ${mockito.version}
+ test
+
-
-
${project.name}-${revision}${build.number}
@@ -221,52 +241,50 @@
org.apache.maven.plugins
maven-clean-plugin
- 3.1.0
+ 3.4.1
org.apache.maven.plugins
maven-resources-plugin
- 3.1.0
+ 3.3.1
org.apache.maven.plugins
maven-compiler-plugin
- 3.8.0
+ 3.15.0
${java.version}
+ true
org.apache.maven.plugins
maven-surefire-plugin
- 3.0.0-M5
+ 3.5.2
+
+ **/*Test.java
+ **/*Test?.java
+ **/*Test??.java
+
- ${argLine}
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.math=ALL-UNNAMED
--add-opens java.base/java.io=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
- --add-opens
- java.base/java.util.stream=ALL-UNNAMED
+ --add-opens java.base/java.util.stream=ALL-UNNAMED
--add-opens java.base/java.text=ALL-UNNAMED
- --add-opens
- java.base/java.util.regex=ALL-UNNAMED
- --add-opens
- java.base/java.nio.channels.spi=ALL-UNNAMED
+ --add-opens java.base/java.util.regex=ALL-UNNAMED
+ --add-opens java.base/java.nio.channels.spi=ALL-UNNAMED
--add-opens java.base/sun.nio.ch=ALL-UNNAMED
--add-opens java.base/java.net=ALL-UNNAMED
- --add-opens
- java.base/java.util.concurrent=ALL-UNNAMED
+ --add-opens java.base/java.util.concurrent=ALL-UNNAMED
--add-opens java.base/sun.nio.fs=ALL-UNNAMED
--add-opens java.base/sun.nio.cs=ALL-UNNAMED
--add-opens java.base/java.nio.file=ALL-UNNAMED
- --add-opens
- java.base/java.nio.charset=ALL-UNNAMED
- --add-opens
- java.base/java.lang.reflect=ALL-UNNAMED
- --add-opens
- java.logging/java.util.logging=ALL-UNNAMED
+ --add-opens java.base/java.nio.charset=ALL-UNNAMED
+ --add-opens java.base/java.lang.reflect=ALL-UNNAMED
+ --add-opens java.logging/java.util.logging=ALL-UNNAMED
--add-opens java.base/java.lang.ref=ALL-UNNAMED
--add-opens java.base/java.util.jar=ALL-UNNAMED
--add-opens java.base/java.util.zip=ALL-UNNAMED
@@ -276,17 +294,17 @@
org.apache.maven.plugins
maven-jar-plugin
- 3.1.0
+ 3.4.2
org.apache.maven.plugins
maven-javadoc-plugin
- 3.0.1
+ 3.11.2
false
-Xdoclint:none
${java.home}/bin/javadoc
- 16
+ 21
@@ -300,7 +318,7 @@
org.apache.maven.plugins
maven-source-plugin
- 3.0.1
+ 3.3.1
attach-sources
@@ -313,17 +331,17 @@
org.apache.maven.plugins
maven-install-plugin
- 2.5.2
+ 3.1.3
org.apache.maven.plugins
maven-deploy-plugin
- 2.8.2
+ 3.1.3
org.apache.maven.plugins
maven-shade-plugin
- 3.3.1-SNAPSHOT
+ 3.6.0
true
@@ -355,11 +373,11 @@
org.jacoco
jacoco-maven-plugin
- 0.8.10
+ 0.8.12
true
-
**/*Names*
diff --git a/src/main/java/world/bentobox/topblock/TopBlockPladdon.java b/src/main/java/world/bentobox/topblock/TopBlockPladdon.java
index 013c4c2..8c16d90 100644
--- a/src/main/java/world/bentobox/topblock/TopBlockPladdon.java
+++ b/src/main/java/world/bentobox/topblock/TopBlockPladdon.java
@@ -10,8 +10,14 @@
*
*/
public class TopBlockPladdon extends Pladdon {
+
+ private Addon addon;
+
@Override
public Addon getAddon() {
- return new TopBlock();
+ if (addon == null) {
+ addon = new TopBlock();
+ }
+ return addon;
}
}
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index 73998fd..920e306 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -1,7 +1,7 @@
name: BentoBox-TopBlock
main: world.bentobox.topblock.TopBlockPladdon
version: ${project.version}${build.number}
-api-version: "1.21""
+api-version: "1.21"
authors: [tastybento]
contributors: ["The BentoBoxWorld Community"]
From 6ef414f75e8e14ea276dfd02eb8593bb14099924 Mon Sep 17 00:00:00 2001
From: tastybento
Date: Sat, 25 Apr 2026 20:39:41 -0700
Subject: [PATCH 5/9] Migrate tests to JUnit 5 + MockBukkit
Replaces the old JUnit 4 + PowerMock TopBlockManagerTest (which had
been broken since the Java 17 migration with `Cannot redefine
singleton Server` errors) with a JUnit 5 + MockBukkit suite
following the CaveBlock pattern:
- CommonTestSetup: shared base wiring up Bukkit, BentoBox singleton,
IslandWorldManager / IslandsManager / PlayersManager / Locales /
Placeholders / Notifier / Hooks mocks
- WhiteBox: tiny reflective static-field setter for injecting the
BentoBox singleton
- TestWorldSettings: minimal WorldSettings impl
- TopBlockTest: addon load / enable / disable / reload, with a
synthesised in-memory addon.jar holding config.yml and
panels/top_panel.yml
- TopBlockManagerTest: data flow through getOneBlockData and
formatLevel (covers shorthand k / M / G branches)
- PlaceholderManagerTest: getMemberNames sort order and
out-of-range rank handling
- mocks/ServerMocks.java removed (replaced by MockBukkit)
Total: 22 tests passing (TopBlock 6, TopBlockManager 13,
PlaceholderManager 3).
Also adds CLAUDE.md describing the addon's architecture and the
gotchas that were time-consuming to rediscover (Pladdon split,
private @EventHandler trap, locale resource filtering, surefire
--add-opens requirement).
Co-Authored-By: Claude Opus 4.7
---
CLAUDE.md | 60 +++
.../bentobox/topblock/CommonTestSetup.java | 221 +++++++++++
.../topblock/PlaceholderManagerTest.java | 96 +++++
.../bentobox/topblock/TestWorldSettings.java | 345 ++++++++++++++++++
.../topblock/TopBlockManagerTest.java | 266 +++++---------
.../world/bentobox/topblock/TopBlockTest.java | 151 ++++++++
.../world/bentobox/topblock/WhiteBox.java | 13 +
.../bentobox/topblock/mocks/ServerMocks.java | 118 ------
8 files changed, 982 insertions(+), 288 deletions(-)
create mode 100644 CLAUDE.md
create mode 100644 src/test/java/world/bentobox/topblock/CommonTestSetup.java
create mode 100644 src/test/java/world/bentobox/topblock/PlaceholderManagerTest.java
create mode 100644 src/test/java/world/bentobox/topblock/TestWorldSettings.java
create mode 100644 src/test/java/world/bentobox/topblock/TopBlockTest.java
create mode 100644 src/test/java/world/bentobox/topblock/WhiteBox.java
delete mode 100644 src/test/java/world/bentobox/topblock/mocks/ServerMocks.java
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..b9a34d7
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,60 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project
+
+TopBlock is a BentoBox addon that produces a Top Ten ranking for the AOneBlock game mode based on how many magic blocks each island has mined. It is **not** standalone — it depends on the BentoBox plugin and the AOneBlock addon being present at runtime, and refuses to enable otherwise.
+
+## Build & Test
+
+Maven project, Java 21, Paper 1.21.11 API, BentoBox 3.14.0, AOneBlock 1.18.0.
+
+- Build (default goal is `clean package`): `mvn package` — produces a shaded jar in `target/` named `TopBlock-.jar`. The shade plugin bundles only `lv.id.bonne:panelutils`; everything else is `provided`.
+- Run tests: `mvn test`
+- Run a single test class: `mvn test -Dtest=TopBlockManagerTest`
+- Run a single test method: `mvn test -Dtest=TopBlockManagerTest#testFormatLevelShorthandKilo`
+- The Surefire config sets a long list of `--add-opens` JVM flags — required for Mockito + MockBukkit reflection on Java 21; do not remove them when tweaking the build.
+
+Version handling is driven by Maven properties: `build.version` is the human version (currently 1.1.0), `revision` resolves to `${build.version}-SNAPSHOT` locally and to `${build.version}` under the `master` profile (activated by `GIT_BRANCH=origin/master` on Jenkins). `build.number` is `-LOCAL` locally, `-b` on CI, empty on master. Don't hand-edit `` — bump `build.version`.
+
+## Runtime entry points (Pladdon pattern)
+
+There are **two** main classes and the distinction matters:
+
+- `TopBlockPladdon` (referenced by `plugin.yml`) is the Bukkit-facing `Pladdon`. Spigot loads this; its only job is `getAddon() → new TopBlock()`.
+- `TopBlock` (referenced by `addon.yml`) is the BentoBox `Addon`. All real lifecycle (`onLoad`, `onEnable`, `onDisable`) lives here.
+
+`onEnable` looks up the AOneBlock addon via `getPlugin().getAddonsManager().getAddonByName("aoneblock")`; if missing or not a `GameModeAddon`, the addon disables itself. The `/ topblock` command is registered against AOneBlock's player command, not as a top-level command.
+
+## Data flow
+
+`TopBlockManager` is a `Listener` that reacts to `BentoBoxReadyEvent` (handler is `public void onBentoBoxReady` — Bukkit silently skips private @EventHandler methods, which is what broke the addon historically) to start a repeating Bukkit task. The task period is `settings.getRefreshTime() * 20L * 60` ticks (minutes → ticks). Each tick of the task:
+
+1. Calls `AOneBlock.getBlockListener().getAllIslands()` — this reads every island, so the refresh interval is intentionally coarse (default 5 min, min 1 min).
+2. Builds a fresh `List` (record of island + blockNumber + lifetime + phaseName) — sorted at read time via `Comparator` on `lifetime` then `blockNumber`.
+3. Updates `PlaceholderManager`'s cached snapshot.
+
+Placeholders are registered once via a `runTaskLater` 10-tick delay after the first ready event (so PAPI / BentoBox's `PlaceholdersManager` is up). Names follow `island__top_<1..10>` and are scoped to the AOneBlock `GameModeAddon`. The `TopBlock.TEN` constant is the source of truth for the list size.
+
+## Panel
+
+`TopLevelPanel` uses BentoBox's `TemplatedPanelBuilder`. The template file is shipped in `src/main/resources/panels/top_panel.yml` and copied to the data folder on load via `saveResource("panels/top_panel.yml", false)` — players' edits to the on-disk file persist across restarts. Localization keys live under `topblock.gui.buttons.island.*` in `src/main/resources/locales/en-US.yml`. The icon material can be overridden per-player via the `topblock.icon.` permission.
+
+The panel has no click actions (TopBlock doesn't bundle Warp/Visit hooks like Level does). The YAML still declares `warp`/`visit` actions with tooltips, but no click handler is registered — clicking does nothing.
+
+## Resource filtering
+
+`pom.xml` filters `src/main/resources` (so `${version}` etc. in `addon.yml` / `plugin.yml` get substituted) **except** `src/main/resources/locales`, which is copied verbatim to `./locales` to avoid Maven mangling YAML colons / placeholder syntax in translations.
+
+## Tests
+
+JUnit 5 + Mockito + MockBukkit. Test classes extend `CommonTestSetup` which:
+- Mocks `Bukkit` statically and provides a real `MockBukkit.mock()` server (needed for Tag/Material initialisation).
+- Injects the BentoBox singleton via `WhiteBox.setInternalState(BentoBox.class, "instance", plugin)`.
+- Sets up the standard graph of mocks: `IslandWorldManager`, `IslandsManager`, `PlayersManager`, `LocalesManager`, `PlaceholdersManager`, `Notifier`, `HooksManager`, `BlueprintsManager`.
+- Calls `User.setPlugin(plugin)` and pre-creates a `User` instance for `mockPlayer` (uuid `tastybento`).
+
+`TestWorldSettings` returns `"TopBlock"` for friendly name and `"topblock."` for permission prefix. The addon test (`TopBlockTest`) builds an in-memory `addon.jar` containing `config.yml` + `panels/top_panel.yml` because `Addon.saveResource` reads from a real JarFile.
+
+JaCoCo excludes `**/*Names*` to avoid synthetic-field issues on JavaBeans — keep that exclusion if adding similar classes.
diff --git a/src/test/java/world/bentobox/topblock/CommonTestSetup.java b/src/test/java/world/bentobox/topblock/CommonTestSetup.java
new file mode 100644
index 0000000..30c864b
--- /dev/null
+++ b/src/test/java/world/bentobox/topblock/CommonTestSetup.java
@@ -0,0 +1,221 @@
+package world.bentobox.topblock;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.logging.Logger;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Player.Spigot;
+import org.bukkit.inventory.ItemFactory;
+import org.bukkit.inventory.PlayerInventory;
+import org.bukkit.plugin.PluginManager;
+import org.bukkit.scheduler.BukkitScheduler;
+import org.bukkit.util.Vector;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.mockbukkit.mockbukkit.MockBukkit;
+import org.mockbukkit.mockbukkit.ServerMock;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
+
+import com.google.common.collect.ImmutableSet;
+
+import world.bentobox.bentobox.BentoBox;
+import world.bentobox.bentobox.api.configuration.WorldSettings;
+import world.bentobox.bentobox.api.user.Notifier;
+import world.bentobox.bentobox.api.user.User;
+import world.bentobox.bentobox.database.objects.Island;
+import world.bentobox.bentobox.database.objects.Players;
+import world.bentobox.bentobox.managers.BlueprintsManager;
+import world.bentobox.bentobox.managers.FlagsManager;
+import world.bentobox.bentobox.managers.HooksManager;
+import world.bentobox.bentobox.managers.IslandWorldManager;
+import world.bentobox.bentobox.managers.IslandsManager;
+import world.bentobox.bentobox.managers.LocalesManager;
+import world.bentobox.bentobox.managers.PlaceholdersManager;
+import world.bentobox.bentobox.managers.PlayersManager;
+import world.bentobox.bentobox.util.Util;
+
+/**
+ * Common test setup for TopBlock tests. Call super.setUp() in subclass @BeforeEach.
+ */
+public abstract class CommonTestSetup {
+
+ protected UUID uuid = UUID.randomUUID();
+
+ @Mock
+ protected Player mockPlayer;
+ @Mock
+ protected PluginManager pim;
+ @Mock
+ protected ItemFactory itemFactory;
+ @Mock
+ protected Location location;
+ @Mock
+ protected World world;
+ @Mock
+ protected IslandWorldManager iwm;
+ @Mock
+ protected IslandsManager im;
+ @Mock
+ protected Island island;
+ @Mock
+ protected BentoBox plugin;
+ @Mock
+ protected PlayerInventory inv;
+ @Mock
+ protected Notifier notifier;
+ @Mock
+ protected FlagsManager fm;
+ @Mock
+ protected Spigot spigot;
+ @Mock
+ protected HooksManager hooksManager;
+ @Mock
+ protected BlueprintsManager bm;
+ @Mock
+ protected BukkitScheduler sch;
+ @Mock
+ protected LocalesManager lm;
+ @Mock
+ protected PlaceholdersManager phm;
+
+ protected ServerMock server;
+ protected MockedStatic mockedBukkit;
+ protected MockedStatic mockedUtil;
+ protected AutoCloseable closeable;
+
+ @BeforeEach
+ @SuppressWarnings("java:S1130")
+ public void setUp() throws Exception {
+ closeable = MockitoAnnotations.openMocks(this);
+ server = MockBukkit.mock();
+
+ // Inject BentoBox singleton
+ WhiteBox.setInternalState(BentoBox.class, "instance", plugin);
+
+ // Force Tag static fields to initialise under the real server
+ @SuppressWarnings("unused")
+ var unusedTagRef = org.bukkit.Tag.LEAVES;
+
+ // Static Bukkit mock
+ mockedBukkit = Mockito.mockStatic(Bukkit.class, Mockito.RETURNS_DEEP_STUBS);
+ mockedBukkit.when(Bukkit::getMinecraftVersion).thenReturn("1.21.10");
+ mockedBukkit.when(Bukkit::getBukkitVersion).thenReturn("");
+ mockedBukkit.when(Bukkit::getPluginManager).thenReturn(pim);
+ mockedBukkit.when(Bukkit::getItemFactory).thenReturn(itemFactory);
+ mockedBukkit.when(Bukkit::getServer).thenReturn(server);
+ mockedBukkit.when(Bukkit::getScheduler).thenReturn(sch);
+
+ // Location
+ when(location.getWorld()).thenReturn(world);
+ when(location.getBlockX()).thenReturn(0);
+ when(location.getBlockY()).thenReturn(0);
+ when(location.getBlockZ()).thenReturn(0);
+ when(location.toVector()).thenReturn(new Vector(0, 0, 0));
+ when(location.clone()).thenReturn(location);
+
+ // PlayersManager
+ PlayersManager pm = mock(PlayersManager.class);
+ when(plugin.getPlayers()).thenReturn(pm);
+ Players players = mock(Players.class);
+ when(players.getMetaData()).thenReturn(Optional.empty());
+ when(pm.getPlayer(any(UUID.class))).thenReturn(players);
+
+ // Player
+ when(mockPlayer.getUniqueId()).thenReturn(uuid);
+ when(mockPlayer.getLocation()).thenReturn(location);
+ when(mockPlayer.getWorld()).thenReturn(world);
+ when(mockPlayer.getName()).thenReturn("tastybento");
+ when(mockPlayer.getInventory()).thenReturn(inv);
+ when(mockPlayer.spigot()).thenReturn(spigot);
+ when(mockPlayer.getType()).thenReturn(EntityType.PLAYER);
+
+ User.setPlugin(plugin);
+ User.clearUsers();
+ User.getInstance(mockPlayer);
+
+ // IWM
+ when(plugin.getIWM()).thenReturn(iwm);
+ when(iwm.inWorld(any(Location.class))).thenReturn(true);
+ when(iwm.inWorld(any(World.class))).thenReturn(true);
+ when(iwm.getFriendlyName(any())).thenReturn("TopBlock");
+ when(iwm.getAddon(any())).thenReturn(Optional.empty());
+
+ // WorldSettings
+ WorldSettings worldSet = new TestWorldSettings();
+ when(iwm.getWorldSettings(any())).thenReturn(worldSet);
+
+ // IslandsManager
+ when(plugin.getIslands()).thenReturn(im);
+ when(im.getProtectedIslandAt(any())).thenReturn(Optional.of(island));
+ when(island.isAllowed(any())).thenReturn(false);
+ when(island.isAllowed(any(User.class), any())).thenReturn(false);
+ when(island.getOwner()).thenReturn(uuid);
+ when(island.getMemberSet()).thenReturn(ImmutableSet.of(uuid));
+
+ // Locales & Placeholders
+ when(lm.get(any(), any())).thenAnswer((Answer) invocation -> invocation.getArgument(1, String.class));
+ when(plugin.getPlaceholdersManager()).thenReturn(phm);
+ when(phm.replacePlaceholders(any(), any())).thenAnswer((Answer) invocation -> invocation.getArgument(1, String.class));
+ when(plugin.getLocalesManager()).thenReturn(lm);
+
+ // Notifier
+ when(plugin.getNotifier()).thenReturn(notifier);
+
+ // Logger — Addon.getLogger() delegates to plugin.getLogger()
+ when(plugin.getLogger()).thenReturn(Logger.getLogger("TopBlock-test"));
+
+ // BentoBox settings (fake players feature)
+ world.bentobox.bentobox.Settings settings = new world.bentobox.bentobox.Settings();
+ when(plugin.getSettings()).thenReturn(settings);
+
+ // Util static mock
+ mockedUtil = Mockito.mockStatic(Util.class, Mockito.CALLS_REAL_METHODS);
+ mockedUtil.when(() -> Util.getWorld(any())).thenReturn(mock(World.class));
+ Util.setPlugin(plugin);
+ mockedUtil.when(() -> Util.findFirstMatchingEnum(any(), any())).thenCallRealMethod();
+
+ // Hooks
+ when(hooksManager.getHook(anyString())).thenReturn(Optional.empty());
+ when(plugin.getHooks()).thenReturn(hooksManager);
+
+ // BlueprintsManager
+ when(plugin.getBlueprintsManager()).thenReturn(bm);
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception {
+ mockedBukkit.closeOnDemand();
+ mockedUtil.closeOnDemand();
+ closeable.close();
+ MockBukkit.unmock();
+ User.clearUsers();
+ Mockito.framework().clearInlineMocks();
+ deleteAll(new File("database"));
+ deleteAll(new File("database_backup"));
+ }
+
+ protected static void deleteAll(File file) throws IOException {
+ if (file.exists()) {
+ Files.walk(file.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
+ }
+ }
+}
diff --git a/src/test/java/world/bentobox/topblock/PlaceholderManagerTest.java b/src/test/java/world/bentobox/topblock/PlaceholderManagerTest.java
new file mode 100644
index 0000000..8cd3249
--- /dev/null
+++ b/src/test/java/world/bentobox/topblock/PlaceholderManagerTest.java
@@ -0,0 +1,96 @@
+package world.bentobox.topblock;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+
+import com.google.common.collect.ImmutableSet;
+
+import world.bentobox.aoneblock.AOneBlock;
+import world.bentobox.aoneblock.dataobjects.OneBlockIslands;
+import world.bentobox.aoneblock.listeners.BlockListener;
+import world.bentobox.bentobox.managers.PlayersManager;
+import world.bentobox.bentobox.managers.RanksManager;
+import world.bentobox.topblock.config.ConfigSettings;
+
+class PlaceholderManagerTest extends CommonTestSetup {
+
+ @Mock
+ private TopBlock addon;
+ @Mock
+ private AOneBlock aob;
+ @Mock
+ private BlockListener bl;
+ @Mock
+ private PlayersManager playersMgr;
+
+ private TopBlockManager tbm;
+ private PlaceholderManager phMgr;
+
+ @Override
+ @BeforeEach
+ public void setUp() throws Exception {
+ super.setUp();
+
+ ConfigSettings settings = new ConfigSettings();
+ when(addon.getPlugin()).thenReturn(plugin);
+ when(addon.getSettings()).thenReturn(settings);
+ when(addon.getaOneBlock()).thenReturn(aob);
+ when(addon.getIslands()).thenReturn(im);
+ when(addon.getPlayers()).thenReturn(playersMgr);
+ when(aob.getBlockListener()).thenReturn(bl);
+ when(im.getIslandById(anyString())).thenReturn(Optional.of(island));
+
+ // Single island in top ten
+ OneBlockIslands ob = new OneBlockIslands(UUID.randomUUID().toString());
+ ob.setBlockNumber(80);
+ ob.setLifetime(250);
+ ob.setPhaseName("Underground");
+ when(bl.getAllIslands()).thenReturn(List.of(ob));
+
+ tbm = new TopBlockManager(addon);
+ when(addon.getManager()).thenReturn(tbm);
+ tbm.getOneBlockData();
+
+ phMgr = new PlaceholderManager(addon);
+ phMgr.updateTopTen();
+ }
+
+ @Test
+ void testGetMemberNamesEmptyForSingleMemberIsland() {
+ when(island.getMembers()).thenReturn(java.util.Collections.emptyMap());
+
+ assertEquals("", phMgr.getMemberNames(1));
+ }
+
+ @Test
+ void testGetMemberNamesPastEndReturnsEmpty() {
+ // Rank 5 with only 1 island in the list → empty
+ assertEquals("", phMgr.getMemberNames(5));
+ }
+
+ @Test
+ void testGetMemberNamesJoinsMembers() {
+ UUID a = UUID.randomUUID();
+ UUID b = UUID.randomUUID();
+ when(island.getMembers()).thenReturn(java.util.Map.of(
+ a, RanksManager.MEMBER_RANK,
+ b, RanksManager.SUB_OWNER_RANK));
+ when(island.getMemberSet()).thenReturn(ImmutableSet.of(a, b));
+ when(playersMgr.getName(a)).thenReturn("Alice");
+ when(playersMgr.getName(b)).thenReturn("Bob");
+
+ String names = phMgr.getMemberNames(1);
+ // SUB_OWNER_RANK > MEMBER_RANK, so Bob comes first
+ assertEquals("Bob,Alice", names);
+ }
+}
diff --git a/src/test/java/world/bentobox/topblock/TestWorldSettings.java b/src/test/java/world/bentobox/topblock/TestWorldSettings.java
new file mode 100644
index 0000000..40c862a
--- /dev/null
+++ b/src/test/java/world/bentobox/topblock/TestWorldSettings.java
@@ -0,0 +1,345 @@
+package world.bentobox.topblock;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.bukkit.Difficulty;
+import org.bukkit.GameMode;
+import org.bukkit.entity.EntityType;
+import org.eclipse.jdt.annotation.NonNull;
+
+import world.bentobox.bentobox.api.configuration.WorldSettings;
+import world.bentobox.bentobox.api.flags.Flag;
+
+/**
+ * Minimal WorldSettings implementation for use in tests.
+ */
+public class TestWorldSettings implements WorldSettings {
+
+ private long epoch;
+
+ @Override
+ public GameMode getDefaultGameMode() {
+ return GameMode.SURVIVAL;
+ }
+
+ @SuppressWarnings("removal")
+ @Override
+ public Map getDefaultIslandFlags() {
+ return Collections.emptyMap();
+ }
+
+ @SuppressWarnings("removal")
+ @Override
+ public Map getDefaultIslandSettings() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public Difficulty getDifficulty() {
+ return Difficulty.NORMAL;
+ }
+
+ @Override
+ public void setDifficulty(Difficulty difficulty) {
+ // unused
+ }
+
+ @Override
+ public String getFriendlyName() {
+ return "TopBlock";
+ }
+
+ @Override
+ public int getIslandDistance() {
+ return 0;
+ }
+
+ @Override
+ public int getIslandHeight() {
+ return 0;
+ }
+
+ @Override
+ public int getIslandProtectionRange() {
+ return 0;
+ }
+
+ @Override
+ public int getIslandStartX() {
+ return 0;
+ }
+
+ @Override
+ public int getIslandStartZ() {
+ return 0;
+ }
+
+ @Override
+ public int getIslandXOffset() {
+ return 0;
+ }
+
+ @Override
+ public int getIslandZOffset() {
+ return 0;
+ }
+
+ @Override
+ public List getIvSettings() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public int getMaxHomes() {
+ return 3;
+ }
+
+ @Override
+ public int getMaxIslands() {
+ return 0;
+ }
+
+ @Override
+ public int getMaxTeamSize() {
+ return 4;
+ }
+
+ @Override
+ public int getNetherSpawnRadius() {
+ return 10;
+ }
+
+ @Override
+ public String getPermissionPrefix() {
+ return "topblock.";
+ }
+
+ @Override
+ public Set getRemoveMobsWhitelist() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public int getSeaHeight() {
+ return 0;
+ }
+
+ @Override
+ public List getHiddenFlags() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List getVisitorBannedCommands() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public Map getWorldFlags() {
+ return new HashMap<>();
+ }
+
+ @Override
+ public String getWorldName() {
+ return "topblock-world";
+ }
+
+ @Override
+ public boolean isDragonSpawn() {
+ return false;
+ }
+
+ @Override
+ public boolean isEndGenerate() {
+ return true;
+ }
+
+ @Override
+ public boolean isEndIslands() {
+ return true;
+ }
+
+ @Override
+ public boolean isNetherGenerate() {
+ return true;
+ }
+
+ @Override
+ public boolean isNetherIslands() {
+ return true;
+ }
+
+ @Override
+ public boolean isOnJoinResetEnderChest() {
+ return false;
+ }
+
+ @Override
+ public boolean isOnJoinResetInventory() {
+ return false;
+ }
+
+ @Override
+ public boolean isOnJoinResetMoney() {
+ return false;
+ }
+
+ @Override
+ public boolean isOnJoinResetHealth() {
+ return false;
+ }
+
+ @Override
+ public boolean isOnJoinResetHunger() {
+ return false;
+ }
+
+ @Override
+ public boolean isOnJoinResetXP() {
+ return false;
+ }
+
+ @Override
+ public @NonNull List getOnJoinCommands() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean isOnLeaveResetEnderChest() {
+ return false;
+ }
+
+ @Override
+ public boolean isOnLeaveResetInventory() {
+ return false;
+ }
+
+ @Override
+ public boolean isOnLeaveResetMoney() {
+ return false;
+ }
+
+ @Override
+ public boolean isOnLeaveResetHealth() {
+ return false;
+ }
+
+ @Override
+ public boolean isOnLeaveResetHunger() {
+ return false;
+ }
+
+ @Override
+ public boolean isOnLeaveResetXP() {
+ return false;
+ }
+
+ @Override
+ public @NonNull List getOnLeaveCommands() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean isUseOwnGenerator() {
+ return false;
+ }
+
+ @Override
+ public boolean isWaterUnsafe() {
+ return false;
+ }
+
+ @Override
+ public List getGeoLimitSettings() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public int getResetLimit() {
+ return 0;
+ }
+
+ @Override
+ public long getResetEpoch() {
+ return epoch;
+ }
+
+ @Override
+ public void setResetEpoch(long timestamp) {
+ this.epoch = timestamp;
+ }
+
+ @Override
+ public boolean isTeamJoinDeathReset() {
+ return false;
+ }
+
+ @Override
+ public int getDeathsMax() {
+ return 0;
+ }
+
+ @Override
+ public boolean isDeathsCounted() {
+ return true;
+ }
+
+ @Override
+ public boolean isDeathsResetOnNewIsland() {
+ return true;
+ }
+
+ @Override
+ public boolean isAllowSetHomeInNether() {
+ return false;
+ }
+
+ @Override
+ public boolean isAllowSetHomeInTheEnd() {
+ return false;
+ }
+
+ @Override
+ public boolean isRequireConfirmationToSetHomeInNether() {
+ return false;
+ }
+
+ @Override
+ public boolean isRequireConfirmationToSetHomeInTheEnd() {
+ return false;
+ }
+
+ @Override
+ public int getBanLimit() {
+ return 10;
+ }
+
+ @Override
+ public boolean isLeaversLoseReset() {
+ return true;
+ }
+
+ @Override
+ public boolean isKickedKeepInventory() {
+ return true;
+ }
+
+ @Override
+ public boolean isCreateIslandOnFirstLoginEnabled() {
+ return false;
+ }
+
+ @Override
+ public int getCreateIslandOnFirstLoginDelay() {
+ return 0;
+ }
+
+ @Override
+ public boolean isCreateIslandOnFirstLoginAbortOnLogout() {
+ return false;
+ }
+}
diff --git a/src/test/java/world/bentobox/topblock/TopBlockManagerTest.java b/src/test/java/world/bentobox/topblock/TopBlockManagerTest.java
index 708ba74..9f6a2ce 100644
--- a/src/test/java/world/bentobox/topblock/TopBlockManagerTest.java
+++ b/src/test/java/world/bentobox/topblock/TopBlockManagerTest.java
@@ -1,243 +1,169 @@
package world.bentobox.topblock;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
-import org.bukkit.Bukkit;
-import org.bukkit.Server;
-import org.eclipse.jdt.annotation.NonNull;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.powermock.api.mockito.PowerMockito;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
import world.bentobox.aoneblock.AOneBlock;
import world.bentobox.aoneblock.dataobjects.OneBlockIslands;
import world.bentobox.aoneblock.listeners.BlockListener;
-import world.bentobox.bentobox.api.user.User;
-import world.bentobox.bentobox.database.objects.Island;
-import world.bentobox.bentobox.managers.IslandsManager;
import world.bentobox.topblock.TopBlockManager.TopTenData;
import world.bentobox.topblock.config.ConfigSettings;
-import world.bentobox.topblock.mocks.ServerMocks;
-/**
- * @author tastybento
- *
- */
-@RunWith(PowerMockRunner.class)
-@PrepareForTest(Bukkit.class)
-public class TopBlockManagerTest {
+class TopBlockManagerTest extends CommonTestSetup {
@Mock
private TopBlock addon;
@Mock
- private Island island;
-
- private TopBlockManager tbm;
- @Mock
private AOneBlock aob;
-
@Mock
- private IslandsManager im;
+ private BlockListener bl;
+ private TopBlockManager tbm;
+ private ConfigSettings settings;
- /**
- * @throws java.lang.Exception
- */
- @Before
+ @Override
+ @BeforeEach
public void setUp() throws Exception {
- Server server = ServerMocks.newServer();
-
- PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS);
- when(Bukkit.getServer()).thenReturn(server);
-
- List list = new ArrayList<>();
- OneBlockIslands i = new OneBlockIslands(UUID.randomUUID().toString());
- i.setLifetime(100);
- i.setBlockNumber(100);
- i.setPhaseName("phasy");
- list.add(i);
+ super.setUp();
- // Island manager
+ settings = new ConfigSettings();
+ when(addon.getPlugin()).thenReturn(plugin);
+ when(addon.getSettings()).thenReturn(settings);
+ when(addon.getaOneBlock()).thenReturn(aob);
when(addon.getIslands()).thenReturn(im);
- when(im.getIslandById(anyString())).thenReturn(Optional.of(island));
- // AOneBlock
- BlockListener bl = mock(BlockListener.class); // This class uses static initializations so if it is mocked as a field, it will spark an issue
- when(bl.getAllIslands()).thenReturn(list);
when(aob.getBlockListener()).thenReturn(bl);
- when(addon.getaOneBlock()).thenReturn(aob);
+ when(im.getIslandById(anyString())).thenReturn(Optional.of(island));
+
tbm = new TopBlockManager(addon);
}
- @After
- public void tearDown() {
- ServerMocks.unsetBukkitServer();
- User.clearUsers();
- Mockito.framework().clearInlineMocks();
+ private static OneBlockIslands ob(int blockNumber, long lifetime, String phase) {
+ OneBlockIslands i = new OneBlockIslands(UUID.randomUUID().toString());
+ i.setBlockNumber(blockNumber);
+ i.setLifetime(lifetime);
+ i.setPhaseName(phase);
+ return i;
}
- /**
- * Test method for {@link world.bentobox.topblock.TopBlockManager#TopBlockManager(world.bentobox.topblock.TopBlock)}.
- */
@Test
- public void testTopBlockManager() {
- assertNotNull(tbm);
+ void testGetTopTenEmptyByDefault() {
+ assertTrue(tbm.getTopTen(10).isEmpty());
}
- /**
- * Test method for {@link world.bentobox.topblock.TopBlockManager.TopTenData}.
- */
@Test
- public void testTopTenDataSame() {
- TopTenData ttd = new TopTenData(island, 0, 0, "phase one");
- TopTenData ttd2 = new TopTenData(island, 0, 0, "phase one");
- assertEquals(ttd, ttd2);
+ void testGetOneBlockDataPopulatesTopTen() {
+ when(bl.getAllIslands()).thenReturn(List.of(
+ ob(50, 100, "Plains"),
+ ob(80, 250, "Underground")));
+
+ tbm.getOneBlockData();
+
+ List top = tbm.getTopTen(10);
+ assertEquals(2, top.size());
+ // Sorted descending by lifetime
+ assertEquals(250L, top.get(0).lifetime());
+ assertEquals(100L, top.get(1).lifetime());
}
- /**
- * Test method for {@link world.bentobox.topblock.TopBlockManager.TopTenData}.
- */
@Test
- public void testTopTenDataBlockDifferent() {
- TopTenData ttd = new TopTenData(island, 1000, 0, "phase one");
- TopTenData ttd2 = new TopTenData(island, 0, 0, "phase one");
- assertNotEquals(ttd, ttd2);
+ void testGetOneBlockDataFiltersZeroLifetime() {
+ when(bl.getAllIslands()).thenReturn(List.of(
+ ob(0, 0, "Plains"),
+ ob(80, 250, "Underground")));
+
+ tbm.getOneBlockData();
+
+ List top = tbm.getTopTen(10);
+ assertEquals(1, top.size());
+ assertEquals(250L, top.get(0).lifetime());
}
- /**
- * Test method for {@link world.bentobox.topblock.TopBlockManager.TopTenData}.
- */
@Test
- public void testTopTenDataLifetimeDifferent() {
- TopTenData ttd = new TopTenData(island, 0, 0, "phase one");
- TopTenData ttd2 = new TopTenData(island, 0, 10000, "phase one");
- assertNotEquals(ttd, ttd2);
+ void testGetOneBlockDataSkipsIslandsWithoutBentoBoxIsland() {
+ when(im.getIslandById(anyString())).thenReturn(Optional.empty());
+ when(bl.getAllIslands()).thenReturn(List.of(ob(80, 250, "Underground")));
+
+ tbm.getOneBlockData();
+
+ assertTrue(tbm.getTopTen(10).isEmpty());
}
- /**
- * Test method for {@link world.bentobox.topblock.TopBlockManager.TopTenData}.
- */
@Test
- public void testTopTenDataPhaseDifferent() {
- TopTenData ttd = new TopTenData(island, 0, 0, "phase one");
- TopTenData ttd2 = new TopTenData(island, 0, 0, "phase two");
- assertNotEquals(ttd, ttd2);
+ void testGetOneBlockDataReplacesPreviousResults() {
+ when(bl.getAllIslands()).thenReturn(List.of(ob(80, 250, "Underground")));
+ tbm.getOneBlockData();
+ assertEquals(1, tbm.getTopTen(10).size());
+
+ when(bl.getAllIslands()).thenReturn(List.of());
+ tbm.getOneBlockData();
+ assertTrue(tbm.getTopTen(10).isEmpty());
}
- /**
- * Test method for {@link world.bentobox.topblock.TopBlockManager.TopTenData}.
- */
@Test
- public void testTopTenDataGreater() {
- TopTenData ttd = new TopTenData(island, 10000, 0, "phase fifty");
- TopTenData ttd2 = new TopTenData(island, 0, 0, "phase two");
- List list = new ArrayList<>();
- list.add(ttd);
- list.add(ttd2);
- list = list.stream().sorted(Collections.reverseOrder()).toList();
- assertEquals(ttd, list.get(0));
- assertEquals(ttd2, list.get(1));
+ void testGetTopTenLimitsSize() {
+ when(bl.getAllIslands()).thenReturn(List.of(
+ ob(10, 10, "a"),
+ ob(20, 20, "b"),
+ ob(30, 30, "c")));
+ tbm.getOneBlockData();
+
+ assertEquals(2, tbm.getTopTen(2).size());
}
- /**
- * Test method for {@link world.bentobox.topblock.TopBlockManager.TopTenData}.
- */
@Test
- public void testTopTenDataLess() {
- TopTenData ttd = new TopTenData(island, 0, 0, "phase one");
- TopTenData ttd2 = new TopTenData(island, 10000, 0, "phase fifty");
- List list = new ArrayList<>();
- list.add(ttd);
- list.add(ttd2);
- list = list.stream().sorted(Collections.reverseOrder()).toList();
- assertEquals(ttd2, list.get(0));
- assertEquals(ttd, list.get(1));
+ void testFormatLevelNullReturnsEmpty() {
+ assertEquals("", tbm.formatLevel(null));
}
- /**
- * Test method for {@link world.bentobox.topblock.TopBlockManager.TopTenData}.
- */
@Test
- public void testTopTenDataGreaterLifetime() {
- TopTenData ttd = new TopTenData(island, 100, 10100, "phase fifty");
- TopTenData ttd2 = new TopTenData(island, 1000, 0, "phase two");
- List list = new ArrayList<>();
- list.add(ttd);
- list.add(ttd2);
- list = list.stream().sorted(Collections.reverseOrder()).toList();
- assertEquals(ttd, list.get(0));
- assertEquals(ttd2, list.get(1));
+ void testFormatLevelNoShorthandReturnsRawString() {
+ settings.setShorthand(false);
+ assertEquals("104556", tbm.formatLevel(104556L));
}
- /**
- * Test method for {@link world.bentobox.topblock.TopBlockManager.TopTenData}.
- */
@Test
- public void testTopTenDataGreaterLifetime2() {
- TopTenData ttd = new TopTenData(island, 100, 10100, "phase fifty");
- TopTenData ttd2 = new TopTenData(island, 100, 0, "phase two");
- List list = new ArrayList<>();
- list.add(ttd2);
- list.add(ttd);
- list = list.stream().sorted(Collections.reverseOrder()).toList();
- assertEquals(ttd, list.get(0));
- assertEquals(ttd2, list.get(1));
+ void testFormatLevelShorthandUnderThousandUnchanged() {
+ settings.setShorthand(true);
+ assertEquals("999", tbm.formatLevel(999L));
}
-
- /**
- * Test method for {@link world.bentobox.topblock.TopBlockManager#getOneBlockData()}.
- */
@Test
- public void testGetOneBlockData() {
- this.tbm.getOneBlockData();
- @NonNull
- List list = tbm.getTopTen(10);
- TopTenData t = list.get(0);
- assertEquals(100, t.lifetime());
- assertEquals(100, t.blockNumber());
- assertEquals("phasy", t.phaseName());
-
+ void testFormatLevelShorthandKilo() {
+ settings.setShorthand(true);
+ assertEquals("10.5k", tbm.formatLevel(10500L));
}
- /**
- * Test method for {@link world.bentobox.topblock.TopBlockManager#formatLevel(java.lang.Long)}.
- */
@Test
- public void testFormatLevel() {
- ConfigSettings settings = new ConfigSettings();
+ void testFormatLevelShorthandMega() {
settings.setShorthand(true);
- when(addon.getSettings()).thenReturn(settings);
- assertEquals("12.3G", tbm.formatLevel(12345678349L));
- settings.setShorthand(false);
- when(addon.getSettings()).thenReturn(settings);
- assertEquals("12345678349", tbm.formatLevel(12345678349L));
+ assertEquals("1.5M", tbm.formatLevel(1_527_314L));
}
- /**
- * Test method for {@link world.bentobox.topblock.TopBlockManager#getTopTen(int)}.
- */
@Test
- public void testGetTopTen() {
- List list = tbm.getTopTen(10);
- assertTrue(list.isEmpty());
+ void testFormatLevelShorthandGiga() {
+ settings.setShorthand(true);
+ assertEquals("3.9G", tbm.formatLevel(3_874_130_021L));
}
+ @Test
+ void testTopTenDataRecordFields() {
+ TopTenData d = new TopTenData(island, 42, 1234L, "phasy");
+ assertNotNull(d);
+ assertEquals(42, d.blockNumber());
+ assertEquals(1234L, d.lifetime());
+ assertEquals("phasy", d.phaseName());
+ assertEquals(island, d.island());
+ }
}
diff --git a/src/test/java/world/bentobox/topblock/TopBlockTest.java b/src/test/java/world/bentobox/topblock/TopBlockTest.java
new file mode 100644
index 0000000..4f2c33e
--- /dev/null
+++ b/src/test/java/world/bentobox/topblock/TopBlockTest.java
@@ -0,0 +1,151 @@
+package world.bentobox.topblock;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import world.bentobox.bentobox.api.addons.Addon.State;
+import world.bentobox.bentobox.api.addons.AddonDescription;
+import world.bentobox.bentobox.database.AbstractDatabaseHandler;
+import world.bentobox.bentobox.database.DatabaseSetup;
+import world.bentobox.bentobox.managers.AddonsManager;
+import world.bentobox.bentobox.managers.CommandsManager;
+import world.bentobox.topblock.config.ConfigSettings;
+
+class TopBlockTest extends CommonTestSetup {
+
+ private static final String CONFIG_YML =
+ """
+ refresh-time: 5
+ shorthand: false
+ """;
+
+ private static final String TOP_PANEL_YML = "top_panel:\n type: INVENTORY\n";
+
+ @Mock
+ private AddonsManager am;
+
+ private TopBlock addon;
+ private MockedStatic mockDb;
+
+ @SuppressWarnings("unchecked")
+ @Override
+ @BeforeEach
+ public void setUp() throws Exception {
+ super.setUp();
+
+ // Database mock
+ AbstractDatabaseHandler