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");