From cd55357291b16f85c41fa32f430477743d634c20 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 11 Oct 2025 16:00:20 -0700 Subject: [PATCH 01/14] Version 2.22.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ee3d307..80507e9 100644 --- a/pom.xml +++ b/pom.xml @@ -67,7 +67,7 @@ -LOCAL - 2.22.0 + 2.22.1 BentoBoxWorld_Level bentobox-world https://sonarcloud.io From c13774c8507d931e52df73b95570eba565a04d5a Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 11 Oct 2025 16:00:30 -0700 Subject: [PATCH 02/14] Remove 'chain' from config --- src/main/resources/blockconfig.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/blockconfig.yml b/src/main/resources/blockconfig.yml index 6eebbbc..353f3d6 100644 --- a/src/main/resources/blockconfig.yml +++ b/src/main/resources/blockconfig.yml @@ -141,7 +141,6 @@ blocks: carved_pumpkin: 2 cauldron: 10 cave_air: 0 - chain: 2 chain_command_block: 0 chest: 8 chipped_anvil: 9 From 50596192cf2e3f39a273b17d2097e112f475f349 Mon Sep 17 00:00:00 2001 From: tastybento Date: Thu, 27 Nov 2025 17:53:35 -0800 Subject: [PATCH 03/14] WIP - not working yet --- pom.xml | 88 ++-- .../world/bentobox/level/LevelsManager.java | 4 +- .../bentobox/level/calculators/Results.java | 2 +- .../bentobox/level/config/BlockConfig.java | 4 +- .../world/bentobox/level/CommonTestSetup.java | 336 ++++++++++++++ .../java/world/bentobox/level/LevelTest.java | 109 +---- .../bentobox/level/LevelsManagerTest.java | 216 ++++----- .../level/PlaceholderManagerTest.java | 216 +++++---- .../bentobox/level/TestWorldSettings.java | 409 ++++++++++++++++++ .../java/world/bentobox/level/WhiteBox.java | 26 ++ .../level/commands/AdminStatsCommandTest.java | 172 +++----- .../commands/AdminTopRemoveCommandTest.java | 164 ++----- .../bentobox/level/mocks/ServerMocks.java | 118 ----- 13 files changed, 1136 insertions(+), 728 deletions(-) create mode 100644 src/test/java/world/bentobox/level/CommonTestSetup.java create mode 100644 src/test/java/world/bentobox/level/TestWorldSettings.java create mode 100644 src/test/java/world/bentobox/level/WhiteBox.java delete mode 100644 src/test/java/world/bentobox/level/mocks/ServerMocks.java diff --git a/pom.xml b/pom.xml index 80507e9..7c9d7f4 100644 --- a/pom.xml +++ b/pom.xml @@ -52,10 +52,12 @@ UTF-8 21 - 2.0.9 + 5.10.2 + 5.11.0 + v1.21-SNAPSHOT - 1.21.5-R0.1-SNAPSHOT - 3.7.4 + 1.21.10-R0.1-SNAPSHOT + 3.10.2 1.12.0 @@ -67,7 +69,7 @@ -LOCAL - 2.22.1 + 2.23.0 BentoBoxWorld_Level bentobox-world https://sonarcloud.io @@ -124,24 +126,24 @@ - spigot-repo - https://hub.spigotmc.org/nexus/content/repositories/snapshots + jitpack.io + https://jitpack.io - codemc - https://repo.codemc.org/repository/maven-snapshots/ + codemc-repo + https://repo.codemc.org/repository/maven-public - codemc-repo - https://repo.codemc.org/repository/maven-public/ + papermc + https://repo.papermc.io/repository/maven-public/ - bentoboxworld - https://repo.codemc.org/repository/bentoboxworld/ + codemc + https://repo.codemc.org/repository/maven-snapshots/ - jitpack.io - https://jitpack.io + bentoboxworld + https://repo.codemc.org/repository/bentoboxworld/ @@ -172,36 +174,49 @@ - + - org.spigotmc - spigot-api - ${spigot.version} - provided - - + 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-junit-jupiter + 5.11.0 + test + org.mockito mockito-core - 3.11.1 + ${mockito.version} test + - org.powermock - powermock-module-junit4 - ${powermock.version} - test - - - org.powermock - powermock-api-mockito2 - ${powermock.version} - test + io.papermc.paper + paper-api + ${paper.version} + provided world.bentobox bentobox - 3.7.4-SNAPSHOT + 3.10.0 world.bentobox @@ -354,7 +369,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.13.0 + 3.14.1 ${java.version} @@ -362,10 +377,11 @@ org.apache.maven.plugins maven-surefire-plugin - 3.5.2 + 3.5.4 ${argLine} + -XX:+EnableDynamicAgentLoading --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED @@ -478,7 +494,7 @@ org.jacoco jacoco-maven-plugin - 0.8.10 + 0.8.13 true diff --git a/src/main/java/world/bentobox/level/LevelsManager.java b/src/main/java/world/bentobox/level/LevelsManager.java index ac9c46b..555a239 100644 --- a/src/main/java/world/bentobox/level/LevelsManager.java +++ b/src/main/java/world/bentobox/level/LevelsManager.java @@ -57,6 +57,7 @@ public LevelsManager(Level addon) { // Set up the database handler to store and retrieve data // Note that these are saved by the BentoBox database handler = new Database<>(addon, IslandLevels.class); + // Initialize the cache levelsCache = new HashMap<>(); // Initialize top ten lists @@ -237,7 +238,6 @@ private long getNumBlocks(final long initialLevel) throws ParseException, IOExce * * @param world - world where the island is * @param targetPlayer - target player UUID - * @param ownerOnly - return level only if the target player is the owner * @return Level of the player's island or zero if player is unknown or UUID is * null */ @@ -318,6 +318,7 @@ public IslandLevels getLevelsData(@NonNull Island island) { } else { levelsCache.put(id, new IslandLevels(id)); } + System.out.println("ddd = " + levelsCache.get(id).getLevel()); // Return cached value return levelsCache.get(id); } @@ -492,7 +493,6 @@ public void setInitialIslandCount(@NonNull Island island, long lv) { * member * * @param world - world - * @param island - island * @param lv - level */ public void setIslandLevel(@NonNull World world, @NonNull UUID targetPlayer, long lv) { diff --git a/src/main/java/world/bentobox/level/calculators/Results.java b/src/main/java/world/bentobox/level/calculators/Results.java index 6dbd2a5..db8124c 100644 --- a/src/main/java/world/bentobox/level/calculators/Results.java +++ b/src/main/java/world/bentobox/level/calculators/Results.java @@ -173,7 +173,7 @@ public long getInitialCount() { } /** - * @param long1 the initialCount to set + * @param count the initialCount to set */ public void setInitialCount(Long count) { this.initialCount.set(count); diff --git a/src/main/java/world/bentobox/level/config/BlockConfig.java b/src/main/java/world/bentobox/level/config/BlockConfig.java index feb7495..1ebfcba 100644 --- a/src/main/java/world/bentobox/level/config/BlockConfig.java +++ b/src/main/java/world/bentobox/level/config/BlockConfig.java @@ -240,7 +240,7 @@ public Integer getValue(World world, Object obj) { /** * Return true if the block should be hidden - * @param m block material or entity type of spawner + * @param obj object that can be a material or string * @return true if hidden */ public boolean isHiddenBlock(Object obj) { @@ -254,7 +254,7 @@ public boolean isHiddenBlock(Object obj) { /** * Return true if the block should not be hidden - * @param m block material + * @param obj object that can be a material or string * @return false if hidden */ public boolean isNotHiddenBlock(Object obj) { diff --git a/src/test/java/world/bentobox/level/CommonTestSetup.java b/src/test/java/world/bentobox/level/CommonTestSetup.java new file mode 100644 index 0000000..876424a --- /dev/null +++ b/src/test/java/world/bentobox/level/CommonTestSetup.java @@ -0,0 +1,336 @@ +package world.bentobox.level; + +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.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +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.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.entity.Player.Spigot; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.inventory.ItemFactory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.metadata.FixedMetadataValue; +import org.bukkit.metadata.MetadataValue; +import org.bukkit.plugin.PluginManager; +import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.util.Vector; +import org.eclipse.jdt.annotation.Nullable; +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.ArgumentCaptor; +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 net.md_5.bungee.api.chat.TextComponent; +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.Settings; +import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.commands.CompositeCommand; +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 items for testing. Don't forget to use super.setUp()! + *

