From 400ef77a948cc9755c59f1c70edee87cfce77c4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 23:11:56 +0000 Subject: [PATCH 01/13] Initial plan From ec58c0827808d1235bb1a04904847cb04478298c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 23:33:22 +0000 Subject: [PATCH 02/13] Add trial chambers support: fix trial spawner copying and advancement tracking Co-authored-by: tastybento <4407265+tastybento@users.noreply.github.com> --- .../chunks/BoxedBlockPopulator.java | 19 +++++++++++++++++++ .../chunks/BoxedChunkGenerator.java | 8 ++++++++ .../boxed/listeners/NewAreaListener.java | 2 +- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java b/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java index df27d4c..2795869 100644 --- a/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java +++ b/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java @@ -10,6 +10,8 @@ import org.bukkit.block.Banner; import org.bukkit.block.BlockState; import org.bukkit.block.CreatureSpawner; +import org.bukkit.block.TrialSpawner; +import org.bukkit.spawner.TrialSpawnerConfiguration; import org.bukkit.entity.Entity; import org.bukkit.generator.BlockPopulator; import org.bukkit.generator.LimitedRegion; @@ -20,6 +22,7 @@ import world.bentobox.bentobox.blueprints.dataobjects.BlueprintBlock; import world.bentobox.bentobox.blueprints.dataobjects.BlueprintCreatureSpawner; +import world.bentobox.bentobox.blueprints.dataobjects.BlueprintTrialSpawner; import world.bentobox.bentobox.util.Pair; import world.bentobox.boxed.Boxed; import world.bentobox.boxed.generators.chunks.AbstractBoxedChunkGenerator.ChestData; @@ -98,6 +101,10 @@ public void setBlockState(BlockState bs, BlueprintBlock bpBlock) { else if (bs instanceof CreatureSpawner spawner) { setSpawner(spawner, bpBlock.getCreatureSpawner()); } + // Trial spawners + else if (bs instanceof TrialSpawner trialSpawner && bpBlock.getTrialSpawner() != null) { + setTrialSpawner(trialSpawner, bpBlock.getTrialSpawner()); + } // Banners else if (bs instanceof Banner banner && bpBlock.getBannerPatterns() != null) { bpBlock.getBannerPatterns().removeIf(Objects::isNull); @@ -129,4 +136,16 @@ public void setSpawner(CreatureSpawner spawner, BlueprintCreatureSpawner s) { spawner.update(true, false); } + /** + * Set the trial spawner configuration from the blueprint + * + * @param trialSpawner - trial spawner + * @param bts - blueprint trial spawner + */ + public void setTrialSpawner(TrialSpawner trialSpawner, BlueprintTrialSpawner bts) { + TrialSpawnerConfiguration config = trialSpawner.getNormalConfiguration(); + trialSpawner.setOminous(bts.configTrialSpawner(config)); + trialSpawner.update(true, false); + } + } diff --git a/src/main/java/world/bentobox/boxed/generators/chunks/BoxedChunkGenerator.java b/src/main/java/world/bentobox/boxed/generators/chunks/BoxedChunkGenerator.java index e47099b..fd6dd38 100644 --- a/src/main/java/world/bentobox/boxed/generators/chunks/BoxedChunkGenerator.java +++ b/src/main/java/world/bentobox/boxed/generators/chunks/BoxedChunkGenerator.java @@ -17,6 +17,7 @@ import org.bukkit.block.BlockState; import org.bukkit.block.CreatureSpawner; import org.bukkit.block.Sign; +import org.bukkit.block.TrialSpawner; import org.bukkit.entity.AbstractHorse; import org.bukkit.entity.Ageable; import org.bukkit.entity.ChestedHorse; @@ -36,6 +37,7 @@ import world.bentobox.bentobox.blueprints.dataobjects.BlueprintBlock; import world.bentobox.bentobox.blueprints.dataobjects.BlueprintCreatureSpawner; import world.bentobox.bentobox.blueprints.dataobjects.BlueprintEntity; +import world.bentobox.bentobox.blueprints.dataobjects.BlueprintTrialSpawner; import world.bentobox.boxed.Boxed; /** @@ -175,6 +177,12 @@ private BlueprintBlock getBluePrintBlock(Block block) { b.setCreatureSpawner(getSpawner(spawner)); } + // Trial spawner + if (blockState instanceof TrialSpawner trialSpawner) { + b.setTrialSpawner(new BlueprintTrialSpawner(trialSpawner.isOminous(), + trialSpawner.isOminous() ? trialSpawner.getOminousConfiguration() : trialSpawner.getNormalConfiguration())); + } + // Banners if (blockState instanceof Banner banner) { b.setBannerPatterns(banner.getPatterns()); diff --git a/src/main/java/world/bentobox/boxed/listeners/NewAreaListener.java b/src/main/java/world/bentobox/boxed/listeners/NewAreaListener.java index 0dd552c..57cfee5 100644 --- a/src/main/java/world/bentobox/boxed/listeners/NewAreaListener.java +++ b/src/main/java/world/bentobox/boxed/listeners/NewAreaListener.java @@ -79,7 +79,7 @@ public class NewAreaListener implements Listener { "mineshaft", "mineshaft_mesa", "monument", "nether_fossil", "ocean_ruin_cold", "ocean_ruin_warm", "pillager_outpost", "ruined_portal_desert", "ruined_portal_jungle", "ruined_portal_mountain", "ruined_portal_nether", "ruined_portal_ocean", "ruined_portal_swamp", "ruined_portal", "shipwreck_beached", - "shipwreck", "stronghold", "swamp_hut", "village_desert", "village_plains", "village_savanna", + "shipwreck", "stronghold", "swamp_hut", "trial_chambers", "village_desert", "village_plains", "village_savanna", "village_snowy", "village_taiga"); private final Boxed addon; private final File structureFile; From b73d0fcaa5ceaf46a8c66f275ec3bfddf802cf8c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:39:48 +0000 Subject: [PATCH 03/13] Initial plan From 02c00a9efd65f2107bb41115c104fee1ce9841cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:46:24 +0000 Subject: [PATCH 04/13] Update Boxed docs: add Flags, Placeholders, structures.yml, config options, and Regionerator sections Co-authored-by: tastybento <4407265+tastybento@users.noreply.github.com> --- README.md | 127 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 114 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d9ae27a..e4fc994 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A game mode where you are boxed into a tiny space that only expands by completin * Border - shows the box ### Warning!! -Boxed requires **a lot of RAM** and can take up to **10 minutes** to boot up for the first time as it pre-generates the worlds. After the initial start, it will start up much quicker. With 12GB of RAM running on a fast ARM-based system, it takes ~ 8 minutes for the first boot. If you do not have enough RAM then weird things will happen to you server including strange errors about chunks and things like that. To dedicate enough RAM to your JVM, use the correct flags during startup. Here is my `start.sh` for running on Paper 1.19.4: +Boxed requires **a lot of RAM** and can take up to **10 minutes** to boot up for the first time as it pre-generates the worlds. After the initial start, it will start up much quicker. With 12GB of RAM running on a fast ARM-based system, it takes ~ 8 minutes for the first boot. If you do not have enough RAM then weird things will happen to your server including strange errors about chunks and things like that. To dedicate enough RAM to your JVM, use the correct flags during startup. Here is my `start.sh` for running on Paper 1.19.4: ``` #!/bin/sh java -Xms12G -Xmx12G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 -XX:G1MixedGCLiveThresholdPercent=90 -XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:+PerfDisableSharedMem -XX:MaxTenuringThreshold=1 -XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M -XX:G1ReservePercent=20 -XX:InitiatingHeapOccupancyPercent=15 -Dusing.aikars.flags=https://mcflags.emc.gs -Daikars.new.flags=true -jar paper-1.19.4.jar nogui @@ -20,14 +20,14 @@ java -Xms12G -Xmx12G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMill ### Quick Start 1. Place Boxed addon into the BentoBox addons folder along with InvSwitcher and Border (use the latest versions!). -2. (Optional) Installed the Datapack for custom advancements - https://github.com/BentoBoxWorld/BoxedDataPack/ -4. Restart the server - new worlds will be created. This will take a while! -5. Login -6. Type `/boxed` to start. -7. Turn off advancement announcements `/gamerule announceAdvancements false` otherwise there is a lot of spam from the server when players get advancements. +2. (Optional) Install the Datapack for custom advancements - https://github.com/BentoBoxWorld/BoxedDataPack/ +3. Restart the server - new worlds will be created. This will take a while! +4. Login. +5. Type `/boxed` to start. +6. Turn off advancement announcements with `/gamerule announceAdvancements false` to avoid spam, or set `boxed.broadcast-advancements: true` in `config.yml` to have Boxed broadcast them instead. -* You will start by a tree. The is a chest with some handy items in it. (This is the island blueprint) +* You will start next to a tree. There is a chest with some handy items in it. (This is the island blueprint) * The only area you can operate on is your box that shows as a border. * To make your box bigger, complete advancements. * Check your progress with the Advancements screen, (L-key). @@ -47,20 +47,44 @@ java -Xms12G -Xmx12G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMill ### config.yml The config is very similar to BSkyBlock, AcidIsland, etc. -Each player will have a land of their own to explore up to the limit of the island distance value. The default is 400, so the land will be 800 x 800 blocks. The land is semi-random, but each player will get roughly the same layout (see the biomes config). Structures such as villages, broken nether gates, shipwrecks, etc. are random and so some players may get them, others not. In a future version, switching off structures will be a config option. Strongholds are switched off and do not exist. Each player's land is surrounded by seas of different temperatures. If the border is not solid, then players can theoretically explore other lands. +Each player will have a land of their own to explore up to the limit of the island distance value. The default is 320, so the land will be 640 x 640 blocks. The land is semi-random, but each player will get roughly the same layout (see the biomes config). Structures such as villages, broken nether gates, shipwrecks, etc. are random and so some players may get them, others not. Strongholds are switched off and do not exist. Each player's land is surrounded by seas of different temperatures. If the border is not solid, then players can theoretically explore other lands. *World Seed* -The world seed is what it is used to generate the lands. I recommend keeping this value. If you change it the land may be very different. +The world seed is used to generate the lands. It is recommended to keep this value. If you change it the land may be very different. Note that changing the seed mid-game requires a full reset of your databases and worlds. + +*Key Boxed-specific settings:* + +| Setting | Default | Description | +|---------|---------|-------------| +| `boxed.ignore-advancements` | `false` | If `true`, advancements will not change the size of the box. | +| `boxed.broadcast-advancements` | `false` | If `true`, Boxed will broadcast new advancements. Recommended: set the game rule `/gamerule announceAdvancements false` and use this setting instead. | +| `boxed.deny-visitor-advancements` | `true` | If `true`, visitors cannot earn advancements. Note: visitors will still receive other rewards such as experience. | +| `world.allow-structures` | `false` | Allow vanilla structures to generate in the seed world. | ### Blueprint There is one blueprint "island" that is used to generate the tree, chest and blocks below down to y = 5. The default height of the surface is about y = 65, so the blueprint has to be about 60 blocks tall. If you make any good blueprints, please share them! ### advancements.yml -This file contains all the advancements and how much your box should grow if you get one. The file can contain custom advancements if you have them. The default is for most recipe advancements to give nothing. +This file contains all the advancements and how much your box should grow if you get one. The file can contain custom advancements if you have them. + +There are settings at the top of the file: + +| Setting | Default | Description | +|---------|---------|-------------| +| `settings.default-root-increase` | `0` | Score applied when a root (tab-opening) advancement is earned. Typically left at 0 to avoid rewarding players simply for unlocking a new tab. | +| `settings.unknown-advancement-increase` | `1` | Default box increase for any advancement not listed in this file. Useful for custom advancements added via a data pack — you don't need to list every new advancement manually. | +| `settings.unknown-recipe-increase` | `0` | Default box increase for recipe advancements not listed in this file. | +| `settings.automatic-scoring` | `true` | If `true`, uses a proprietary algorithm to automatically score advancements. If `false`, each advancement must be scored manually. | Example: -``` +```yaml +# Lists how many blocks the box will increase when advancement occurs +settings: + default-root-increase: 0 + unknown-advancement-increase: 1 + unknown-recipe-increase: 0 + automatic-scoring: true advancements: 'minecraft:adventure/adventuring_time': 1 'minecraft:adventure/arbalistic': 1 @@ -69,7 +93,7 @@ advancements: 'minecraft:adventure/honey_block_slide': 1 'minecraft:adventure/kill_a_mob': 1 ... - ``` +``` ### biomes.yml The player's land has biomes and they are defined here. It's not possible to define where the biomes are right now, only what affect they have on the terrain. @@ -81,8 +105,85 @@ Setting ocean biomes to higher height numbers will result in the ocean floor bei A lot of these numbers are rough guesses right now and if you come up with better values, please share them! +### structures.yml +This file records which Minecraft structures should be placed in each new player's box when their area is first created. Structures are stored relative to the island center and are placed in overworld (`normal`) and nether sections. + +Admins can place structures in-game using the `/boxadmin place` command: + +``` +/boxadmin place [x y z] [ROTATION] [MIRROR] [NO_MOBS] +``` + +| Argument | Description | +|----------|-------------| +| `` | Minecraft structure name (tab-complete to see available structures) | +| `[x y z]` | Coordinates where the structure should be placed. Use `~` for the current position. | +| `[ROTATION]` | Optional rotation: `NONE`, `CLOCKWISE_90`, `CLOCKWISE_180`, `COUNTERCLOCKWISE_90` | +| `[MIRROR]` | Optional mirror: `NONE`, `LEFT_RIGHT`, `FRONT_BACK` | +| `[NO_MOBS]` | Optional flag to suppress mob spawning from this structure | + +To undo the last placed structure: `/boxadmin place undo` + +When a structure is placed via this command while standing in a player box, it is automatically saved to `structures.yml` and will be placed in all future boxes. + + +## Flags + +Boxed registers two flags unique to this gamemode. + +### ALLOW_MOVE_BOX (World Setting) +Controls whether box-moving via ender pearl is enabled at all in this world. This is a world-level toggle visible in the BentoBox admin settings. + +* **Type:** World Setting +* **Default:** Enabled + +### MOVE_BOX (Protection Flag) +Controls which rank of island member is allowed to move the box by throwing ender pearls from within it. Only shown and active when `ALLOW_MOVE_BOX` is enabled. + +* **Type:** Protection (Island Setting) +* **Default:** Owner only +* **Icon:** Composter + +Players can find this setting under `/box settings` (look for the Composter icon). + + +## Placeholders + +The following PlaceholderAPI placeholders are registered by Boxed: + +| Placeholder | Description | +|-------------|-------------| +| `%boxed_island_advancements%` | The number of advancements earned by the player's island (based on the player's island membership). | +| `%boxed_visited_island_advancements%` | The number of advancements earned by the island the player is currently standing on. | + ## Custom Advancements -To find out how to add custom advacements to your server, watch the tutorial video [here](https://www.youtube.com/watch?v=zNzQvIbweQs)! +To find out how to add custom advancements to your server, watch the tutorial video [here](https://www.youtube.com/watch?v=zNzQvIbweQs)! + +Download the official [Boxed DataPack](https://github.com/BentoBoxWorld/BoxedDataPack) for extra custom advancements. + + +## Using Regionerator + +*Note: This plugin is designed to delete unused regions of your world! Make sure you take backups if you use it! Use at your own risk!* + +[Regionerator](https://github.com/Jikoo/Regionerator) is a plugin that gradually deletes unused chunks to keep world sizes low. It supports BentoBox and respects box boundaries. It can be used to delete box chunks so that they can be regenerated. As Boxed uses seed worlds to copy from, these can appear to be unused by Regionerator and deleted, which means that startup becomes very slow. To avoid this, set the seed worlds as exempt from its deletions by adding these entries to the `worlds` section of the Regionerator config file: + +```yaml +worlds: + boxed_world/seed_base: + days-till-flag-expires: -1 + boxed_world/seed: + days-till-flag-expires: -1 + default: + days-till-flag-expires: 0 +``` + +To get the most out of Regionerator, change the BentoBox `config.yml` to *not* delete chunks when an island is removed. This leaves deletion up to Regionerator and it will clean up the chunks if the unused area is large enough. Set `keep-previous-island-on-reset: true`: + +```yaml +deletion: + keep-previous-island-on-reset: true +``` From 65b96b4ff4204025eddf610eb99d78d48339f011 Mon Sep 17 00:00:00 2001 From: tastybento Date: Thu, 9 Apr 2026 15:12:48 -0700 Subject: [PATCH 05/13] Migrate test stack to JUnit 5 + Mockito mockStatic + MockBukkit Replaces PowerMock/Spigot API with Paper-only 1.21.11 and BentoBox 3.13.0, introduces a shared CommonTestSetup base class, and drops the hand-rolled ServerMocks helper. Also fixes AdvancementsManager for the Paper 1.21.11 AdvancementDisplay API change (getX/getY removed) and forks javac to dodge a JDK 25 in-process compiler NPE. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 64 +++ pom.xml | 84 ++-- .../bentobox/boxed/AdvancementsManager.java | 10 +- .../boxed/AdvancementsManagerTest.java | 109 ++--- .../world/bentobox/boxed/CommonTestSetup.java | 316 ++++++++++++++ .../world/bentobox/boxed/SettingsTest.java | 38 +- .../bentobox/boxed/TestWorldSettings.java | 409 ++++++++++++++++++ .../java/world/bentobox/boxed/WhiteBox.java | 26 ++ .../listeners/EnderPearlListenerTest.java | 147 +++---- .../bentobox/boxed/mocks/ServerMocks.java | 119 ----- 10 files changed, 982 insertions(+), 340 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src/test/java/world/bentobox/boxed/CommonTestSetup.java create mode 100644 src/test/java/world/bentobox/boxed/TestWorldSettings.java create mode 100644 src/test/java/world/bentobox/boxed/WhiteBox.java delete mode 100644 src/test/java/world/bentobox/boxed/mocks/ServerMocks.java diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1eb92cc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,64 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project + +Boxed is a BentoBox GameModeAddon for Minecraft (Paper) where each player is confined to a small expandable box. Completing Minecraft advancements grows the box. Built against `bentobox` 3.13.0, Paper API 1.21.11, Java 21. + +## Build / Test + +- Build (default goal is `clean package`): `mvn clean package` +- Run all tests: `mvn test` +- Run a single test class: `mvn test -Dtest=AdvancementsManagerTest` +- Run a single test method: `mvn test -Dtest=AdvancementsManagerTest#testMethodName` +- Jacoco coverage report is generated as part of the build (`target/site/jacoco/`). + +The `build.version` property in `pom.xml` is the canonical version. The `ci` and `master` Maven profiles activate from Jenkins env vars (`BUILD_NUMBER`, `GIT_BRANCH=origin/master`) to compute the final artifact name; don't hand-edit `revision`. + +Surefire is configured with a long list of `--add-opens` JVM args required by Mockito's inline mock maker on Java 21+ — if you add new tests that touch JDK internals, extend that `argLine` in `pom.xml` rather than fighting module access errors locally. The `maven-compiler-plugin` runs with `true` to work around a JDK 25 in-process javac NPE (`this.hashes is null`). + +## Architecture + +Boxed is a BentoBox addon, not a standalone plugin. `Boxed` extends `GameModeAddon` and is loaded by BentoBox at runtime. `BoxedPladdon` is the Paper plugin-loader shim so Paper recognises the jar. + +### The two-world "seed + game" generator model + +This is the core concept and touches almost everything: + +1. **Seed world** (`/seed`, and `/seed_nether`): a real vanilla-ish world generated once using `BoxedSeedChunkGenerator` + `SeedBiomeGenerator` / `NetherSeedBiomeGenerator`. It's where Minecraft's normal terrain + structures (villages, shipwrecks, fortresses, etc.) actually get generated by the server. +2. **Game world** (``, `_nether`): the world players actually play in. It uses `BoxedChunkGenerator` (a subclass of `AbstractBoxedChunkGenerator`) which does **not** generate terrain from scratch — instead, `Boxed.copyChunks(...)` pre-reads every chunk inside `islandDistance` from the seed world during `createWorlds()` and stores them in the chunk generator. When the game world asks for a chunk, the generator serves back the pre-captured copy. + +This is why first boot is extremely slow and RAM-hungry (see `README.md` warnings): the entire seed region is force-loaded up front. Any change to world generation, structure handling, or world naming must respect both worlds and the copy step in `Boxed.copyChunks()` / `createOverWorld()` / `createNether()`. The `generatorMaps` / `generatorMap` fields in `Boxed.java` route world names → generators for `getDefaultWorldGenerator` (used by Multiverse and similar world-management plugins) and for the hook in `allLoaded()` that calls `WorldManagementHook.registerWorld`. + +`isUsesNewChunkGeneration()` returns `true`, which tells BentoBox this addon uses the modern chunk-generation API. + +### Advancements drive box size + +`AdvancementsManager` is the other key subsystem. Box growth is data-driven from `advancements.yml`: each advancement key maps to an integer "box growth" increment. `AdvancementListener` watches for player advancement events and asks the manager to update the island's protection-range. Per-island state lives in `objects/IslandAdvancements.java` (a BentoBox `DataObject` persisted via its database layer). `AdvancementsManager.save()` is called in `onDisable()` — any new cached state it holds should be flushed there too. + +Because advancements are per-world in vanilla but Boxed runs multiple worlds on one server, the **InvSwitcher** addon is required at runtime to keep advancements separate between worlds. The code logs a warning if InvSwitcher/Border aren't installed but does not hard-fail. + +### Structures + +`NewAreaListener` plus `objects/IslandStructures.java`, `BoxedJigsawBlock`, `BoxedStructureBlock`, and `ToBePlacedStructures` handle placing/tracking vanilla structures inside player boxes. `AdminPlaceStructureCommand` is the admin-side hook for manual placement. If you're adding structure logic, both the "captured from seed world" pathway and the "placed into player box" pathway need to stay in sync. + +### Flags + +`Boxed.MOVE_BOX` (protection flag, owner-only by default) and `Boxed.ALLOW_MOVE_BOX` (world setting) gate the enderpearl-box-teleport feature implemented in `EnderPearlListener`. They are scoped to this game mode only via `setGameModes(...)` in `onEnable()`, and `MOVE_BOX` is conditionally registered/unregistered depending on `ALLOW_MOVE_BOX` — keep that conditional registration intact if you touch flag setup. + +### Resources packaged at build time + +`pom.xml` filters `src/main/resources` but copies `structures/*.nbt`, `locales/*.yml`, and `blueprints/*.blu|*.json` unfiltered into the jar at specific targetPaths. New resource types need a matching `` block or they won't ship. + +## Testing notes + +Tests use JUnit 5 (Jupiter) + Mockito 5 `mockStatic` + MockBukkit (`v1.21-SNAPSHOT` via jitpack.io). There is no PowerMock and no custom `ServerMocks` helper. All test classes extend `CommonTestSetup`, which: + +- Calls `MockBukkit.mock()` and registers `Mockito.mockStatic(Bukkit.class, RETURNS_DEEP_STUBS)` — use the inherited `mockedBukkit` field to stub `Bukkit.*` calls rather than creating your own. +- Exposes ready-made `@Mock` fields (`plugin`, `mockPlayer`, `world`, `location`, `iwm`, `im`, `island`, `pim`, `itemFactory`, `inv`, `notifier`, `fm`, `spigot`, `hooksManager`, `bm`, `sch`, `lm`, `phm`) plus the MockBukkit `server` and `mockedUtil` statics. Don't re-declare these in subclasses. +- Forces `org.bukkit.Tag.LEAVES` to initialise **before** the static Bukkit mock is installed — if you touch tag-related code and see stale deep-stubs across tests, broaden that list in `CommonTestSetup`. + +A small `WhiteBox` helper (reflection-based private static field setter) replaces PowerMock's `Whitebox.setInternalState`. Any test that needs its own `MockedStatic<...>` (e.g. `DatabaseSetup`, `User`) must create it **after** `super.setUp()` and close it via `closeOnDemand()` **before** `super.tearDown()` — the parent's `Mockito.framework().clearInlineMocks()` in tearDown will otherwise corrupt the local static. + +Jacoco excludes `org/bukkit/Material*` to avoid "class too large to mock" failures; keep that exclusion if you rearrange the build section. diff --git a/pom.xml b/pom.xml index d1985af..e40f006 100644 --- a/pom.xml +++ b/pom.xml @@ -52,11 +52,12 @@ UTF-8 21 - 2.0.9 + 5.10.2 + 5.11.0 + v1.21-SNAPSHOT - 1.21.10-R0.1-SNAPSHOT - 1.21.10-R0.1-SNAPSHOT - 3.9.0 + 1.21.11-R0.1-SNAPSHOT + 3.13.0 ${build.version}-SNAPSHOT @@ -112,11 +113,14 @@ - - spigot-repo - https://hub.spigotmc.org/nexus/content/repositories/snapshots + + + jitpack.io + https://jitpack.io + true + true - + papermc https://repo.papermc.io/repository/maven-public/ @@ -144,42 +148,51 @@ - + - org.spigotmc - spigot-api - ${spigot.version} + io.papermc.paper + paper-api + ${paper.version} provided - + - org.mockito - mockito-core - 3.11.1 + world.bentobox + bentobox + ${bentobox.version} + provided + + + + com.github.MockBukkit + MockBukkit + ${mock-bukkit.version} test + - org.powermock - powermock-module-junit4 - ${powermock.version} + org.junit.jupiter + junit-jupiter-api + ${junit.version} test - org.powermock - powermock-api-mockito2 - ${powermock.version} + org.junit.jupiter + junit-jupiter-engine + ${junit.version} test + - world.bentobox - bentobox - ${bentobox.version} - provided + org.mockito + mockito-junit-jupiter + ${mockito.version} + test - net.kyori - adventure-api - 4.24.0 + org.mockito + mockito-core + ${mockito.version} test @@ -237,7 +250,7 @@ org.apache.maven.plugins maven-resources-plugin - 3.1.0 + 3.2.0 blu @@ -248,15 +261,16 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.13.0 ${java.version} + true org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M5 + 3.5.4 @@ -326,7 +340,7 @@ org.apache.maven.plugins maven-source-plugin - 3.0.1 + 3.2.1 attach-sources @@ -339,12 +353,12 @@ org.apache.maven.plugins maven-install-plugin - 2.5.2 + 3.0.0-M1 org.apache.maven.plugins maven-deploy-plugin - 2.8.2 + 3.0.0-M1 org.jacoco diff --git a/src/main/java/world/bentobox/boxed/AdvancementsManager.java b/src/main/java/world/bentobox/boxed/AdvancementsManager.java index d8adc0f..2540103 100644 --- a/src/main/java/world/bentobox/boxed/AdvancementsManager.java +++ b/src/main/java/world/bentobox/boxed/AdvancementsManager.java @@ -250,9 +250,13 @@ public int getScore(Advancement a) { } if (advConfig.getBoolean("settings.automatic-scoring")) { if (!a.getKey().getKey().contains("recipes") && a.getDisplay() != null) { - float x = a.getDisplay().getX(); - float y = a.getDisplay().getY(); - return (int) Math.round(Math.sqrt(x * x + y * y)); + // Paper 1.21.11 removed getX()/getY() from AdvancementDisplay, so we can no + // longer compute the distance-based score. Fall back to the hard-coded value + // from advancements.yml when present, otherwise the unknown default. + if (advConfig.contains(adv)) { + return advConfig.getInt(adv, this.unknownAdvChange); + } + return this.unknownAdvChange; } else { return 0; } diff --git a/src/test/java/world/bentobox/boxed/AdvancementsManagerTest.java b/src/test/java/world/bentobox/boxed/AdvancementsManagerTest.java index bcd862b..83394f9 100644 --- a/src/test/java/world/bentobox/boxed/AdvancementsManagerTest.java +++ b/src/test/java/world/bentobox/boxed/AdvancementsManagerTest.java @@ -1,10 +1,9 @@ package world.bentobox.boxed; -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.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -18,95 +17,64 @@ import java.lang.reflect.InvocationTargetException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Comparator; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import org.bukkit.Bukkit; import org.bukkit.NamespacedKey; -import org.bukkit.World; import org.bukkit.advancement.Advancement; -import org.bukkit.advancement.AdvancementDisplay; import org.bukkit.entity.Player; import org.eclipse.jdt.annotation.NonNull; -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.Mock; +import org.mockito.MockedStatic; import org.mockito.Mockito; -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.user.User; + +import io.papermc.paper.advancement.AdvancementDisplay; 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.IslandsManager; import world.bentobox.bentobox.managers.RanksManager; -import world.bentobox.bentobox.util.Util; import world.bentobox.boxed.objects.IslandAdvancements; /** * @author tastybento * */ -@RunWith(PowerMockRunner.class) -@PrepareForTest({Bukkit.class, BentoBox.class, DatabaseSetup.class, Util.class}) -public class AdvancementsManagerTest { +public class AdvancementsManagerTest extends CommonTestSetup { - private static AbstractDatabaseHandler h; @Mock - private BentoBox plugin; - @Mock - private Settings pluginSettings; - - + private world.bentobox.bentobox.Settings pluginSettings; @Mock private Boxed addon; private AdvancementsManager am; private File dataFolder; @Mock - private Island island; - @Mock private Player player; @Mock private Advancement advancement; @Mock - private World world; - @Mock - private IslandsManager im; - @Mock private AdvancementDisplay display; + private MockedStatic mockedDatabaseSetup; + private AbstractDatabaseHandler h; + @SuppressWarnings("unchecked") - @BeforeClass - public static void beforeClass() throws IllegalAccessException, InvocationTargetException, IntrospectionException { - // This has to be done beforeClass otherwise the tests will interfere with each other + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + // Database static mock (local — CommonTestSetup does not handle this one) h = mock(AbstractDatabaseHandler.class); - // Database - PowerMockito.mockStatic(DatabaseSetup.class); + mockedDatabaseSetup = Mockito.mockStatic(DatabaseSetup.class); DatabaseSetup dbSetup = mock(DatabaseSetup.class); - when(DatabaseSetup.getDatabase()).thenReturn(dbSetup); + mockedDatabaseSetup.when(DatabaseSetup::getDatabase).thenReturn(dbSetup); when(dbSetup.getHandler(any())).thenReturn(h); when(h.saveObject(any())).thenReturn(CompletableFuture.completedFuture(true)); - } - - /** - * @throws java.lang.Exception - */ - @Before - public void setUp() throws Exception { when(addon.getPlugin()).thenReturn(plugin); - // Set up plugin - Whitebox.setInternalState(BentoBox.class, "instance", plugin); // The database type has to be created one line before the thenReturn() to work! DatabaseType value = DatabaseType.JSON; @@ -128,17 +96,13 @@ public void setUp() throws Exception { UUID uuid = UUID.randomUUID(); when(player.getUniqueId()).thenReturn(uuid); - NamespacedKey key = NamespacedKey.fromString("adventure/honey_block_slide"); // Advancement when(advancement.getKey()).thenReturn(key); - when(display.getX()).thenReturn(9F); - when(display.getY()).thenReturn(0F); when(advancement.getDisplay()).thenReturn(display); // Bukkit - PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS); - when(Bukkit.getAdvancement(any(NamespacedKey.class))).thenReturn(advancement); + mockedBukkit.when(() -> org.bukkit.Bukkit.getAdvancement(any(NamespacedKey.class))).thenReturn(advancement); // Island when(addon.getIslands()).thenReturn(im); @@ -146,28 +110,18 @@ public void setUp() throws Exception { when(island.getRank(uuid)).thenReturn(RanksManager.MEMBER_RANK); when(island.getProtectionRange()).thenReturn(5); - am = new AdvancementsManager(addon); } /** * @throws java.lang.Exception - exception */ - @After + @Override + @AfterEach public void tearDown() throws Exception { - deleteAll(new File("database")); + mockedDatabaseSetup.closeOnDemand(); deleteAll(dataFolder); - 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(); } /** @@ -176,7 +130,9 @@ private static void deleteAll(File file) throws IOException { */ @Test public void testAdvancementsManagerNoFile() throws Exception { - tearDown(); + // Delete the advancements.yml file so the constructor logs an error. Do NOT tear + // down the full mock infrastructure — we still need it for the second manager. + deleteAll(dataFolder); am = new AdvancementsManager(addon); verify(addon).logError("advancements.yml cannot be found!"); } @@ -312,12 +268,13 @@ public void testAddAdvancementPlayerAdvancement() { /** * Test method for {@link world.bentobox.boxed.AdvancementsManager#addAdvancement(org.bukkit.entity.Player, org.bukkit.advancement.Advancement)}. + * A null display means the advancement cannot be scored automatically. */ @Test public void testAddAdvancementPlayerAdvancementZeroScore() { - when(display.getX()).thenReturn(0F); + when(advancement.getDisplay()).thenReturn(null); assertEquals(0, am.addAdvancement(player, advancement)); - verify(island, never()).setProtectionRange(anyInt()); + verify(island, never()).setProtectionRange(org.mockito.ArgumentMatchers.anyInt()); } /** diff --git a/src/test/java/world/bentobox/boxed/CommonTestSetup.java b/src/test/java/world/bentobox/boxed/CommonTestSetup.java new file mode 100644 index 0000000..346f421 --- /dev/null +++ b/src/test/java/world/bentobox/boxed/CommonTestSetup.java @@ -0,0 +1,316 @@ +package world.bentobox.boxed; + +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.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 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; + + protected ServerMock server; + + protected MockedStatic mockedBukkit; + protected MockedStatic mockedUtil; + + protected AutoCloseable closeable; + + @Mock + protected BukkitScheduler sch; + @Mock + protected LocalesManager lm; + + @Mock + protected PlaceholdersManager phm; + + + @BeforeEach + public void setUp() throws Exception { + // 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); + + // Force Tag static fields (e.g. Tag.LEAVES) to be initialized NOW, while the + // real MockBukkit server is active, rather than later when mockedBukkit is in + // place. If Tag fields load under mockedBukkit (RETURNS_DEEP_STUBS), they + // become Mockito deep-stub mocks that stale across tests after clearInlineMocks(). + @SuppressWarnings("unused") + var unusedTagRef = org.bukkit.Tag.LEAVES; + + // Register the static mock + mockedBukkit = Mockito.mockStatic(Bukkit.class, Mockito.RETURNS_DEEP_STUBS); + mockedBukkit.when(Bukkit::getMinecraftVersion).thenReturn("1.21.11"); + mockedBukkit.when(Bukkit::getBukkitVersion).thenReturn(""); + mockedBukkit.when(Bukkit::getPluginManager).thenReturn(pim); + mockedBukkit.when(Bukkit::getItemFactory).thenReturn(itemFactory); + mockedBukkit.when(Bukkit::getServer).thenReturn(server); + // 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(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); + when(mockPlayer.getWorld()).thenReturn(world); + + 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("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(mockPlayer.getMetadata(anyString())).thenReturn(Collections.singletonList(mdv)); + + // 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); + + // Fake players + world.bentobox.bentobox.Settings settings = new world.bentobox.bentobox.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); + } + + /** + * @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 exploded 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/boxed/SettingsTest.java b/src/test/java/world/bentobox/boxed/SettingsTest.java index bfc2473..482ac69 100644 --- a/src/test/java/world/bentobox/boxed/SettingsTest.java +++ b/src/test/java/world/bentobox/boxed/SettingsTest.java @@ -1,8 +1,8 @@ package world.bentobox.boxed; -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.Mockito.verify; import java.util.Collections; @@ -12,34 +12,30 @@ import org.bukkit.Difficulty; import org.bukkit.entity.EntityType; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.reflect.Whitebox; - -import world.bentobox.bentobox.BentoBox; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; /** * @author tastybento * */ -@RunWith(PowerMockRunner.class) -@PrepareForTest({ BentoBox.class }) -public class SettingsTest { +public class SettingsTest extends CommonTestSetup { - @Mock - private BentoBox plugin; Settings s; - @Before - public void setUp() { - // Set up plugin - Whitebox.setInternalState(BentoBox.class, "instance", plugin); + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); s = new Settings(); } + + @Override + @AfterEach + public void tearDown() throws Exception { + super.tearDown(); + } /** * Test method for {@link world.bentobox.boxed.Settings#setFriendlyName(java.lang.String)}. */ diff --git a/src/test/java/world/bentobox/boxed/TestWorldSettings.java b/src/test/java/world/bentobox/boxed/TestWorldSettings.java new file mode 100644 index 0000000..04df7ee --- /dev/null +++ b/src/test/java/world/bentobox/boxed/TestWorldSettings.java @@ -0,0 +1,409 @@ +package world.bentobox.boxed; + +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) { + // Do nothing + + } + + @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/boxed/WhiteBox.java b/src/test/java/world/bentobox/boxed/WhiteBox.java new file mode 100644 index 0000000..20f46b9 --- /dev/null +++ b/src/test/java/world/bentobox/boxed/WhiteBox.java @@ -0,0 +1,26 @@ +package world.bentobox.boxed; + +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/boxed/listeners/EnderPearlListenerTest.java b/src/test/java/world/bentobox/boxed/listeners/EnderPearlListenerTest.java index 228def6..17555ae 100644 --- a/src/test/java/world/bentobox/boxed/listeners/EnderPearlListenerTest.java +++ b/src/test/java/world/bentobox/boxed/listeners/EnderPearlListenerTest.java @@ -1,8 +1,8 @@ package world.bentobox.boxed.listeners; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +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.anyFloat; import static org.mockito.ArgumentMatchers.anyString; @@ -18,7 +18,6 @@ import java.util.Optional; import java.util.UUID; -import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Sound; @@ -35,39 +34,27 @@ import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; import org.bukkit.util.BoundingBox; import org.bukkit.util.Vector; -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.mockito.MockedStatic; import org.mockito.Mockito; -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.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.util.Util; import world.bentobox.boxed.Boxed; +import world.bentobox.boxed.CommonTestSetup; import world.bentobox.boxed.Settings; -import world.bentobox.boxed.mocks.ServerMocks; /** * @author tastybento * */ -@RunWith(PowerMockRunner.class) -@PrepareForTest({Bukkit.class, BentoBox.class, User.class, Util.class }) -public class EnderPearlListenerTest { +public class EnderPearlListenerTest extends CommonTestSetup { - @Mock - private BentoBox plugin; @Mock private Boxed addon; @Mock @@ -77,12 +64,6 @@ public class EnderPearlListenerTest { @Mock private Location to; @Mock - private World world; - @Mock - private IslandsManager im; - @Mock - private Island island; - @Mock private Island anotherIsland; @Mock private Island spawn; @@ -92,56 +73,49 @@ public class EnderPearlListenerTest { private EnderPearl projectile; @Mock private Block hitBlock; - + private Settings settings; private EnderPearlListener epl; - @Mock - private IslandWorldManager iwm; + private MockedStatic mockedUser; - /** - * @throws java.lang.Exception - */ - @Before + @Override + @BeforeEach public void setUp() throws Exception { - ServerMocks.newServer(); - // Set up plugin - plugin = mock(BentoBox.class); - Whitebox.setInternalState(BentoBox.class, "instance", plugin); - + super.setUp(); + + // Local static mock for User (parent already wired User.setPlugin + one cached mockPlayer) + mockedUser = Mockito.mockStatic(User.class, Mockito.CALLS_REAL_METHODS); + mockedUser.when(() -> User.getInstance(any(Player.class))).thenReturn(user); + when(plugin.getIWM()).thenReturn(iwm); - + when(plugin.getFlagsManager()).thenReturn(fm); - - PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS); - - PowerMockito.mockStatic(User.class, Mockito.RETURNS_MOCKS); - when(User.getInstance(any(Player.class))).thenReturn(user); // Settings settings = new Settings(); when(addon.getSettings()).thenReturn(settings); when(iwm.getWorldSettings(world)).thenReturn(settings); when(iwm.inWorld(world)).thenReturn(true); - + // Locations when(to.getWorld()).thenReturn(world); when(from.getWorld()).thenReturn(world); - when(from.toVector()).thenReturn(new Vector(1,2,3)); - when(to.toVector()).thenReturn(new Vector(6,7,8)); + when(from.toVector()).thenReturn(new Vector(1, 2, 3)); + when(to.toVector()).thenReturn(new Vector(6, 7, 8)); when(world.getEnvironment()).thenReturn(Environment.NORMAL); - + // In game world when(addon.inWorld(any(World.class))).thenReturn(true); when(addon.inWorld(any(Location.class))).thenReturn(true); - + // User when(user.getPlayer()).thenReturn(player); when(player.getGameMode()).thenReturn(GameMode.SURVIVAL); when(player.getWorld()).thenReturn(world); - when(user.getMetaData(anyString())).thenReturn(Optional.empty()); // No meta data + when(user.getMetaData(anyString())).thenReturn(Optional.empty()); when(player.getLocation()).thenReturn(from); when(user.getLocation()).thenReturn(from); - + // Islands when(island.onIsland(any())).thenReturn(true); // Default on island when(im.getIsland(world, user)).thenReturn(island); @@ -150,7 +124,8 @@ public void setUp() throws Exception { when(im.getProtectedIslandAt(any())).thenReturn(Optional.of(island)); when(island.getUniqueId()).thenReturn("uniqueID"); when(island.getProtectionCenter()).thenReturn(from); - when(island.getProtectionBoundingBox()).thenReturn(BoundingBox.of(new Vector(0,0,0), new Vector(50,50,50))); + when(island.getProtectionBoundingBox()) + .thenReturn(BoundingBox.of(new Vector(0, 0, 0), new Vector(50, 50, 50))); when(island.getRange()).thenReturn(3); when(im.isSafeLocation(any())).thenReturn(true); // safe for now when(island.getPlayersOnIsland()).thenReturn(List.of(player)); @@ -160,7 +135,7 @@ public void setUp() throws Exception { when(island.getMemberSet()).thenReturn(ImmutableSet.of(UUID.randomUUID())); // Another island when(anotherIsland.getUniqueId()).thenReturn("another_uniqueID"); - + // Projectiles when(projectile.getType()).thenReturn(EntityType.ENDER_PEARL); when(projectile.getShooter()).thenReturn(player); @@ -168,15 +143,15 @@ public void setUp() throws Exception { when(hitBlock.getWorld()).thenReturn(world); when(hitBlock.getRelative(BlockFace.UP)).thenReturn(hitBlock); Boxed.ALLOW_MOVE_BOX.setSetting(world, true); - + epl = new EnderPearlListener(addon); } - @After - public void tearDown() { - ServerMocks.unsetBukkitServer(); - User.clearUsers(); - Mockito.framework().clearInlineMocks(); + @Override + @AfterEach + public void tearDown() throws Exception { + mockedUser.closeOnDemand(); + super.tearDown(); } /** @@ -197,7 +172,7 @@ public void testOnPlayerTeleportNotAllowed() { assertTrue(e.isCancelled()); verify(user).sendMessage("boxed.general.errors.no-teleport-outside"); } - + /** * Test method for {@link world.bentobox.boxed.listeners.EnderPearlListener#onPlayerTeleport(org.bukkit.event.player.PlayerTeleportEvent)}. */ @@ -209,7 +184,7 @@ public void testOnPlayerTeleportNotSurvival() { assertFalse(e.isCancelled()); verify(user, never()).sendMessage("boxed.general.errors.no-teleport-outside"); } - + /** * Test method for {@link world.bentobox.boxed.listeners.EnderPearlListener#onPlayerTeleport(org.bukkit.event.player.PlayerTeleportEvent)}. */ @@ -221,7 +196,7 @@ public void testOnPlayerTeleportNullTo() { assertFalse(e.isCancelled()); verify(user, never()).sendMessage("boxed.general.errors.no-teleport-outside"); } - + /** * Test method for {@link world.bentobox.boxed.listeners.EnderPearlListener#onPlayerTeleport(org.bukkit.event.player.PlayerTeleportEvent)}. */ @@ -233,7 +208,7 @@ public void testOnPlayerTeleportToSpawn() { assertFalse(e.isCancelled()); verify(user, never()).sendMessage("boxed.general.errors.no-teleport-outside"); } - + /** * Test method for {@link world.bentobox.boxed.listeners.EnderPearlListener#onPlayerTeleport(org.bukkit.event.player.PlayerTeleportEvent)}. */ @@ -249,7 +224,7 @@ public void testOnPlayerTeleportNotInWorldAllowed() { /** * Test method for {@link world.bentobox.boxed.listeners.EnderPearlListener#onEnderPearlLand(org.bukkit.event.entity.ProjectileHitEvent)}. - * @throws IOException + * @throws IOException */ @Test public void testOnEnderPearlLandNotEnderPearl() throws IOException { @@ -260,10 +235,10 @@ public void testOnEnderPearlLandNotEnderPearl() throws IOException { verify(user, never()).sendMessage("boxed.general.errors.no-teleport-outside"); verifyFailure(); } - + /** * Test method for {@link world.bentobox.boxed.listeners.EnderPearlListener#onEnderPearlLand(org.bukkit.event.entity.ProjectileHitEvent)}. - * @throws IOException + * @throws IOException */ @Test public void testOnEnderPearlLandNullHitBlock() throws IOException { @@ -273,10 +248,10 @@ public void testOnEnderPearlLandNullHitBlock() throws IOException { verify(user, never()).sendMessage("boxed.general.errors.no-teleport-outside"); verifyFailure(); } - + /** * Test method for {@link world.bentobox.boxed.listeners.EnderPearlListener#onEnderPearlLand(org.bukkit.event.entity.ProjectileHitEvent)}. - * @throws IOException + * @throws IOException */ @Test public void testOnEnderPearlLandNotInWorld() throws IOException { @@ -290,7 +265,7 @@ public void testOnEnderPearlLandNotInWorld() throws IOException { /** * Test method for {@link world.bentobox.boxed.listeners.EnderPearlListener#onEnderPearlLand(org.bukkit.event.entity.ProjectileHitEvent)}. - * @throws IOException + * @throws IOException */ @Test public void testOnEnderPearlLandNotMovingBox() throws IOException { @@ -301,10 +276,10 @@ public void testOnEnderPearlLandNotMovingBox() throws IOException { verify(user, never()).sendMessage("boxed.general.errors.no-teleport-outside"); verifyFailure(); } - + /** * Test method for {@link world.bentobox.boxed.listeners.EnderPearlListener#onEnderPearlLand(org.bukkit.event.entity.ProjectileHitEvent)}. - * @throws IOException + * @throws IOException */ @Test public void testOnEnderPearlLandNonHuman() throws IOException { @@ -316,10 +291,10 @@ public void testOnEnderPearlLandNonHuman() throws IOException { verify(user, never()).sendMessage("boxed.general.errors.no-teleport-outside"); verifyFailure(); } - + /** * Test method for {@link world.bentobox.boxed.listeners.EnderPearlListener#onEnderPearlLand(org.bukkit.event.entity.ProjectileHitEvent)}. - * @throws IOException + * @throws IOException */ @Test public void testOnEnderPearlLandUserHasNoIsland() throws IOException { @@ -330,10 +305,10 @@ public void testOnEnderPearlLandUserHasNoIsland() throws IOException { verify(user, never()).sendMessage("boxed.general.errors.no-teleport-outside"); verifyFailure(); } - + /** * Test method for {@link world.bentobox.boxed.listeners.EnderPearlListener#onEnderPearlLand(org.bukkit.event.entity.ProjectileHitEvent)}. - * @throws IOException + * @throws IOException */ @Test public void testOnEnderPearlNotOnIslandWhenThrowing() throws IOException { @@ -343,7 +318,7 @@ public void testOnEnderPearlNotOnIslandWhenThrowing() throws IOException { assertFalse(e.isCancelled()); verifyFailure(); } - + private void verifyFailure() throws IOException { verify(user, never()).sendMessage("boxed.general.errors.no-teleport-outside"); verify(im, never()).setHomeLocation(any(UUID.class), any()); @@ -351,10 +326,10 @@ private void verifyFailure() throws IOException { verify(island, never()).setSpawnPoint(any(), any()); verify(player, never()).playSound(any(Location.class), any(Sound.class), anyFloat(), anyFloat()); } - + /** * Test method for {@link world.bentobox.boxed.listeners.EnderPearlListener#onEnderPearlLand(org.bukkit.event.entity.ProjectileHitEvent)}. - * @throws IOException + * @throws IOException */ @Test public void testOnEnderPearlLandHuman() throws IOException { @@ -367,10 +342,10 @@ public void testOnEnderPearlLandHuman() throws IOException { verify(island).setSpawnPoint(Environment.NORMAL, to); verify(player).playSound(to, Sound.ENTITY_GENERIC_EXPLODE, 2F, 2F); } - + /** * Test method for {@link world.bentobox.boxed.listeners.EnderPearlListener#onEnderPearlLand(org.bukkit.event.entity.ProjectileHitEvent)}. - * @throws IOException + * @throws IOException */ @Test public void testOnEnderPearlThrewToDifferentIsland() throws IOException { @@ -380,10 +355,10 @@ public void testOnEnderPearlThrewToDifferentIsland() throws IOException { assertTrue(e.isCancelled()); verify(user).sendMessage("boxed.general.errors.no-teleport-outside"); } - + /** * Test method for {@link world.bentobox.boxed.listeners.EnderPearlListener#onEnderPearlLand(org.bukkit.event.entity.ProjectileHitEvent)}. - * @throws IOException + * @throws IOException */ @Test public void testOnEnderPearlThrewToNonIsland() throws IOException { @@ -393,10 +368,10 @@ public void testOnEnderPearlThrewToNonIsland() throws IOException { assertTrue(e.isCancelled()); verify(user).sendMessage("boxed.general.errors.no-teleport-outside"); } - + /** * Test method for {@link world.bentobox.boxed.listeners.EnderPearlListener#onEnderPearlLand(org.bukkit.event.entity.ProjectileHitEvent)}. - * @throws IOException + * @throws IOException */ @Test public void testOnEnderPearlCannotSetProtectionCenter() throws IOException { diff --git a/src/test/java/world/bentobox/boxed/mocks/ServerMocks.java b/src/test/java/world/bentobox/boxed/mocks/ServerMocks.java deleted file mode 100644 index 9533491..0000000 --- a/src/test/java/world/bentobox/boxed/mocks/ServerMocks.java +++ /dev/null @@ -1,119 +0,0 @@ -package world.bentobox.boxed.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 { - - @SuppressWarnings({ "deprecation", "unchecked" }) - 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((NamespacedKey) 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 8b3665d4ca0cd9d10b0e69055ef3cf7da93c6660 Mon Sep 17 00:00:00 2001 From: tastybento Date: Thu, 9 Apr 2026 16:36:08 -0700 Subject: [PATCH 06/13] Expand test coverage with new listener and placeholder tests Adds PlaceholdersManagerTest (8 tests) and AdvancementListenerTest (25 tests) for previously untested classes, plus 6 branch-coverage tests in AdvancementsManagerTest for getScore, addAdvancement, and checkIslandSize edge cases. Full suite: 112 tests passing. Co-Authored-By: Claude Opus 4.6 --- .../boxed/AdvancementsManagerTest.java | 65 +++ .../boxed/PlaceholdersManagerTest.java | 108 +++++ .../listeners/AdvancementListenerTest.java | 370 ++++++++++++++++++ 3 files changed, 543 insertions(+) create mode 100644 src/test/java/world/bentobox/boxed/PlaceholdersManagerTest.java create mode 100644 src/test/java/world/bentobox/boxed/listeners/AdvancementListenerTest.java diff --git a/src/test/java/world/bentobox/boxed/AdvancementsManagerTest.java b/src/test/java/world/bentobox/boxed/AdvancementsManagerTest.java index 83394f9..6bc561a 100644 --- a/src/test/java/world/bentobox/boxed/AdvancementsManagerTest.java +++ b/src/test/java/world/bentobox/boxed/AdvancementsManagerTest.java @@ -293,4 +293,69 @@ public void testGetScoreAdvancement() { assertEquals(9, am.getScore(advancement)); } + /** + * Test method for {@link world.bentobox.boxed.AdvancementsManager#getScore(org.bukkit.advancement.Advancement)}. + * Root advancements fall back to settings.default-root-increase (0 in the shipped config). + */ + @Test + public void testGetScoreAdvancementRoot() { + when(advancement.getKey()).thenReturn(NamespacedKey.fromString("story/root")); + assertEquals(0, am.getScore(advancement)); + } + + /** + * Test method for {@link world.bentobox.boxed.AdvancementsManager#getScore(org.bukkit.advancement.Advancement)}. + * Recipe advancements always score settings.unknown-recipe-increase (0 in the shipped config). + */ + @Test + public void testGetScoreAdvancementRecipe() { + when(advancement.getKey()).thenReturn(NamespacedKey.fromString("recipes/brewing/blaze_powder")); + assertEquals(0, am.getScore(advancement)); + } + + /** + * Test method for {@link world.bentobox.boxed.AdvancementsManager#addAdvancement(org.bukkit.entity.Player, org.bukkit.advancement.Advancement)}. + * No island for this player means no expansion and a zero score. + */ + @Test + public void testAddAdvancementPlayerAdvancementNullIsland() { + when(im.getIsland(world, player.getUniqueId())).thenReturn(null); + assertEquals(0, am.addAdvancement(player, advancement)); + verify(island, never()).setProtectionRange(org.mockito.ArgumentMatchers.anyInt()); + } + + /** + * Test method for {@link world.bentobox.boxed.AdvancementsManager#addAdvancement(org.bukkit.entity.Player, org.bukkit.advancement.Advancement)}. + * Visitors (rank below MEMBER_RANK) cannot expand the island. + */ + @Test + public void testAddAdvancementPlayerAdvancementVisitorRank() { + when(island.getRank(player.getUniqueId())).thenReturn(RanksManager.VISITOR_RANK); + assertEquals(0, am.addAdvancement(player, advancement)); + verify(island, never()).setProtectionRange(org.mockito.ArgumentMatchers.anyInt()); + } + + /** + * Test method for {@link world.bentobox.boxed.AdvancementsManager#addAdvancement(org.bukkit.entity.Player, org.bukkit.advancement.Advancement)}. + * An advancement that's already been recorded on the island cannot grant a second expansion. + */ + @Test + public void testAddAdvancementPlayerAdvancementAlreadyHas() { + // Seed the island with the exact same namespaced key the manager will try to record. + am.addAdvancement(island, advancement.getKey().toString()); + assertEquals(0, am.addAdvancement(player, advancement)); + } + + /** + * Test method for {@link world.bentobox.boxed.AdvancementsManager#checkIslandSize(world.bentobox.bentobox.database.objects.Island)}. + * Positive diff case: one scoring advancement grows a size-1 island to size 10. + */ + @Test + public void testCheckIslandSizePositiveDiff() { + when(island.getProtectionRange()).thenReturn(1); + am.addAdvancement(island, "adventure/honey_block_slide"); + assertEquals(9, am.checkIslandSize(island)); + verify(island).setProtectionRange(10); + } + } diff --git a/src/test/java/world/bentobox/boxed/PlaceholdersManagerTest.java b/src/test/java/world/bentobox/boxed/PlaceholdersManagerTest.java new file mode 100644 index 0000000..e6815b3 --- /dev/null +++ b/src/test/java/world/bentobox/boxed/PlaceholdersManagerTest.java @@ -0,0 +1,108 @@ +package world.bentobox.boxed; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.bukkit.Location; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import world.bentobox.bentobox.api.user.User; +import world.bentobox.boxed.objects.IslandAdvancements; + +/** + * @author tastybento + */ +public class PlaceholdersManagerTest extends CommonTestSetup { + + @Mock + private Boxed addon; + @Mock + private AdvancementsManager advManager; + @Mock + private IslandAdvancements islandAdv; + @Mock + private User user; + @Mock + private Location userLocation; + + private PlaceholdersManager phm; + private UUID uuid; + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + uuid = UUID.randomUUID(); + when(user.getUniqueId()).thenReturn(uuid); + when(user.getLocation()).thenReturn(userLocation); + + when(addon.getIslands()).thenReturn(im); + when(addon.getOverWorld()).thenReturn(world); + when(addon.getAdvManager()).thenReturn(advManager); + + when(advManager.getIsland(island)).thenReturn(islandAdv); + when(islandAdv.getAdvancements()).thenReturn(List.of("a", "b", "c")); + + phm = new PlaceholdersManager(addon); + } + + @Override + @AfterEach + public void tearDown() throws Exception { + super.tearDown(); + } + + @Test + public void testGetCountNullUser() { + assertEquals("", phm.getCount(null)); + } + + @Test + public void testGetCountNullUuid() { + when(user.getUniqueId()).thenReturn(null); + assertEquals("", phm.getCount(user)); + } + + @Test + public void testGetCountNoIsland() { + when(im.getIsland(world, user)).thenReturn(null); + assertEquals("", phm.getCount(user)); + } + + @Test + public void testGetCountReturnsAdvancementCount() { + when(im.getIsland(world, user)).thenReturn(island); + assertEquals("3", phm.getCount(user)); + } + + @Test + public void testGetCountByLocationNullUser() { + assertEquals("", phm.getCountByLocation(null)); + } + + @Test + public void testGetCountByLocationNullLocation() { + when(user.getLocation()).thenReturn(null); + assertEquals("", phm.getCountByLocation(user)); + } + + @Test + public void testGetCountByLocationNoIslandAtLocation() { + when(im.getIslandAt(userLocation)).thenReturn(Optional.empty()); + assertEquals("", phm.getCountByLocation(user)); + } + + @Test + public void testGetCountByLocationReturnsAdvancementCount() { + when(im.getIslandAt(userLocation)).thenReturn(Optional.of(island)); + assertEquals("3", phm.getCountByLocation(user)); + } +} diff --git a/src/test/java/world/bentobox/boxed/listeners/AdvancementListenerTest.java b/src/test/java/world/bentobox/boxed/listeners/AdvancementListenerTest.java new file mode 100644 index 0000000..aef32d3 --- /dev/null +++ b/src/test/java/world/bentobox/boxed/listeners/AdvancementListenerTest.java @@ -0,0 +1,370 @@ +package world.bentobox.boxed.listeners; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.Sound; +import org.bukkit.World; +import org.bukkit.advancement.Advancement; +import org.bukkit.advancement.AdvancementProgress; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerAdvancementDoneEvent; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerPortalEvent; +import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; +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.events.island.IslandNewIslandEvent; +import world.bentobox.bentobox.api.events.team.TeamJoinedEvent; +import world.bentobox.bentobox.api.events.team.TeamLeaveEvent; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.boxed.AdvancementsManager; +import world.bentobox.boxed.Boxed; +import world.bentobox.boxed.CommonTestSetup; +import world.bentobox.boxed.Settings; + +/** + * @author tastybento + */ +public class AdvancementListenerTest extends CommonTestSetup { + + @Mock + private Boxed addon; + @Mock + private AdvancementsManager advManager; + @Mock + private Player player; + @Mock + private Advancement advancement; + @Mock + private AdvancementProgress progress; + @Mock + private User user; + @Mock + private Location playerLocation; + + private Settings settings; + private UUID playerUuid; + + private MockedStatic mockedUser; + + private AdvancementListener listener; + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + // Local User mock (parent only cached mockPlayer, not our local player mock) + mockedUser = Mockito.mockStatic(User.class, Mockito.CALLS_REAL_METHODS); + mockedUser.when(() -> User.getInstance(any(Player.class))).thenReturn(user); + mockedUser.when(() -> User.getInstance(any(UUID.class))).thenReturn(user); + + // Empty advancement iterator so the constructor's 5 lookups all return null + mockedBukkit.when(Bukkit::advancementIterator).thenAnswer(inv -> Collections.emptyIterator()); + + // Settings — real instance, tweak per test + settings = new Settings(); + + // Addon wiring + when(addon.getPlugin()).thenReturn(plugin); + when(addon.getSettings()).thenReturn(settings); + when(addon.getIslands()).thenReturn(im); + when(addon.getOverWorld()).thenReturn(world); + when(addon.getAdvManager()).thenReturn(advManager); + when(addon.inWorld(any(World.class))).thenReturn(true); + when(addon.inWorld(any(Location.class))).thenReturn(true); + + // Player + playerUuid = UUID.randomUUID(); + when(player.getUniqueId()).thenReturn(playerUuid); + when(player.getName()).thenReturn("tester"); + when(player.getWorld()).thenReturn(world); + when(player.getGameMode()).thenReturn(GameMode.SURVIVAL); + when(player.getLocation()).thenReturn(playerLocation); + when(player.getAdvancementProgress(any(Advancement.class))).thenReturn(progress); + + // User + when(user.getPlayer()).thenReturn(player); + when(user.getName()).thenReturn("tester"); + when(user.getUniqueId()).thenReturn(playerUuid); + when(user.getWorld()).thenReturn(world); + when(user.getLocation()).thenReturn(playerLocation); + when(user.isOnline()).thenReturn(true); + when(user.getTranslationOrNothing(anyString())).thenReturn(""); + + // Advancement + NamespacedKey key = NamespacedKey.fromString("minecraft:adventure/honey_block_slide"); + when(advancement.getKey()).thenReturn(key); + when(advancement.getCriteria()).thenReturn(Set.of("crit1", "crit2")); + when(progress.isDone()).thenReturn(false); + when(progress.getAwardedCriteria()).thenReturn(Set.of("crit1")); + + // Islands + when(im.getIsland(world, user)).thenReturn(island); + when(im.getIslandAt(any(Location.class))).thenReturn(Optional.of(island)); + when(island.getMemberSet()).thenReturn(com.google.common.collect.ImmutableSet.of(playerUuid)); + when(advManager.getScore(anyString())).thenReturn(9); + when(advManager.addAdvancement(any(Player.class), any(Advancement.class))).thenReturn(9); + + listener = new AdvancementListener(addon); + } + + @Override + @AfterEach + public void tearDown() throws Exception { + mockedUser.closeOnDemand(); + super.tearDown(); + } + + // ---------- constructor ---------- + + @Test + public void testConstructor() { + assertNotNull(listener); + } + + // ---------- onAdvancement ---------- + + private PlayerAdvancementDoneEvent advancementDoneEvent() { + PlayerAdvancementDoneEvent e = mock(PlayerAdvancementDoneEvent.class); + when(e.getPlayer()).thenReturn(player); + when(e.getAdvancement()).thenReturn(advancement); + return e; + } + + @Test + public void testOnAdvancementNotSurvival() { + when(player.getGameMode()).thenReturn(GameMode.CREATIVE); + listener.onAdvancement(advancementDoneEvent()); + verify(advManager, never()).addAdvancement(any(Player.class), any(Advancement.class)); + } + + @Test + public void testOnAdvancementIgnoreSetting() { + settings.setIgnoreAdvancements(true); + listener.onAdvancement(advancementDoneEvent()); + verify(advManager, never()).addAdvancement(any(Player.class), any(Advancement.class)); + } + + @Test + public void testOnAdvancementNotInWorld() { + when(addon.inWorld(world)).thenReturn(false); + listener.onAdvancement(advancementDoneEvent()); + verify(advManager, never()).addAdvancement(any(Player.class), any(Advancement.class)); + } + + @Test + public void testOnAdvancementVisitorDenied() { + settings.setDenyVisitorAdvancements(true); + // player is NOT in the member set + when(island.getMemberSet()).thenReturn(com.google.common.collect.ImmutableSet.of(UUID.randomUUID())); + listener.onAdvancement(advancementDoneEvent()); + // Criteria should be revoked + verify(progress).revokeCriteria("crit1"); + verify(progress).revokeCriteria("crit2"); + // And advManager should NOT have awarded + verify(advManager, never()).addAdvancement(any(Player.class), any(Advancement.class)); + // Notify fired (score > 0) + verify(user).notify(eq("boxed.adv-disallowed"), anyString(), anyString(), anyString(), anyString()); + } + + @Test + public void testOnAdvancementMemberGrantsAndSchedulesTellTeam() { + listener.onAdvancement(advancementDoneEvent()); + verify(advManager).addAdvancement(eq(player), eq(advancement)); + // tellTeam is scheduled one tick later + verify(sch).runTask(eq(plugin), any(Runnable.class)); + } + + @Test + public void testOnAdvancementZeroScoreDoesNotSchedule() { + when(advManager.addAdvancement(any(Player.class), any(Advancement.class))).thenReturn(0); + listener.onAdvancement(advancementDoneEvent()); + verify(sch, never()).runTask(eq(plugin), any(Runnable.class)); + } + + // ---------- onPortal ---------- + + private PlayerPortalEvent portalEvent(TeleportCause cause) { + PlayerPortalEvent e = mock(PlayerPortalEvent.class); + when(e.getPlayer()).thenReturn(player); + when(e.getCause()).thenReturn(cause); + return e; + } + + @Test + public void testOnPortalNetherNoException() { + // With null netherAdvancement fields, giveAdv short-circuits — we just assert no throw. + listener.onPortal(portalEvent(TeleportCause.NETHER_PORTAL)); + } + + @Test + public void testOnPortalEndNoException() { + listener.onPortal(portalEvent(TeleportCause.END_PORTAL)); + } + + @Test + public void testOnPortalNotSurvival() { + when(player.getGameMode()).thenReturn(GameMode.CREATIVE); + // should early return — no NPE even though we don't stub the cause + listener.onPortal(portalEvent(TeleportCause.NETHER_PORTAL)); + } + + @Test + public void testOnPortalNotInWorld() { + when(addon.inWorld(world)).thenReturn(false); + listener.onPortal(portalEvent(TeleportCause.NETHER_PORTAL)); + } + + // ---------- syncAdvancements ---------- + + @Test + public void testSyncAdvancementsIgnoreSetting() { + settings.setIgnoreAdvancements(true); + listener.syncAdvancements(user); + verify(user, never()).sendMessage(anyString(), any()); + } + + @Test + public void testSyncAdvancementsNoIsland() { + when(im.getIsland(world, user)).thenReturn(null); + listener.syncAdvancements(user); + verify(user, never()).sendMessage(anyString(), any()); + } + + @Test + public void testSyncAdvancementsSizeIncreased() { + when(advManager.checkIslandSize(island)).thenReturn(3); + // Return a non-null IslandAdvancements stub for grantAdv's iteration + when(advManager.getIsland(island)) + .thenReturn(mock(world.bentobox.boxed.objects.IslandAdvancements.class)); + listener.syncAdvancements(user); + verify(user).sendMessage(eq("boxed.size-changed"), anyString(), eq("3")); + verify(player).playSound(eq(playerLocation), eq(Sound.ENTITY_PLAYER_LEVELUP), org.mockito.ArgumentMatchers.anyFloat(), org.mockito.ArgumentMatchers.anyFloat()); + } + + @Test + public void testSyncAdvancementsSizeDecreased() { + when(advManager.checkIslandSize(island)).thenReturn(-2); + when(advManager.getIsland(island)) + .thenReturn(mock(world.bentobox.boxed.objects.IslandAdvancements.class)); + listener.syncAdvancements(user); + verify(user).sendMessage(eq("boxed.size-decreased"), anyString(), eq("2")); + } + + // ---------- onPlayerJoin / onPlayerEnterWorld ---------- + + @Test + public void testOnPlayerJoinNotInWorld() { + when(addon.inWorld(world)).thenReturn(false); + PlayerJoinEvent e = mock(PlayerJoinEvent.class); + when(e.getPlayer()).thenReturn(player); + listener.onPlayerJoin(e); + // syncAdvancements not called → no island lookup + verify(im, never()).getIsland(world, user); + } + + @Test + public void testOnPlayerEnterWorldDifferentWorld() { + World otherWorld = mock(World.class); + when(otherWorld.getName()).thenReturn("other_world"); + when(world.getName()).thenReturn("boxed_world"); + when(player.getWorld()).thenReturn(otherWorld); + PlayerChangedWorldEvent e = mock(PlayerChangedWorldEvent.class); + when(e.getPlayer()).thenReturn(player); + // Util.getWorld(otherWorld) returns a mocked world (parent stubs Util.getWorld(any()) + // to return a fresh mock World), so it won't equal addon.getOverWorld() + listener.onPlayerEnterWorld(e); + verify(im, never()).getIsland(world, user); + } + + // ---------- onTeamJoinTime / onTeamLeaveTime / onFirstTime ---------- + + @Test + public void testOnTeamJoinTimeSettingDisabled() { + // isOnJoinResetAdvancements defaults to false + TeamJoinedEvent e = mock(TeamJoinedEvent.class); + when(e.getPlayerUUID()).thenReturn(playerUuid); + listener.onTeamJoinTime(e); + verify(im, never()).getIsland(world, user); + } + + @Test + public void testOnTeamLeaveTimeIgnoreAdvancements() { + settings.setIgnoreAdvancements(true); + TeamLeaveEvent e = mock(TeamLeaveEvent.class); + listener.onTeamLeaveTime(e); + // Short-circuit before User lookup + verify(e, never()).getPlayerUUID(); + } + + @Test + public void testOnFirstTimeIgnoreAdvancements() { + settings.setIgnoreAdvancements(true); + IslandNewIslandEvent e = mock(IslandNewIslandEvent.class); + listener.onFirstTime(e); + verify(e, never()).getPlayerUUID(); + } + + // ---------- static helpers ---------- + + @Test + public void testGetAdvancementNotFound() { + assertNull(AdvancementListener.getAdvancement("minecraft:story/nonexistent")); + } + + @Test + public void testGetAdvancementFound() { + Advancement a = mock(Advancement.class); + when(a.getKey()).thenReturn(NamespacedKey.fromString("minecraft:story/root")); + mockedBukkit.when(Bukkit::advancementIterator).thenAnswer(inv -> List.of(a).iterator()); + assertEquals(a, AdvancementListener.getAdvancement("minecraft:story/root")); + } + + @Test + public void testGiveAdvNullAdvancement() { + // No throw, no interaction + AdvancementListener.giveAdv(player, null); + verify(player, never()).getAdvancementProgress(any(Advancement.class)); + } + + @Test + public void testGiveAdvNotDoneAwardsCriteria() { + AdvancementListener.giveAdv(player, advancement); + verify(progress).awardCriteria("crit1"); + verify(progress).awardCriteria("crit2"); + } + + @Test + public void testGiveAdvAlreadyDoneNoOp() { + when(progress.isDone()).thenReturn(true); + AdvancementListener.giveAdv(player, advancement); + verify(progress, never()).awardCriteria(anyString()); + } +} From aa1cb9b081b9e65501a92fe335fa889484bed93e Mon Sep 17 00:00:00 2001 From: tastybento Date: Thu, 9 Apr 2026 16:55:32 -0700 Subject: [PATCH 07/13] Cancel pending structure pastes when island is deleted Pending structures in the active build queue continued to be pasted after a box was reset or deleted. Drain itemsToBuild on IslandDeleteEvent to match the existing cleanup of the pending map and persistent DB queue. Bump version to 3.4.0. Co-Authored-By: Claude Opus 4.6 --- pom.xml | 2 +- .../java/world/bentobox/boxed/listeners/NewAreaListener.java | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e40f006..1c3e530 100644 --- a/pom.xml +++ b/pom.xml @@ -63,7 +63,7 @@ -LOCAL - 3.3.0 + 3.4.0 BentoBoxWorld_Boxed bentobox-world diff --git a/src/main/java/world/bentobox/boxed/listeners/NewAreaListener.java b/src/main/java/world/bentobox/boxed/listeners/NewAreaListener.java index 57cfee5..a5d306b 100644 --- a/src/main/java/world/bentobox/boxed/listeners/NewAreaListener.java +++ b/src/main/java/world/bentobox/boxed/listeners/NewAreaListener.java @@ -361,6 +361,9 @@ public void onIslandDeleted(IslandDeleteEvent event) { // Remove from in-memory cache islandStructureCache.remove(deletedIslandId); + // Remove from active build queue so we don't paste into a deleted island + itemsToBuild.removeIf(record -> event.getIsland().inIslandSpace(record.location())); + // Remove from in-memory pending structures for (List records : pending.values()) { records.removeIf(record -> event.getIsland().inIslandSpace(record.location())); From 4395d01d386dc113ece855253e11b762cb06eb15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 23:04:51 +0000 Subject: [PATCH 08/13] Guard advancement reset handling to Boxed islands --- .../boxed/listeners/AdvancementListener.java | 3 +++ .../boxed/listeners/AdvancementListenerTest.java | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/main/java/world/bentobox/boxed/listeners/AdvancementListener.java b/src/main/java/world/bentobox/boxed/listeners/AdvancementListener.java index dcdf779..930c9d0 100644 --- a/src/main/java/world/bentobox/boxed/listeners/AdvancementListener.java +++ b/src/main/java/world/bentobox/boxed/listeners/AdvancementListener.java @@ -285,6 +285,9 @@ public void onTeamLeaveTime(TeamLeaveEvent e) { @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onFirstTime(IslandNewIslandEvent e) { if (addon.getSettings().isIgnoreAdvancements()) return; + if (!addon.inWorld(e.getIsland().getWorld())) { + return; + } User user = User.getInstance(e.getPlayerUUID()); if (user != null) { clearAndSetAdv(user, addon.getSettings().isOnJoinResetAdvancements(), addon.getSettings().getOnJoinGrantAdvancements()); diff --git a/src/test/java/world/bentobox/boxed/listeners/AdvancementListenerTest.java b/src/test/java/world/bentobox/boxed/listeners/AdvancementListenerTest.java index aef32d3..5918ef6 100644 --- a/src/test/java/world/bentobox/boxed/listeners/AdvancementListenerTest.java +++ b/src/test/java/world/bentobox/boxed/listeners/AdvancementListenerTest.java @@ -332,6 +332,18 @@ public void testOnFirstTimeIgnoreAdvancements() { verify(e, never()).getPlayerUUID(); } + @Test + public void testOnFirstTimeNotInBoxedWorld() { + IslandNewIslandEvent e = mock(IslandNewIslandEvent.class); + when(e.getIsland()).thenReturn(island); + when(island.getWorld()).thenReturn(world); + when(addon.inWorld(world)).thenReturn(false); + + listener.onFirstTime(e); + + verify(e, never()).getPlayerUUID(); + } + // ---------- static helpers ---------- @Test From f08440aa7a89c56e9b66d88a0e39a30fec416a7b Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 29 May 2026 16:18:49 -0700 Subject: [PATCH 09/13] Pin MockBukkit to Maven Central 4.110.0 instead of jitpack snapshot The floating jitpack snapshot v1.21-SNAPSHOT resolves to an ephemeral git-described build whose jar/POM get evicted, breaking CI even on unchanged commits. Switch to the equivalent stable Maven Central coordinates (org.mockbukkit.mockbukkit:mockbukkit-v1.21:4.110.0). Co-Authored-By: Claude Opus 4.8 --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 1c3e530..7ed1ba8 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,7 @@ 5.10.2 5.11.0 - v1.21-SNAPSHOT + 4.110.0 1.21.11-R0.1-SNAPSHOT 3.13.0 @@ -164,8 +164,8 @@ - com.github.MockBukkit - MockBukkit + org.mockbukkit.mockbukkit + mockbukkit-v1.21 ${mock-bukkit.version} test From 0ba05da941627df724d9c28671d541956b8f6d81 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 29 May 2026 17:19:19 -0700 Subject: [PATCH 10/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../boxed/generators/chunks/BoxedBlockPopulator.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java b/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java index 2795869..7a2217b 100644 --- a/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java +++ b/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java @@ -143,9 +143,11 @@ public void setSpawner(CreatureSpawner spawner, BlueprintCreatureSpawner s) { * @param bts - blueprint trial spawner */ public void setTrialSpawner(TrialSpawner trialSpawner, BlueprintTrialSpawner bts) { - TrialSpawnerConfiguration config = trialSpawner.getNormalConfiguration(); - trialSpawner.setOminous(bts.configTrialSpawner(config)); + boolean ominous = bts.configTrialSpawner(trialSpawner.getNormalConfiguration()); + bts.configTrialSpawner(trialSpawner.getOminousConfiguration()); + trialSpawner.setOminous(ominous); trialSpawner.update(true, false); } + } } From 1498ebc2759a95a7b28fc5467650cab6f50f43ff Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 29 May 2026 17:19:37 -0700 Subject: [PATCH 11/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/test/java/world/bentobox/boxed/CommonTestSetup.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/java/world/bentobox/boxed/CommonTestSetup.java b/src/test/java/world/bentobox/boxed/CommonTestSetup.java index 346f421..cb8b821 100644 --- a/src/test/java/world/bentobox/boxed/CommonTestSetup.java +++ b/src/test/java/world/bentobox/boxed/CommonTestSetup.java @@ -260,10 +260,13 @@ public void tearDown() throws Exception { protected static void deleteAll(File file) throws IOException { if (file.exists()) { - Files.walk(file.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + try (var walk = Files.walk(file.toPath())) { + walk.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } } } + } /** * Check that spigot sent the message From b38eab74c7274d882f5ec955d045863b42df02f7 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 29 May 2026 17:21:15 -0700 Subject: [PATCH 12/13] Simplify advancement reset logic in onFirstTime event --- .../world/bentobox/boxed/listeners/AdvancementListener.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/world/bentobox/boxed/listeners/AdvancementListener.java b/src/main/java/world/bentobox/boxed/listeners/AdvancementListener.java index dcdf779..726cd05 100644 --- a/src/main/java/world/bentobox/boxed/listeners/AdvancementListener.java +++ b/src/main/java/world/bentobox/boxed/listeners/AdvancementListener.java @@ -286,9 +286,7 @@ public void onTeamLeaveTime(TeamLeaveEvent e) { public void onFirstTime(IslandNewIslandEvent e) { if (addon.getSettings().isIgnoreAdvancements()) return; User user = User.getInstance(e.getPlayerUUID()); - if (user != null) { - clearAndSetAdv(user, addon.getSettings().isOnJoinResetAdvancements(), addon.getSettings().getOnJoinGrantAdvancements()); - } + clearAndSetAdv(user, addon.getSettings().isOnJoinResetAdvancements(), addon.getSettings().getOnJoinGrantAdvancements()); } From 4973f8a7fffa9ae1ed1ebf2d55d083b19ffe8be6 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 29 May 2026 17:38:38 -0700 Subject: [PATCH 13/13] Fix stray closing braces breaking the build An extra '}' in BoxedBlockPopulator.setTrialSpawner and in CommonTestSetup.deleteAll closed each class prematurely, so the remaining members were parsed as top-level declarations. javac with --release 21 rejects this (BoxedBlockPopulator.java:153 "class, interface, enum, or record expected"), failing CI. Remove the stray braces; full clean build + 113 tests pass on JDK 21. Co-Authored-By: Claude Opus 4.8 --- .../bentobox/boxed/generators/chunks/BoxedBlockPopulator.java | 1 - src/test/java/world/bentobox/boxed/CommonTestSetup.java | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java b/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java index 7a2217b..c94e27d 100644 --- a/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java +++ b/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java @@ -148,6 +148,5 @@ public void setTrialSpawner(TrialSpawner trialSpawner, BlueprintTrialSpawner bts trialSpawner.setOminous(ominous); trialSpawner.update(true, false); } - } } diff --git a/src/test/java/world/bentobox/boxed/CommonTestSetup.java b/src/test/java/world/bentobox/boxed/CommonTestSetup.java index cb8b821..0962436 100644 --- a/src/test/java/world/bentobox/boxed/CommonTestSetup.java +++ b/src/test/java/world/bentobox/boxed/CommonTestSetup.java @@ -266,7 +266,6 @@ protected static void deleteAll(File file) throws IOException { } } - } /** * Check that spigot sent the message