diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 12bdefc..0091885 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,22 +11,22 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Set up JDK 17 - uses: actions/setup-java@v3 + - name: Set up JDK 21 + uses: actions/setup-java@v4 with: - distribution: 'adopt' - java-version: '17' + distribution: 'temurin' + java-version: '21' - name: Cache SonarCloud packages - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Cache Maven packages - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} @@ -35,4 +35,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \ No newline at end of file + run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b9a34d7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,60 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project + +TopBlock is a BentoBox addon that produces a Top Ten ranking for the AOneBlock game mode based on how many magic blocks each island has mined. It is **not** standalone — it depends on the BentoBox plugin and the AOneBlock addon being present at runtime, and refuses to enable otherwise. + +## Build & Test + +Maven project, Java 21, Paper 1.21.11 API, BentoBox 3.14.0, AOneBlock 1.18.0. + +- Build (default goal is `clean package`): `mvn package` — produces a shaded jar in `target/` named `TopBlock-.jar`. The shade plugin bundles only `lv.id.bonne:panelutils`; everything else is `provided`. +- Run tests: `mvn test` +- Run a single test class: `mvn test -Dtest=TopBlockManagerTest` +- Run a single test method: `mvn test -Dtest=TopBlockManagerTest#testFormatLevelShorthandKilo` +- The Surefire config sets a long list of `--add-opens` JVM flags — required for Mockito + MockBukkit reflection on Java 21; do not remove them when tweaking the build. + +Version handling is driven by Maven properties: `build.version` is the human version (currently 1.1.0), `revision` resolves to `${build.version}-SNAPSHOT` locally and to `${build.version}` under the `master` profile (activated by `GIT_BRANCH=origin/master` on Jenkins). `build.number` is `-LOCAL` locally, `-b` on CI, empty on master. Don't hand-edit `` — bump `build.version`. + +## Runtime entry points (Pladdon pattern) + +There are **two** main classes and the distinction matters: + +- `TopBlockPladdon` (referenced by `plugin.yml`) is the Bukkit-facing `Pladdon`. Spigot loads this; its only job is `getAddon() → new TopBlock()`. +- `TopBlock` (referenced by `addon.yml`) is the BentoBox `Addon`. All real lifecycle (`onLoad`, `onEnable`, `onDisable`) lives here. + +`onEnable` looks up the AOneBlock addon via `getPlugin().getAddonsManager().getAddonByName("aoneblock")`; if missing or not a `GameModeAddon`, the addon disables itself. The `/ topblock` command is registered against AOneBlock's player command, not as a top-level command. + +## Data flow + +`TopBlockManager` is a `Listener` that reacts to `BentoBoxReadyEvent` (handler is `public void onBentoBoxReady` — Bukkit silently skips private @EventHandler methods, which is what broke the addon historically) to start a repeating Bukkit task. The task period is `settings.getRefreshTime() * 20L * 60` ticks (minutes → ticks). Each tick of the task: + +1. Calls `AOneBlock.getBlockListener().getAllIslands()` — this reads every island, so the refresh interval is intentionally coarse (default 5 min, min 1 min). +2. Builds a fresh `List` (record of island + blockNumber + lifetime + phaseName) — sorted at read time via `Comparator` on `lifetime` then `blockNumber`. +3. Updates `PlaceholderManager`'s cached snapshot. + +Placeholders are registered once via a `runTaskLater` 10-tick delay after the first ready event (so PAPI / BentoBox's `PlaceholdersManager` is up). Names follow `island__top_<1..10>` and are scoped to the AOneBlock `GameModeAddon`. The `TopBlock.TEN` constant is the source of truth for the list size. + +## Panel + +`TopLevelPanel` uses BentoBox's `TemplatedPanelBuilder`. The template file is shipped in `src/main/resources/panels/top_panel.yml` and copied to the data folder on load via `saveResource("panels/top_panel.yml", false)` — players' edits to the on-disk file persist across restarts. Localization keys live under `topblock.gui.buttons.island.*` in `src/main/resources/locales/en-US.yml`. The icon material can be overridden per-player via the `topblock.icon.` permission. + +The panel has no click actions (TopBlock doesn't bundle Warp/Visit hooks like Level does). The YAML still declares `warp`/`visit` actions with tooltips, but no click handler is registered — clicking does nothing. + +## Resource filtering + +`pom.xml` filters `src/main/resources` (so `${version}` etc. in `addon.yml` / `plugin.yml` get substituted) **except** `src/main/resources/locales`, which is copied verbatim to `./locales` to avoid Maven mangling YAML colons / placeholder syntax in translations. + +## Tests + +JUnit 5 + Mockito + MockBukkit. Test classes extend `CommonTestSetup` which: +- Mocks `Bukkit` statically and provides a real `MockBukkit.mock()` server (needed for Tag/Material initialisation). +- Injects the BentoBox singleton via `WhiteBox.setInternalState(BentoBox.class, "instance", plugin)`. +- Sets up the standard graph of mocks: `IslandWorldManager`, `IslandsManager`, `PlayersManager`, `LocalesManager`, `PlaceholdersManager`, `Notifier`, `HooksManager`, `BlueprintsManager`. +- Calls `User.setPlugin(plugin)` and pre-creates a `User` instance for `mockPlayer` (uuid `tastybento`). + +`TestWorldSettings` returns `"TopBlock"` for friendly name and `"topblock."` for permission prefix. The addon test (`TopBlockTest`) builds an in-memory `addon.jar` containing `config.yml` + `panels/top_panel.yml` because `Addon.saveResource` reads from a real JarFile. + +JaCoCo excludes `**/*Names*` to avoid synthetic-field issues on JavaBeans — keep that exclusion if adding similar classes. diff --git a/README.md b/README.md index 2dc732f..55c5d44 100644 --- a/README.md +++ b/README.md @@ -3,37 +3,77 @@ ## About -Add-on for BentoBox to calculate island levels for AOneBlock specifically. Ranks are determined by how many magic blocks have been mined - the count. +TopBlock is a [BentoBox](https://github.com/BentoBoxWorld/BentoBox) addon that produces a Top Ten ranking for the [AOneBlock](https://github.com/BentoBoxWorld/AOneBlock) game mode based on how many magic blocks each island has mined. + +## Requirements + +- Paper 1.21.x (Spigot is no longer supported) +- Java 21 +- BentoBox 3.14.0 or later +- AOneBlock 1.18.0 or later ## How to use -1. Place the level addon jar in the addons folder of the BentoBox plugin. Make sure you have AOneBlock installed too! -2. Restart the server -3. The addon will create a data folder and inside the folder will be a config.yml -4. Edit the config.yml how you want. -5. Restart the server if you make a change +1. Drop the TopBlock jar into your server's `plugins/BentoBox/addons/` folder. AOneBlock must already be installed there too. +2. Restart the server. TopBlock will create `addons/TopBlock/config.yml` and `addons/TopBlock/panels/top_panel.yml`. +3. Edit `config.yml` if you want to tune anything (see below) and restart the server again to apply. + +## Configuration + +`addons/TopBlock/config.yml`: + +| Option | Default | Description | +|----------------|---------|-------------------------------------------------------------------------------------------------------------------| +| `refresh-time` | `5` | How often the Top Ten is recalculated, in minutes. Minimum 1. Each refresh reads every island, so don't set it too low. | +| `shorthand` | `false` | If `true`, format large counts using units — `10,500` becomes `10.5k`, `1,527,314` becomes `1.5M`, etc. | + +The panel layout lives in `addons/TopBlock/panels/top_panel.yml`. Edits are preserved across restarts. ## Commands -`/ob topblock` - this shows the Top Ten +`/ob topblock` (alias: `/oneblock topblock`) — opens the Top Ten panel. + +To get into the top ten, a player just needs to mine at least one magic block on their AOneBlock island. The list refreshes every `refresh-time` minutes; a player who just started mining may need to wait that long before appearing. ## Permissions -Permissions are given automatically to players as listed below. If your permissions plugin strips permissions then you may have to allocate these manually. Note that if a player doesn't have the `intopten` permission, they will not be listed in the top ten. -``` -permissions: +Permissions are given automatically to players. If your permissions plugin strips defaults, allocate them manually: + +```yaml +permissions: 'aoneblock.island.topblock': - description: Player can use TopBlock command + description: Player can use the TopBlock command + default: true + 'aoneblock.intopten': + description: Player's island will be listed in the top ten. Remove from admins or testers to hide them. default: true ``` +If an island owner is **online** and lacks `aoneblock.intopten`, their island is excluded from the top ten panel and from placeholders. **Offline** owners are always included — to hide an admin or tester, remove the perm from the player who can actually log in. Removing the perm from an entire group (e.g. ops) excludes everyone in that group while online. + +The icon shown for each rank can be overridden per player by granting `aoneblock.topblock.icon.` (for example `aoneblock.topblock.icon.diamond_block`). Without an override, the rank icon is the player's head. + ## Placeholders ``` -%aoneblock_island_player_name_top_RANK% where RANK is 1 to 10 - Island owner's name -%aoneblock_island_member_names_top_RANK% where RANK is 1 to 10 - Name of island team members -%aoneblock_island_phase_name_top_RANK% where RANK is 1 to 10 - Name of the phase they have reached -%aoneblock_island_phase_number_top_RANK% where RANK is 1 to 10 - Phase number, e.g. Plains is 1, Underground is 2, etc. -%aoneblock_island_count_top_RANK% where RANK is 1 to 10 - Block Count of magic blocks mined this round -%aoneblock_island_lifetime_top_RANK% where RANK is 1 to 10 - Lifetime count of magic blocks mined +%aoneblock_island_player_name_top_RANK% - Island owner's name +%aoneblock_island_member_names_top_RANK% - Comma-separated team members (highest rank first) +%aoneblock_island_phase_name_top_RANK% - Name of the phase the island has reached +%aoneblock_island_phase_number_top_RANK% - Phase number, e.g. Plains is 1, Underground is 2 +%aoneblock_island_count_top_RANK% - Block count of magic blocks mined this round +%aoneblock_island_lifetime_top_RANK% - Lifetime count of magic blocks mined ``` + +`RANK` is `1` to `10`. If fewer than `RANK` islands qualify for the top ten, the placeholder returns an empty string. + +## Building from source + +```bash +mvn clean package +``` + +Produces `target/TopBlock-.jar`. + +## License + +[EPL-2.0](LICENSE) diff --git a/pom.xml b/pom.xml index 1e61b93..40b67a5 100644 --- a/pom.xml +++ b/pom.xml @@ -50,14 +50,16 @@ UTF-8 UTF-8 - 17 + 21 - 2.0.9 + 5.10.2 + 5.11.0 + v1.21-SNAPSHOT - 1.21.3-R0.1-SNAPSHOT - 2.7.1-SNAPSHOT + 1.21.11-R0.1-SNAPSHOT + 3.14.0-SNAPSHOT - 1.12.3-SNAPSHOT + 1.18.0 1.1.0 @@ -65,7 +67,7 @@ -LOCAL - 1.1.0 + 2.0.0 BentoBoxWorld_TopBlock bentobox-world https://sonarcloud.io @@ -75,7 +77,7 @@ - ci @@ -89,13 +91,13 @@ - - - - master @@ -122,8 +124,8 @@ - spigot-repo - https://hub.spigotmc.org/nexus/content/repositories/snapshots + papermc + https://repo.papermc.io/repository/maven-public/ bentoboxworld @@ -134,38 +136,23 @@ https://repo.codemc.org/repository/maven-public/ - codemc-public - https://repo.codemc.org/repository/maven-public/ + codemc + https://repo.codemc.org/repository/maven-snapshots/ + + + jitpack.io + https://jitpack.io - + - org.spigotmc - spigot-api - ${spigot.version} + io.papermc.paper + paper-api + ${paper.version} provided - - - org.mockito - mockito-core - 3.11.1 - test - - - org.powermock - powermock-module-junit4 - ${powermock.version} - test - - - org.powermock - powermock-api-mockito2 - ${powermock.version} - test - world.bentobox bentobox @@ -184,24 +171,57 @@ ${panelutils.version} - org.eclipse.jdt org.eclipse.jdt.annotation 2.2.600 + + + com.github.MockBukkit + MockBukkit + ${mock-bukkit.version} + test + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + - - ${project.name}-${revision}${build.number} @@ -221,52 +241,50 @@ org.apache.maven.plugins maven-clean-plugin - 3.1.0 + 3.4.1 org.apache.maven.plugins maven-resources-plugin - 3.1.0 + 3.3.1 org.apache.maven.plugins maven-compiler-plugin - 3.8.0 + 3.15.0 ${java.version} + true org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M5 + 3.5.2 + + **/*Test.java + **/*Test?.java + **/*Test??.java + - ${argLine} --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED - --add-opens - java.base/java.util.stream=ALL-UNNAMED + --add-opens java.base/java.util.stream=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED - --add-opens - java.base/java.util.regex=ALL-UNNAMED - --add-opens - java.base/java.nio.channels.spi=ALL-UNNAMED + --add-opens java.base/java.util.regex=ALL-UNNAMED + --add-opens java.base/java.nio.channels.spi=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED - --add-opens - java.base/java.util.concurrent=ALL-UNNAMED + --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/sun.nio.fs=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/java.nio.file=ALL-UNNAMED - --add-opens - java.base/java.nio.charset=ALL-UNNAMED - --add-opens - java.base/java.lang.reflect=ALL-UNNAMED - --add-opens - java.logging/java.util.logging=ALL-UNNAMED + --add-opens java.base/java.nio.charset=ALL-UNNAMED + --add-opens java.base/java.lang.reflect=ALL-UNNAMED + --add-opens java.logging/java.util.logging=ALL-UNNAMED --add-opens java.base/java.lang.ref=ALL-UNNAMED --add-opens java.base/java.util.jar=ALL-UNNAMED --add-opens java.base/java.util.zip=ALL-UNNAMED @@ -276,17 +294,17 @@ org.apache.maven.plugins maven-jar-plugin - 3.1.0 + 3.4.2 org.apache.maven.plugins maven-javadoc-plugin - 3.0.1 + 3.11.2 false -Xdoclint:none ${java.home}/bin/javadoc - 16 + 21 @@ -300,7 +318,7 @@ org.apache.maven.plugins maven-source-plugin - 3.0.1 + 3.3.1 attach-sources @@ -313,17 +331,17 @@ org.apache.maven.plugins maven-install-plugin - 2.5.2 + 3.1.3 org.apache.maven.plugins maven-deploy-plugin - 2.8.2 + 3.1.3 org.apache.maven.plugins maven-shade-plugin - 3.3.1-SNAPSHOT + 3.6.0 true @@ -355,11 +373,11 @@ org.jacoco jacoco-maven-plugin - 0.8.10 + 0.8.12 true - **/*Names* diff --git a/src/main/java/world/bentobox/topblock/TopBlockManager.java b/src/main/java/world/bentobox/topblock/TopBlockManager.java index 1039a01..1f9b159 100644 --- a/src/main/java/world/bentobox/topblock/TopBlockManager.java +++ b/src/main/java/world/bentobox/topblock/TopBlockManager.java @@ -9,8 +9,10 @@ import java.util.Map; import java.util.Objects; import java.util.TreeMap; +import java.util.UUID; import org.bukkit.Bukkit; +import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; @@ -67,7 +69,7 @@ public TopBlockManager(TopBlock addon) { } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - private void startMonitoring(BentoBoxReadyEvent e) { + public void onBentoBoxReady(BentoBoxReadyEvent e) { // Load the top ten from AOneBlock every so often Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), () -> { // Update TopTen @@ -83,11 +85,31 @@ void getOneBlockData() { AOneBlock ob = addon.getaOneBlock(); topTen.clear(); ob.getBlockListener().getAllIslands().stream().filter(i -> i.getLifetime() > 0).forEach(i -> - // Get player island. - addon.getIslands().getIslandById(i.getUniqueId()).ifPresent(island -> + addon.getIslands().getIslandById(i.getUniqueId()) + .filter(this::ownerInTopTen) + .ifPresent(island -> topTen.add(new TopTenData(island, i.getBlockNumber(), i.getLifetime(), i.getPhaseName())))); } + /** + * Returns true if the island's owner should be listed in the top ten. + * Offline owners always pass — admins must remove the perm from a player + * who can actually log in. An online owner without the {@code intopten} + * permission is excluded. + */ + private boolean ownerInTopTen(Island island) { + UUID owner = island.getOwner(); + if (owner == null) { + return false; + } + Player player = Bukkit.getPlayer(owner); + if (player == null) { + return true; + } + String permPrefix = addon.getPlugin().getIWM().getPermissionPrefix(island.getWorld()); + return player.hasPermission(permPrefix + "intopten"); + } + /** * Get the string representation of the level. May be converted to shorthand notation, e.g., 104556 = 10.5k * @param lvl - long value to represent diff --git a/src/main/java/world/bentobox/topblock/TopBlockPladdon.java b/src/main/java/world/bentobox/topblock/TopBlockPladdon.java index 013c4c2..8c16d90 100644 --- a/src/main/java/world/bentobox/topblock/TopBlockPladdon.java +++ b/src/main/java/world/bentobox/topblock/TopBlockPladdon.java @@ -10,8 +10,14 @@ * */ public class TopBlockPladdon extends Pladdon { + + private Addon addon; + @Override public Addon getAddon() { - return new TopBlock(); + if (addon == null) { + addon = new TopBlock(); + } + return addon; } } diff --git a/src/main/java/world/bentobox/topblock/panels/TopLevelPanel.java b/src/main/java/world/bentobox/topblock/panels/TopLevelPanel.java index dcaa634..1347f47 100644 --- a/src/main/java/world/bentobox/topblock/panels/TopLevelPanel.java +++ b/src/main/java/world/bentobox/topblock/panels/TopLevelPanel.java @@ -27,96 +27,84 @@ /** - * This panel opens top likes panel + * This panel opens the top ten panel for AOneBlock. */ -public class TopLevelPanel -{ - // --------------------------------------------------------------------- - // Section: Internal Constructor - // --------------------------------------------------------------------- +public class TopLevelPanel { + private static final String REFERENCE = "topblock.gui.buttons.island."; + private static final String PLAYER = "[player]"; - /** - * This is internal constructor. It is used internally in current class to avoid creating objects everywhere. - * - * @param addon Level object. - * @param user User who opens Panel. - * @param world World where gui is opened - * @param permissionPrefix Permission Prefix - */ - private TopLevelPanel(TopBlock addon, User user, World world, String permissionPrefix) - { + private final TopBlock addon; + private final User user; + private final World world; + private final String iconPermission; + private final List topIslands; + + + private TopLevelPanel(TopBlock addon, User user, World world, String permissionPrefix) { this.addon = addon; this.user = user; this.world = world; - this.iconPermission = permissionPrefix + "topblock.icon"; - this.topIslands = this.addon.getManager().getTopTen(TopBlock.TEN); } /** - * Build method manages current panel opening. It uses BentoBox PanelAPI that is easy to use and users can get nice - * panels. + * Open the panel for a user. */ - public void build() - { - TemplatedPanelBuilder panelBuilder = new TemplatedPanelBuilder(); + public static void openPanel(TopBlock addon, User user, World world, String permissionPrefix) { + new TopLevelPanel(addon, user, world, permissionPrefix).build(); + } + + private void build() { + TemplatedPanelBuilder panelBuilder = new TemplatedPanelBuilder(); panelBuilder.user(this.user); panelBuilder.world(this.world); - panelBuilder.template("top_panel", new File(this.addon.getDataFolder(), "panels")); - - //panelBuilder.registerTypeBuilder("VIEW", this::createViewerButton); panelBuilder.registerTypeBuilder("TOP", this::createPlayerButton); - - // Register unknown type builder. panelBuilder.build(); } - // --------------------------------------------------------------------- - // Section: Methods - // --------------------------------------------------------------------- + private PanelItem createPlayerButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot itemSlot) { + int index = (int) template.dataMap().getOrDefault("index", 0); + + if (index < 1) { + return this.createFallback(template.fallback(), index); + } + TopTenData record = this.topIslands.size() < index ? null : this.topIslands.get(index - 1); - /** - * Creates fallback based on template. - * @param template Template record for fallback button. - * @param index Place of the fallback. - * @return Fallback panel item. - */ - private PanelItem createFallback(ItemTemplateRecord template, long index) - { - if (template == null) - { - return null; + if (record == null) { + return this.createFallback(template.fallback(), index); } + return this.createIslandIcon(template, record, index); + } + + private PanelItem createFallback(ItemTemplateRecord template, long index) { + if (template == null) { + return null; + } PanelItemBuilder builder = new PanelItemBuilder(); - if (template.icon() != null) - { + if (template.icon() != null) { builder.icon(template.icon().clone()); } - if (template.title() != null) - { + if (template.title() != null) { builder.name(this.user.getTranslation(this.world, template.title(), TextVariables.NAME, String.valueOf(index))); - } - else - { + } else { builder.name(this.user.getTranslation(this.world, REFERENCE, TextVariables.NAME, String.valueOf(index))); } - if (template.description() != null) - { + if (template.description() != null) { builder.description(this.user.getTranslation(this.world, template.description(), TextVariables.NUMBER, String.valueOf(index))); } @@ -127,45 +115,10 @@ private PanelItem createFallback(ItemTemplateRecord template, long index) } - /** - * This method creates player icon with warp functionality. - * - * @return PanelItem for PanelBuilder. - */ - private PanelItem createPlayerButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot itemSlot) - { - int index = (int) template.dataMap().getOrDefault("index", 0); + private PanelItem createIslandIcon(ItemTemplateRecord template, TopTenData record, int index) { + Island island = record.island(); - if (index < 1) - { - return this.createFallback(template.fallback(), index); - } - - TopTenData islandTopRecord = this.topIslands.size() < index ? null : this.topIslands.get(index - 1); - - if (islandTopRecord == null) - { - return this.createFallback(template.fallback(), index); - } - - return this.createIslandIcon(template, islandTopRecord, index); - } - - - /** - * This method creates button from template for given island top record. - * @param template Icon Template. - * @param islandTopRecord Island Top Record. - * @param index Place Index. - * @return PanelItem for PanelBuilder. - */ - private PanelItem createIslandIcon(ItemTemplateRecord template, TopTenData islandTopRecord, int index) - { - // Get player island. - Island island = islandTopRecord.island(); - - if (island == null) - { + if (island == null) { return this.createFallback(template.fallback(), index); } @@ -173,327 +126,109 @@ private PanelItem createIslandIcon(ItemTemplateRecord template, TopTenData islan this.populateIslandIcon(builder, template, island); this.populateIslandTitle(builder, template, island); - this.populateIslandDescription(builder, template, island, islandTopRecord, index); + this.populateIslandDescription(builder, template, island, record, index); builder.amount(index); - /* - // Get only possible actions, by removing all inactive ones. - List activeActions = new ArrayList<>(template.actions()); - - activeActions.removeIf(action -> - { - switch (action.actionType().toUpperCase()) - { - case "WARP" -> { - return island.getOwner() == null || - this.addon.getWarpHook() == null || - !this.addon.getWarpHook().getWarpSignsManager().hasWarp(this.world, island.getOwner()); - } - case "VISIT" -> { - return island.getOwner() == null || - this.addon.getVisitHook() == null || - !this.addon.getVisitHook().getAddonManager().preprocessTeleportation(this.user, island); - } - case "VIEW" -> { - return island.getOwner() == null || - !island.getMemberSet(RanksManager.MEMBER_RANK).contains(this.user.getUniqueId()); - } - default -> { - return false; - } - } - }); - - // Add Click handler - builder.clickHandler((panel, user, clickType, i) -> - { - for (ItemTemplateRecord.ActionRecords action : activeActions) - { - if (clickType == action.clickType() || action.clickType() == ClickType.UNKNOWN) - { - switch (action.actionType().toUpperCase()) - { - case "WARP" -> { - this.user.closeInventory(); - this.addon.getWarpHook().getWarpSignsManager().warpPlayer(this.world, this.user, island.getOwner()); - } - case "VISIT" -> { - // The command call implementation solves necessity to check for all visits options, - // like cool down, confirmation and preprocess in single go. Would it be better to write - // all logic here? - - this.addon.getPlugin().getIWM().getAddon(this.world). - flatMap(GameModeAddon::getPlayerCommand).ifPresent(command -> - { - String mainCommand = - this.addon.getVisitHook().getSettings().getPlayerMainCommand(); - - if (!mainCommand.isBlank()) - { - this.user.closeInventory(); - this.user.performCommand(command.getTopLabel() + " " + mainCommand + " " + island.getOwner()); - } - }); - } - case "VIEW" -> { - this.user.closeInventory(); - // Open Detailed GUI. - DetailsPanel.openPanel(this.addon, this.world, this.user); - } - } - } - } - return true; - }); - - // Collect tooltips. - List tooltips = activeActions.stream(). - filter(action -> action.tooltip() != null). - map(action -> this.user.getTranslation(this.world, action.tooltip())). - filter(text -> !text.isBlank()). - collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); - - // Add tooltips. - if (!tooltips.isEmpty()) - { - // Empty line and tooltips. - builder.description(""); - builder.description(tooltips); - } - */ return builder.build(); } - /** - * Populate given panel item builder name with values from template and island objects. - * - * @param builder the builder - * @param template the template - * @param island the island - */ - private void populateIslandTitle(PanelItemBuilder builder, - ItemTemplateRecord template, - Island island) - { - // Get Island Name + private void populateIslandTitle(PanelItemBuilder builder, ItemTemplateRecord template, Island island) { String nameText; - if (island.getName() == null || island.getName().isEmpty()) - { + if (island.getName() == null || island.getName().isEmpty()) { nameText = this.user.getTranslation(REFERENCE + "owners-island", PLAYER, - island.getOwner() == null ? - this.user.getTranslation(REFERENCE + "unknown") : - this.addon.getPlayers().getName(island.getOwner())); - } - else - { + island.getOwner() == null + ? this.user.getTranslation(REFERENCE + "unknown") + : this.addon.getPlayers().getName(island.getOwner())); + } else { nameText = island.getName(); } - // Template specific title is always more important than custom one. - if (template.title() != null && !template.title().isBlank()) - { + if (template.title() != null && !template.title().isBlank()) { builder.name(this.user.getTranslation(this.world, template.title(), TextVariables.NAME, nameText)); - } - else - { + } else { builder.name(this.user.getTranslation(REFERENCE + "name", TextVariables.NAME, nameText)); } } - /** - * Populate given panel item builder icon with values from template and island objects. - * - * @param builder the builder - * @param template the template - * @param island the island - */ - private void populateIslandIcon(PanelItemBuilder builder, - ItemTemplateRecord template, - Island island) - { + private void populateIslandIcon(PanelItemBuilder builder, ItemTemplateRecord template, Island island) { User owner = island.getOwner() == null ? null : User.getInstance(island.getOwner()); - // Get permission or island icon - String permissionIcon = owner == null ? null : - Utils.getPermissionValue(owner, this.iconPermission, null); + String permissionIcon = owner == null ? null + : Utils.getPermissionValue(owner, this.iconPermission, null); - Material material; + Material material = (permissionIcon != null && !permissionIcon.equals("*")) + ? Material.matchMaterial(permissionIcon) + : null; - if (permissionIcon != null && !permissionIcon.equals("*")) - { - material = Material.matchMaterial(permissionIcon); - } - else - { - material = null; - } - - if (material != null) - { - if (!material.equals(Material.PLAYER_HEAD)) - { + if (material != null) { + if (!material.equals(Material.PLAYER_HEAD)) { builder.icon(material); - } - else - { + } else { builder.icon(owner.getName()); } - } - else if (template.icon() != null) - { + } else if (template.icon() != null) { builder.icon(template.icon().clone()); - } - else if (owner != null) - { + } else if (owner != null) { builder.icon(owner.getName()); - } - else - { + } else { builder.icon(Material.PLAYER_HEAD); } } - /** - * Populate given panel item builder description with values from template and island objects. - * - * @param builder the builder - * @param template the template - * @param island the island - * @param islandTopRecord the top record object - * @param index place index. - */ - private void populateIslandDescription(PanelItemBuilder builder, - ItemTemplateRecord template, - Island island, - TopTenData islandTopRecord, - int index) - { - // Get Owner Name + private void populateIslandDescription(PanelItemBuilder builder, ItemTemplateRecord template, + Island island, TopTenData record, int index) { + String ownerText = this.user.getTranslation(REFERENCE + "owner", PLAYER, - island.getOwner() == null ? - this.user.getTranslation(REFERENCE + "unknown") : - this.addon.getPlayers().getName(island.getOwner())); + island.getOwner() == null + ? this.user.getTranslation(REFERENCE + "unknown") + : this.addon.getPlayers().getName(island.getOwner())); - // Get Members Text String memberText; - - if (island.getMemberSet().size() > 1) - { + if (island.getMemberSet().size() > 1) { StringBuilder memberBuilder = new StringBuilder( this.user.getTranslationOrNothing(REFERENCE + "members-title")); - - for (UUID uuid : island.getMemberSet()) - { + for (UUID uuid : island.getMemberSet()) { User u = User.getInstance(uuid); - - if (memberBuilder.length() > 0) - { + if (memberBuilder.length() > 0) { memberBuilder.append("\n"); } - - memberBuilder.append( - this.user.getTranslationOrNothing(REFERENCE + "member", - PLAYER, u.getName())); + memberBuilder.append(this.user.getTranslationOrNothing(REFERENCE + "member", + PLAYER, u.getName())); } - memberText = memberBuilder.toString(); - } - else - { + } else { memberText = ""; } String placeText = this.user.getTranslation(REFERENCE + "place", TextVariables.NUMBER, String.valueOf(index)); - String levelText = this.user.getTranslation(REFERENCE + "count", - TextVariables.NUMBER, this.addon.getManager().formatLevel((long)islandTopRecord.blockNumber())); + String countText = this.user.getTranslation(REFERENCE + "count", + TextVariables.NUMBER, this.addon.getManager().formatLevel((long) record.blockNumber())); String lifetimeText = this.user.getTranslation(REFERENCE + "lifetime", - TextVariables.NUMBER, this.addon.getManager().formatLevel(islandTopRecord.lifetime())); - - // Template specific description is always more important than custom one. - if (template.description() != null && !template.description().isBlank()) - { - builder.description(this.user.getTranslation(this.world, template.description(), - "[owner]", ownerText, - "[members]", memberText, - "[count]", levelText, - "[lifetime]", lifetimeText, - "[place]", placeText). - replaceAll("(?m)^[ \\t]*\\r?\\n", ""). - replaceAll("(? topIslands; } diff --git a/src/main/java/world/bentobox/topblock/util/ConversationUtils.java b/src/main/java/world/bentobox/topblock/util/ConversationUtils.java deleted file mode 100644 index 26fba78..0000000 --- a/src/main/java/world/bentobox/topblock/util/ConversationUtils.java +++ /dev/null @@ -1,120 +0,0 @@ -// -// Created by BONNe -// Copyright - 2021 -// - - -package world.bentobox.topblock.util; - - -import org.bukkit.conversations.*; -import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.jdt.annotation.Nullable; -import java.util.function.Consumer; - -import world.bentobox.bentobox.BentoBox; -import world.bentobox.bentobox.api.user.User; - - -public class ConversationUtils -{ - private ConversationUtils() {} - // --------------------------------------------------------------------- - // Section: Conversation API implementation - // --------------------------------------------------------------------- - - - /** - * This method will close opened gui and writes question in chat. After players answers on question in chat, message - * will trigger consumer and gui will reopen. - * - * @param consumer Consumer that accepts player output text. - * @param question Message that will be displayed in chat when player triggers conversion. - * @param user User who is targeted with current confirmation. - */ - public static void createStringInput(Consumer consumer, - User user, - @NonNull String question, - @Nullable String successMessage) - { - // Text input message. - StringPrompt stringPrompt = new StringPrompt() - { - @Override - public @NonNull String getPromptText(@NonNull ConversationContext context) - { - user.closeInventory(); - return question; - } - - - @Override - public @NonNull Prompt acceptInput(@NonNull ConversationContext context, @Nullable String input) - { - consumer.accept(input); - return ConversationUtils.endMessagePrompt(successMessage); - } - }; - - new ConversationFactory(BentoBox.getInstance()). - withPrefix(context -> user.getTranslation("level.conversations.prefix")). - withFirstPrompt(stringPrompt). - // On cancel conversation will be closed. - withLocalEcho(false). - withTimeout(90). - withEscapeSequence(user.getTranslation("level.conversations.cancel-string")). - // Use null value in consumer to detect if user has abandoned conversation. - addConversationAbandonedListener(ConversationUtils.getAbandonListener(consumer, user)). - buildConversation(user.getPlayer()). - begin(); - } - - - /** - * This is just a simple end message prompt that displays requested message. - * - * @param message Message that will be displayed. - * @return MessagePrompt that displays given message and exists from conversation. - */ - private static MessagePrompt endMessagePrompt(@Nullable String message) - { - return new MessagePrompt() - { - @Override - public @NonNull String getPromptText(@NonNull ConversationContext context) - { - return message == null ? "" : message; - } - - - @Override - protected @Nullable Prompt getNextPrompt(@NonNull ConversationContext context) - { - return Prompt.END_OF_CONVERSATION; - } - }; - } - - - /** - * This method creates and returns abandon listener for every conversation. - * - * @param consumer Consumer which must return null value. - * @param user User who was using conversation. - * @return ConversationAbandonedListener instance. - */ - private static ConversationAbandonedListener getAbandonListener(Consumer consumer, User user) - { - return abandonedEvent -> - { - if (!abandonedEvent.gracefulExit()) - { - consumer.accept(null); - // send cancell message - abandonedEvent.getContext().getForWhom().sendRawMessage( - user.getTranslation("level.conversations.prefix") + - user.getTranslation("level.conversations.cancelled")); - } - }; - } -} diff --git a/src/main/java/world/bentobox/topblock/util/Utils.java b/src/main/java/world/bentobox/topblock/util/Utils.java index 7794f47..b8e7a2b 100644 --- a/src/main/java/world/bentobox/topblock/util/Utils.java +++ b/src/main/java/world/bentobox/topblock/util/Utils.java @@ -3,37 +3,19 @@ // Copyright - 2021 // - package world.bentobox.topblock.util; - -import org.bukkit.Material; -import org.bukkit.permissions.PermissionAttachmentInfo; import java.util.List; import java.util.stream.Collectors; -import world.bentobox.bentobox.api.user.User; -import world.bentobox.bentobox.hooks.LangUtilsHook; +import org.bukkit.permissions.PermissionAttachmentInfo; +import world.bentobox.bentobox.api.user.User; -public class Utils -{ - private static final String LEVEL_MATERIALS = "level.materials."; +public class Utils { private Utils() {} - /** - * This method sends a message to the user with appended "prefix" text before message. - * @param user User who receives message. - * @param translationText Translation text of the message. - * @param parameters Parameters for the translation text. - */ - public static void sendMessage(User user, String translationText, String... parameters) - { - user.sendMessage(user.getTranslation( "level.conversations.prefix") + - user.getTranslation( translationText, parameters)); - } - /** * This method gets string value of given permission prefix. If user does not have given permission or it have all @@ -44,34 +26,28 @@ public static void sendMessage(User user, String translationText, String... para * @param defaultValue Default value that will be returned if permission not found. * @return String value that follows permissionPrefix. */ - public static String getPermissionValue(User user, String permissionPrefix, String defaultValue) - { - if (user.isPlayer()) - { - if (permissionPrefix.endsWith(".")) - { + public static String getPermissionValue(User user, String permissionPrefix, String defaultValue) { + if (user.isPlayer()) { + if (permissionPrefix.endsWith(".")) { permissionPrefix = permissionPrefix.substring(0, permissionPrefix.length() - 1); } String permPrefix = permissionPrefix + "."; - List permissions = user.getEffectivePermissions().stream(). - map(PermissionAttachmentInfo::getPermission). - filter(permission -> permission.startsWith(permPrefix)). - collect(Collectors.toList()); + List permissions = user.getEffectivePermissions().stream() + .map(PermissionAttachmentInfo::getPermission) + .filter(permission -> permission.startsWith(permPrefix)) + .collect(Collectors.toList()); - for (String permission : permissions) - { - if (permission.contains(permPrefix + "*")) - { + for (String permission : permissions) { + if (permission.contains(permPrefix + "*")) { // * means all. So continue to search more specific. continue; } String[] parts = permission.split(permPrefix); - if (parts.length > 1) - { + if (parts.length > 1) { return parts[1]; } } @@ -79,151 +55,4 @@ public static String getPermissionValue(User user, String permissionPrefix, Stri return defaultValue; } - - - /** - * This method allows to get next value from array list after given value. - * - * @param values Array that should be searched for given value. - * @param currentValue Value which next element should be found. - * @param Instance of given object. - * @return Next value after currentValue in values array. - */ - public static T getNextValue(T[] values, T currentValue) - { - for (int i = 0; i < values.length; i++) - { - if (values[i].equals(currentValue)) - { - if (i + 1 == values.length) - { - return values[0]; - } - else - { - return values[i + 1]; - } - } - } - - return currentValue; - } - - - /** - * This method allows to get previous value from array list after given value. - * - * @param values Array that should be searched for given value. - * @param currentValue Value which previous element should be found. - * @param Instance of given object. - * @return Previous value before currentValue in values array. - */ - public static T getPreviousValue(T[] values, T currentValue) - { - for (int i = 0; i < values.length; i++) - { - if (values[i].equals(currentValue)) - { - if (i > 0) - { - return values[i - 1]; - } - else - { - return values[values.length - 1]; - } - } - } - - return currentValue; - } - - - /** - * Prettify Material object for user. - * @param object Object that must be pretty. - * @param user User who will see the object. - * @return Prettified string for Material. - */ - public static String prettifyObject(Material object, User user) - { - // Nothing to translate - if (object == null) - { - return ""; - } - - // Find addon structure with: - // [addon]: - // materials: - // [material]: - // name: [name] - String translation = user.getTranslationOrNothing(LEVEL_MATERIALS + object.name().toLowerCase() + ".name"); - - if (!translation.isEmpty()) - { - // We found our translation. - return translation; - } - - // Find addon structure with: - // [addon]: - // materials: - // [material]: [name] - - translation = user.getTranslationOrNothing(LEVEL_MATERIALS + object.name().toLowerCase()); - - if (!translation.isEmpty()) - { - // We found our translation. - return translation; - } - - // Find general structure with: - // materials: - // [material]: [name] - - translation = user.getTranslationOrNothing("materials." + object.name().toLowerCase()); - - if (!translation.isEmpty()) - { - // We found our translation. - return translation; - } - - // Use Lang Utils Hook to translate material - return LangUtilsHook.getMaterialName(object, user); - } - - - /** - * Prettify Material object description for user. - * @param object Object that must be pretty. - * @param user User who will see the object. - * @return Prettified description string for Material. - */ - public static String prettifyDescription(Material object, User user) - { - // Nothing to translate - if (object == null) - { - return ""; - } - - // Find addon structure with: - // [addon]: - // materials: - // [material]: - // description: [text] - String translation = user.getTranslationOrNothing(LEVEL_MATERIALS + object.name().toLowerCase() + ".description"); - - if (!translation.isEmpty()) - { - // We found our translation. - return translation; - } - - // No text to return. - return ""; - } } diff --git a/src/main/resources/addon.yml b/src/main/resources/addon.yml index 75a6bce..9180b5d 100755 --- a/src/main/resources/addon.yml +++ b/src/main/resources/addon.yml @@ -8,7 +8,10 @@ authors: tastybento depend: AOneBlock -permissions: +permissions: 'aoneblock.island.topblock': description: Player can use TopBlock command default: true + 'aoneblock.intopten': + description: Player's island will be listed in the top ten. Remove from admins or testers to hide them. + default: true diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 73998fd..920e306 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ name: BentoBox-TopBlock main: world.bentobox.topblock.TopBlockPladdon version: ${project.version}${build.number} -api-version: "1.21"" +api-version: "1.21" authors: [tastybento] contributors: ["The BentoBoxWorld Community"] diff --git a/src/test/java/world/bentobox/topblock/CommonTestSetup.java b/src/test/java/world/bentobox/topblock/CommonTestSetup.java new file mode 100644 index 0000000..f68c679 --- /dev/null +++ b/src/test/java/world/bentobox/topblock/CommonTestSetup.java @@ -0,0 +1,224 @@ +package world.bentobox.topblock; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.Optional; +import java.util.UUID; +import java.util.logging.Logger; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.entity.Player.Spigot; +import org.bukkit.inventory.ItemFactory; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.plugin.PluginManager; +import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.util.Vector; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; + +import com.google.common.collect.ImmutableSet; + +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.configuration.WorldSettings; +import world.bentobox.bentobox.api.user.Notifier; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.database.objects.Players; +import world.bentobox.bentobox.managers.BlueprintsManager; +import world.bentobox.bentobox.managers.FlagsManager; +import world.bentobox.bentobox.managers.HooksManager; +import world.bentobox.bentobox.managers.IslandWorldManager; +import world.bentobox.bentobox.managers.IslandsManager; +import world.bentobox.bentobox.managers.LocalesManager; +import world.bentobox.bentobox.managers.PlaceholdersManager; +import world.bentobox.bentobox.managers.PlayersManager; +import world.bentobox.bentobox.util.Util; + +/** + * Common test setup for TopBlock tests. Call super.setUp() in subclass @BeforeEach. + */ +public abstract class CommonTestSetup { + + protected UUID uuid = UUID.randomUUID(); + + @Mock + protected Player mockPlayer; + @Mock + protected PluginManager pim; + @Mock + protected ItemFactory itemFactory; + @Mock + protected Location location; + @Mock + protected World world; + @Mock + protected IslandWorldManager iwm; + @Mock + protected IslandsManager im; + @Mock + protected Island island; + @Mock + protected BentoBox plugin; + @Mock + protected PlayerInventory inv; + @Mock + protected Notifier notifier; + @Mock + protected FlagsManager fm; + @Mock + protected Spigot spigot; + @Mock + protected HooksManager hooksManager; + @Mock + protected BlueprintsManager bm; + @Mock + protected BukkitScheduler sch; + @Mock + protected LocalesManager lm; + @Mock + protected PlaceholdersManager phm; + + protected ServerMock server; + protected MockedStatic mockedBukkit; + protected MockedStatic mockedUtil; + protected AutoCloseable closeable; + + @BeforeEach + @SuppressWarnings("java:S1130") + public void setUp() throws Exception { + closeable = MockitoAnnotations.openMocks(this); + server = MockBukkit.mock(); + + // Inject BentoBox singleton + WhiteBox.setInternalState(BentoBox.class, "instance", plugin); + + // Force Tag static fields to initialise under the real server + @SuppressWarnings("unused") + var unusedTagRef = org.bukkit.Tag.LEAVES; + + // Static Bukkit mock + mockedBukkit = Mockito.mockStatic(Bukkit.class, Mockito.RETURNS_DEEP_STUBS); + mockedBukkit.when(Bukkit::getMinecraftVersion).thenReturn("1.21.10"); + mockedBukkit.when(Bukkit::getBukkitVersion).thenReturn(""); + mockedBukkit.when(Bukkit::getPluginManager).thenReturn(pim); + mockedBukkit.when(Bukkit::getItemFactory).thenReturn(itemFactory); + mockedBukkit.when(Bukkit::getServer).thenReturn(server); + mockedBukkit.when(Bukkit::getScheduler).thenReturn(sch); + // By default treat island owners as offline so the intopten filter + // does not exclude them. Tests that need an online owner can override. + mockedBukkit.when(() -> Bukkit.getPlayer(any(UUID.class))).thenReturn(null); + + // Location + when(location.getWorld()).thenReturn(world); + when(location.getBlockX()).thenReturn(0); + when(location.getBlockY()).thenReturn(0); + when(location.getBlockZ()).thenReturn(0); + when(location.toVector()).thenReturn(new Vector(0, 0, 0)); + when(location.clone()).thenReturn(location); + + // PlayersManager + PlayersManager pm = mock(PlayersManager.class); + when(plugin.getPlayers()).thenReturn(pm); + Players players = mock(Players.class); + when(players.getMetaData()).thenReturn(Optional.empty()); + when(pm.getPlayer(any(UUID.class))).thenReturn(players); + + // Player + when(mockPlayer.getUniqueId()).thenReturn(uuid); + when(mockPlayer.getLocation()).thenReturn(location); + when(mockPlayer.getWorld()).thenReturn(world); + when(mockPlayer.getName()).thenReturn("tastybento"); + when(mockPlayer.getInventory()).thenReturn(inv); + when(mockPlayer.spigot()).thenReturn(spigot); + when(mockPlayer.getType()).thenReturn(EntityType.PLAYER); + + User.setPlugin(plugin); + User.clearUsers(); + User.getInstance(mockPlayer); + + // IWM + when(plugin.getIWM()).thenReturn(iwm); + when(iwm.inWorld(any(Location.class))).thenReturn(true); + when(iwm.inWorld(any(World.class))).thenReturn(true); + when(iwm.getFriendlyName(any())).thenReturn("TopBlock"); + when(iwm.getAddon(any())).thenReturn(Optional.empty()); + + // WorldSettings + WorldSettings worldSet = new TestWorldSettings(); + when(iwm.getWorldSettings(any())).thenReturn(worldSet); + + // IslandsManager + when(plugin.getIslands()).thenReturn(im); + when(im.getProtectedIslandAt(any())).thenReturn(Optional.of(island)); + when(island.isAllowed(any())).thenReturn(false); + when(island.isAllowed(any(User.class), any())).thenReturn(false); + when(island.getOwner()).thenReturn(uuid); + when(island.getMemberSet()).thenReturn(ImmutableSet.of(uuid)); + + // Locales & Placeholders + when(lm.get(any(), any())).thenAnswer((Answer) invocation -> invocation.getArgument(1, String.class)); + when(plugin.getPlaceholdersManager()).thenReturn(phm); + when(phm.replacePlaceholders(any(), any())).thenAnswer((Answer) invocation -> invocation.getArgument(1, String.class)); + when(plugin.getLocalesManager()).thenReturn(lm); + + // Notifier + when(plugin.getNotifier()).thenReturn(notifier); + + // Logger — Addon.getLogger() delegates to plugin.getLogger() + when(plugin.getLogger()).thenReturn(Logger.getLogger("TopBlock-test")); + + // BentoBox settings (fake players feature) + world.bentobox.bentobox.Settings settings = new world.bentobox.bentobox.Settings(); + when(plugin.getSettings()).thenReturn(settings); + + // Util static mock + mockedUtil = Mockito.mockStatic(Util.class, Mockito.CALLS_REAL_METHODS); + mockedUtil.when(() -> Util.getWorld(any())).thenReturn(mock(World.class)); + Util.setPlugin(plugin); + mockedUtil.when(() -> Util.findFirstMatchingEnum(any(), any())).thenCallRealMethod(); + + // Hooks + when(hooksManager.getHook(anyString())).thenReturn(Optional.empty()); + when(plugin.getHooks()).thenReturn(hooksManager); + + // BlueprintsManager + when(plugin.getBlueprintsManager()).thenReturn(bm); + } + + @AfterEach + public void tearDown() throws Exception { + mockedBukkit.closeOnDemand(); + mockedUtil.closeOnDemand(); + closeable.close(); + MockBukkit.unmock(); + User.clearUsers(); + Mockito.framework().clearInlineMocks(); + deleteAll(new File("database")); + deleteAll(new File("database_backup")); + } + + protected static void deleteAll(File file) throws IOException { + if (file.exists()) { + Files.walk(file.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } + } +} diff --git a/src/test/java/world/bentobox/topblock/PlaceholderManagerTest.java b/src/test/java/world/bentobox/topblock/PlaceholderManagerTest.java new file mode 100644 index 0000000..8cd3249 --- /dev/null +++ b/src/test/java/world/bentobox/topblock/PlaceholderManagerTest.java @@ -0,0 +1,96 @@ +package world.bentobox.topblock; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import com.google.common.collect.ImmutableSet; + +import world.bentobox.aoneblock.AOneBlock; +import world.bentobox.aoneblock.dataobjects.OneBlockIslands; +import world.bentobox.aoneblock.listeners.BlockListener; +import world.bentobox.bentobox.managers.PlayersManager; +import world.bentobox.bentobox.managers.RanksManager; +import world.bentobox.topblock.config.ConfigSettings; + +class PlaceholderManagerTest extends CommonTestSetup { + + @Mock + private TopBlock addon; + @Mock + private AOneBlock aob; + @Mock + private BlockListener bl; + @Mock + private PlayersManager playersMgr; + + private TopBlockManager tbm; + private PlaceholderManager phMgr; + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + ConfigSettings settings = new ConfigSettings(); + when(addon.getPlugin()).thenReturn(plugin); + when(addon.getSettings()).thenReturn(settings); + when(addon.getaOneBlock()).thenReturn(aob); + when(addon.getIslands()).thenReturn(im); + when(addon.getPlayers()).thenReturn(playersMgr); + when(aob.getBlockListener()).thenReturn(bl); + when(im.getIslandById(anyString())).thenReturn(Optional.of(island)); + + // Single island in top ten + OneBlockIslands ob = new OneBlockIslands(UUID.randomUUID().toString()); + ob.setBlockNumber(80); + ob.setLifetime(250); + ob.setPhaseName("Underground"); + when(bl.getAllIslands()).thenReturn(List.of(ob)); + + tbm = new TopBlockManager(addon); + when(addon.getManager()).thenReturn(tbm); + tbm.getOneBlockData(); + + phMgr = new PlaceholderManager(addon); + phMgr.updateTopTen(); + } + + @Test + void testGetMemberNamesEmptyForSingleMemberIsland() { + when(island.getMembers()).thenReturn(java.util.Collections.emptyMap()); + + assertEquals("", phMgr.getMemberNames(1)); + } + + @Test + void testGetMemberNamesPastEndReturnsEmpty() { + // Rank 5 with only 1 island in the list → empty + assertEquals("", phMgr.getMemberNames(5)); + } + + @Test + void testGetMemberNamesJoinsMembers() { + UUID a = UUID.randomUUID(); + UUID b = UUID.randomUUID(); + when(island.getMembers()).thenReturn(java.util.Map.of( + a, RanksManager.MEMBER_RANK, + b, RanksManager.SUB_OWNER_RANK)); + when(island.getMemberSet()).thenReturn(ImmutableSet.of(a, b)); + when(playersMgr.getName(a)).thenReturn("Alice"); + when(playersMgr.getName(b)).thenReturn("Bob"); + + String names = phMgr.getMemberNames(1); + // SUB_OWNER_RANK > MEMBER_RANK, so Bob comes first + assertEquals("Bob,Alice", names); + } +} diff --git a/src/test/java/world/bentobox/topblock/TestWorldSettings.java b/src/test/java/world/bentobox/topblock/TestWorldSettings.java new file mode 100644 index 0000000..40c862a --- /dev/null +++ b/src/test/java/world/bentobox/topblock/TestWorldSettings.java @@ -0,0 +1,345 @@ +package world.bentobox.topblock; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.bukkit.Difficulty; +import org.bukkit.GameMode; +import org.bukkit.entity.EntityType; +import org.eclipse.jdt.annotation.NonNull; + +import world.bentobox.bentobox.api.configuration.WorldSettings; +import world.bentobox.bentobox.api.flags.Flag; + +/** + * Minimal WorldSettings implementation for use in tests. + */ +public class TestWorldSettings implements WorldSettings { + + private long epoch; + + @Override + public GameMode getDefaultGameMode() { + return GameMode.SURVIVAL; + } + + @SuppressWarnings("removal") + @Override + public Map getDefaultIslandFlags() { + return Collections.emptyMap(); + } + + @SuppressWarnings("removal") + @Override + public Map getDefaultIslandSettings() { + return Collections.emptyMap(); + } + + @Override + public Difficulty getDifficulty() { + return Difficulty.NORMAL; + } + + @Override + public void setDifficulty(Difficulty difficulty) { + // unused + } + + @Override + public String getFriendlyName() { + return "TopBlock"; + } + + @Override + public int getIslandDistance() { + return 0; + } + + @Override + public int getIslandHeight() { + return 0; + } + + @Override + public int getIslandProtectionRange() { + return 0; + } + + @Override + public int getIslandStartX() { + return 0; + } + + @Override + public int getIslandStartZ() { + return 0; + } + + @Override + public int getIslandXOffset() { + return 0; + } + + @Override + public int getIslandZOffset() { + return 0; + } + + @Override + public List getIvSettings() { + return Collections.emptyList(); + } + + @Override + public int getMaxHomes() { + return 3; + } + + @Override + public int getMaxIslands() { + return 0; + } + + @Override + public int getMaxTeamSize() { + return 4; + } + + @Override + public int getNetherSpawnRadius() { + return 10; + } + + @Override + public String getPermissionPrefix() { + return "topblock."; + } + + @Override + public Set getRemoveMobsWhitelist() { + return Collections.emptySet(); + } + + @Override + public int getSeaHeight() { + return 0; + } + + @Override + public List getHiddenFlags() { + return Collections.emptyList(); + } + + @Override + public List getVisitorBannedCommands() { + return Collections.emptyList(); + } + + @Override + public Map getWorldFlags() { + return new HashMap<>(); + } + + @Override + public String getWorldName() { + return "topblock-world"; + } + + @Override + public boolean isDragonSpawn() { + return false; + } + + @Override + public boolean isEndGenerate() { + return true; + } + + @Override + public boolean isEndIslands() { + return true; + } + + @Override + public boolean isNetherGenerate() { + return true; + } + + @Override + public boolean isNetherIslands() { + return true; + } + + @Override + public boolean isOnJoinResetEnderChest() { + return false; + } + + @Override + public boolean isOnJoinResetInventory() { + return false; + } + + @Override + public boolean isOnJoinResetMoney() { + return false; + } + + @Override + public boolean isOnJoinResetHealth() { + return false; + } + + @Override + public boolean isOnJoinResetHunger() { + return false; + } + + @Override + public boolean isOnJoinResetXP() { + return false; + } + + @Override + public @NonNull List getOnJoinCommands() { + return Collections.emptyList(); + } + + @Override + public boolean isOnLeaveResetEnderChest() { + return false; + } + + @Override + public boolean isOnLeaveResetInventory() { + return false; + } + + @Override + public boolean isOnLeaveResetMoney() { + return false; + } + + @Override + public boolean isOnLeaveResetHealth() { + return false; + } + + @Override + public boolean isOnLeaveResetHunger() { + return false; + } + + @Override + public boolean isOnLeaveResetXP() { + return false; + } + + @Override + public @NonNull List getOnLeaveCommands() { + return Collections.emptyList(); + } + + @Override + public boolean isUseOwnGenerator() { + return false; + } + + @Override + public boolean isWaterUnsafe() { + return false; + } + + @Override + public List getGeoLimitSettings() { + return Collections.emptyList(); + } + + @Override + public int getResetLimit() { + return 0; + } + + @Override + public long getResetEpoch() { + return epoch; + } + + @Override + public void setResetEpoch(long timestamp) { + this.epoch = timestamp; + } + + @Override + public boolean isTeamJoinDeathReset() { + return false; + } + + @Override + public int getDeathsMax() { + return 0; + } + + @Override + public boolean isDeathsCounted() { + return true; + } + + @Override + public boolean isDeathsResetOnNewIsland() { + return true; + } + + @Override + public boolean isAllowSetHomeInNether() { + return false; + } + + @Override + public boolean isAllowSetHomeInTheEnd() { + return false; + } + + @Override + public boolean isRequireConfirmationToSetHomeInNether() { + return false; + } + + @Override + public boolean isRequireConfirmationToSetHomeInTheEnd() { + return false; + } + + @Override + public int getBanLimit() { + return 10; + } + + @Override + public boolean isLeaversLoseReset() { + return true; + } + + @Override + public boolean isKickedKeepInventory() { + return true; + } + + @Override + public boolean isCreateIslandOnFirstLoginEnabled() { + return false; + } + + @Override + public int getCreateIslandOnFirstLoginDelay() { + return 0; + } + + @Override + public boolean isCreateIslandOnFirstLoginAbortOnLogout() { + return false; + } +} diff --git a/src/test/java/world/bentobox/topblock/TopBlockManagerTest.java b/src/test/java/world/bentobox/topblock/TopBlockManagerTest.java index 708ba74..dd7a94b 100644 --- a/src/test/java/world/bentobox/topblock/TopBlockManagerTest.java +++ b/src/test/java/world/bentobox/topblock/TopBlockManagerTest.java @@ -1,243 +1,235 @@ package world.bentobox.topblock; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.UUID; import org.bukkit.Bukkit; -import org.bukkit.Server; -import org.eclipse.jdt.annotation.NonNull; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.Mock; -import org.mockito.Mockito; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; import world.bentobox.aoneblock.AOneBlock; import world.bentobox.aoneblock.dataobjects.OneBlockIslands; import world.bentobox.aoneblock.listeners.BlockListener; -import world.bentobox.bentobox.api.user.User; -import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.bentobox.managers.IslandsManager; import world.bentobox.topblock.TopBlockManager.TopTenData; import world.bentobox.topblock.config.ConfigSettings; -import world.bentobox.topblock.mocks.ServerMocks; -/** - * @author tastybento - * - */ -@RunWith(PowerMockRunner.class) -@PrepareForTest(Bukkit.class) -public class TopBlockManagerTest { +class TopBlockManagerTest extends CommonTestSetup { @Mock private TopBlock addon; @Mock - private Island island; - - private TopBlockManager tbm; - @Mock private AOneBlock aob; - @Mock - private IslandsManager im; + private BlockListener bl; + private TopBlockManager tbm; + private ConfigSettings settings; - /** - * @throws java.lang.Exception - */ - @Before + @Override + @BeforeEach public void setUp() throws Exception { - Server server = ServerMocks.newServer(); - - PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS); - when(Bukkit.getServer()).thenReturn(server); + super.setUp(); - List list = new ArrayList<>(); - OneBlockIslands i = new OneBlockIslands(UUID.randomUUID().toString()); - i.setLifetime(100); - i.setBlockNumber(100); - i.setPhaseName("phasy"); - list.add(i); - - // Island manager + settings = new ConfigSettings(); + when(addon.getPlugin()).thenReturn(plugin); + when(addon.getSettings()).thenReturn(settings); + when(addon.getaOneBlock()).thenReturn(aob); when(addon.getIslands()).thenReturn(im); - when(im.getIslandById(anyString())).thenReturn(Optional.of(island)); - // AOneBlock - BlockListener bl = mock(BlockListener.class); // This class uses static initializations so if it is mocked as a field, it will spark an issue - when(bl.getAllIslands()).thenReturn(list); when(aob.getBlockListener()).thenReturn(bl); - when(addon.getaOneBlock()).thenReturn(aob); + when(im.getIslandById(anyString())).thenReturn(Optional.of(island)); + when(island.getWorld()).thenReturn(world); + when(iwm.getPermissionPrefix(any())).thenReturn("aoneblock."); + tbm = new TopBlockManager(addon); } - @After - public void tearDown() { - ServerMocks.unsetBukkitServer(); - User.clearUsers(); - Mockito.framework().clearInlineMocks(); + private static OneBlockIslands ob(int blockNumber, long lifetime, String phase) { + OneBlockIslands i = new OneBlockIslands(UUID.randomUUID().toString()); + i.setBlockNumber(blockNumber); + i.setLifetime(lifetime); + i.setPhaseName(phase); + return i; } - /** - * Test method for {@link world.bentobox.topblock.TopBlockManager#TopBlockManager(world.bentobox.topblock.TopBlock)}. - */ @Test - public void testTopBlockManager() { - assertNotNull(tbm); + void testGetTopTenEmptyByDefault() { + assertTrue(tbm.getTopTen(10).isEmpty()); } - /** - * Test method for {@link world.bentobox.topblock.TopBlockManager.TopTenData}. - */ @Test - public void testTopTenDataSame() { - TopTenData ttd = new TopTenData(island, 0, 0, "phase one"); - TopTenData ttd2 = new TopTenData(island, 0, 0, "phase one"); - assertEquals(ttd, ttd2); + void testGetOneBlockDataPopulatesTopTen() { + when(bl.getAllIslands()).thenReturn(List.of( + ob(50, 100, "Plains"), + ob(80, 250, "Underground"))); + + tbm.getOneBlockData(); + + List top = tbm.getTopTen(10); + assertEquals(2, top.size()); + // Sorted descending by lifetime + assertEquals(250L, top.get(0).lifetime()); + assertEquals(100L, top.get(1).lifetime()); } - /** - * Test method for {@link world.bentobox.topblock.TopBlockManager.TopTenData}. - */ @Test - public void testTopTenDataBlockDifferent() { - TopTenData ttd = new TopTenData(island, 1000, 0, "phase one"); - TopTenData ttd2 = new TopTenData(island, 0, 0, "phase one"); - assertNotEquals(ttd, ttd2); + void testGetOneBlockDataFiltersZeroLifetime() { + when(bl.getAllIslands()).thenReturn(List.of( + ob(0, 0, "Plains"), + ob(80, 250, "Underground"))); + + tbm.getOneBlockData(); + + List top = tbm.getTopTen(10); + assertEquals(1, top.size()); + assertEquals(250L, top.get(0).lifetime()); } - /** - * Test method for {@link world.bentobox.topblock.TopBlockManager.TopTenData}. - */ @Test - public void testTopTenDataLifetimeDifferent() { - TopTenData ttd = new TopTenData(island, 0, 0, "phase one"); - TopTenData ttd2 = new TopTenData(island, 0, 10000, "phase one"); - assertNotEquals(ttd, ttd2); + void testGetOneBlockDataSkipsIslandsWithoutBentoBoxIsland() { + when(im.getIslandById(anyString())).thenReturn(Optional.empty()); + when(bl.getAllIslands()).thenReturn(List.of(ob(80, 250, "Underground"))); + + tbm.getOneBlockData(); + + assertTrue(tbm.getTopTen(10).isEmpty()); } - /** - * Test method for {@link world.bentobox.topblock.TopBlockManager.TopTenData}. - */ @Test - public void testTopTenDataPhaseDifferent() { - TopTenData ttd = new TopTenData(island, 0, 0, "phase one"); - TopTenData ttd2 = new TopTenData(island, 0, 0, "phase two"); - assertNotEquals(ttd, ttd2); + void testGetOneBlockDataReplacesPreviousResults() { + when(bl.getAllIslands()).thenReturn(List.of(ob(80, 250, "Underground"))); + tbm.getOneBlockData(); + assertEquals(1, tbm.getTopTen(10).size()); + + when(bl.getAllIslands()).thenReturn(List.of()); + tbm.getOneBlockData(); + assertTrue(tbm.getTopTen(10).isEmpty()); } - /** - * Test method for {@link world.bentobox.topblock.TopBlockManager.TopTenData}. - */ @Test - public void testTopTenDataGreater() { - TopTenData ttd = new TopTenData(island, 10000, 0, "phase fifty"); - TopTenData ttd2 = new TopTenData(island, 0, 0, "phase two"); - List list = new ArrayList<>(); - list.add(ttd); - list.add(ttd2); - list = list.stream().sorted(Collections.reverseOrder()).toList(); - assertEquals(ttd, list.get(0)); - assertEquals(ttd2, list.get(1)); + void testGetTopTenLimitsSize() { + when(bl.getAllIslands()).thenReturn(List.of( + ob(10, 10, "a"), + ob(20, 20, "b"), + ob(30, 30, "c"))); + tbm.getOneBlockData(); + + assertEquals(2, tbm.getTopTen(2).size()); } - /** - * Test method for {@link world.bentobox.topblock.TopBlockManager.TopTenData}. - */ @Test - public void testTopTenDataLess() { - TopTenData ttd = new TopTenData(island, 0, 0, "phase one"); - TopTenData ttd2 = new TopTenData(island, 10000, 0, "phase fifty"); - List list = new ArrayList<>(); - list.add(ttd); - list.add(ttd2); - list = list.stream().sorted(Collections.reverseOrder()).toList(); - assertEquals(ttd2, list.get(0)); - assertEquals(ttd, list.get(1)); + void testFormatLevelNullReturnsEmpty() { + assertEquals("", tbm.formatLevel(null)); } - /** - * Test method for {@link world.bentobox.topblock.TopBlockManager.TopTenData}. - */ @Test - public void testTopTenDataGreaterLifetime() { - TopTenData ttd = new TopTenData(island, 100, 10100, "phase fifty"); - TopTenData ttd2 = new TopTenData(island, 1000, 0, "phase two"); - List list = new ArrayList<>(); - list.add(ttd); - list.add(ttd2); - list = list.stream().sorted(Collections.reverseOrder()).toList(); - assertEquals(ttd, list.get(0)); - assertEquals(ttd2, list.get(1)); + void testFormatLevelNoShorthandReturnsRawString() { + settings.setShorthand(false); + assertEquals("104556", tbm.formatLevel(104556L)); } - /** - * Test method for {@link world.bentobox.topblock.TopBlockManager.TopTenData}. - */ @Test - public void testTopTenDataGreaterLifetime2() { - TopTenData ttd = new TopTenData(island, 100, 10100, "phase fifty"); - TopTenData ttd2 = new TopTenData(island, 100, 0, "phase two"); - List list = new ArrayList<>(); - list.add(ttd2); - list.add(ttd); - list = list.stream().sorted(Collections.reverseOrder()).toList(); - assertEquals(ttd, list.get(0)); - assertEquals(ttd2, list.get(1)); + void testFormatLevelShorthandUnderThousandUnchanged() { + settings.setShorthand(true); + assertEquals("999", tbm.formatLevel(999L)); } - - /** - * Test method for {@link world.bentobox.topblock.TopBlockManager#getOneBlockData()}. - */ @Test - public void testGetOneBlockData() { - this.tbm.getOneBlockData(); - @NonNull - List list = tbm.getTopTen(10); - TopTenData t = list.get(0); - assertEquals(100, t.lifetime()); - assertEquals(100, t.blockNumber()); - assertEquals("phasy", t.phaseName()); + void testFormatLevelShorthandKilo() { + settings.setShorthand(true); + assertEquals("10.5k", tbm.formatLevel(10500L)); + } - } + @Test + void testFormatLevelShorthandMega() { + settings.setShorthand(true); + assertEquals("1.5M", tbm.formatLevel(1_527_314L)); + } - /** - * Test method for {@link world.bentobox.topblock.TopBlockManager#formatLevel(java.lang.Long)}. - */ @Test - public void testFormatLevel() { - ConfigSettings settings = new ConfigSettings(); + void testFormatLevelShorthandGiga() { settings.setShorthand(true); - when(addon.getSettings()).thenReturn(settings); - assertEquals("12.3G", tbm.formatLevel(12345678349L)); - settings.setShorthand(false); - when(addon.getSettings()).thenReturn(settings); - assertEquals("12345678349", tbm.formatLevel(12345678349L)); + assertEquals("3.9G", tbm.formatLevel(3_874_130_021L)); + } + + @Test + void testGetOneBlockDataExcludesOnlineOwnerWithoutIntoptenPerm() { + Player p = org.mockito.Mockito.mock(Player.class); + when(p.hasPermission("aoneblock.intopten")).thenReturn(false); + mockedBukkit.when(() -> Bukkit.getPlayer(any(UUID.class))).thenReturn(p); + + when(bl.getAllIslands()).thenReturn(List.of( + new OneBlockIslands(UUID.randomUUID().toString()) {{ + setBlockNumber(80); + setLifetime(250); + setPhaseName("Underground"); + }})); + tbm.getOneBlockData(); + + assertTrue(tbm.getTopTen(10).isEmpty()); + } + + @Test + void testGetOneBlockDataIncludesOnlineOwnerWithIntoptenPerm() { + Player p = org.mockito.Mockito.mock(Player.class); + when(p.hasPermission("aoneblock.intopten")).thenReturn(true); + mockedBukkit.when(() -> Bukkit.getPlayer(any(UUID.class))).thenReturn(p); + + when(bl.getAllIslands()).thenReturn(List.of( + new OneBlockIslands(UUID.randomUUID().toString()) {{ + setBlockNumber(80); + setLifetime(250); + setPhaseName("Underground"); + }})); + tbm.getOneBlockData(); + + assertEquals(1, tbm.getTopTen(10).size()); } - /** - * Test method for {@link world.bentobox.topblock.TopBlockManager#getTopTen(int)}. - */ @Test - public void testGetTopTen() { - List list = tbm.getTopTen(10); - assertTrue(list.isEmpty()); + void testGetOneBlockDataIncludesOfflineOwner() { + // CommonTestSetup already stubs Bukkit.getPlayer -> null + when(bl.getAllIslands()).thenReturn(List.of( + new OneBlockIslands(UUID.randomUUID().toString()) {{ + setBlockNumber(80); + setLifetime(250); + setPhaseName("Underground"); + }})); + tbm.getOneBlockData(); + + assertEquals(1, tbm.getTopTen(10).size()); } + @Test + void testGetOneBlockDataExcludesIslandWithoutOwner() { + when(island.getOwner()).thenReturn(null); + when(bl.getAllIslands()).thenReturn(List.of( + new OneBlockIslands(UUID.randomUUID().toString()) {{ + setBlockNumber(80); + setLifetime(250); + setPhaseName("Underground"); + }})); + tbm.getOneBlockData(); + + assertTrue(tbm.getTopTen(10).isEmpty()); + } + + @Test + void testTopTenDataRecordFields() { + TopTenData d = new TopTenData(island, 42, 1234L, "phasy"); + assertNotNull(d); + assertEquals(42, d.blockNumber()); + assertEquals(1234L, d.lifetime()); + assertEquals("phasy", d.phaseName()); + assertEquals(island, d.island()); + } } diff --git a/src/test/java/world/bentobox/topblock/TopBlockTest.java b/src/test/java/world/bentobox/topblock/TopBlockTest.java new file mode 100644 index 0000000..4f2c33e --- /dev/null +++ b/src/test/java/world/bentobox/topblock/TopBlockTest.java @@ -0,0 +1,151 @@ +package world.bentobox.topblock; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import world.bentobox.bentobox.api.addons.Addon.State; +import world.bentobox.bentobox.api.addons.AddonDescription; +import world.bentobox.bentobox.database.AbstractDatabaseHandler; +import world.bentobox.bentobox.database.DatabaseSetup; +import world.bentobox.bentobox.managers.AddonsManager; +import world.bentobox.bentobox.managers.CommandsManager; +import world.bentobox.topblock.config.ConfigSettings; + +class TopBlockTest extends CommonTestSetup { + + private static final String CONFIG_YML = + """ + refresh-time: 5 + shorthand: false + """; + + private static final String TOP_PANEL_YML = "top_panel:\n type: INVENTORY\n"; + + @Mock + private AddonsManager am; + + private TopBlock addon; + private MockedStatic mockDb; + + @SuppressWarnings("unchecked") + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + // Database mock + AbstractDatabaseHandler h = mock(AbstractDatabaseHandler.class); + mockDb = Mockito.mockStatic(DatabaseSetup.class); + DatabaseSetup dbSetup = mock(DatabaseSetup.class); + mockDb.when(DatabaseSetup::getDatabase).thenReturn(dbSetup); + when(dbSetup.getHandler(any())).thenReturn(h); + when(h.saveObject(any())).thenReturn(CompletableFuture.completedFuture(true)); + + // CommandsManager + CommandsManager cm = mock(CommandsManager.class); + when(plugin.getCommandsManager()).thenReturn(cm); + + // AddonsManager — no aoneblock present + when(plugin.getAddonsManager()).thenReturn(am); + when(am.getGameModeAddons()).thenReturn(Collections.emptyList()); + when(am.getAddonByName("aoneblock")).thenReturn(Optional.empty()); + + // FlagsManager + when(plugin.getFlagsManager()).thenReturn(fm); + when(fm.getFlags()).thenReturn(Collections.emptyList()); + + // Build a JAR with config.yml + the panel resource that onLoad copies + addon = new TopBlock(); + File jFile = new File("addon.jar"); + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jFile))) { + addJarEntry(jos, "config.yml", CONFIG_YML); + addJarEntry(jos, "panels/top_panel.yml", TOP_PANEL_YML); + } + addon.setDataFolder(new File("addons/TopBlock")); + addon.setFile(jFile); + addon.setDescription(new AddonDescription.Builder("bentobox", "TopBlock", "1.0.0") + .description("test").authors("tastybento").build()); + } + + @Override + @AfterEach + public void tearDown() throws Exception { + if (mockDb != null) { + mockDb.closeOnDemand(); + } + super.tearDown(); + new File("addon.jar").delete(); + deleteAll(new File("addons")); + } + + private static void addJarEntry(JarOutputStream jos, String name, String content) throws Exception { + JarEntry entry = new JarEntry(name); + jos.putNextEntry(entry); + jos.write(content.getBytes(StandardCharsets.UTF_8)); + jos.closeEntry(); + } + + @Test + void testGetSettingsNullBeforeLoad() { + assertNull(addon.getSettings()); + } + + @Test + void testOnLoad() { + addon.onLoad(); + assertNotNull(addon.getSettings()); + } + + @Test + void testOnLoadSettingsDefaults() { + addon.onLoad(); + ConfigSettings s = addon.getSettings(); + assertNotNull(s); + // refresh-time clamps to >=1; default in YAML is 5 + org.junit.jupiter.api.Assertions.assertEquals(5, s.getRefreshTime()); + org.junit.jupiter.api.Assertions.assertFalse(s.isShorthand()); + } + + @Test + void testOnEnableWithoutAOneBlockDisables() { + addon.onLoad(); + addon.onEnable(); + // AOneBlock not present → addon disables itself + assertTrue(addon.getState() == State.DISABLED); + // Manager is still constructed before the AOneBlock lookup + assertNotNull(addon.getManager()); + } + + @Test + void testOnDisable() { + addon.onDisable(); + assertNotNull(addon); + } + + @Test + void testOnReload() { + addon.onLoad(); + addon.onReload(); + assertNotNull(addon.getSettings()); + } +} diff --git a/src/test/java/world/bentobox/topblock/WhiteBox.java b/src/test/java/world/bentobox/topblock/WhiteBox.java new file mode 100644 index 0000000..94eb230 --- /dev/null +++ b/src/test/java/world/bentobox/topblock/WhiteBox.java @@ -0,0 +1,13 @@ +package world.bentobox.topblock; + +public class WhiteBox { + public static void setInternalState(Class targetClass, String fieldName, Object value) { + try { + java.lang.reflect.Field field = targetClass.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(null, value); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException("Failed to set static field '" + fieldName + "' on class " + targetClass.getName(), e); + } + } +} diff --git a/src/test/java/world/bentobox/topblock/mocks/ServerMocks.java b/src/test/java/world/bentobox/topblock/mocks/ServerMocks.java deleted file mode 100644 index 06ad3db..0000000 --- a/src/test/java/world/bentobox/topblock/mocks/ServerMocks.java +++ /dev/null @@ -1,118 +0,0 @@ -package world.bentobox.topblock.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