+ * Sets up BentoBox plugin, pluginManager and ItemFactory. + * Location, world, playersManager and player. + * IWM, Addon and WorldSettings. IslandManager with one + * island with protection and nothing allowed by default. + * Owner of island is player with same UUID. + * Locales, placeholders. + * @author tastybento + * + */ +public abstract class CommonTestSetup { + + protected UUID uuid = UUID.randomUUID(); + + @Mock + protected GameModeAddon gameModeAddon; + @Mock + protected Level addon; + @Mock + protected Player p; + @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; + + protected ServerMock server; + + protected MockedStatic mockedBukkit; + protected MockedStatic mockedUtil; + + protected AutoCloseable closeable; + + @Mock + protected BukkitScheduler sch; + @Mock + protected LocalesManager lm; + @Mock + protected CompositeCommand ic; + + + @BeforeEach + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + // Processes the @Mock annotations and initializes the field + closeable = MockitoAnnotations.openMocks(this); + server = MockBukkit.mock(); + // Bukkit + // Set up plugin + WhiteBox.setInternalState(BentoBox.class, "instance", plugin); + + // Register the static 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); + + // World + when(world.toString()).thenReturn("world"); + when(world.getName()).thenReturn("BSkyBlock_world"); + + // 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); // Paper + + // Players Manager and meta data + 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(p.getUniqueId()).thenReturn(uuid); + when(p.getLocation()).thenReturn(location); + when(p.getWorld()).thenReturn(world); + when(p.getName()).thenReturn("tastybento"); + when(p.getInventory()).thenReturn(inv); + when(p.spigot()).thenReturn(spigot); + when(p.getType()).thenReturn(EntityType.PLAYER); + when(p.getWorld()).thenReturn(world); + + User.setPlugin(plugin); + User.clearUsers(); + User.getInstance(p); + + // 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("BSkyBlock"); + // Addon + when(iwm.getAddon(any())).thenReturn(Optional.empty()); + + // World Settings + WorldSettings worldSet = new TestWorldSettings(); + when(iwm.getWorldSettings(any())).thenReturn(worldSet); + + // Island Manager + when(plugin.getIslands()).thenReturn(im); + Optional optionalIsland = Optional.of(island); + when(im.getProtectedIslandAt(any())).thenReturn(optionalIsland); + + // Island - nothing is allowed by default + 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)); + + // Enable reporting from Flags class + MetadataValue mdv = new FixedMetadataValue(plugin, "_why_debug"); + when(p.getMetadata(anyString())).thenReturn(Collections.singletonList(mdv)); + + // Locales & Placeholders + when(lm.get(any(), any())).thenAnswer((Answer) invocation -> invocation.getArgument(1, String.class)); + PlaceholdersManager phm = mock(PlaceholdersManager.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); + + // Fake players + Settings settings = new Settings(); + when(plugin.getSettings()).thenReturn(settings); + + //Util + mockedUtil = Mockito.mockStatic(Util.class, Mockito.CALLS_REAL_METHODS); + mockedUtil.when(() -> Util.getWorld(any())).thenReturn(mock(World.class)); + Util.setPlugin(plugin); + + // Util + mockedUtil.when(() -> Util.findFirstMatchingEnum(any(), any())).thenCallRealMethod(); + // Util translate color codes (used in user translate methods) + //mockedUtil.when(() -> translateColorCodes(anyString())).thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); + + // Server & Scheduler + mockedBukkit.when(() -> Bukkit.getScheduler()).thenReturn(sch); + + // Hooks + when(hooksManager.getHook(anyString())).thenReturn(Optional.empty()); + when(plugin.getHooks()).thenReturn(hooksManager); + + // Blueprints Manager + when(plugin.getBlueprintsManager()).thenReturn(bm); + + // Addon + when(addon.getPlugin()).thenReturn(plugin); + when(addon.getIslands()).thenReturn(im); + when(addon.getIslandsManager()).thenReturn(im); + + // Command + when(ic.getAddon()).thenReturn(addon); + when(ic.getPermissionPrefix()).thenReturn("bskyblock."); + when(ic.getLabel()).thenReturn("island"); + when(ic.getTopLabel()).thenReturn("island"); + when(ic.getWorld()).thenReturn(world); + when(ic.getTopLabel()).thenReturn("bsb"); + + } + + /** + * @throws Exception + */ + @AfterEach + public void tearDown() throws Exception { + // IMPORTANT: Explicitly close the mock to prevent leakage + 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); + } + + } + + /** + * Check that spigot sent the message + * @param message - message to check + */ + public void checkSpigotMessage(String expectedMessage) { + checkSpigotMessage(expectedMessage, 1); + } + + @SuppressWarnings("deprecation") + public void checkSpigotMessage(String expectedMessage, int expectedOccurrences) { + // Capture the argument passed to spigot().sendMessage(...) if messages are sent + ArgumentCaptor captor = ArgumentCaptor.forClass(TextComponent.class); + + // Verify that sendMessage() was called at least 0 times (capture any sent messages) + verify(spigot, atLeast(0)).sendMessage(captor.capture()); + + // Get all captured TextComponents + List capturedMessages = captor.getAllValues(); + + // Count the number of occurrences of the expectedMessage in the captured messages + long actualOccurrences = capturedMessages.stream().map(component -> component.toLegacyText()) // Convert each TextComponent to plain text + .filter(messageText -> messageText.contains(expectedMessage)) // Check if the message contains the expected text + .count(); // Count how many times the expected message appears + + // Assert that the number of occurrences matches the expectedOccurrences + assertEquals(expectedOccurrences, + actualOccurrences, "Expected message occurrence mismatch: " + expectedMessage); + } + + /** + * Get the explode event + * @param entity + * @param l + * @param list + * @return + */ + public EntityExplodeEvent getExplodeEvent(Entity entity, Location l, List list) { + //return new EntityExplodeEvent(entity, l, list, 0, null); + return new EntityExplodeEvent(entity, l, list, 0, null); + } + + public PlayerDeathEvent getPlayerDeathEvent(Player player, List drops, int droppedExp, int newExp, + int newTotalExp, int newLevel, @Nullable String deathMessage) { + //Technically this null is not allowed, but it works right now + return new PlayerDeathEvent(player, null, drops, droppedExp, newExp, + newTotalExp, newLevel, deathMessage); + } + +} diff --git a/src/test/java/world/bentobox/level/LevelTest.java b/src/test/java/world/bentobox/level/LevelTest.java index 2dd0e87..4993741 100644 --- a/src/test/java/world/bentobox/level/LevelTest.java +++ b/src/test/java/world/bentobox/level/LevelTest.java @@ -17,86 +17,56 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; -import java.util.Comparator; import java.util.Optional; import java.util.UUID; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import java.util.logging.Logger; -import org.bukkit.Bukkit; -import org.bukkit.Server; -import org.bukkit.UnsafeValues; -import org.bukkit.World; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemFactory; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.plugin.PluginManager; -import org.bukkit.scheduler.BukkitScheduler; import org.eclipse.jdt.annotation.NonNull; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +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 org.mockito.stubbing.Answer; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.reflect.Whitebox; -import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.Settings; import world.bentobox.bentobox.api.addons.AddonDescription; import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.DatabaseSetup.DatabaseType; -import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.hooks.ItemsAdderHook; import world.bentobox.bentobox.managers.AddonsManager; import world.bentobox.bentobox.managers.CommandsManager; 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.PlaceholdersManager; import world.bentobox.bentobox.util.Util; import world.bentobox.level.config.BlockConfig; import world.bentobox.level.config.ConfigSettings; import world.bentobox.level.listeners.IslandActivitiesListeners; import world.bentobox.level.listeners.JoinLeaveListener; -import world.bentobox.level.mocks.ServerMocks; /** * @author tastybento * */ -@SuppressWarnings("deprecation") -@RunWith(PowerMockRunner.class) -@PrepareForTest({ Bukkit.class, BentoBox.class, User.class, Util.class, ItemsAdderHook.class }) -public class LevelTest { +public class LevelTest extends CommonTestSetup { private static File jFile; @Mock private User user; @Mock - private IslandsManager im; - @Mock - private Island island; - @Mock - private BentoBox plugin; - @Mock private FlagsManager fm; @Mock private GameModeAddon gameMode; @Mock private AddonsManager am; - @Mock - private BukkitScheduler scheduler; @Mock private Settings pluginSettings; @@ -111,18 +81,14 @@ public class LevelTest { private CompositeCommand cmd; @Mock private CompositeCommand adminCmd; - @Mock - private World world; - private UUID uuid; - @Mock - private PluginManager pim; @Mock private BlockConfig blockConfig; @Mock private HooksManager hm; + private MockedStatic itemsAdderMock; - @BeforeClass + @BeforeAll public static void beforeClass() throws IOException { // Make the addon jar jFile = new File("addon.jar"); @@ -151,29 +117,25 @@ public static void beforeClass() throws IOException { /** * @throws java.lang.Exception */ - @Before + @Override + @BeforeEach public void setUp() throws Exception { + super.setUp(); when(plugin.getHooks()).thenReturn(hm); - Server server = ServerMocks.newServer(); - // Set up plugin - Whitebox.setInternalState(BentoBox.class, "instance", plugin); - when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger()); - // The database type has to be created one line before the thenReturn() to work! DatabaseType value = DatabaseType.JSON; when(plugin.getSettings()).thenReturn(pluginSettings); when(pluginSettings.getDatabaseType()).thenReturn(value); // ItemsAdderHook - PowerMockito.mockStatic(ItemsAdderHook.class, Mockito.RETURNS_MOCKS); - when(ItemsAdderHook.isInRegistry(anyString())).thenReturn(true); + itemsAdderMock = Mockito.mockStatic(ItemsAdderHook.class, Mockito.RETURNS_MOCKS); + itemsAdderMock.when(() -> ItemsAdderHook.isInRegistry(anyString())).thenReturn(true); // Command manager CommandsManager cm = mock(CommandsManager.class); when(plugin.getCommandsManager()).thenReturn(cm); // Player - Player p = mock(Player.class); // Sometimes use Mockito.withSettings().verboseLogging() when(user.isOp()).thenReturn(false); uuid = UUID.randomUUID(); @@ -182,29 +144,17 @@ public void setUp() throws Exception { when(user.getName()).thenReturn("tastybento"); User.setPlugin(plugin); - // Island World Manager - IslandWorldManager iwm = mock(IslandWorldManager.class); - when(plugin.getIWM()).thenReturn(iwm); - // Player has island to begin with when(im.getIsland(Mockito.any(), Mockito.any(UUID.class))).thenReturn(island); - when(plugin.getIslands()).thenReturn(im); // Locales // Return the reference (USE THIS IN THE FUTURE) when(user.getTranslation(Mockito.anyString())) .thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); - // Server - PowerMockito.mockStatic(Bukkit.class); - when(Bukkit.getServer()).thenReturn(server); - when(Bukkit.getLogger()).thenReturn(Logger.getAnonymousLogger()); - when(Bukkit.getPluginManager()).thenReturn(mock(PluginManager.class)); - when(Bukkit.getBukkitVersion()).thenReturn(""); // Util - PowerMockito.mockStatic(Util.class, Mockito.RETURNS_MOCKS); - when(Util.inTest()).thenReturn(true); + mockedUtil.when(() -> Util.inTest()).thenReturn(true); // Addon addon = new Level(); @@ -236,37 +186,22 @@ public void setUp() throws Exception { when(plugin.getFlagsManager()).thenReturn(fm); when(fm.getFlags()).thenReturn(Collections.emptyList()); - // Bukkit - when(Bukkit.getScheduler()).thenReturn(scheduler); - ItemMeta meta = mock(ItemMeta.class); - ItemFactory itemFactory = mock(ItemFactory.class); - when(itemFactory.getItemMeta(any())).thenReturn(meta); - when(Bukkit.getItemFactory()).thenReturn(itemFactory); - UnsafeValues unsafe = mock(UnsafeValues.class); - when(unsafe.getDataVersion()).thenReturn(777); - when(Bukkit.getUnsafe()).thenReturn(unsafe); - when(Bukkit.getPluginManager()).thenReturn(pim); - // placeholders when(plugin.getPlaceholdersManager()).thenReturn(phm); - // World - when(world.getName()).thenReturn("bskyblock-world"); - // Island - when(island.getWorld()).thenReturn(world); - when(island.getOwner()).thenReturn(uuid); } /** * @throws java.lang.Exception */ - @After + @Override + @AfterEach public void tearDown() throws Exception { - ServerMocks.unsetBukkitServer(); + super.tearDown(); deleteAll(new File("database")); } - @AfterClass + @AfterAll public static void cleanUp() throws Exception { new File("addon.jar").delete(); new File("config.yml").delete(); @@ -274,12 +209,6 @@ public static void cleanUp() throws Exception { deleteAll(new File("addons")); } - private static void deleteAll(File file) throws IOException { - if (file.exists()) { - Files.walk(file.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); - } - } - /** * Test method for {@link world.bentobox.level.Level#allLoaded() */ diff --git a/src/test/java/world/bentobox/level/LevelsManagerTest.java b/src/test/java/world/bentobox/level/LevelsManagerTest.java index a790604..d80da07 100644 --- a/src/test/java/world/bentobox/level/LevelsManagerTest.java +++ b/src/test/java/world/bentobox/level/LevelsManagerTest.java @@ -7,92 +7,62 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.beans.IntrospectionException; import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; import org.bukkit.Bukkit; import org.bukkit.World; -import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemFactory; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.plugin.PluginManager; -import org.bukkit.scheduler.BukkitScheduler; -import org.junit.After; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.stubbing.Answer; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.reflect.Whitebox; -import com.google.common.collect.ImmutableSet; - -import world.bentobox.bentobox.BentoBox; -import world.bentobox.bentobox.Settings; -import world.bentobox.bentobox.api.panels.builders.PanelBuilder; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.AbstractDatabaseHandler; import world.bentobox.bentobox.database.DatabaseSetup; -import world.bentobox.bentobox.database.DatabaseSetup.DatabaseType; import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.bentobox.managers.IslandWorldManager; -import world.bentobox.bentobox.managers.IslandsManager; import world.bentobox.bentobox.managers.PlayersManager; import world.bentobox.level.calculators.Pipeliner; import world.bentobox.level.calculators.Results; import world.bentobox.level.config.ConfigSettings; import world.bentobox.level.objects.IslandLevels; +import world.bentobox.level.objects.LevelsData; import world.bentobox.level.objects.TopTenData; /** * @author tastybento * */ -@RunWith(PowerMockRunner.class) -@PrepareForTest({ Bukkit.class, BentoBox.class, DatabaseSetup.class, PanelBuilder.class }) -public class LevelsManagerTest { +public class LevelsManagerTest extends CommonTestSetup { @Mock - private static AbstractDatabaseHandler handler; - @Mock - Level addon; + private AbstractDatabaseHandler handler; @Mock - private BentoBox plugin; + private AbstractDatabaseHandler levelsDataHandler; @Mock - private Settings pluginSettings; + private AbstractDatabaseHandler topTenHandler; // Class under test private LevelsManager lm; @Mock - private Island island; - @Mock private Pipeliner pipeliner; private CompletableFuture cf; - private UUID uuid; - @Mock - private World world; - @Mock - private Player player; private ConfigSettings settings; @Mock @@ -102,76 +72,48 @@ public class LevelsManagerTest { @Mock private Inventory inv; @Mock - private IslandWorldManager iwm; - @Mock - private PluginManager pim; - @Mock private IslandLevels levelsData; - @Mock - private IslandsManager im; - @Mock - private BukkitScheduler scheduler; - - @SuppressWarnings("unchecked") - @BeforeClass - public static void beforeClass() { - // This has to be done beforeClass otherwise the tests will interfere with each - // other - handler = mock(AbstractDatabaseHandler.class); - // Database - PowerMockito.mockStatic(DatabaseSetup.class); - DatabaseSetup dbSetup = mock(DatabaseSetup.class); - when(DatabaseSetup.getDatabase()).thenReturn(dbSetup); - when(dbSetup.getHandler(any())).thenReturn(handler); - } + + protected Object savedObject; + private MockedStatic mockedDatabaseSetup; /** * @throws java.lang.Exception */ - @SuppressWarnings("deprecation") - @Before + @SuppressWarnings({ "deprecation", "unchecked" }) + @Override + @BeforeEach public void setUp() throws Exception { - when(addon.getPlugin()).thenReturn(plugin); - // Set up plugin - Whitebox.setInternalState(BentoBox.class, "instance", plugin); - - // Bukkit - PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS); - when(Bukkit.getWorld(anyString())).thenReturn(world); - when(Bukkit.getPluginManager()).thenReturn(pim); - when(Bukkit.getPlayer(any(UUID.class))).thenReturn(player); - when(Bukkit.getScheduler()).thenReturn(scheduler); - - // The database type has to be created one line before the thenReturn() to work! - DatabaseType value = DatabaseType.JSON; - when(plugin.getSettings()).thenReturn(pluginSettings); - when(pluginSettings.getDatabaseType()).thenReturn(value); - + super.setUp(); + // Clear any lingering database + deleteAll(new File("database")); + deleteAll(new File("database_backup")); + // Database + handler = mock(AbstractDatabaseHandler.class); + levelsDataHandler = mock(AbstractDatabaseHandler.class); + topTenHandler = mock(AbstractDatabaseHandler.class); + // Database + mockedDatabaseSetup = Mockito.mockStatic(DatabaseSetup.class); + DatabaseSetup dbSetup = mock(DatabaseSetup.class); + mockedDatabaseSetup.when(() -> DatabaseSetup.getDatabase()).thenReturn(dbSetup); + when(dbSetup.getHandler(eq(IslandLevels.class))).thenReturn(handler); + when(dbSetup.getHandler(eq(LevelsData.class))).thenReturn(levelsDataHandler); + when(dbSetup.getHandler(eq(TopTenData.class))).thenReturn(topTenHandler); + + this.databaseSetup(handler); + this.databaseSetup(levelsDataHandler); + this.databaseSetup(topTenHandler); + savedObject = null; + + // Pipeliner when(addon.getPipeliner()).thenReturn(pipeliner); cf = new CompletableFuture<>(); when(pipeliner.addIsland(any())).thenReturn(cf); - // Island - when(addon.getIslands()).thenReturn(im); - uuid = UUID.randomUUID(); - ImmutableSet iset = ImmutableSet.of(uuid); - when(island.getMemberSet()).thenReturn(iset); - when(island.getOwner()).thenReturn(uuid); - when(island.getWorld()).thenReturn(world); - when(island.getUniqueId()).thenReturn(uuid.toString()); - // Default to uuid's being island owners - when(im.hasIsland(eq(world), any(UUID.class))).thenReturn(true); - when(im.getIsland(world, uuid)).thenReturn(island); - when(im.getIslandById(anyString())).thenReturn(Optional.of(island)); - when(im.getIslandById(anyString(), eq(false))).thenReturn(Optional.of(island)); - // Player - when(player.getUniqueId()).thenReturn(uuid); - when(player.hasPermission(anyString())).thenReturn(true); - - // World - when(world.getName()).thenReturn("bskyblock-world"); + when(p.getUniqueId()).thenReturn(uuid); + when(p.hasPermission(anyString())).thenReturn(true); // Settings settings = new ConfigSettings(); @@ -181,7 +123,7 @@ public void setUp() throws Exception { when(user.getTranslation(anyString())).thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); when(user.getTranslation(eq("island.top.gui-heading"), eq("[name]"), anyString(), eq("[rank]"), anyString())).thenReturn("gui-heading"); when(user.getTranslation(eq("island.top.island-level"),eq("[level]"), anyString())).thenReturn("island-level"); - when(user.getPlayer()).thenReturn(player); + when(user.getPlayer()).thenReturn(p); // Player Manager when(addon.getPlayers()).thenReturn(pm); @@ -196,17 +138,12 @@ public void setUp() throws Exception { "player9", "player10" ); - // Mock item factory (for itemstacks) - ItemFactory itemFactory = mock(ItemFactory.class); - when(Bukkit.getItemFactory()).thenReturn(itemFactory); - ItemMeta itemMeta = mock(ItemMeta.class); - when(itemFactory.getItemMeta(any())).thenReturn(itemMeta); // Has perms - when(player.hasPermission(anyString())).thenReturn(true); + when(p.hasPermission(anyString())).thenReturn(true); // Make island levels - List islands = new ArrayList<>(); + List islands = new ArrayList<>(); for (long i = -5; i < 5; i ++) { IslandLevels il = new IslandLevels(UUID.randomUUID().toString()); il.setInitialCount(null); @@ -223,34 +160,55 @@ public void setUp() throws Exception { when(levelsData.getInitialCount()).thenReturn(null); when(levelsData.getUniqueId()).thenReturn(uuid.toString()); when(handler.loadObject(anyString())).thenReturn(levelsData ); + System.out.println("Hanlder = " + handler); + // Island Manager + when(island.getOwner()).thenReturn(uuid); + when(island.getUniqueId()).thenReturn(uuid.toString()); + when(im.getIsland(world, uuid)).thenReturn(island); // Inventory GUI - when(Bukkit.createInventory(any(), anyInt(), anyString())).thenReturn(inv); - - // IWM - when(plugin.getIWM()).thenReturn(iwm); - when(iwm.getPermissionPrefix(any())).thenReturn("bskyblock."); + mockedBukkit.when(() -> Bukkit.createInventory(any(), anyInt(), anyString())).thenReturn(inv); lm = new LevelsManager(addon); - } /** * @throws java.lang.Exception */ - @After + @Override + @AfterEach public void tearDown() throws Exception { - deleteAll(new File("database")); - User.clearUsers(); - Mockito.framework().clearInlineMocks(); - } - - private static void deleteAll(File file) throws IOException { - if (file.exists()) { - Files.walk(file.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); - } + super.tearDown(); + handler.close(); + this.levelsDataHandler.close(); + this.topTenHandler.close(); } + + @SuppressWarnings("unchecked") + private void databaseSetup(AbstractDatabaseHandler h) throws Exception { + // Save objects + when(h.saveObject(any())).thenReturn(CompletableFuture.completedFuture(true)); + // Capture the parameter passed to saveObject() and store it in savedObject + doAnswer(invocation -> { + savedObject = invocation.getArgument(0); + return CompletableFuture.completedFuture(true); + }).when(h).saveObject(any()); + + // Now when loadObject() is called, return the savedObject + when(h.loadObject(any())).thenAnswer(invocation -> savedObject); + + // Delete object + doAnswer(invocation -> { + savedObject = null; + return null; + }).when(h).deleteObject(any()); + + doAnswer(invocation -> { + savedObject = null; + return null; + }).when(h).deleteID(anyString()); + } /** * Test method for @@ -293,6 +251,12 @@ public void testGetInitialCount() { /** * Test method for * {@link world.bentobox.level.LevelsManager#getIslandLevel(org.bukkit.World, java.util.UUID)}. + * @throws IntrospectionException + * @throws NoSuchMethodException + * @throws ClassNotFoundException + * @throws InvocationTargetException + * @throws IllegalAccessException + * @throws InstantiationException */ @Test public void testGetIslandLevel() { @@ -397,9 +361,7 @@ public void testHasTopTenPerm() { public void testLoadTopTens() { ArgumentCaptor task = ArgumentCaptor.forClass(Runnable.class); lm.loadTopTens(); - PowerMockito.verifyStatic(Bukkit.class); // 1 - Bukkit.getScheduler(); - verify(scheduler).runTaskAsynchronously(eq(plugin), task.capture()); // Capture the task in the scheduler + verify(sch).runTaskAsynchronously(eq(plugin), task.capture()); // Capture the task in the scheduler task.getValue().run(); // run it verify(addon).log("Generating rankings"); verify(addon).log("Generated rankings for bskyblock-world"); diff --git a/src/test/java/world/bentobox/level/PlaceholderManagerTest.java b/src/test/java/world/bentobox/level/PlaceholderManagerTest.java index 10bf0d0..ff3046b 100644 --- a/src/test/java/world/bentobox/level/PlaceholderManagerTest.java +++ b/src/test/java/world/bentobox/level/PlaceholderManagerTest.java @@ -22,20 +22,16 @@ import org.bukkit.Location; import org.bukkit.World; import org.eclipse.jdt.annotation.NonNull; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +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.stubbing.Answer; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.addons.AddonDescription; import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.bentobox.managers.IslandsManager; import world.bentobox.bentobox.managers.PlaceholdersManager; import world.bentobox.bentobox.managers.PlayersManager; import world.bentobox.bentobox.managers.RanksManager; @@ -45,16 +41,10 @@ * @author tastybento * */ -@RunWith(PowerMockRunner.class) -@PrepareForTest({ BentoBox.class }) -public class PlaceholderManagerTest { +public class PlaceholderManagerTest extends CommonTestSetup { - @Mock - private Level addon; @Mock private GameModeAddon gm; - @Mock - private BentoBox plugin; private PlaceholderManager phm; @Mock @@ -62,25 +52,19 @@ public class PlaceholderManagerTest { @Mock private LevelsManager lm; @Mock - private World world; - @Mock - private IslandsManager im; - @Mock - private Island island; - @Mock private User user; private static final Map names = new LinkedHashMap<>(); static { - names.put(UUID.randomUUID(), "tasty"); - names.put(UUID.randomUUID(), "bento"); - names.put(UUID.randomUUID(), "fred"); - names.put(UUID.randomUUID(), "bonne"); - names.put(UUID.randomUUID(), "cyprien"); - names.put(UUID.randomUUID(), "mael"); - names.put(UUID.randomUUID(), "joe"); - names.put(UUID.randomUUID(), "horacio"); - names.put(UUID.randomUUID(), "steph"); - names.put(UUID.randomUUID(), "vicky"); + names.put(UUID.randomUUID(), "tasty"); + names.put(UUID.randomUUID(), "bento"); + names.put(UUID.randomUUID(), "fred"); + names.put(UUID.randomUUID(), "bonne"); + names.put(UUID.randomUUID(), "cyprien"); + names.put(UUID.randomUUID(), "mael"); + names.put(UUID.randomUUID(), "joe"); + names.put(UUID.randomUUID(), "horacio"); + names.put(UUID.randomUUID(), "steph"); + names.put(UUID.randomUUID(), "vicky"); } private Map islands = new HashMap<>(); private Map map = new LinkedHashMap<>(); @@ -92,17 +76,18 @@ public class PlaceholderManagerTest { /** * @throws java.lang.Exception */ - @Before + @Override + @BeforeEach public void setUp() throws Exception { - when(addon.getPlugin()).thenReturn(plugin); - + super.setUp(); + // Users when(addon.getPlayers()).thenReturn(pm); - + // Users when(user.getWorld()).thenReturn(world); when(user.getLocation()).thenReturn(mock(Location.class)); - + int i = 0; for (Entry n : names.entrySet()) { UUID uuid = UUID.randomUUID(); // Random island ID @@ -123,24 +108,23 @@ public void setUp() throws Exception { Map members = new HashMap<>(); names.forEach((uuid, l) -> members.put(uuid, RanksManager.MEMBER_RANK)); islands.values().forEach(is -> is.setMembers(members)); - - + + // Placeholders manager for plugin when(plugin.getPlaceholdersManager()).thenReturn(bpm); - + // Game mode AddonDescription desc = new AddonDescription.Builder("bentobox", "AOneBlock", "1.3").description("test").authors("tasty").build(); when(gm.getDescription()).thenReturn(desc); when(gm.getOverWorld()).thenReturn(world); when(gm.inWorld(world)).thenReturn(true); - + // Islands when(im.getIsland(any(World.class), any(User.class))).thenReturn(island); when(im.getIslandAt(any(Location.class))).thenReturn(Optional.of(island)); when(im.getIslandById(anyString())).thenAnswer((Answer>) invocation -> Optional.of(islands.get(invocation.getArgument(0, String.class)))); when(im.getIslands(any(), any(UUID.class))).thenReturn(new ArrayList<>(islands.values())); - when(addon.getIslands()).thenReturn(im); - + // Levels Manager when(lm.getIslandLevel(any(), any())).thenReturn(1234567L); when(lm.getIslandLevelString(any(), any())).thenReturn("1234567"); @@ -149,7 +133,7 @@ public void setUp() throws Exception { when(lm.getTopTen(world, Level.TEN)).thenReturn(map); when(lm.getWeightedTopTen(world, Level.TEN)).thenReturn(map2); when(lm.formatLevel(any())).thenAnswer((Answer) invocation -> invocation.getArgument(0, Long.class).toString()); - + data = new IslandLevels("uniqueId"); data.setTotalPoints(12345678); when(lm.getLevelsData(island)).thenReturn(data); @@ -158,13 +142,19 @@ public void setUp() throws Exception { phm = new PlaceholderManager(addon); } + @Override + @AfterEach + public void tearDown() throws Exception { + super.tearDown(); + } + /** * Test method for * {@link world.bentobox.level.PlaceholderManager#PlaceholderManager(world.bentobox.level.Level)}. */ @Test public void testPlaceholderManager() { - verify(addon).getPlugin(); + verify(addon).getPlugin(); } /** @@ -173,32 +163,32 @@ public void testPlaceholderManager() { */ @Test public void testRegisterPlaceholders() { - phm.registerPlaceholders(gm); - // Island Level - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_island_level"), any()); - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_island_level_raw"), any()); - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_island_total_points"), any()); - - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_points_to_next_level"), any()); - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_island_level_max"), any()); - - // Visited Island Level - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_visited_island_level"), any()); - - // Register Top Ten Placeholders - for (int i = 1; i < 11; i++) { - // Name - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_top_name_" + i), any()); - // Island Name - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_top_island_name_" + i), any()); - // Members - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_top_members_" + i), any()); - // Level - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_top_value_" + i), any()); - } - - // Personal rank - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_rank_value"), any()); + phm.registerPlaceholders(gm); + // Island Level + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_island_level"), any()); + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_island_level_raw"), any()); + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_island_total_points"), any()); + + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_points_to_next_level"), any()); + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_island_level_max"), any()); + + // Visited Island Level + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_visited_island_level"), any()); + + // Register Top Ten Placeholders + for (int i = 1; i < 11; i++) { + // Name + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_top_name_" + i), any()); + // Island Name + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_top_island_name_" + i), any()); + // Members + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_top_members_" + i), any()); + // Level + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_top_value_" + i), any()); + } + + // Personal rank + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_rank_value"), any()); } @@ -208,14 +198,14 @@ public void testRegisterPlaceholders() { */ @Test public void testGetRankName() { - // Test extremes - assertEquals("tasty", phm.getRankName(world, 0, false)); - assertEquals("vicky", phm.getRankName(world, 100, false)); - // Test the ranks - int rank = 1; - for (String name : names.values()) { - assertEquals(name, phm.getRankName(world, rank++, false)); - } + // Test extremes + assertEquals("tasty", phm.getRankName(world, 0, false)); + assertEquals("vicky", phm.getRankName(world, 100, false)); + // Test the ranks + int rank = 1; + for (String name : names.values()) { + assertEquals(name, phm.getRankName(world, rank++, false)); + } } @@ -225,14 +215,14 @@ public void testGetRankName() { */ @Test public void testGetRankIslandName() { - // Test extremes - assertEquals("tasty's island", phm.getRankIslandName(world, 0, false)); - assertEquals("vicky's island", phm.getRankIslandName(world, 100, false)); - // Test the ranks - int rank = 1; - for (String name : names.values()) { - assertEquals(name + "'s island", phm.getRankIslandName(world, rank++, false)); - } + // Test extremes + assertEquals("tasty's island", phm.getRankIslandName(world, 0, false)); + assertEquals("vicky's island", phm.getRankIslandName(world, 100, false)); + // Test the ranks + int rank = 1; + for (String name : names.values()) { + assertEquals(name + "'s island", phm.getRankIslandName(world, rank++, false)); + } } @@ -242,19 +232,19 @@ public void testGetRankIslandName() { */ @Test public void testGetRankMembers() { - // Test extremes - check(1, phm.getRankMembers(world, 0, false)); - check(2, phm.getRankMembers(world, 100, false)); - // Test the ranks - for (int rank = 1; rank < 11; rank++) { - check(3, phm.getRankMembers(world, rank, false)); - } + // Test extremes + check(1, phm.getRankMembers(world, 0, false)); + check(2, phm.getRankMembers(world, 100, false)); + // Test the ranks + for (int rank = 1; rank < 11; rank++) { + check(3, phm.getRankMembers(world, rank, false)); + } } void check(int indicator, String list) { - for (String n : names.values()) { - assertTrue(n + " is missing for test " + indicator, list.contains(n)); - } + for (String n : names.values()) { + assertTrue(n + " is missing for test " + indicator, list.contains(n)); + } } /** @@ -263,13 +253,13 @@ void check(int indicator, String list) { */ @Test public void testGetRankLevel() { - // Test extremes - assertEquals("100", phm.getRankLevel(world, 0, false)); - assertEquals("91", phm.getRankLevel(world, 100, false)); - // Test the ranks - for (int rank = 1; rank < 11; rank++) { - assertEquals(String.valueOf(101 - rank), phm.getRankLevel(world, rank, false)); - } + // Test extremes + assertEquals("100", phm.getRankLevel(world, 0, false)); + assertEquals("91", phm.getRankLevel(world, 100, false)); + // Test the ranks + for (int rank = 1; rank < 11; rank++) { + assertEquals(String.valueOf(101 - rank), phm.getRankLevel(world, rank, false)); + } } @@ -279,13 +269,13 @@ public void testGetRankLevel() { */ @Test public void testGetWeightedRankLevel() { - // Test extremes - assertEquals("100", phm.getRankLevel(world, 0, true)); - assertEquals("91", phm.getRankLevel(world, 100, true)); - // Test the ranks - for (int rank = 1; rank < 11; rank++) { - assertEquals(String.valueOf(101 - rank), phm.getRankLevel(world, rank, true)); - } + // Test extremes + assertEquals("100", phm.getRankLevel(world, 0, true)); + assertEquals("91", phm.getRankLevel(world, 100, true)); + // Test the ranks + for (int rank = 1; rank < 11; rank++) { + assertEquals(String.valueOf(101 - rank), phm.getRankLevel(world, rank, true)); + } } @@ -295,7 +285,7 @@ public void testGetWeightedRankLevel() { */ @Test public void testGetVisitedIslandLevelNullUser() { - assertEquals("", phm.getVisitedIslandLevel(gm, null)); + assertEquals("", phm.getVisitedIslandLevel(gm, null)); } @@ -307,7 +297,7 @@ public void testGetVisitedIslandLevelUserNotInWorld() { // Another world when(user.getWorld()).thenReturn(mock(World.class)); assertEquals("", phm.getVisitedIslandLevel(gm, user)); - + } /** @@ -316,7 +306,7 @@ public void testGetVisitedIslandLevelUserNotInWorld() { */ @Test public void testGetVisitedIslandLevel() { - assertEquals("1234567", phm.getVisitedIslandLevel(gm, user)); + assertEquals("1234567", phm.getVisitedIslandLevel(gm, user)); } @@ -327,7 +317,7 @@ public void testGetVisitedIslandLevel() { public void testGetVisitedIslandLevelNoIsland() { when(im.getIslandAt(any(Location.class))).thenReturn(Optional.empty()); assertEquals("0", phm.getVisitedIslandLevel(gm, user)); - + } } diff --git a/src/test/java/world/bentobox/level/TestWorldSettings.java b/src/test/java/world/bentobox/level/TestWorldSettings.java new file mode 100644 index 0000000..291d577 --- /dev/null +++ b/src/test/java/world/bentobox/level/TestWorldSettings.java @@ -0,0 +1,409 @@ +package world.bentobox.level; + +import java.util.Collections; +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; + +/** + * Class for tests that require world settings + * @author tastybento + * + */ +public class TestWorldSettings implements WorldSettings { + + private long epoch; + + @Override + public GameMode getDefaultGameMode() { + + return GameMode.SURVIVAL; + } + + @Override + public Map getDefaultIslandFlags() { + + return Collections.emptyMap(); + } + + @Override + public Map getDefaultIslandSettings() { + + return Collections.emptyMap(); + } + + @Override + public Difficulty getDifficulty() { + + return Difficulty.EASY; + } + + @Override + public void setDifficulty(Difficulty difficulty) { + + + } + + @Override + public String getFriendlyName() { + + return "friendly_name"; + } + + @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 "perm."; + } + + @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 Collections.emptyMap(); + } + + @Override + public String getWorldName() { + + return "world_name"; + } + + @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/level/WhiteBox.java b/src/test/java/world/bentobox/level/WhiteBox.java new file mode 100644 index 0000000..a0f2e51 --- /dev/null +++ b/src/test/java/world/bentobox/level/WhiteBox.java @@ -0,0 +1,26 @@ +package world.bentobox.level; + +public class WhiteBox { + /** + * Sets the value of a private static field using Java Reflection. + * @param targetClass The class containing the static field. + * @param fieldName The name of the private static field. + * @param value The value to set the field to. + */ + public static void setInternalState(Class targetClass, String fieldName, Object value) { + try { + // 1. Get the Field object from the class + java.lang.reflect.Field field = targetClass.getDeclaredField(fieldName); + + // 2. Make the field accessible (required for private fields) + field.setAccessible(true); + + // 3. Set the new value. The first argument is 'null' for static fields. + field.set(null, value); + + } catch (NoSuchFieldException | IllegalAccessException e) { + // Wrap reflection exceptions in a runtime exception for clarity + throw new RuntimeException("Failed to set static field '" + fieldName + "' on class " + targetClass.getName(), e); + } + } +} diff --git a/src/test/java/world/bentobox/level/commands/AdminStatsCommandTest.java b/src/test/java/world/bentobox/level/commands/AdminStatsCommandTest.java index 21d26d0..55f4f55 100644 --- a/src/test/java/world/bentobox/level/commands/AdminStatsCommandTest.java +++ b/src/test/java/world/bentobox/level/commands/AdminStatsCommandTest.java @@ -1,11 +1,10 @@ package world.bentobox.level.commands; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -16,131 +15,62 @@ import java.util.Random; import java.util.UUID; -import org.bukkit.Bukkit; -import org.bukkit.Server; import org.bukkit.World; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemFactory; -import org.bukkit.inventory.meta.ItemMeta; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.Mock; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.reflect.Whitebox; -import world.bentobox.bentobox.BentoBox; -import world.bentobox.bentobox.api.addons.GameModeAddon; -import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.user.User; -import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.bentobox.managers.IslandWorldManager; -import world.bentobox.bentobox.managers.IslandsManager; -import world.bentobox.bentobox.managers.LocalesManager; import world.bentobox.bentobox.managers.PlayersManager; -import world.bentobox.level.Level; +import world.bentobox.level.CommonTestSetup; import world.bentobox.level.LevelsManager; import world.bentobox.level.objects.TopTenData; /** * @author tastybento */ -@RunWith(PowerMockRunner.class) -@PrepareForTest({ Bukkit.class, BentoBox.class }) -public class AdminStatsCommandTest { +public class AdminStatsCommandTest extends CommonTestSetup { @Mock - private CompositeCommand ic; - private UUID uuid; - @Mock - private User user; - @Mock - private IslandsManager im; - @Mock - private Island island; - @Mock - private Level addon; - @Mock - private World world; - @Mock - private IslandWorldManager iwm; - @Mock - private GameModeAddon gameModeAddon; - @Mock - private Player p; - @Mock - private LocalesManager lm; + private LevelsManager manager; @Mock private PlayersManager pm; - + @Mock + private User user; + private AdminStatsCommand asc; private TopTenData ttd; - @Mock - private LevelsManager manager; - @Mock - private Server server; - - @Before - public void setUp() { - // Set up plugin - BentoBox plugin = mock(BentoBox.class); - Whitebox.setInternalState(BentoBox.class, "instance", plugin); - User.setPlugin(plugin); - when(addon.getPlugin()).thenReturn(plugin); - - // Addon - when(ic.getAddon()).thenReturn(addon); - when(ic.getPermissionPrefix()).thenReturn("bskyblock."); - when(ic.getLabel()).thenReturn("island"); - when(ic.getTopLabel()).thenReturn("island"); - when(ic.getWorld()).thenReturn(world); - when(ic.getTopLabel()).thenReturn("bsb"); - - // IWM friendly name - when(plugin.getIWM()).thenReturn(iwm); - when(iwm.getFriendlyName(any())).thenReturn("BSkyBlock"); - - // World - when(world.toString()).thenReturn("world"); - when(world.getName()).thenReturn("BSkyBlock_world"); - - // Player manager - when(plugin.getPlayers()).thenReturn(pm); - when(pm.getUser(anyString())).thenReturn(user); - // topTen - when(addon.getManager()).thenReturn(manager); - // User - uuid = UUID.randomUUID(); - when(user.getUniqueId()).thenReturn(uuid); - when(user.getTranslation(any())).thenAnswer(invocation -> invocation.getArgument(0, String.class)); - - // Bukkit - PowerMockito.mockStatic(Bukkit.class); - when(Bukkit.getServer()).thenReturn(server); - // Mock item factory (for itemstacks) - ItemFactory itemFactory = mock(ItemFactory.class); - ItemMeta itemMeta = mock(ItemMeta.class); - when(itemFactory.getItemMeta(any())).thenReturn(itemMeta); - when(server.getItemFactory()).thenReturn(itemFactory); - when(Bukkit.getItemFactory()).thenReturn(itemFactory); - // Top ten - ttd = new TopTenData(world); - Map topten = new HashMap<>(); - Random r = new Random(); - for (int i = 0; i < 1000; i++) { - topten.put(UUID.randomUUID().toString(), r.nextLong(20000)); - } - ttd.setTopTen(topten); - asc = new AdminStatsCommand(addon, ic); + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + // Player manager + when(plugin.getPlayers()).thenReturn(pm); + when(pm.getUser(anyString())).thenReturn(user); + // topTen + when(addon.getManager()).thenReturn(manager); + // User + when(user.getUniqueId()).thenReturn(uuid); + when(user.getTranslation(any())).thenAnswer(invocation -> invocation.getArgument(0, String.class)); + + // Top ten + ttd = new TopTenData(world); + Map topten = new HashMap<>(); + Random r = new Random(); + for (int i = 0; i < 1000; i++) { + topten.put(UUID.randomUUID().toString(), r.nextLong(20000)); + } + ttd.setTopTen(topten); + asc = new AdminStatsCommand(addon, ic); } - @After - public void tearDown() { - User.clearUsers(); + @Override + @AfterEach + public void tearDown() throws Exception { + super.tearDown(); } /** @@ -149,9 +79,9 @@ public void tearDown() { */ @Test public void testSetup() { - assertEquals("bskyblock.admin.stats", asc.getPermission()); - assertFalse(asc.isOnlyPlayer()); - assertEquals("admin.stats.description", asc.getDescription()); + assertEquals("bskyblock.admin.stats", asc.getPermission()); + assertFalse(asc.isOnlyPlayer()); + assertEquals("admin.stats.description", asc.getDescription()); } @@ -161,9 +91,9 @@ public void testSetup() { */ @Test public void testExecuteUserStringListOfString() { - assertFalse(asc.execute(user, "", List.of())); - verify(user).sendMessage("admin.stats.title"); - verify(user).sendMessage("admin.stats.no-data"); + assertFalse(asc.execute(user, "", List.of())); + verify(user).sendMessage("admin.stats.title"); + verify(user).sendMessage("admin.stats.no-data"); } /** @@ -172,12 +102,12 @@ public void testExecuteUserStringListOfString() { */ @Test public void testExecuteUserStringListOfStringLevels() { - Map map = new HashMap<>(); - map.put(world, ttd); - when(manager.getTopTenLists()).thenReturn(map); - assertTrue(asc.execute(user, "", List.of())); - verify(user).sendMessage("admin.stats.title"); - verify(user, never()).sendMessage("admin.stats.no-data"); + Map map = new HashMap<>(); + map.put(world, ttd); + when(manager.getTopTenLists()).thenReturn(map); + assertTrue(asc.execute(user, "", List.of())); + verify(user).sendMessage("admin.stats.title"); + verify(user, never()).sendMessage("admin.stats.no-data"); } } diff --git a/src/test/java/world/bentobox/level/commands/AdminTopRemoveCommandTest.java b/src/test/java/world/bentobox/level/commands/AdminTopRemoveCommandTest.java index 8a49c25..41a9c3a 100644 --- a/src/test/java/world/bentobox/level/commands/AdminTopRemoveCommandTest.java +++ b/src/test/java/world/bentobox/level/commands/AdminTopRemoveCommandTest.java @@ -5,7 +5,6 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -13,33 +12,15 @@ import java.util.List; import java.util.UUID; -import org.bukkit.Bukkit; -import org.bukkit.Server; -import org.bukkit.World; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemFactory; -import org.bukkit.inventory.meta.ItemMeta; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.Mock; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.reflect.Whitebox; - -import world.bentobox.bentobox.BentoBox; -import world.bentobox.bentobox.api.addons.GameModeAddon; -import world.bentobox.bentobox.api.commands.CompositeCommand; + import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; -import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.bentobox.managers.IslandWorldManager; -import world.bentobox.bentobox.managers.IslandsManager; -import world.bentobox.bentobox.managers.LocalesManager; import world.bentobox.bentobox.managers.PlayersManager; -import world.bentobox.level.Level; +import world.bentobox.level.CommonTestSetup; import world.bentobox.level.LevelsManager; import world.bentobox.level.objects.TopTenData; @@ -47,98 +28,45 @@ * @author tastybento * */ -@RunWith(PowerMockRunner.class) -@PrepareForTest({ Bukkit.class, BentoBox.class }) -public class AdminTopRemoveCommandTest { +public class AdminTopRemoveCommandTest extends CommonTestSetup { - @Mock - private CompositeCommand ic; - private UUID uuid; @Mock private User user; @Mock - private IslandsManager im; - @Mock - private Island island; - @Mock - private Level addon; - @Mock - private World world; - @Mock - private IslandWorldManager iwm; - @Mock - private GameModeAddon gameModeAddon; - @Mock - private Player p; - @Mock - private LocalesManager lm; - @Mock private PlayersManager pm; - - private AdminTopRemoveCommand atrc; @Mock private TopTenData ttd; @Mock private LevelsManager manager; - @Mock - private Server server; - - @Before - public void setUp() { - // Set up plugin - BentoBox plugin = mock(BentoBox.class); - Whitebox.setInternalState(BentoBox.class, "instance", plugin); - User.setPlugin(plugin); - - // Addon - when(ic.getAddon()).thenReturn(addon); - when(ic.getPermissionPrefix()).thenReturn("bskyblock."); - when(ic.getLabel()).thenReturn("island"); - when(ic.getTopLabel()).thenReturn("island"); - when(ic.getWorld()).thenReturn(world); - when(ic.getTopLabel()).thenReturn("bsb"); - - // IWM friendly name - when(plugin.getIWM()).thenReturn(iwm); - when(iwm.getFriendlyName(any())).thenReturn("BSkyBlock"); - - // World - when(world.toString()).thenReturn("world"); - when(world.getName()).thenReturn("BSkyBlock_world"); - - // Player manager - when(plugin.getPlayers()).thenReturn(pm); - when(pm.getUser(anyString())).thenReturn(user); - // topTen - when(addon.getManager()).thenReturn(manager); - // User - uuid = UUID.randomUUID(); - when(user.getUniqueId()).thenReturn(uuid); - when(user.getTranslation(any())).thenAnswer(invocation -> invocation.getArgument(0, String.class)); - // Island - when(island.getUniqueId()).thenReturn(uuid.toString()); - when(island.getOwner()).thenReturn(uuid); - // Island Manager - when(plugin.getIslands()).thenReturn(im); - when(im.getIslands(any(), any(User.class))).thenReturn(List.of(island)); - when(im.getIslands(any(), any(UUID.class))).thenReturn(List.of(island)); - - // Bukkit - PowerMockito.mockStatic(Bukkit.class); - when(Bukkit.getServer()).thenReturn(server); - // Mock item factory (for itemstacks) - ItemFactory itemFactory = mock(ItemFactory.class); - ItemMeta itemMeta = mock(ItemMeta.class); - when(itemFactory.getItemMeta(any())).thenReturn(itemMeta); - when(server.getItemFactory()).thenReturn(itemFactory); - when(Bukkit.getItemFactory()).thenReturn(itemFactory); - - atrc = new AdminTopRemoveCommand(addon, ic); + + private AdminTopRemoveCommand atrc; + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + // Player manager + when(plugin.getPlayers()).thenReturn(pm); + when(pm.getUser(anyString())).thenReturn(user); + // topTen + when(addon.getManager()).thenReturn(manager); + // User + when(user.getUniqueId()).thenReturn(uuid); + when(user.getTranslation(any())).thenAnswer(invocation -> invocation.getArgument(0, String.class)); + // Island + when(island.getUniqueId()).thenReturn(uuid.toString()); + when(island.getOwner()).thenReturn(uuid); + // Island Manager + when(im.getIslands(any(), any(User.class))).thenReturn(List.of(island)); + when(im.getIslands(any(), any(UUID.class))).thenReturn(List.of(island)); + + atrc = new AdminTopRemoveCommand(addon, ic); } - @After - public void tearDown() { - User.clearUsers(); + @Override + @AfterEach + public void tearDown() throws Exception { + super.tearDown(); } /** @@ -147,8 +75,8 @@ public void tearDown() { */ @Test public void testAdminTopRemoveCommand() { - assertEquals("remove", atrc.getLabel()); - assertEquals("delete", atrc.getAliases().get(0)); + assertEquals("remove", atrc.getLabel()); + assertEquals("delete", atrc.getAliases().get(0)); } /** @@ -157,10 +85,10 @@ public void testAdminTopRemoveCommand() { */ @Test public void testSetup() { - assertEquals("bskyblock.admin.top.remove", atrc.getPermission()); - assertEquals("admin.top.remove.parameters", atrc.getParameters()); - assertEquals("admin.top.remove.description", atrc.getDescription()); - assertFalse(atrc.isOnlyPlayer()); + assertEquals("bskyblock.admin.top.remove", atrc.getPermission()); + assertEquals("admin.top.remove.parameters", atrc.getParameters()); + assertEquals("admin.top.remove.description", atrc.getDescription()); + assertFalse(atrc.isOnlyPlayer()); } @@ -170,8 +98,8 @@ public void testSetup() { */ @Test public void testCanExecuteWrongArgs() { - assertFalse(atrc.canExecute(user, "delete", Collections.emptyList())); - verify(user).sendMessage("commands.help.header", TextVariables.LABEL, "BSkyBlock"); + assertFalse(atrc.canExecute(user, "delete", Collections.emptyList())); + verify(user).sendMessage("commands.help.header", TextVariables.LABEL, "BSkyBlock"); } /** @@ -190,7 +118,7 @@ public void testCanExecuteUnknown() { */ @Test public void testCanExecuteKnown() { - assertTrue(atrc.canExecute(user, "delete", Collections.singletonList("tastybento"))); + assertTrue(atrc.canExecute(user, "delete", Collections.singletonList("tastybento"))); } /** @@ -199,10 +127,10 @@ public void testCanExecuteKnown() { */ @Test public void testExecuteUserStringListOfString() { - testCanExecuteKnown(); - assertTrue(atrc.execute(user, "delete", Collections.singletonList("tastybento"))); - verify(manager).removeEntry(world, uuid.toString()); - verify(user).sendMessage("general.success"); + testCanExecuteKnown(); + assertTrue(atrc.execute(user, "delete", Collections.singletonList("tastybento"))); + verify(manager).removeEntry(world, uuid.toString()); + verify(user).sendMessage("general.success"); } } diff --git a/src/test/java/world/bentobox/level/mocks/ServerMocks.java b/src/test/java/world/bentobox/level/mocks/ServerMocks.java deleted file mode 100644 index 568b529..0000000 --- a/src/test/java/world/bentobox/level/mocks/ServerMocks.java +++ /dev/null @@ -1,118 +0,0 @@ -package world.bentobox.level.mocks; - -import static org.mockito.ArgumentMatchers.notNull; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.logging.Logger; - -import org.bukkit.Bukkit; -import org.bukkit.Keyed; -import org.bukkit.NamespacedKey; -import org.bukkit.Registry; -import org.bukkit.Server; -import org.bukkit.Tag; -import org.bukkit.UnsafeValues; -import org.eclipse.jdt.annotation.NonNull; - -public final class ServerMocks { - - public static @NonNull Server newServer() { - Server mock = mock(Server.class); - - Logger noOp = mock(Logger.class); - when(mock.getLogger()).thenReturn(noOp); - when(mock.isPrimaryThread()).thenReturn(true); - - // Unsafe - UnsafeValues unsafe = mock(UnsafeValues.class); - when(mock.getUnsafe()).thenReturn(unsafe); - - // Server must be available before tags can be mocked. - Bukkit.setServer(mock); - - // Bukkit has a lot of static constants referencing registry values. To initialize those, the - // registries must be able to be fetched before the classes are touched. - Map, Object> registers = new HashMap<>(); - - doAnswer(invocationGetRegistry -> registers.computeIfAbsent(invocationGetRegistry.getArgument(0), clazz -> { - Registry registry = mock(Registry.class); - Map cache = new HashMap<>(); - doAnswer(invocationGetEntry -> { - NamespacedKey key = invocationGetEntry.getArgument(0); - // Some classes (like BlockType and ItemType) have extra generics that will be - // erased during runtime calls. To ensure accurate typing, grab the constant's field. - // This approach also allows us to return null for unsupported keys. - Class constantClazz; - try { - //noinspection unchecked - constantClazz = (Class) clazz - .getField(key.getKey().toUpperCase(Locale.ROOT).replace('.', '_')).getType(); - } catch (ClassCastException e) { - throw new RuntimeException(e); - } catch (NoSuchFieldException e) { - return null; - } - - return cache.computeIfAbsent(key, key1 -> { - Keyed keyed = mock(constantClazz); - doReturn(key).when(keyed).getKey(); - return keyed; - }); - }).when(registry).get(notNull()); - return registry; - })).when(mock).getRegistry(notNull()); - - // Tags are dependent on registries, but use a different method. - // This will set up blank tags for each constant; all that needs to be done to render them - // functional is to re-mock Tag#getValues. - doAnswer(invocationGetTag -> { - Tag tag = mock(Tag.class); - doReturn(invocationGetTag.getArgument(1)).when(tag).getKey(); - doReturn(Set.of()).when(tag).getValues(); - doAnswer(invocationIsTagged -> { - Keyed keyed = invocationIsTagged.getArgument(0); - Class type = invocationGetTag.getArgument(2); - if (!type.isAssignableFrom(keyed.getClass())) { - return null; - } - // Since these are mocks, the exact instance might not be equal. Consider equal keys equal. - return tag.getValues().contains(keyed) - || tag.getValues().stream().anyMatch(value -> value.getKey().equals(keyed.getKey())); - }).when(tag).isTagged(notNull()); - return tag; - }).when(mock).getTag(notNull(), notNull(), notNull()); - - // Once the server is all set up, touch BlockType and ItemType to initialize. - // This prevents issues when trying to access dependent methods from a Material constant. - try { - Class.forName("org.bukkit.inventory.ItemType"); - Class.forName("org.bukkit.block.BlockType"); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - - return mock; - } - - public static void unsetBukkitServer() { - try { - Field server = Bukkit.class.getDeclaredField("server"); - server.setAccessible(true); - server.set(null, null); - } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) { - throw new RuntimeException(e); - } - } - - private ServerMocks() { - } - -} \ No newline at end of file From 6a2abbebb6a7f230ac6e5dccec5ef794b177f1ae Mon Sep 17 00:00:00 2001 From: tastybento Date: Thu, 27 Nov 2025 22:54:14 -0800 Subject: [PATCH 04/14] Improvements --- .../bentobox/level/LevelsManagerTest.java | 156 +++++++++--------- .../level/PlaceholderManagerTest.java | 6 +- 2 files changed, 85 insertions(+), 77 deletions(-) diff --git a/src/test/java/world/bentobox/level/LevelsManagerTest.java b/src/test/java/world/bentobox/level/LevelsManagerTest.java index d80da07..21c3c4f 100644 --- a/src/test/java/world/bentobox/level/LevelsManagerTest.java +++ b/src/test/java/world/bentobox/level/LevelsManagerTest.java @@ -7,42 +7,45 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.beans.IntrospectionException; import java.io.File; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; import org.bukkit.Bukkit; import org.bukkit.World; +import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; +import org.junit.Test; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.stubbing.Answer; +import com.google.common.collect.ImmutableSet; + +import world.bentobox.bentobox.Settings; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.AbstractDatabaseHandler; import world.bentobox.bentobox.database.DatabaseSetup; +import world.bentobox.bentobox.database.DatabaseSetup.DatabaseType; import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.managers.IslandWorldManager; import world.bentobox.bentobox.managers.PlayersManager; import world.bentobox.level.calculators.Pipeliner; import world.bentobox.level.calculators.Results; import world.bentobox.level.config.ConfigSettings; import world.bentobox.level.objects.IslandLevels; -import world.bentobox.level.objects.LevelsData; import world.bentobox.level.objects.TopTenData; /** @@ -52,17 +55,17 @@ public class LevelsManagerTest extends CommonTestSetup { @Mock - private AbstractDatabaseHandler handler; - @Mock - private AbstractDatabaseHandler levelsDataHandler; + private AbstractDatabaseHandler handler; @Mock - private AbstractDatabaseHandler topTenHandler; + private Settings pluginSettings; // Class under test private LevelsManager lm; @Mock private Pipeliner pipeliner; private CompletableFuture cf; + @Mock + private Player player; private ConfigSettings settings; @Mock @@ -72,48 +75,67 @@ public class LevelsManagerTest extends CommonTestSetup { @Mock private Inventory inv; @Mock + private IslandWorldManager iwm; + @Mock private IslandLevels levelsData; - - protected Object savedObject; - private MockedStatic mockedDatabaseSetup; + //@Mock + //private BukkitScheduler scheduler; /** * @throws java.lang.Exception */ - @SuppressWarnings({ "deprecation", "unchecked" }) + @SuppressWarnings("unchecked") @Override @BeforeEach public void setUp() throws Exception { super.setUp(); - // Clear any lingering database - deleteAll(new File("database")); - deleteAll(new File("database_backup")); - // Database + handler = mock(AbstractDatabaseHandler.class); - levelsDataHandler = mock(AbstractDatabaseHandler.class); - topTenHandler = mock(AbstractDatabaseHandler.class); // Database - mockedDatabaseSetup = Mockito.mockStatic(DatabaseSetup.class); + MockedStatic mockedDatabaseSetup = Mockito.mockStatic(DatabaseSetup.class); DatabaseSetup dbSetup = mock(DatabaseSetup.class); mockedDatabaseSetup.when(() -> DatabaseSetup.getDatabase()).thenReturn(dbSetup); - when(dbSetup.getHandler(eq(IslandLevels.class))).thenReturn(handler); - when(dbSetup.getHandler(eq(LevelsData.class))).thenReturn(levelsDataHandler); - when(dbSetup.getHandler(eq(TopTenData.class))).thenReturn(topTenHandler); - - this.databaseSetup(handler); - this.databaseSetup(levelsDataHandler); - this.databaseSetup(topTenHandler); - savedObject = null; - - + when(dbSetup.getHandler(any())).thenReturn(handler); + when(addon.getPlugin()).thenReturn(plugin); + + // Bukkit + /* + when(Bukkit.getWorld(anyString())).thenReturn(world); + when(Bukkit.getPluginManager()).thenReturn(pim); + when(Bukkit.getPlayer(any(UUID.class))).thenReturn(player); + when(Bukkit.getScheduler()).thenReturn(scheduler); + */ + + // The database type has to be created one line before the thenReturn() to work! + DatabaseType value = DatabaseType.JSON; + when(plugin.getSettings()).thenReturn(pluginSettings); + when(pluginSettings.getDatabaseType()).thenReturn(value); + // Pipeliner when(addon.getPipeliner()).thenReturn(pipeliner); cf = new CompletableFuture<>(); when(pipeliner.addIsland(any())).thenReturn(cf); + // Island + //when(addon.getIslands()).thenReturn(im); + //uuid = UUID.randomUUID(); + ImmutableSet iset = ImmutableSet.of(uuid); + when(island.getMemberSet()).thenReturn(iset); + when(island.getOwner()).thenReturn(uuid); + when(island.getWorld()).thenReturn(world); + when(island.getUniqueId()).thenReturn(uuid.toString()); + // Default to uuid's being island owners + when(im.hasIsland(eq(world), any(UUID.class))).thenReturn(true); + when(im.getIsland(world, uuid)).thenReturn(island); + when(im.getIslandById(anyString())).thenReturn(Optional.of(island)); + when(im.getIslandById(anyString(), eq(false))).thenReturn(Optional.of(island)); + // Player - when(p.getUniqueId()).thenReturn(uuid); - when(p.hasPermission(anyString())).thenReturn(true); + when(player.getUniqueId()).thenReturn(uuid); + when(player.hasPermission(anyString())).thenReturn(true); + + // World + when(world.getName()).thenReturn("bskyblock-world"); // Settings settings = new ConfigSettings(); @@ -123,7 +145,7 @@ public void setUp() throws Exception { when(user.getTranslation(anyString())).thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); when(user.getTranslation(eq("island.top.gui-heading"), eq("[name]"), anyString(), eq("[rank]"), anyString())).thenReturn("gui-heading"); when(user.getTranslation(eq("island.top.island-level"),eq("[level]"), anyString())).thenReturn("island-level"); - when(user.getPlayer()).thenReturn(p); + when(user.getPlayer()).thenReturn(player); // Player Manager when(addon.getPlayers()).thenReturn(pm); @@ -138,12 +160,18 @@ public void setUp() throws Exception { "player9", "player10" ); - + // Mock item factory (for itemstacks) + /* + ItemFactory itemFactory = mock(ItemFactory.class); + when(Bukkit.getItemFactory()).thenReturn(itemFactory); + ItemMeta itemMeta = mock(ItemMeta.class); + when(itemFactory.getItemMeta(any())).thenReturn(itemMeta); +*/ // Has perms - when(p.hasPermission(anyString())).thenReturn(true); + when(player.hasPermission(anyString())).thenReturn(true); // Make island levels - List islands = new ArrayList<>(); + List islands = new ArrayList<>(); for (long i = -5; i < 5; i ++) { IslandLevels il = new IslandLevels(UUID.randomUUID().toString()); il.setInitialCount(null); @@ -160,17 +188,17 @@ public void setUp() throws Exception { when(levelsData.getInitialCount()).thenReturn(null); when(levelsData.getUniqueId()).thenReturn(uuid.toString()); when(handler.loadObject(anyString())).thenReturn(levelsData ); - System.out.println("Hanlder = " + handler); - // Island Manager - when(island.getOwner()).thenReturn(uuid); - when(island.getUniqueId()).thenReturn(uuid.toString()); - when(im.getIsland(world, uuid)).thenReturn(island); // Inventory GUI mockedBukkit.when(() -> Bukkit.createInventory(any(), anyInt(), anyString())).thenReturn(inv); + // IWM + // when(plugin.getIWM()).thenReturn(iwm); + when(iwm.getPermissionPrefix(any())).thenReturn("bskyblock."); + lm = new LevelsManager(addon); + } /** @@ -180,36 +208,17 @@ public void setUp() throws Exception { @AfterEach public void tearDown() throws Exception { super.tearDown(); - handler.close(); - this.levelsDataHandler.close(); - this.topTenHandler.close(); + deleteAll(new File("database")); + User.clearUsers(); + Mockito.framework().clearInlineMocks(); } - - @SuppressWarnings("unchecked") - private void databaseSetup(AbstractDatabaseHandler h) throws Exception { - // Save objects - when(h.saveObject(any())).thenReturn(CompletableFuture.completedFuture(true)); - // Capture the parameter passed to saveObject() and store it in savedObject - doAnswer(invocation -> { - savedObject = invocation.getArgument(0); - return CompletableFuture.completedFuture(true); - }).when(h).saveObject(any()); - - // Now when loadObject() is called, return the savedObject - when(h.loadObject(any())).thenAnswer(invocation -> savedObject); - - // Delete object - doAnswer(invocation -> { - savedObject = null; - return null; - }).when(h).deleteObject(any()); - - doAnswer(invocation -> { - savedObject = null; - return null; - }).when(h).deleteID(anyString()); - } - +/* + private static void deleteAll(File file) throws IOException { + if (file.exists()) { + Files.walk(file.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } + } +*/ /** * Test method for * {@link world.bentobox.level.LevelsManager#calculateLevel(UUID, world.bentobox.bentobox.database.objects.Island)}. @@ -251,12 +260,6 @@ public void testGetInitialCount() { /** * Test method for * {@link world.bentobox.level.LevelsManager#getIslandLevel(org.bukkit.World, java.util.UUID)}. - * @throws IntrospectionException - * @throws NoSuchMethodException - * @throws ClassNotFoundException - * @throws InvocationTargetException - * @throws IllegalAccessException - * @throws InstantiationException */ @Test public void testGetIslandLevel() { @@ -361,6 +364,7 @@ public void testHasTopTenPerm() { public void testLoadTopTens() { ArgumentCaptor task = ArgumentCaptor.forClass(Runnable.class); lm.loadTopTens(); + mockedBukkit.verify(() -> Bukkit.getScheduler()); verify(sch).runTaskAsynchronously(eq(plugin), task.capture()); // Capture the task in the scheduler task.getValue().run(); // run it verify(addon).log("Generating rankings"); diff --git a/src/test/java/world/bentobox/level/PlaceholderManagerTest.java b/src/test/java/world/bentobox/level/PlaceholderManagerTest.java index ff3046b..3b6b02d 100644 --- a/src/test/java/world/bentobox/level/PlaceholderManagerTest.java +++ b/src/test/java/world/bentobox/level/PlaceholderManagerTest.java @@ -35,6 +35,7 @@ import world.bentobox.bentobox.managers.PlaceholdersManager; import world.bentobox.bentobox.managers.PlayersManager; import world.bentobox.bentobox.managers.RanksManager; +import world.bentobox.level.config.BlockConfig; import world.bentobox.level.objects.IslandLevels; /** @@ -72,6 +73,8 @@ public class PlaceholderManagerTest extends CommonTestSetup { private @NonNull IslandLevels data; @Mock private PlayersManager pm; + @Mock + private BlockConfig bc; /** * @throws java.lang.Exception @@ -81,8 +84,9 @@ public class PlaceholderManagerTest extends CommonTestSetup { public void setUp() throws Exception { super.setUp(); - // Users + // Addon when(addon.getPlayers()).thenReturn(pm); + when(addon.getBlockConfig()).thenReturn(bc); // Users when(user.getWorld()).thenReturn(world); From a5ccb7bd6436f40711904f04f98673fca11d7903 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 28 Nov 2025 08:46:35 -0800 Subject: [PATCH 05/14] Fixed test --- src/test/java/world/bentobox/level/LevelTest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/java/world/bentobox/level/LevelTest.java b/src/test/java/world/bentobox/level/LevelTest.java index 4993741..550509a 100644 --- a/src/test/java/world/bentobox/level/LevelTest.java +++ b/src/test/java/world/bentobox/level/LevelTest.java @@ -16,6 +16,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.Collections; import java.util.Optional; import java.util.UUID; @@ -23,6 +24,7 @@ import java.util.jar.JarOutputStream; import java.util.logging.Logger; +import org.bukkit.Bukkit; import org.eclipse.jdt.annotation.NonNull; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; @@ -95,11 +97,11 @@ public static void beforeClass() throws IOException { // Copy over config file from src folder Path fromPath = Paths.get("src/main/resources/config.yml"); Path path = Paths.get("config.yml"); - Files.copy(fromPath, path); + Files.copy(fromPath, path, StandardCopyOption.REPLACE_EXISTING); // Copy over block config file from src folder fromPath = Paths.get("src/main/resources/blockconfig.yml"); path = Paths.get("blockconfig.yml"); - Files.copy(fromPath, path); + Files.copy(fromPath, path, StandardCopyOption.REPLACE_EXISTING); try (JarOutputStream tempJarOutputStream = new JarOutputStream(new FileOutputStream(jFile))) { // Added the new files to the jar. try (FileInputStream fis = new FileInputStream(path.toFile())) { @@ -214,6 +216,7 @@ public static void cleanUp() throws Exception { */ @Test public void testAllLoaded() { + mockedBukkit.when(() -> Bukkit.getWorld("acidisland_world")).thenReturn(null); addon.allLoaded(); verify(plugin).log("[Level] Level hooking into BSkyBlock"); verify(cmd, times(4)).getAddon(); // 4 commands From 3b4163c0936495009b779abbe2e69e2e03dd791a Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 28 Nov 2025 08:48:16 -0800 Subject: [PATCH 06/14] Remove debug. --- src/main/java/world/bentobox/level/LevelsManager.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/world/bentobox/level/LevelsManager.java b/src/main/java/world/bentobox/level/LevelsManager.java index 555a239..d32bced 100644 --- a/src/main/java/world/bentobox/level/LevelsManager.java +++ b/src/main/java/world/bentobox/level/LevelsManager.java @@ -318,7 +318,6 @@ public IslandLevels getLevelsData(@NonNull Island island) { } else { levelsCache.put(id, new IslandLevels(id)); } - System.out.println("ddd = " + levelsCache.get(id).getLevel()); // Return cached value return levelsCache.get(id); } From b58e621b738bad4a1030972a72688badf05f1261 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 28 Nov 2025 08:51:26 -0800 Subject: [PATCH 07/14] Remove commented out code --- .../bentobox/level/LevelsManagerTest.java | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/src/test/java/world/bentobox/level/LevelsManagerTest.java b/src/test/java/world/bentobox/level/LevelsManagerTest.java index 21c3c4f..ab96e75 100644 --- a/src/test/java/world/bentobox/level/LevelsManagerTest.java +++ b/src/test/java/world/bentobox/level/LevelsManagerTest.java @@ -98,14 +98,6 @@ public void setUp() throws Exception { when(dbSetup.getHandler(any())).thenReturn(handler); when(addon.getPlugin()).thenReturn(plugin); - // Bukkit - /* - when(Bukkit.getWorld(anyString())).thenReturn(world); - when(Bukkit.getPluginManager()).thenReturn(pim); - when(Bukkit.getPlayer(any(UUID.class))).thenReturn(player); - when(Bukkit.getScheduler()).thenReturn(scheduler); - */ - // The database type has to be created one line before the thenReturn() to work! DatabaseType value = DatabaseType.JSON; when(plugin.getSettings()).thenReturn(pluginSettings); @@ -117,8 +109,6 @@ public void setUp() throws Exception { when(pipeliner.addIsland(any())).thenReturn(cf); // Island - //when(addon.getIslands()).thenReturn(im); - //uuid = UUID.randomUUID(); ImmutableSet iset = ImmutableSet.of(uuid); when(island.getMemberSet()).thenReturn(iset); when(island.getOwner()).thenReturn(uuid); @@ -160,13 +150,6 @@ public void setUp() throws Exception { "player9", "player10" ); - // Mock item factory (for itemstacks) - /* - ItemFactory itemFactory = mock(ItemFactory.class); - when(Bukkit.getItemFactory()).thenReturn(itemFactory); - ItemMeta itemMeta = mock(ItemMeta.class); - when(itemFactory.getItemMeta(any())).thenReturn(itemMeta); -*/ // Has perms when(player.hasPermission(anyString())).thenReturn(true); // Make island levels @@ -212,13 +195,7 @@ public void tearDown() throws Exception { User.clearUsers(); Mockito.framework().clearInlineMocks(); } -/* - private static void deleteAll(File file) throws IOException { - if (file.exists()) { - Files.walk(file.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); - } - } -*/ + /** * Test method for * {@link world.bentobox.level.LevelsManager#calculateLevel(UUID, world.bentobox.bentobox.database.objects.Island)}. @@ -233,9 +210,6 @@ public void testCalculateLevel() { cf.complete(results); assertEquals(10000L, lm.getLevelsData(island).getLevel()); - // Map tt = lm.getTopTen(world, 10); - // assertEquals(1, tt.size()); - // assertTrue(tt.get(uuid) == 10000); assertEquals(10000L, lm.getIslandMaxLevel(world, uuid)); results.setLevel(5000); From cc9fda14e68a56233aeb9be715a5c0a44ac26fc6 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 28 Nov 2025 09:55:27 -0800 Subject: [PATCH 08/14] Add block limit count placeholder #391 Examples: %bskyblock_island_limit_cobblestone% %bskyblock_island_limit_netherrack% These are fixed values from the blockconfig.yml file --- .../bentobox/level/PlaceholderManager.java | 182 +++++++++++++----- .../bentobox/level/config/BlockConfig.java | 7 + 2 files changed, 136 insertions(+), 53 deletions(-) diff --git a/src/main/java/world/bentobox/level/PlaceholderManager.java b/src/main/java/world/bentobox/level/PlaceholderManager.java index 668969c..a50348e 100644 --- a/src/main/java/world/bentobox/level/PlaceholderManager.java +++ b/src/main/java/world/bentobox/level/PlaceholderManager.java @@ -7,11 +7,11 @@ import java.util.UUID; import java.util.stream.Collectors; -import org.bukkit.Keyed; +import org.bukkit.Bukkit; +import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.Registry; import org.bukkit.World; -import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockState; import org.bukkit.block.CreatureSpawner; @@ -19,10 +19,9 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.BlockStateMeta; import org.bukkit.inventory.meta.ItemMeta; -import org.eclipse.jdt.annotation.Nullable; -import org.bukkit.Bukkit; import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.persistence.PersistentDataType; +import org.eclipse.jdt.annotation.Nullable; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.addons.GameModeAddon; @@ -35,9 +34,12 @@ import world.bentobox.level.objects.TopTenData; /** - * Handles Level placeholders - * - * @author tastybento + * Handles registration and resolution of Level placeholders for the Level addon. + * + * The class implements: + * - registering placeholders via the BentoBox PlaceholdersManager + * - resolving top-ten and per-island level values + * - mapping blocks/items/spawners to the identifier used by IslandLevels * */ public class PlaceholderManager { @@ -50,6 +52,23 @@ public PlaceholderManager(Level addon) { this.plugin = addon.getPlugin(); } + /** + * Register placeholders for a given GameModeAddon. + * + * This method registers a number of placeholders with BentoBox's PlaceholdersManager: + * - island level placeholders (formatted, raw, owner-only) + * - points / points-to-next-level placeholders + * - top-ten placeholders (name, island name, members, level) for ranks 1..10 + * - visited island placeholder + * - mainhand & looking placeholders (value and count) + * - dynamic placeholders for each configured block key from the BlockConfig + * + * The registered placeholders call into the Level manager and IslandLevels to fetch + * values. Safety checks are performed so that missing players, islands or data return "0" + * or empty strings rather than throwing exceptions. + * + * @param gm the GameModeAddon for which placeholders are being registered + */ protected void registerPlaceholders(GameModeAddon gm) { if (plugin.getPlaceholdersManager() == null) return; @@ -170,15 +189,17 @@ protected void registerPlaceholders(GameModeAddon gm) { // Format the key for the placeholder name (e.g., minecraft_stone, pig_spawner) String placeholderSuffix = configKey.replace(':', '_').replace('.', '_').toLowerCase(); - // Register value placeholder - bpm.registerPlaceholder(addon, gm.getDescription().getName().toLowerCase() + "_island_value_" + placeholderSuffix, + // Register value placeholders + String placeholder = gm.getDescription().getName().toLowerCase() + "_island_value_" + placeholderSuffix; + bpm.registerPlaceholder(addon, placeholder, user -> String.valueOf(Objects.requireNonNullElse( // Use the configKey directly, getValue handles String keys addon.getBlockConfig().getValue(gm.getOverWorld(), configKey), 0)) ); - // Register count placeholder - bpm.registerPlaceholder(addon, gm.getDescription().getName().toLowerCase() + "_island_count_" + placeholderSuffix, + // Register count placeholders + placeholder = gm.getDescription().getName().toLowerCase() + "_island_count_" + placeholderSuffix; + bpm.registerPlaceholder(addon, placeholder, user -> { // Convert the String configKey back to the expected Object type (EntityType, Material, String) // for IslandLevels lookup. @@ -189,15 +210,27 @@ protected void registerPlaceholders(GameModeAddon gm) { ); }); } + // Register limit placeholders + addon.getBlockConfig().getBlockLimits().forEach((configKey, configValue) -> { + // Format the key for the placeholder name (e.g., minecraft_stone, pig_spawner) + String placeholderSuffix = configKey.replace(':', '_').replace('.', '_').toLowerCase(); + String placeholder = gm.getDescription().getName().toLowerCase() + "_island_limit_" + placeholderSuffix; + bpm.registerPlaceholder(addon, placeholder, user -> String.valueOf(configValue)); + }); } /** * Get the name of the owner of the island who holds the rank in this world. - * - * @param world world - * @param rank rank 1 to 10 - * @param weighted if true, then the weighted rank name is returned - * @return rank name + * + * Behavior / notes: + * - rank is clamped between 1 and Level.TEN + * - when weighted == true, the weighted top-ten is used; otherwise the plain top-ten is used + * - returns an empty string if a rank is not available or owner is null + * + * @param world world to look up the ranking in + * @param rank 1-based rank (will be clamped) + * @param weighted whether to use the weighted top-ten + * @return owner name or empty string */ String getRankName(World world, int rank, boolean weighted) { // Ensure rank is within bounds @@ -216,12 +249,14 @@ String getRankName(World world, int rank, boolean weighted) { } /** - * Get the island name for this rank - * - * @param world world - * @param rank rank 1 to 10 - * @param weighted if true, then the weighted rank name is returned - * @return name of island or nothing if there isn't one + * Get the island name for this rank. + * + * Similar behavior to getRankName, but returns the island's name (or empty string). + * + * @param world world to look up the island in + * @param rank 1-based rank (clamped) + * @param weighted whether to use the weighted list + * @return name of island or empty string */ String getRankIslandName(World world, int rank, boolean weighted) { // Ensure rank is within bounds @@ -237,12 +272,16 @@ String getRankIslandName(World world, int rank, boolean weighted) { } /** - * Gets a comma separated string of island member names - * - * @param world world - * @param rank rank to request - * @param weighted if true, then the weighted rank name is returned - * @return comma separated string of island member names + * Gets a comma separated string of island member names for a given ranked island. + * + * - Members are filtered to those at or above RanksManager.MEMBER_RANK. + * - Members are sorted by rank descending for consistent ordering. + * - If the island is missing or has no members, returns an empty string. + * + * @param world world to look up + * @param rank rank in the top-ten (1..10) + * @param weighted whether to use weighted top-ten + * @return comma-separated member names, or empty string */ String getRankMembers(World world, int rank, boolean weighted) { // Ensure rank is within bounds @@ -269,12 +308,15 @@ String getRankMembers(World world, int rank, boolean weighted) { } /** - * Get the level for the rank requested - * - * @param world world - * @param rank rank wanted - * @param weighted true if weighted (level/number of team members) - * @return level for the rank requested + * Get the level for the rank requested. + * + * - Returns a formatted level string using the manager's formatLevel helper. + * - If a value is missing, manager.formatLevel receives null which should handle the fallback. + * + * @param world world to query + * @param rank rank 1..10 (clamped) + * @param weighted whether to fetch weighted level + * @return string representation of the level for the rank */ String getRankLevel(World world, int rank, boolean weighted) { // Ensure rank is within bounds @@ -288,11 +330,11 @@ String getRankLevel(World world, int rank, boolean weighted) { } /** - * Return the rank of the player in a world - * + * Return the rank of the player in a world. + * * @param world world * @param user player - * @return rank where 1 is the top rank. + * @return rank where 1 is the top rank as a String; returns empty string for null user */ private String getRankValue(World world, User user) { if (user == null) { @@ -304,6 +346,13 @@ private String getRankValue(World world, User user) { .values().stream().filter(l -> l > level).count() + 1); } + /** + * Return the level for the island the user is currently visiting (if any). + * + * @param gm the GameModeAddon (used to map to the overworld) + * @param user the user to check + * @return island level string for the visited island, or empty/ "0" when not applicable + */ String getVisitedIslandLevel(GameModeAddon gm, User user) { if (user == null || !gm.inWorld(user.getWorld())) return ""; @@ -314,10 +363,16 @@ String getVisitedIslandLevel(GameModeAddon gm, User user) { /** * Gets the most specific identifier object for a block. - * NOTE: Does not currently support getting custom block IDs (e.g., ItemsAdder) - * directly from the Block object due to hook limitations. - * @param block The block - * @return EntityType, Material, or null if air/invalid. + * + * The identifier is one of: + * - EntityType for mob spawners (when the spawner block contains a specific spawned type) + * - Material for regular blocks + * - null for air or unknown/invalid blocks + * + * This is used to map the block to the same identifier the BlockConfig and IslandLevels use. + * + * @param block The block to inspect, null-safe + * @return an EntityType or Material, or null for air/unknown */ @Nullable private Object getBlockIdentifier(@Nullable Block block) { @@ -345,13 +400,22 @@ private Object getBlockIdentifier(@Nullable Block block) { /** * Gets the most specific identifier object for an ItemStack. - * Prioritizes standard Bukkit methods for spawners. - * Adds support for reading "spawnermeta:type" NBT tag via PDC. - * Returns null for spawners if the specific type cannot be determined. - * Supports ItemsAdder items. - * @param itemStack The ItemStack - * @return EntityType, Material (for standard blocks), String (for custom items), - * or null (if air, invalid, or unidentified spawner). + * + * This method attempts to: + * 1) Resolve a specific EntityType for spawner items via BlockStateMeta or a PersistentDataContainer key. + * If the exact spawned mob cannot be determined, it returns null for spawner items so counts + * are not incorrectly attributed. + * 2) If ItemsAdder is present, check for custom item Namespaced ID and return it (String). + * 3) Fallback to returning the Material for block-like items, otherwise null for non-blocks. + * + * The return type is one of: + * - EntityType (specific spawner type) + * - Material (normal block-type items) + * - String (custom items IDs like ItemsAdder) + * - null (air, invalid item, or unidentified spawner item) + * + * @param itemStack the item to inspect (may be null) + * @return EntityType, Material, String, or null */ @Nullable private Object getItemIdentifier(@Nullable ItemStack itemStack) { @@ -422,8 +486,14 @@ private Object getItemIdentifier(@Nullable ItemStack itemStack) { } /** - * Helper method to convert a String key from the config (e.g., "pig_spawner", "minecraft:stone") - * back into the corresponding Object (EntityType, Material, String) used by IslandLevels. + * Convert a configuration key string (from the block config) into the identifier object + * used by IslandLevels. + * + * - Handles "pig_spawner" style keys and resolves them to EntityType where possible. + * - Resolves namespaced Material keys using Bukkit's Registry. + * - Returns the original string for custom items (ItemsAdder) when present in registry. + * - Returns Material.SPAWNER for generic "spawner" key, otherwise null if unresolvable. + * * @param configKey The key string from block config. * @return EntityType, Material, String identifier, or null if not resolvable. */ @@ -482,10 +552,12 @@ private Object getObjectFromConfigKey(String configKey) { /** * Gets the block count for a specific identifier object in a user's island. + * This is a thin wrapper that validates inputs and returns "0" when missing. + * * @param gm GameModeAddon * @param user User requesting the count * @param identifier The identifier object (EntityType, Material, String) - * @return String representation of the count. + * @return String representation of the count (zero when not available) */ private String getBlockCount(GameModeAddon gm, User user, @Nullable Object identifier) { if (user == null || identifier == null) { @@ -496,7 +568,12 @@ private String getBlockCount(GameModeAddon gm, User user, @Nullable Object ident /** * Gets the block count for a specific identifier object from IslandLevels. - * This now correctly uses EntityType or Material as keys based on `DetailsPanel`'s logic. + * + * - Fetches the Island for the user and then the IslandLevels data. + * - IslandLevels stores counts in two maps (mdCount and uwCount) depending on how values + * are classified; we add both to provide the complete count. + * - Returns "0" if island or data is unavailable. + * * @param gm GameModeAddon * @param user User to get count for * @param identifier The identifier object (EntityType, Material, String) @@ -520,5 +597,4 @@ private String getBlockCountForUser(GameModeAddon gm, User user, Object identifi return String.valueOf(count); } - } diff --git a/src/main/java/world/bentobox/level/config/BlockConfig.java b/src/main/java/world/bentobox/level/config/BlockConfig.java index 1ebfcba..b554264 100644 --- a/src/main/java/world/bentobox/level/config/BlockConfig.java +++ b/src/main/java/world/bentobox/level/config/BlockConfig.java @@ -289,4 +289,11 @@ public Map> getWorldBlockValues() { return worldBlockValues; } + /** + * @return the blockLimits + */ + public Map getBlockLimits() { + return blockLimits; + } + } From 2613fb38b37d81e38f83d18fb2122dbb9d0c5630 Mon Sep 17 00:00:00 2001 From: tastybento Date: Thu, 19 Feb 2026 22:58:11 -0800 Subject: [PATCH 09/14] Add CLAUDE.md with build commands and architecture overview Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8bb5bd9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,96 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**Level** is a BentoBox add-on for Minecraft that calculates island levels based on block types and counts, maintains top-ten leaderboards, and provides competitive metrics for players on game modes like BSkyBlock and AcidIsland. + +## Build & Test Commands + +```bash +# Build +mvn clean package + +# Run all tests +mvn test + +# Run a single test class +mvn test -Dtest=LevelTest + +# Run a specific test method +mvn test -Dtest=LevelTest#testMethodName + +# Full build with coverage +mvn verify +``` + +Java 21 is required. The build produces a shaded JAR (includes PanelUtils). + +## Architecture + +### Entry Points +- `LevelPladdon` — Bukkit plugin entry point; instantiates `Level` via the `Pladdon` interface +- `Level` — main addon class; loads config, registers commands/listeners/placeholders, and hooks into optional third-party plugins + +### Lifecycle +`onLoad()` → `onEnable()` → `allLoaded()` + +`allLoaded()` is where integrations with other BentoBox add-ons (Warps, Visit) are established, since those may not be loaded yet during `onEnable()`. + +### Key Classes + +| Class | Role | +|---|---| +| `LevelsManager` | Central manager: island level cache, top-ten lists, database reads/writes | +| `Pipeliner` | Async queue; limits concurrent island calculations (configurable) | +| `IslandLevelCalculator` | Core chunk-scanning algorithm; supports multiple block stacker plugins | +| `Results` | Data object returned by a completed calculation | +| `ConfigSettings` | Main config bound to `config.yml` via BentoBox's `@ConfigEntry` annotations | +| `BlockConfig` | Block point-value mappings from `blockconfig.yml` | +| `PlaceholderManager` | Registers PlaceholderAPI placeholders | + +### Package Layout +``` +world/bentobox/level/ +├── calculators/ # IslandLevelCalculator, Pipeliner, Results, EquationEvaluator +├── commands/ # Player and admin sub-commands +├── config/ # ConfigSettings, BlockConfig +├── events/ # IslandPreLevelEvent, IslandLevelCalculatedEvent +├── listeners/ # Island activity, join/leave, migration listeners +├── objects/ # IslandLevels, TopTenData (database-persisted objects) +├── panels/ # GUI panels (top-ten, details, block values) +├── requests/ # API request handlers for inter-addon queries +└── util/ # Utils, ConversationUtils, CachedData +``` + +### Island Level Calculation Flow +1. A calculation request enters `Pipeliner` (async queue, default concurrency = 1) +2. `IslandLevelCalculator` scans island chunks using chunk snapshots (non-blocking) +3. Block counts are looked up against `BlockConfig` point values +4. An equation (configurable formula) converts total points → island level +5. Results are stored via `LevelsManager` and fired as `IslandLevelCalculatedEvent` + +### Optional Plugin Integrations +Level hooks into these plugins when present: WildStacker, RoseStacker, UltimateStacker (block counts), AdvancedChests, ItemsAdder, Oraxen (custom blocks), and the BentoBox Warps/Visit add-ons. + +## Testing + +Tests live in `src/test/java/world/bentobox/level/`. The framework is JUnit 5 + Mockito 5 + MockBukkit. `CommonTestSetup` is a shared base class that sets up the MockBukkit server and BentoBox mocks — extend it for new test classes. + +JaCoCo coverage reports are generated during `mvn verify`. + +## Configuration Resources + +| File | Location in JAR | Purpose | +|---|---|---| +| `config.yml` | `src/main/resources/` | Main settings (level cost formula, world inclusion, etc.) | +| `blockconfig.yml` | `src/main/resources/` | Points per block type | +| `locales/` | `src/main/resources/locales/` | Translation strings | +| `panels/` | `src/main/resources/panels/` | GUI layout definitions | + +## Code Conventions + +- Null safety via Eclipse JDT annotations (`@NonNull`, `@Nullable`) — honour these on public APIs +- BentoBox framework patterns: `CompositeCommand` for commands, `@ConfigEntry`/`@ConfigComment` for config, `@StoreAt` for database objects +- Pre- and post-events (`IslandPreLevelEvent`, `IslandLevelCalculatedEvent`) follow BentoBox's cancellable event pattern — fire both when adding new calculation triggers From a6e5d3d753110bd0f45de7636056b127ec2c1689 Mon Sep 17 00:00:00 2001 From: tastybento Date: Thu, 19 Feb 2026 23:05:02 -0800 Subject: [PATCH 10/14] Add Oraxen furniture mechanic support for island level calculation #390 Oraxen furniture is entity-based (item displays / armor stands) rather than block-based, so it was invisible to the existing chunk block scanner. This change adds a post-scan step that iterates entities in each island chunk, identifies Oraxen furniture base entities within the island's protected bounds, and counts them using the same "oraxen:" namespaced key as the existing block mechanic support. Furniture values can be configured in blockconfig.yml using the item ID (e.g. oraxen:my_chair: 5). Co-Authored-By: Claude Sonnet 4.6 --- .../calculators/IslandLevelCalculator.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java index 596f9ce..59fbd2a 100644 --- a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java +++ b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java @@ -31,10 +31,13 @@ import org.bukkit.block.ShulkerBox; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.type.Slab; +import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.BlockStateMeta; +import io.th0rgal.oraxen.mechanics.provided.gameplay.furniture.FurnitureMechanic; + import com.bgsoftware.wildstacker.api.WildStackerAPI; import com.bgsoftware.wildstacker.api.objects.StackedBarrel; import com.google.common.collect.Multiset; @@ -73,6 +76,7 @@ public class IslandLevelCalculator { private final int seaHeight; private final List stackedBlocks = new ArrayList<>(); private final Set chestBlocks = new HashSet<>(); + private final Set furnitureChunks = new HashSet<>(); private final Map spawners = new HashMap<>(); /** @@ -473,6 +477,10 @@ record ChunkPair(World world, Chunk chunk, ChunkSnapshot chunkSnapshot) { } private void scanAsync(ChunkPair cp) { + // Track chunks for Oraxen furniture entity scanning (done on main thread later) + if (BentoBox.getInstance().getHooks().getHook("Oraxen").isPresent()) { + furnitureChunks.add(cp.chunk); + } // Get the chunk coordinates and island boundaries once per chunk scan int chunkX = cp.chunk.getX() << 4; int chunkZ = cp.chunk.getZ() << 4; @@ -741,6 +749,7 @@ public void scanIsland(Pipeliner pipeliner) { // Chunk finished // This was the last chunk. Handle stacked blocks, spawners, chests and exit handleStackedBlocks().thenCompose(v -> handleSpawners()).thenCompose(v -> handleChests()) + .thenCompose(v -> handleOraxenFurniture()) .thenRun(() -> { this.tidyUp(); this.getR().complete(getResults()); @@ -782,6 +791,50 @@ private CompletableFuture handleChests() { return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); } + /** + * Scans entities in each island chunk for Oraxen furniture and counts them toward the island level. + * Furniture is entity-based in Oraxen (item displays / armor stands), so it is invisible to the + * normal block scanner. Only the base entity of each furniture piece is counted to avoid + * double-counting multi-entity furniture. + * + * @return a CompletableFuture that completes when all chunks have been checked + */ + private CompletableFuture handleOraxenFurniture() { + if (!BentoBox.getInstance().getHooks().getHook("Oraxen").isPresent() || furnitureChunks.isEmpty()) { + return CompletableFuture.completedFuture(null); + } + int minX = island.getMinProtectedX(); + int maxX = island.getMaxProtectedX(); + int minZ = island.getMinProtectedZ(); + int maxZ = island.getMaxProtectedZ(); + List> futures = new ArrayList<>(); + for (Chunk chunk : furnitureChunks) { + CompletableFuture future = Util.getChunkAtAsync(chunk.getWorld(), chunk.getX(), chunk.getZ()) + .thenAccept(c -> { + for (Entity entity : c.getEntities()) { + // Only count the root/base entity of each furniture piece + if (!OraxenHook.isBaseEntity(entity)) { + continue; + } + Location loc = entity.getLocation(); + // Confirm entity is within the island's protected bounds + if (loc.getBlockX() < minX || loc.getBlockX() >= maxX + || loc.getBlockZ() < minZ || loc.getBlockZ() >= maxZ) { + continue; + } + FurnitureMechanic mechanic = OraxenHook.getFurnitureMechanic(entity); + if (mechanic == null) { + continue; + } + boolean belowSeaLevel = seaHeight > 0 && loc.getBlockY() <= seaHeight; + checkBlock("oraxen:" + mechanic.getItemID(), belowSeaLevel); + } + }); + futures.add(future); + } + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + } + private CompletableFuture handleStackedBlocks() { // Deal with any stacked blocks List> futures = new ArrayList<>(); From ee1dd24822b4db0f933605fda89a29c75bd22f57 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 20 Feb 2026 10:07:39 -0800 Subject: [PATCH 11/14] Update Paper API to 1.21.11 to match MockBukkit MockBukkit dev-9b384aa is built against Paper API 1.21.11-R0.1-SNAPSHOT. Aligning the project's Paper dependency eliminates the version-mismatch warning and allows all 22 tests to pass cleanly. Co-Authored-By: Claude Sonnet 4.6 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7c9d7f4..961b3e7 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ 5.11.0 v1.21-SNAPSHOT - 1.21.10-R0.1-SNAPSHOT + 1.21.11-R0.1-SNAPSHOT 3.10.2 1.12.0 From 6a319f3ff6774d8f813d99c2a1c397c1aeae0d44 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 20 Feb 2026 10:07:39 -0800 Subject: [PATCH 12/14] Update Paper API to 1.21.11 to match MockBukkit MockBukkit dev-9b384aa is built against Paper API 1.21.11-R0.1-SNAPSHOT. Aligning the project's Paper dependency eliminates the version-mismatch warning and allows all 22 tests to pass cleanly. Co-Authored-By: Claude Sonnet 4.6 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7c9d7f4..961b3e7 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ 5.11.0 v1.21-SNAPSHOT - 1.21.10-R0.1-SNAPSHOT + 1.21.11-R0.1-SNAPSHOT 3.10.2 1.12.0 From 6b224dcdf1d10e14b635020e022ed0c13ee8e569 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 20 Feb 2026 10:01:38 -0800 Subject: [PATCH 13/14] Add Nexo custom block and furniture support for island level calculation #389 Nexo is the successor to Oraxen and has its own API for custom blocks and furniture. This adds full support for both: Custom blocks (noteblock, stringblock, chorusblock mechanics): - NexoBlocks.customBlockMechanic(Location) detects any Nexo custom block at a location during the async chunk scan, counted as "nexo:" - NexoItems.exists/idFromItem detects Nexo items inside containers Furniture mechanic: - After the block scan, NexoFurniture.furnitureMechanic(Entity) scans entities in each island chunk; entities with a non-null mechanic are the furniture base entities and are counted as "nexo:" Configuration: - Values are set in blockconfig.yml using the item ID, e.g.: nexo:my_chair: 5 - isNexo() helper in Level respects the disabled-plugin-hooks config list - BlockConfig.isOther() validates nexo: keys via NexoItems.exists() - Utils.prettifyObject() strips the nexo: prefix for display names Co-Authored-By: Claude Sonnet 4.6 --- pom.xml | 19 +++++ src/main/java/world/bentobox/level/Level.java | 8 ++ .../calculators/IslandLevelCalculator.java | 77 +++++++++++++++++-- .../bentobox/level/config/BlockConfig.java | 5 ++ .../java/world/bentobox/level/util/Utils.java | 2 + 5 files changed, 105 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 961b3e7..a2b10dd 100644 --- a/pom.xml +++ b/pom.xml @@ -171,6 +171,12 @@ Oraxen Repository https://repo.oraxen.com/releases + + + nexo + Nexo Repository + https://repo.nexomc.com/releases + @@ -277,6 +283,19 @@ 4.0.10 provided + + + com.nexomc + nexo + 1.19.1 + + + dev.triumphteam + triumph-gui + + + provided + io.th0rgal diff --git a/src/main/java/world/bentobox/level/Level.java b/src/main/java/world/bentobox/level/Level.java index b9fe809..ae91392 100644 --- a/src/main/java/world/bentobox/level/Level.java +++ b/src/main/java/world/bentobox/level/Level.java @@ -488,4 +488,12 @@ public boolean isItemsAdder() { return !getSettings().isDisableItemsAdder() && getPlugin().getHooks().getHook("ItemsAdder").isPresent(); } + /** + * @return true if the Nexo plugin is enabled and not disabled in config + */ + public boolean isNexo() { + return !getSettings().getDisabledPluginHooks().contains("Nexo") + && Bukkit.getPluginManager().isPluginEnabled("Nexo"); + } + } diff --git a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java index 59fbd2a..a7425eb 100644 --- a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java +++ b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java @@ -43,6 +43,10 @@ import com.google.common.collect.Multiset; import com.google.common.collect.Multiset.Entry; import com.google.common.collect.Multisets; +import com.nexomc.nexo.api.NexoBlocks; +import com.nexomc.nexo.api.NexoFurniture; +import com.nexomc.nexo.api.NexoItems; +import com.nexomc.nexo.mechanics.custom_block.CustomBlockMechanic; import dev.rosewood.rosestacker.api.RoseStackerAPI; import us.lynuxcraft.deadsilenceiv.advancedchests.AdvancedChestsAPI; @@ -426,7 +430,19 @@ private void countItemStack(ItemStack i) { } return; } - + // Check Nexo + if (addon.isNexo() && NexoItems.exists(i)) { + String id = NexoItems.idFromItem(i); + if (id == null) { + return; + } + id = "nexo:" + id; + for (int c = 0; c < i.getAmount(); c++) { + checkBlock(id, false); + } + return; + } + if (i == null || !i.getType().isBlock()) return; @@ -477,8 +493,8 @@ record ChunkPair(World world, Chunk chunk, ChunkSnapshot chunkSnapshot) { } private void scanAsync(ChunkPair cp) { - // Track chunks for Oraxen furniture entity scanning (done on main thread later) - if (BentoBox.getInstance().getHooks().getHook("Oraxen").isPresent()) { +// Track chunks for furniture entity scanning (Oraxen and Nexo are entity-based) + if (BentoBox.getInstance().getHooks().getHook("Oraxen").isPresent() || addon.isNexo()) { furnitureChunks.add(cp.chunk); } // Get the chunk coordinates and island boundaries once per chunk scan @@ -526,10 +542,11 @@ private void processBlock(ChunkPair cp, int x, int y, int z, int globalX, int gl // Create a Location object only when needed for more complex checks. Location loc = null; - // === Custom Block Hooks (ItemsAdder, Oraxen) === + // === Custom Block Hooks (ItemsAdder, Oraxen, Nexo) === // These hooks can define custom blocks that override vanilla behavior. // They must be checked first. - if (addon.isItemsAdder() || BentoBox.getInstance().getHooks().getHook("Oraxen").isPresent()) { + if (addon.isItemsAdder() || BentoBox.getInstance().getHooks().getHook("Oraxen").isPresent() + || addon.isNexo()) { loc = new Location(cp.world, globalX, y, globalZ); String customBlockId = null; if (addon.isItemsAdder()) { @@ -541,6 +558,12 @@ private void processBlock(ChunkPair cp, int x, int y, int z, int globalX, int gl customBlockId = "oraxen:" + oraxenId; // Make a namespaced ID } } + if (customBlockId == null && addon.isNexo()) { + CustomBlockMechanic nexoMechanic = NexoBlocks.customBlockMechanic(loc); + if (nexoMechanic != null) { + customBlockId = "nexo:" + nexoMechanic.getItemID(); + } + } if (customBlockId != null) { // If a custom block is found, count it and stop further processing for this block. @@ -750,6 +773,7 @@ public void scanIsland(Pipeliner pipeliner) { // This was the last chunk. Handle stacked blocks, spawners, chests and exit handleStackedBlocks().thenCompose(v -> handleSpawners()).thenCompose(v -> handleChests()) .thenCompose(v -> handleOraxenFurniture()) + .thenCompose(v -> handleNexoFurniture()) .thenRun(() -> { this.tidyUp(); this.getR().complete(getResults()); @@ -822,7 +846,7 @@ private CompletableFuture handleOraxenFurniture() { || loc.getBlockZ() < minZ || loc.getBlockZ() >= maxZ) { continue; } - FurnitureMechanic mechanic = OraxenHook.getFurnitureMechanic(entity); + var mechanic = OraxenHook.getFurnitureMechanic(entity); if (mechanic == null) { continue; } @@ -835,6 +859,47 @@ private CompletableFuture handleOraxenFurniture() { return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); } + /** + * Scans entities in each island chunk for Nexo furniture and counts them toward the island level. + * Nexo furniture is entity-based (ItemDisplay entities), so it is invisible to the normal block + * scanner. Only entities for which a FurnitureMechanic can be resolved are counted, which + * naturally filters to base furniture entities. + * + * @return a CompletableFuture that completes when all chunks have been checked + */ + private CompletableFuture handleNexoFurniture() { + if (!addon.isNexo() || furnitureChunks.isEmpty()) { + return CompletableFuture.completedFuture(null); + } + int minX = island.getMinProtectedX(); + int maxX = island.getMaxProtectedX(); + int minZ = island.getMinProtectedZ(); + int maxZ = island.getMaxProtectedZ(); + List> futures = new ArrayList<>(); + for (Chunk chunk : furnitureChunks) { + CompletableFuture future = Util.getChunkAtAsync(chunk.getWorld(), chunk.getX(), chunk.getZ()) + .thenAccept(c -> { + for (Entity entity : c.getEntities()) { + Location loc = entity.getLocation(); + // Confirm entity is within the island's protected bounds + if (loc.getBlockX() < minX || loc.getBlockX() >= maxX + || loc.getBlockZ() < minZ || loc.getBlockZ() >= maxZ) { + continue; + } + // getFurnitureMechanic returns non-null only for furniture base entities + var mechanic = NexoFurniture.furnitureMechanic(entity); + if (mechanic == null) { + continue; + } + boolean belowSeaLevel = seaHeight > 0 && loc.getBlockY() <= seaHeight; + checkBlock("nexo:" + mechanic.getItemID(), belowSeaLevel); + } + }); + futures.add(future); + } + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + } + private CompletableFuture handleStackedBlocks() { // Deal with any stacked blocks List> futures = new ArrayList<>(); diff --git a/src/main/java/world/bentobox/level/config/BlockConfig.java b/src/main/java/world/bentobox/level/config/BlockConfig.java index b554264..2c9793a 100644 --- a/src/main/java/world/bentobox/level/config/BlockConfig.java +++ b/src/main/java/world/bentobox/level/config/BlockConfig.java @@ -19,6 +19,8 @@ import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.EntityType; +import com.nexomc.nexo.api.NexoItems; + import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.hooks.ItemsAdderHook; import world.bentobox.bentobox.hooks.OraxenHook; @@ -105,6 +107,9 @@ private boolean isOther(String key) { if (key.startsWith("oraxen:") && BentoBox.getInstance().getHooks().getHook("Oraxen").isPresent()) { return OraxenHook.exists(key.substring(7)); } + if (key.startsWith("nexo:") && addon.isNexo()) { + return NexoItems.exists(key.substring(5)); + } // Check ItemsAdder return addon.isItemsAdder() && ItemsAdderHook.isInRegistry(key); } diff --git a/src/main/java/world/bentobox/level/util/Utils.java b/src/main/java/world/bentobox/level/util/Utils.java index dff4a02..5c23c0b 100644 --- a/src/main/java/world/bentobox/level/util/Utils.java +++ b/src/main/java/world/bentobox/level/util/Utils.java @@ -161,6 +161,8 @@ public static String prettifyObject(Object object, User user) { // Remove prefix if (key.startsWith("oraxen:")) { key = key.substring(7); + } else if (key.startsWith("nexo:")) { + key = key.substring(5); } } From 3537573e70b4fc0e6ed3edf9db51794aa1ce2dc0 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 20 Feb 2026 14:50:55 -0800 Subject: [PATCH 14/14] Fix custom block placeholders for Oraxen, Nexo, and ItemsAdder #391 - getObjectFromConfigKey: return configKey as-is for unresolved keys instead of only returning when ItemsAdder confirms it; custom block strings (oraxen:x, nexo:x, namespace:id) are stored as String keys in mdCount/uwCount so they must be returned as strings - getItemIdentifier: add Oraxen (OraxenHook.getNamespacedId) and Nexo (NexoItems.idFromItem) checks for _island_count_mainhand - getBlockIdentifier: add Oraxen (OraxenHook.getOraxenBlockID), Nexo (NexoBlocks.customBlockMechanic), and ItemsAdder (ItemsAdderHook.getInCustomRegion) checks for _island_count_looking Co-Authored-By: Claude Sonnet 4.6 --- .../bentobox/level/PlaceholderManager.java | 75 +++++++++++++++---- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/src/main/java/world/bentobox/level/PlaceholderManager.java b/src/main/java/world/bentobox/level/PlaceholderManager.java index a50348e..fa84a8e 100644 --- a/src/main/java/world/bentobox/level/PlaceholderManager.java +++ b/src/main/java/world/bentobox/level/PlaceholderManager.java @@ -23,11 +23,16 @@ import org.bukkit.persistence.PersistentDataType; import org.eclipse.jdt.annotation.Nullable; +import com.nexomc.nexo.api.NexoBlocks; +import com.nexomc.nexo.api.NexoItems; +import com.nexomc.nexo.mechanics.custom_block.CustomBlockMechanic; + import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.hooks.ItemsAdderHook; +import world.bentobox.bentobox.hooks.OraxenHook; import world.bentobox.bentobox.managers.PlaceholdersManager; import world.bentobox.bentobox.managers.RanksManager; import world.bentobox.level.objects.IslandLevels; @@ -394,6 +399,30 @@ private Object getBlockIdentifier(@Nullable Block block) { return Material.SPAWNER; // Return generic spawner material if state invalid } + // Check Oraxen custom blocks (noteblock/stringblock/chorusblock mechanics) + if (BentoBox.getInstance().getHooks().getHook("Oraxen").isPresent()) { + String oraxenId = OraxenHook.getOraxenBlockID(block.getLocation()); + if (oraxenId != null) { + return "oraxen:" + oraxenId; + } + } + + // Check Nexo custom blocks + if (addon.isNexo()) { + CustomBlockMechanic nexoMechanic = NexoBlocks.customBlockMechanic(block.getLocation()); + if (nexoMechanic != null) { + return "nexo:" + nexoMechanic.getItemID(); + } + } + + // Check ItemsAdder custom blocks + if (addon.isItemsAdder()) { + String iaId = ItemsAdderHook.getInCustomRegion(block.getLocation()); + if (iaId != null) { + return iaId; + } + } + // Fallback to the Material for regular blocks return type; } @@ -474,15 +503,31 @@ private Object getItemIdentifier(@Nullable ItemStack itemStack) { } // End of Spawner handling // 2. Handle potential custom items (e.g., ItemsAdder) - if (addon.isItemsAdder()) { - Optional customId = ItemsAdderHook.getNamespacedId(itemStack); - if (customId.isPresent()) { - return customId.get(); // Return the String ID from ItemsAdder - } - } - - // 3. Fallback to Material for regular items that represent blocks - return type.isBlock() ? type : null; + if (addon.isItemsAdder()) { + Optional customId = ItemsAdderHook.getNamespacedId(itemStack); + if (customId.isPresent()) { + return customId.get(); // Return the String ID from ItemsAdder + } + } + + // 3. Handle Oraxen custom items + if (BentoBox.getInstance().getHooks().getHook("Oraxen").isPresent()) { + Optional oraxenId = OraxenHook.getNamespacedId(itemStack); + if (oraxenId.isPresent()) { + return "oraxen:" + oraxenId.get(); + } + } + + // 4. Handle Nexo custom items + if (addon.isNexo()) { + String nexoId = NexoItems.idFromItem(itemStack); + if (nexoId != null) { + return "nexo:" + nexoId; + } + } + + // 5. Fallback to Material for regular items that represent blocks + return type.isBlock() ? type : null; } /** @@ -538,16 +583,14 @@ private Object getObjectFromConfigKey(String configKey) { return material; } - // Assume it's a custom String key (e.g., ItemsAdder) if not resolved yet - if (addon.isItemsAdder() && ItemsAdderHook.isInRegistry(configKey)) { // Use original case key for lookup? - return configKey; - } - // Final check: maybe it's the generic "spawner" key from config? - if(lowerCaseKey.equals("spawner")) { + if (lowerCaseKey.equals("spawner")) { return Material.SPAWNER; } - return null; + + // Return the key as-is for custom blocks (ItemsAdder, Oraxen, Nexo). + // These are stored as String keys in mdCount/uwCount. + return configKey; } /**