From c2a67804262a1c22ed1c7bf972ba610dda81119d Mon Sep 17 00:00:00 2001 From: tastybento Date: Thu, 20 Aug 2026 18:19:47 -0700 Subject: [PATCH] Fix ChunkBlock top ten showing AOneBlock players and Steve heads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both AOneBlock and ChunkBlock use OneBlockIslands as their database class name. BentoBox's JSON handler keys on getSimpleName(), so both game modes share the same database/OneBlockIslands/ folder. When TopBlock refreshes ChunkBlock's data, handler.loadObjects() returns all records including AOneBlock's, causing AOneBlock players to appear in the ChunkBlock panel. Fix by filtering islands in refresh() with hook.getGameMode().inWorld(island.getWorld()) so only islands belonging to the correct game mode are included. Also guard the panel icon path against customised templates where icon: PLAYER_HEAD is uncommented — builder.icon(ItemStack) does not set playerHeadName, so HeadGetter was never invoked and heads stayed as Steve. Additionally, catch LinkageError in onEnable() when constructing game mode hooks, and add softdepend entries in plugin.yml for the Pladdon plugin names so class loading works across plugin loaders. Co-Authored-By: Claude Opus 4.6 (1M context) Claude-Session: https://claude.ai/code/session_01WwpMcJAhXEWeFwtRwg1uNy --- .../world/bentobox/topblock/TopBlock.java | 36 +++++++++++++------ .../bentobox/topblock/TopBlockManager.java | 1 + .../topblock/panels/TopLevelPanel.java | 3 +- src/main/resources/plugin.yml | 6 ++++ .../topblock/PlaceholderManagerTest.java | 3 ++ .../topblock/TopBlockManagerTest.java | 20 +++++++++++ 6 files changed, 58 insertions(+), 11 deletions(-) diff --git a/src/main/java/world/bentobox/topblock/TopBlock.java b/src/main/java/world/bentobox/topblock/TopBlock.java index d0d3336..e450c8d 100644 --- a/src/main/java/world/bentobox/topblock/TopBlock.java +++ b/src/main/java/world/bentobox/topblock/TopBlock.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.function.Supplier; import org.bukkit.World; @@ -65,16 +66,8 @@ public void onEnable() { // Hook into whichever supported game modes are present. Game-mode classes are // only referenced inside the hook constructors, which run after the presence // check, so a game mode that is not installed is never class-loaded. - findGameMode("aoneblock").ifPresent(gm -> { - log("TopBlock hooking into AOneBlock"); - registerCommands(gm); - hooks.add(new AOneBlockHook(gm)); - }); - findGameMode("chunkblock").ifPresent(gm -> { - log("TopBlock hooking into ChunkBlock"); - registerCommands(gm); - hooks.add(new ChunkBlockHook(gm)); - }); + findGameMode("aoneblock").ifPresent(gm -> hook(gm, "AOneBlock", () -> new AOneBlockHook(gm))); + findGameMode("chunkblock").ifPresent(gm -> hook(gm, "ChunkBlock", () -> new ChunkBlockHook(gm))); if (hooks.isEmpty()) { logError("Could not hook into AOneBlock or ChunkBlock. Is at least one loaded?"); @@ -82,6 +75,29 @@ public void onEnable() { } } + /** + * Add a hook for a game mode that the BentoBox API says is present and enabled. + * The game mode's classes still have to be visible to this addon's class loader, which + * is not guaranteed - e.g. if the game mode is installed as a BentoBox addon jar while + * TopBlock is loaded as a Pladdon plugin. If they are not, skip that game mode rather + * than letting the LinkageError mark the whole addon as incompatible. + * @param gm game mode addon + * @param name friendly name of the game mode, for logging + * @param hookSupplier supplier of the hook. Must be a lambda so that the game mode's + * classes are only loaded when it is called. + */ + private void hook(GameModeAddon gm, String name, Supplier hookSupplier) { + try { + TopBlockHook hook = hookSupplier.get(); + registerCommands(gm); + hooks.add(hook); + log("TopBlock hooking into " + name); + } catch (LinkageError e) { + logError(name + " is loaded, but TopBlock cannot see its classes: " + e.getMessage()); + logError("Install " + name + " as a plugin (jar in the plugins folder) so that TopBlock can hook into it."); + } + } + private Optional findGameMode(String name) { return getPlugin().getAddonsManager().getAddonByName(name) .filter(Addon::isEnabled) diff --git a/src/main/java/world/bentobox/topblock/TopBlockManager.java b/src/main/java/world/bentobox/topblock/TopBlockManager.java index 383de34..ce465a7 100644 --- a/src/main/java/world/bentobox/topblock/TopBlockManager.java +++ b/src/main/java/world/bentobox/topblock/TopBlockManager.java @@ -92,6 +92,7 @@ void refresh(TopBlockHook hook) { List data = new ArrayList<>(); hook.getAllIslandData().stream().filter(i -> i.lifetime() > 0).forEach(i -> addon.getIslands().getIslandById(i.uniqueId()) + .filter(island -> hook.getGameMode().inWorld(island.getWorld())) .filter(this::ownerInTopTen) .ifPresent(island -> data.add(new TopTenData(island, i.blockNumber(), i.lifetime(), i.phaseName())))); diff --git a/src/main/java/world/bentobox/topblock/panels/TopLevelPanel.java b/src/main/java/world/bentobox/topblock/panels/TopLevelPanel.java index 1ea1884..20aa2c9 100644 --- a/src/main/java/world/bentobox/topblock/panels/TopLevelPanel.java +++ b/src/main/java/world/bentobox/topblock/panels/TopLevelPanel.java @@ -171,7 +171,8 @@ private void populateIslandIcon(PanelItemBuilder builder, ItemTemplateRecord tem } else { builder.icon(owner.getName()); } - } else if (template.icon() != null) { + } else if (template.icon() != null + && template.icon().getType() != Material.PLAYER_HEAD) { builder.icon(template.icon().clone()); } else if (owner != null) { builder.icon(owner.getName()); diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 920e306..1a14c64 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -3,6 +3,12 @@ main: world.bentobox.topblock.TopBlockPladdon version: ${project.version}${build.number} api-version: "1.21" +# Pladdon names of the game modes we hook into. Soft, because either alone is enough, +# but required so that our plugin class loader can see their classes. +softdepend: + - BentoBox-AOneBlock + - BentoBox-ChunkBlock + authors: [tastybento] contributors: ["The BentoBoxWorld Community"] website: https://bentobox.world diff --git a/src/test/java/world/bentobox/topblock/PlaceholderManagerTest.java b/src/test/java/world/bentobox/topblock/PlaceholderManagerTest.java index 8c8710a..2fdb343 100644 --- a/src/test/java/world/bentobox/topblock/PlaceholderManagerTest.java +++ b/src/test/java/world/bentobox/topblock/PlaceholderManagerTest.java @@ -1,6 +1,7 @@ package world.bentobox.topblock; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -51,6 +52,8 @@ public void setUp() throws Exception { when(addon.getIslands()).thenReturn(im); when(addon.getPlayers()).thenReturn(playersMgr); when(hook.getGameMode()).thenReturn(gameMode); + when(gameMode.inWorld(any(org.bukkit.World.class))).thenReturn(true); + when(island.getWorld()).thenReturn(world); when(im.getIslandById(anyString())).thenReturn(Optional.of(island)); // Single island in top ten diff --git a/src/test/java/world/bentobox/topblock/TopBlockManagerTest.java b/src/test/java/world/bentobox/topblock/TopBlockManagerTest.java index 986cfed..357749c 100644 --- a/src/test/java/world/bentobox/topblock/TopBlockManagerTest.java +++ b/src/test/java/world/bentobox/topblock/TopBlockManagerTest.java @@ -13,11 +13,13 @@ import java.util.UUID; import org.bukkit.Bukkit; +import org.bukkit.World; import org.bukkit.entity.Player; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; +import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.topblock.TopBlockManager.TopTenData; import world.bentobox.topblock.config.ConfigSettings; import world.bentobox.topblock.hooks.IslandBlockData; @@ -29,6 +31,8 @@ class TopBlockManagerTest extends CommonTestSetup { private TopBlock addon; @Mock private TopBlockHook hook; + @Mock + private GameModeAddon gma; private TopBlockManager tbm; private ConfigSettings settings; @@ -45,6 +49,8 @@ public void setUp() throws Exception { when(addon.getIslands()).thenReturn(im); when(im.getIslandById(anyString())).thenReturn(Optional.of(island)); when(island.getWorld()).thenReturn(world); + when(hook.getGameMode()).thenReturn(gma); + when(gma.inWorld(world)).thenReturn(true); when(iwm.getPermissionPrefix(any())).thenReturn("aoneblock."); tbm = new TopBlockManager(addon); @@ -111,6 +117,9 @@ void testRefreshReplacesPreviousResults() { @Test void testTopTensAreSeparatePerHook() { TopBlockHook hook2 = mock(TopBlockHook.class); + GameModeAddon gma2 = mock(GameModeAddon.class); + when(hook2.getGameMode()).thenReturn(gma2); + when(gma2.inWorld(world)).thenReturn(true); when(addon.getHooks()).thenReturn(List.of(hook, hook2)); when(hook.getAllIslandData()).thenReturn(List.of(ib(80, 250, "Underground"))); when(hook2.getAllIslandData()).thenReturn(List.of( @@ -231,6 +240,17 @@ void testRefreshExcludesIslandWithoutOwner() { assertTrue(tbm.getTopTen(hook, 10).isEmpty()); } + @Test + void testRefreshFiltersIslandsFromWrongGameMode() { + World otherWorld = mock(World.class); + when(island.getWorld()).thenReturn(otherWorld); + when(gma.inWorld(otherWorld)).thenReturn(false); + when(hook.getAllIslandData()).thenReturn(List.of(ib(80, 250, "Underground"))); + tbm.refreshAll(); + + assertTrue(tbm.getTopTen(hook, 10).isEmpty()); + } + @Test void testTopTenDataRecordFields() { TopTenData d = new TopTenData(island, 42, 1234L, "phasy");