diff --git a/.github/workflows/modrinth-publish.yml b/.github/workflows/modrinth-publish.yml index 17fa92d8..36bb50e1 100644 --- a/.github/workflows/modrinth-publish.yml +++ b/.github/workflows/modrinth-publish.yml @@ -56,7 +56,7 @@ jobs: # project page under the three-dot menu ("Copy ID") # - name: Publish to Modrinth - uses: cloudnode-pro/modrinth-publish@v2 + uses: cloudnode-pro/modrinth-publish@0be4916ad5f081d936eb5615aa35ce3f0949979c # v2 with: token: ${{ secrets.MODRINTH_TOKEN }} project: ${{ secrets.MODRINTH_PROJECT_ID }} diff --git a/CLAUDE.md b/CLAUDE.md index c1c86ac4..07e14c6f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,3 +54,92 @@ AOneBlock is a BentoBox GameModeAddon for Minecraft. Players start on a single m All tests extend `CommonTestSetup`, which sets up a MockBukkit server, mocks BentoBox and its managers (islands, players, worlds), and tears everything down after each test. Use Mockito and the repository's `world.bentobox.aoneblock.WhiteBox` helper for injecting state into private fields. Phase YAML files under `src/main/resources/phases/` are loaded at startup; tests that exercise `OneBlocksManager` need those resources on the classpath (they are by default via Maven's test resource path). + +## Dependency Source Lookup + +When you need to inspect source code for a dependency (e.g., BentoBox, addons): + +1. **Check local Maven repo first**: `~/.m2/repository/` — sources jars are named `*-sources.jar` +2. **Check the workspace**: Look for sibling directories or Git submodules that may contain the dependency as a local project (e.g., `../bentoBox`, `../addon-*`) +3. **Check Maven local cache for already-extracted sources** before downloading anything +4. Only download a jar or fetch from the internet if the above steps yield nothing useful + +Prefer reading `.java` source files directly from a local Git clone over decompiling or extracting a jar. + +In general, the latest version of BentoBox should be targeted. + +## Project Layout + +Related projects are checked out as siblings under `~/git/`: + +**Core:** +- `bentobox/` — core BentoBox framework + +**Game modes:** +- `addon-acidisland/` — AcidIsland game mode +- `addon-bskyblock/` — BSkyBlock game mode +- `Boxed/` — Boxed game mode (expandable box area) +- `CaveBlock/` — CaveBlock game mode +- `OneBlock/` — AOneBlock game mode +- `SkyGrid/` — SkyGrid game mode +- `RaftMode/` — Raft survival game mode +- `StrangerRealms/` — StrangerRealms game mode +- `Brix/` — plot game mode +- `parkour/` — Parkour game mode +- `poseidon/` — Poseidon game mode +- `gg/` — gg game mode + +**Addons:** +- `addon-level/` — island level calculation +- `addon-challenges/` — challenges system +- `addon-welcomewarpsigns/` — warp signs +- `addon-limits/` — block/entity limits +- `addon-invSwitcher/` / `invSwitcher/` — inventory switcher +- `addon-biomes/` / `Biomes/` — biomes management +- `Bank/` — island bank +- `Border/` — world border for islands +- `Chat/` — island chat +- `CheckMeOut/` — island submission/voting +- `ControlPanel/` — game mode control panel +- `Converter/` — ASkyBlock to BSkyBlock converter +- `DimensionalTrees/` — dimension-specific trees +- `discordwebhook/` — Discord integration +- `Downloads/` — BentoBox downloads site +- `DragonFights/` — per-island ender dragon fights +- `ExtraMobs/` — additional mob spawning rules +- `FarmersDance/` — twerking crop growth +- `GravityFlux/` — gravity addon +- `Greenhouses-addon/` — greenhouse biomes +- `IslandFly/` — island flight permission +- `IslandRankup/` — island rankup system +- `Likes/` — island likes/dislikes +- `Limits/` — block/entity limits +- `lost-sheep/` — lost sheep adventure +- `MagicCobblestoneGenerator/` — custom cobblestone generator +- `PortalStart/` — portal-based island start +- `pp/` — pp addon +- `Regionerator/` — region management +- `Residence/` — residence addon +- `TopBlock/` — top ten for OneBlock +- `TwerkingForTrees/` — twerking tree growth +- `Upgrades/` — island upgrades (Vault) +- `Visit/` — island visiting +- `weblink/` — web link addon +- `CrowdBound/` — CrowdBound addon + +**Data packs:** +- `BoxedDataPack/` — advancement datapack for Boxed + +**Documentation & tools:** +- `docs/` — main documentation site +- `docs-chinese/` — Chinese documentation +- `docs-french/` — French documentation +- `BentoBoxWorld.github.io/` — GitHub Pages site +- `website/` — website +- `translation-tool/` — translation tool + +Check these for source before any network fetch. + +## Key Dependencies (source locations) + +- `world.bentobox:bentobox` → `~/git/bentobox/src/` diff --git a/README.md b/README.md index fa4e4869..ff60c76e 100644 --- a/README.md +++ b/README.md @@ -219,11 +219,12 @@ blocks: # #303: spawn a MythicMob via BentoBox's MythicMobs hook. Requires the MythicMobs # plugin to be installed; otherwise the entry is logged and skipped at runtime. - type: mythic-mob - mob: SkeletalKnight - level: 3 - power: 1.0 - display-name: "Boss" - underlying-block: STONE + mob: SkeletalKnight # MythicMob internal type ID (required) + level: 3 # mob level (optional, default: 1) + power: 1.0 # mob power multiplier (optional, default: 0) + display-name: "Boss" # override display name (optional) + stance: "" # MythicMobs stance string (optional) + underlying-block: STONE # block placed under the mob (optional, default: STONE) probability: 5 ``` @@ -251,6 +252,83 @@ setblock mode flag so the intent is obvious at a glance. > if it works there it will work here. Bad NBT is logged and the spawn is > skipped. +#### MythicMobs configuration + +To use `type: mythic-mob` you need: +1. [MythicMobs](https://www.spigotmc.org/resources/mythicmobs.5702/) (free or premium) installed on the server. +2. BentoBox's built-in MythicMobs hook active (it registers automatically when MythicMobs is detected). + +**Fields:** + +| Field | Required | Default | Description | +|---|---|---|---| +| `mob` | ✅ | — | The internal MythicMobs mob type ID as defined in your MythicMobs config (case-sensitive). | +| `level` | ❌ | `1` | The level at which to spawn the mob. | +| `power` | ❌ | `0` | The power multiplier for the mob. | +| `display-name` | ❌ | mob ID | Override the mob's display name. | +| `stance` | ❌ | `""` | An optional MythicMobs stance string. | +| `underlying-block` | ❌ | `STONE` | The vanilla block placed at the magic-block position before the mob spawns. | +| `probability` | ❌ | — | The relative spawn weight in the phase pool. | + +**Alternative approach using commands:** + +If you experience any issues with MythicMobs skills or abilities when spawning +via `type: mythic-mob`, you can use the MythicMobs `/mm mobs spawn` command +inside the phase's `start-commands` or `end-commands` instead. This spawns the +mob through MythicMobs directly, which ensures all skills and behaviours work +exactly as configured: + +```yaml +start-commands: + # Spawn mob at the island's magic-block position using MythicMobs command. + # Replace , , , with the actual coordinates, or use a + # console command that targets the player's location. + - 'mm mobs spawn MY_MYTHIC_MOB 1 [world],[x],[y],[z]' +``` + +> **Tip:** When spawning MythicMobs via commands you have full control over +> the exact spawn location and can still use all MythicMobs features without +> any compatibility limitations. + +#### ItemsAdder custom blocks + +Requires the [ItemsAdder](https://www.spigotmc.org/resources/itemsadder.73355/) +plugin to be installed. ItemsAdder blocks can be referenced in two ways: + +**Map form** (inside a `blocks:` or `custom-blocks:` list): +```yaml +blocks: + - type: itemsadder + id: namespace:block_id # ItemsAdder block ID (required) + probability: 20 +``` + +**Short form** — place the ItemsAdder block ID directly as a map key (same as +vanilla materials): +```yaml +blocks: + namespace:block_id: 20 +``` + +#### Nexo custom blocks + +Requires the [Nexo](https://www.spigotmc.org/resources/nexo.112709/) plugin to +be installed. Nexo blocks can be referenced in two ways: + +**Map form** (inside a `blocks:` or `custom-blocks:` list): +```yaml +blocks: + - type: nexo + id: nexo_block_id # Nexo block ID (required) + probability: 20 +``` + +**Short form** — place the Nexo block ID directly as a map key: +```yaml +blocks: + nexo_block_id: 20 +``` + If you'd rather leave your existing `blocks:` map-form section untouched, you can put custom entries in a sibling `custom-blocks:` list. Both sections are read and their entries merged into the same weighted pool, so probabilities in diff --git a/pom.xml b/pom.xml index 1b35e9c7..725f2927 100644 --- a/pom.xml +++ b/pom.xml @@ -56,9 +56,10 @@ 5.11.0 v1.21-SNAPSHOT - 3.13.0 + 3.15.0-SNAPSHOT 4.0.10 1.8.0 + 0.0.67 2.6.2 1.3.0 @@ -66,11 +67,12 @@ -LOCAL - 1.23.0 + 1.24.0 BentoBoxWorld_AOneBlock bentobox-world https://sonarcloud.io + ${project.build.directory}/site/jacoco/jacoco.xml @@ -136,7 +138,7 @@ bentoboxworld https://repo.codemc.org/repository/bentoboxworld/ true - false + true @@ -159,6 +161,13 @@ true false + + + momirealms-releases + https://repo.momirealms.net/releases/ + true + false + codemc-repo @@ -271,6 +280,18 @@ + + net.momirealms + craft-engine-bukkit + ${craftengine.version} + provided + + + * + * + + + @@ -343,7 +364,7 @@ **/*Test?.java **/*Test??.java - + @{argLine} --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED diff --git a/src/main/java/world/bentobox/aoneblock/AOneBlock.java b/src/main/java/world/bentobox/aoneblock/AOneBlock.java index 1097865a..601038a2 100644 --- a/src/main/java/world/bentobox/aoneblock/AOneBlock.java +++ b/src/main/java/world/bentobox/aoneblock/AOneBlock.java @@ -22,6 +22,7 @@ import world.bentobox.aoneblock.listeners.BossBarListener; import world.bentobox.aoneblock.listeners.HoloListener; import world.bentobox.aoneblock.listeners.InfoListener; +import world.bentobox.aoneblock.listeners.CraftEngineListener; import world.bentobox.aoneblock.listeners.ItemsAdderListener; import world.bentobox.aoneblock.listeners.JoinLeaveListener; import world.bentobox.aoneblock.listeners.NexoListener; @@ -29,6 +30,7 @@ import world.bentobox.aoneblock.listeners.StartSafetyListener; import world.bentobox.aoneblock.oneblocks.OneBlockCustomBlockCreator; import world.bentobox.aoneblock.oneblocks.OneBlocksManager; +import world.bentobox.aoneblock.oneblocks.customblock.CraftEngineCustomBlock; import world.bentobox.aoneblock.oneblocks.customblock.ItemsAdderCustomBlock; import world.bentobox.aoneblock.oneblocks.customblock.NexoCustomBlock; import world.bentobox.aoneblock.requests.IslandStatsHandler; @@ -57,6 +59,8 @@ public class AOneBlock extends GameModeAddon { private boolean hasItemsAdder = false; /** Whether Nexo is present on the server */ private boolean hasNexo = false; + /** Whether CraftEngine is present on the server */ + private boolean hasCraftEngine = false; /** The addon settings */ private Settings settings; @@ -127,6 +131,13 @@ public void onLoad() { OneBlockCustomBlockCreator.register("nexo", NexoCustomBlock::fromMap); hasNexo = true; } + // Check if CraftEngine exists, if yes register listener + if (Bukkit.getPluginManager().getPlugin("CraftEngine") != null) { + registerListener(new CraftEngineListener(this)); + OneBlockCustomBlockCreator.register(CraftEngineCustomBlock::fromId); + OneBlockCustomBlockCreator.register("craftengine", CraftEngineCustomBlock::fromMap); + hasCraftEngine = true; + } // Save the default config from config.yml saveDefaultConfig(); // Load settings from config.yml. This will check if there are any issues with @@ -208,6 +219,10 @@ public void onEnable() { * @return true if there was an error, false otherwise. */ public boolean loadData() { + if (oneBlockManager == null) { + // oneBlockManager is not yet initialized (addon not fully enabled) + return false; + } try { oneBlockManager.loadPhases(); } catch (IOException e) { @@ -416,6 +431,13 @@ public boolean hasNexo() { return hasNexo; } + /** + * @return true if CraftEngine is on the server + */ + public boolean hasCraftEngine() { + return hasCraftEngine; + } + /** * Set the addon's world. Used only for testing. * @param world world diff --git a/src/main/java/world/bentobox/aoneblock/AOneBlockPlaceholders.java b/src/main/java/world/bentobox/aoneblock/AOneBlockPlaceholders.java index cfc7d2a0..b1fb0372 100644 --- a/src/main/java/world/bentobox/aoneblock/AOneBlockPlaceholders.java +++ b/src/main/java/world/bentobox/aoneblock/AOneBlockPlaceholders.java @@ -21,6 +21,7 @@ public class AOneBlockPlaceholders { private static final TreeMap SCALE; private static final String INFINITE = "aoneblock.placeholders.infinite"; + private static final String UNKNOWN_PHASE = "aoneblock.placeholders.my-island-phase-default"; static { SCALE = new TreeMap<>(); SCALE.put(0D, "&c╍╍╍╍╍╍╍╍"); @@ -163,7 +164,8 @@ public String getCountByLocation(User user) { public String getPhase(User user) { if (user == null || user.getUniqueId() == null) return ""; - return getUsersIsland(user).map(i -> addon.getOneBlocksIsland(i).getPhaseName()).orElse(""); + return getUsersIsland(user).map(i -> addon.getOneBlocksIsland(i).getPhaseName()) + .orElse(user.getTranslation(UNKNOWN_PHASE)); } /** @@ -175,7 +177,7 @@ public String getPhase(User user) { public String getCount(User user) { if (user == null || user.getUniqueId() == null) return ""; - return getUsersIsland(user).map(i -> String.valueOf(addon.getOneBlocksIsland(i).getBlockNumber())).orElse(""); + return getUsersIsland(user).map(i -> String.valueOf(addon.getOneBlocksIsland(i).getBlockNumber())).orElse("0"); } /** @@ -274,7 +276,7 @@ public String getPercentDone(User user) { return getUsersIsland(user).map(i -> { double num = addon.getOneBlockManager().getPercentageDone(addon.getOneBlocksIsland(i)); return Math.round(num) + "%"; - }).orElse(""); + }).orElse("0%"); } /** diff --git a/src/main/java/world/bentobox/aoneblock/Settings.java b/src/main/java/world/bentobox/aoneblock/Settings.java index cc14c3f7..70e0a8a8 100644 --- a/src/main/java/world/bentobox/aoneblock/Settings.java +++ b/src/main/java/world/bentobox/aoneblock/Settings.java @@ -11,8 +11,10 @@ import org.bukkit.Color; import org.bukkit.Difficulty; import org.bukkit.GameMode; +import org.bukkit.Particle; import org.bukkit.block.Biome; import org.bukkit.entity.EntityType; +import org.eclipse.jdt.annotation.Nullable; import world.bentobox.aoneblock.listeners.BlockListener; import world.bentobox.bentobox.BentoBox; @@ -154,6 +156,34 @@ public class Settings implements WorldSettings { @ConfigEntry(path = "world.block-id.particle-color") private Color particleColor = Color.GREEN; + @ConfigComment("Chest particle effects - particles that appear when a special chest spawns based on rarity.") + @ConfigComment("Valid particle names: https://jd.papermc.io/paper/1.21/org/bukkit/Particle.html") + @ConfigComment("Use NONE to disable particles for that rarity level.") + @ConfigComment("Note: The color setting is only used for DUST-type particles.") + @ConfigComment("Particle type for UNCOMMON rarity chests.") + @ConfigEntry(path = "world.chest-particles.uncommon-particle") + private String chestParticleUncommon = "DUST"; + + @ConfigComment("Color of UNCOMMON chest particles (only used for DUST-type particles). Default: yellow") + @ConfigEntry(path = "world.chest-particles.uncommon-color") + private Color chestColorUncommon = Color.YELLOW; + + @ConfigComment("Particle type for RARE rarity chests.") + @ConfigEntry(path = "world.chest-particles.rare-particle") + private String chestParticleRare = "DUST"; + + @ConfigComment("Color of RARE chest particles (only used for DUST-type particles). Default: white") + @ConfigEntry(path = "world.chest-particles.rare-color") + private Color chestColorRare = Color.WHITE; + + @ConfigComment("Particle type for EPIC rarity chests.") + @ConfigEntry(path = "world.chest-particles.epic-particle") + private String chestParticleEpic = "DUST"; + + @ConfigComment("Color of EPIC chest particles (only used for DUST-type particles). Default: fuchsia") + @ConfigEntry(path = "world.chest-particles.epic-color") + private Color chestColorEpic = Color.FUCHSIA; + @ConfigComment("Clear blocks when spawning mobs.") @ConfigComment("Mobs break blocks when they spawn is to prevent players from building a box around the magic block,") @@ -831,8 +861,7 @@ public Map getDefaultIslandSettingNames() * @since 1.21 */ @Override - - @Deprecated + @Deprecated(since = "1.21", forRemoval = true) public Map getDefaultIslandFlags() { return Collections.emptyMap(); } @@ -843,7 +872,7 @@ public Map getDefaultIslandFlags() { * @since 1.21 */ @Override - @Deprecated + @Deprecated(since = "1.21", forRemoval = true) public Map getDefaultIslandSettings() { return Collections.emptyMap(); } @@ -2307,4 +2336,141 @@ public void setActionBarCommand(String actionBarCommand) { this.actionBarCommand = actionBarCommand; } + /** + * Parses a particle name string to a {@link Particle} enum value. + * Returns {@code null} if the name is {@code "NONE"} or {@code null} (disables particle spawning). + * Falls back to {@code defaultParticle} if the name cannot be matched. + * + * @param name the particle name (case-insensitive) + * @param defaultParticle the fallback particle if parsing fails + * @return the parsed {@link Particle}, or {@code null} if disabled + */ + @Nullable + private Particle parseParticle(@Nullable String name, Particle defaultParticle) { + if (name == null || name.equalsIgnoreCase("NONE")) { + return null; + } + try { + return Particle.valueOf(name.toUpperCase(java.util.Locale.ENGLISH)); + } catch (IllegalArgumentException e) { + return defaultParticle; + } + } + + /** + * @return the particle type string for UNCOMMON rarity chests + */ + public String getChestParticleUncommon() { + return chestParticleUncommon; + } + + /** + * @param chestParticleUncommon the particle type string for UNCOMMON chests (use "NONE" to disable) + */ + public void setChestParticleUncommon(String chestParticleUncommon) { + this.chestParticleUncommon = chestParticleUncommon; + } + + /** + * @return the color for UNCOMMON chest particles + */ + public Color getChestColorUncommon() { + return chestColorUncommon == null ? Color.YELLOW : chestColorUncommon; + } + + /** + * @param chestColorUncommon the color for UNCOMMON chest particles + */ + public void setChestColorUncommon(Color chestColorUncommon) { + this.chestColorUncommon = chestColorUncommon; + } + + /** + * @return the particle type string for RARE rarity chests + */ + public String getChestParticleRare() { + return chestParticleRare; + } + + /** + * @param chestParticleRare the particle type string for RARE chests (use "NONE" to disable) + */ + public void setChestParticleRare(String chestParticleRare) { + this.chestParticleRare = chestParticleRare; + } + + /** + * @return the color for RARE chest particles + */ + public Color getChestColorRare() { + return chestColorRare == null ? Color.WHITE : chestColorRare; + } + + /** + * @param chestColorRare the color for RARE chest particles + */ + public void setChestColorRare(Color chestColorRare) { + this.chestColorRare = chestColorRare; + } + + /** + * @return the particle type string for EPIC rarity chests + */ + public String getChestParticleEpic() { + return chestParticleEpic; + } + + /** + * @param chestParticleEpic the particle type string for EPIC chests (use "NONE" to disable) + */ + public void setChestParticleEpic(String chestParticleEpic) { + this.chestParticleEpic = chestParticleEpic; + } + + /** + * @return the color for EPIC chest particles + */ + public Color getChestColorEpic() { + return chestColorEpic == null ? Color.FUCHSIA : chestColorEpic; + } + + /** + * @param chestColorEpic the color for EPIC chest particles + */ + public void setChestColorEpic(Color chestColorEpic) { + this.chestColorEpic = chestColorEpic; + } + + /** + * Resolves the configured chest particle for a given rarity to a {@link Particle} enum value. + * Returns {@code null} if the particle is set to "NONE" (disabling it for that rarity). + * + * @param rarity the chest rarity + * @return the resolved {@link Particle}, or {@code null} if disabled for this rarity + */ + @Nullable + public Particle resolveChestParticle(world.bentobox.aoneblock.oneblocks.OneBlockObject.Rarity rarity) { + return switch (rarity) { + case EPIC -> parseParticle(chestParticleEpic, Particle.DUST); + case RARE -> parseParticle(chestParticleRare, Particle.DUST); + case UNCOMMON -> parseParticle(chestParticleUncommon, Particle.DUST); + default -> null; + }; + } + + /** + * Returns the configured chest particle color for a given rarity. + * + * @param rarity the chest rarity + * @return the {@link Color} for that rarity's chest particles + */ + public Color resolveChestColor(world.bentobox.aoneblock.oneblocks.OneBlockObject.Rarity rarity) { + return switch (rarity) { + case EPIC -> getChestColorEpic(); + case RARE -> getChestColorRare(); + case UNCOMMON -> getChestColorUncommon(); + default -> getChestColorUncommon(); + }; + } + } \ No newline at end of file diff --git a/src/main/java/world/bentobox/aoneblock/generators/ChunkGeneratorWorld.java b/src/main/java/world/bentobox/aoneblock/generators/ChunkGeneratorWorld.java index ba9846f5..4e6e516f 100644 --- a/src/main/java/world/bentobox/aoneblock/generators/ChunkGeneratorWorld.java +++ b/src/main/java/world/bentobox/aoneblock/generators/ChunkGeneratorWorld.java @@ -28,8 +28,12 @@ public ChunkData generateChunks(World world) { return createChunkData(world); } + /** + * @deprecated Paper 1.19+ uses the new ChunkGenerator API; override + * {@link #generateChunks(World)} instead. + */ @Override - @Deprecated + @Deprecated(since = "1.19", forRemoval = true) public ChunkData generateChunkData(World world, Random random, int chunkX, int chunkZ, BiomeGrid biomeGrid) { return generateChunks(world); } diff --git a/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java b/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java index c02e25c1..c066b986 100644 --- a/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java +++ b/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java @@ -25,10 +25,8 @@ import org.bukkit.block.Biome; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; -import org.bukkit.block.BlockState; import org.bukkit.block.BrushableBlock; import org.bukkit.block.Chest; -import org.bukkit.block.data.BlockData; import org.bukkit.block.data.Brushable; import org.bukkit.block.data.type.Leaves; import org.bukkit.entity.Entity; @@ -406,7 +404,7 @@ private ProcessPhaseResult processPhase(Cancellable e, Island i, OneBlockIslands } String currPhaseName = phase.getPhaseName() == null ? "" : phase.getPhaseName(); - handlePhaseChange(is, phase, currPhaseName); + handlePhaseChange(is, currPhaseName); boolean isCurrPhaseNew = !is.getPhaseName().equalsIgnoreCase(currPhaseName); if (isCurrPhaseNew) { @@ -452,10 +450,9 @@ private void handleNewPhase(Player player, Island i, OneBlockIslands is, OneBloc * Handles phase transition mechanics including setting timestamps for phase changes. * * @param is - oneblock island data - * @param phase - current phase * @param currPhaseName - name of current phase */ - private void handlePhaseChange(OneBlockIslands is, OneBlockPhase phase, String currPhaseName) { + private void handlePhaseChange(OneBlockIslands is, String currPhaseName) { OneBlockPhase nextPhase = oneBlocksManager.getPhase(is.getBlockNumber() + 1); if (Objects.requireNonNull(nextPhase).getGotoBlock() != null) { nextPhase = oneBlocksManager.getPhase(nextPhase.getGotoBlock()); @@ -631,17 +628,20 @@ private void setBiome(@NonNull Block block, @Nullable Biome biome) { */ private void breakBlock(@Nullable Player player, Block block, @NonNull OneBlockObject nextBlock, @NonNull Island island) { - ItemStack tool = Objects.requireNonNull(player).getInventory().getItemInMainHand(); + if (player == null) return; + Location playerLoc = player.getLocation(); + if (playerLoc == null) return; + ItemStack tool = player.getInventory().getItemInMainHand(); // Break normally and lift the player up so they don't fall Bukkit.getScheduler().runTask(addon.getPlugin(), () -> this.spawnBlock(nextBlock, block)); - if (player.getLocation().getBlock().equals(block)) { - double delta = 1 - (player.getLocation().getY() - block.getY()); - player.teleport(player.getLocation().add(new Vector(0, delta, 0))); + if (playerLoc.getBlock().equals(block)) { + double delta = 1 - (playerLoc.getY() - block.getY()); + player.teleport(playerLoc.add(new Vector(0, delta, 0))); player.setVelocity(new Vector(0, 0, 0)); - } else if (player.getLocation().getBlock().equals(block.getRelative(BlockFace.UP))) { - player.teleport(player.getLocation()); + } else if (playerLoc.getBlock().equals(block.getRelative(BlockFace.UP))) { + player.teleport(playerLoc); player.setVelocity(new Vector(0, 0, 0)); } @@ -731,20 +731,20 @@ public void onPlayerInteract(PlayerInteractEvent e) { bb.setDusted(dusted); block.setBlockData(bb); playBrushFeedback(block); - // Kick off a continuous-brush session so the player can hold right-click - // and have dusting advance automatically (vanilla feel). The kickoff click - // above already advances one stage; the timer picks up from there. - Player player = e.getPlayer(); - UUID uuid = player.getUniqueId(); - BrushSession existing = brushSessions.get(uuid); - if (existing != null && !existing.block().equals(block)) { - cancelBrushSession(uuid); - existing = null; - } - if (existing == null) { - brushSessions.put(uuid, startContinuousBrush(player, block)); - } + updateBrushSession(e.getPlayer(), block); + } + } + + private void updateBrushSession(Player player, Block block) { + // Kick off a continuous-brush session so the player can hold right-click + // and have dusting advance automatically (vanilla feel). The kickoff click + // above already advances one stage; the timer picks up from there. + UUID uuid = player.getUniqueId(); + BrushSession existing = brushSessions.get(uuid); + if (existing != null && !existing.block().equals(block)) { + cancelBrushSession(uuid); // also removes the key from brushSessions } + brushSessions.computeIfAbsent(uuid, k -> startContinuousBrush(player, block)); } /** @@ -757,30 +757,27 @@ public void onPlayerInteract(PlayerInteractEvent e) { */ private BrushSession startContinuousBrush(Player player, Block block) { UUID uuid = player.getUniqueId(); - BukkitTask task = Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), new Runnable() { - @Override - public void run() { - // Validate that the player is still actively brushing this block. - if (!player.isOnline() - || player.getInventory().getItemInMainHand().getType() != Material.BRUSH - || !player.isHandRaised() - || !block.equals(player.getTargetBlockExact(5)) - || (block.getType() != Material.SUSPICIOUS_GRAVEL - && block.getType() != Material.SUSPICIOUS_SAND) - || !(block.getBlockData() instanceof Brushable bb)) { - cancelBrushSession(uuid); - return; - } - int dusted = bb.getDusted() + 1; - if (dusted > bb.getMaximumDusted()) { - completeBrush(player, block); - cancelBrushSession(uuid); - return; - } - bb.setDusted(dusted); - block.setBlockData(bb); - playBrushFeedback(block); + BukkitTask task = Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), () -> { + // Validate that the player is still actively brushing this block. + if (!player.isOnline() + || player.getInventory().getItemInMainHand().getType() != Material.BRUSH + || !player.isHandRaised() + || !block.equals(player.getTargetBlockExact(5)) + || (block.getType() != Material.SUSPICIOUS_GRAVEL + && block.getType() != Material.SUSPICIOUS_SAND) + || !(block.getBlockData() instanceof Brushable bb)) { + cancelBrushSession(uuid); + return; + } + int dusted = bb.getDusted() + 1; + if (dusted > bb.getMaximumDusted()) { + completeBrush(player, block); + cancelBrushSession(uuid); + return; } + bb.setDusted(dusted); + block.setBlockData(bb); + playBrushFeedback(block); }, 10L, 10L); return new BrushSession(task, block); } @@ -878,23 +875,20 @@ private void spawnEntity(@NonNull OneBlockObject nextBlock, @NonNull Block block private void fillChest(@NonNull OneBlockObject nextBlock, @NonNull Block block) { Chest chest = (Chest) block.getState(); nextBlock.getChest().forEach(chest.getBlockInventory()::setItem); - Color color = Color.fromBGR(0, 255, 255); // yellow - switch (nextBlock.getRarity()) { - case EPIC: - color = Color.fromBGR(255, 0, 255); // magenta - break; - case RARE: - color = Color.fromBGR(255, 255, 255); // cyan - break; - case UNCOMMON: - // Yellow - break; - default: + OneBlockObject.Rarity rarity = nextBlock.getRarity(); + if (rarity == OneBlockObject.Rarity.COMMON) { // No sparkles for regular chests return; } - block.getWorld().spawnParticle(Particle.DUST, block.getLocation().add(new Vector(0.5, 1.0, 0.5)), 50, 0.5, - 0, 0.5, 1, new Particle.DustOptions(color, 1)); + Particle particle = addon.getSettings().resolveChestParticle(rarity); + if (particle == null) { + // Particles disabled for this rarity + return; + } + Color color = addon.getSettings().resolveChestColor(rarity); + Object particleData = Particle.DUST.equals(particle) ? new Particle.DustOptions(color, 1) : null; + block.getWorld().spawnParticle(particle, block.getLocation().add(new Vector(0.5, 1.0, 0.5)), 50, 0.5, + 0, 0.5, 1, particleData); } /** diff --git a/src/main/java/world/bentobox/aoneblock/listeners/BossBarListener.java b/src/main/java/world/bentobox/aoneblock/listeners/BossBarListener.java index e3e88f3e..966d8d8c 100644 --- a/src/main/java/world/bentobox/aoneblock/listeners/BossBarListener.java +++ b/src/main/java/world/bentobox/aoneblock/listeners/BossBarListener.java @@ -6,6 +6,7 @@ import java.util.UUID; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.boss.BarColor; import org.bukkit.boss.BarStyle; import org.bukkit.boss.BossBar; @@ -209,10 +210,11 @@ public void onExitIsland(IslandExitEvent event) { @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onJoin(PlayerJoinEvent e) { // If the player is on an island then show the bar - if (!addon.inWorld(e.getPlayer().getLocation())) { + Location playerLoc = e.getPlayer().getLocation(); + if (playerLoc == null || !addon.inWorld(playerLoc)) { return; } - addon.getIslands().getIslandAt(e.getPlayer().getLocation()) + addon.getIslands().getIslandAt(playerLoc) .ifPresent(is -> this.tryToShowBossBar(e.getPlayer().getUniqueId(), is)); } diff --git a/src/main/java/world/bentobox/aoneblock/listeners/CheckPhase.java b/src/main/java/world/bentobox/aoneblock/listeners/CheckPhase.java index 2ffbe45f..a070fe77 100644 --- a/src/main/java/world/bentobox/aoneblock/listeners/CheckPhase.java +++ b/src/main/java/world/bentobox/aoneblock/listeners/CheckPhase.java @@ -1,9 +1,6 @@ package world.bentobox.aoneblock.listeners; import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - import org.bukkit.World; import org.bukkit.entity.Player; import org.eclipse.jdt.annotation.NonNull; @@ -66,7 +63,8 @@ void setNewPhase(@Nullable Player player, @NonNull Island i, @NonNull OneBlockIs user = User.getInstance(player); } - String newPhaseName = Objects.requireNonNullElse(phase.getPhaseName(), ""); + String rawPhaseName = phase.getPhaseName(); + String newPhaseName = rawPhaseName == null ? "" : rawPhaseName; // Run previous phase end commands oneBlocksManager.getPhase(is.getPhaseName()).ifPresent(oldPhase -> { @@ -84,8 +82,9 @@ void setNewPhase(@Nullable Player player, @NonNull Island i, @NonNull OneBlockIs }); // Set the phase name is.setPhaseName(newPhaseName); - if (user.isPlayer() && user.isOnline() && addon.inWorld(user.getWorld())) { - user.getPlayer().showTitle(Title.title(Component.text(newPhaseName), Component.empty())); + Player onlinePlayer = user.getPlayer(); + if (user.isPlayer() && user.isOnline() && addon.inWorld(user.getWorld()) && onlinePlayer != null) { + onlinePlayer.showTitle(Title.title(Component.text(newPhaseName), Component.empty())); } // Run phase start commands Util.runCommands(user, @@ -224,6 +223,6 @@ List replacePlaceholders(@Nullable Player player, @NonNull String phaseN .replace("[eco-balance]", String.valueOf(ecoBalance)); }).map(c -> addon.getPlugin().getPlaceholdersManager().replacePlaceholders(player, c)) - .collect(Collectors.toList()); + .toList(); } } diff --git a/src/main/java/world/bentobox/aoneblock/listeners/CraftEngineListener.java b/src/main/java/world/bentobox/aoneblock/listeners/CraftEngineListener.java new file mode 100644 index 00000000..4b57bbb6 --- /dev/null +++ b/src/main/java/world/bentobox/aoneblock/listeners/CraftEngineListener.java @@ -0,0 +1,28 @@ +package world.bentobox.aoneblock.listeners; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; + +import net.momirealms.craftengine.bukkit.api.event.CraftEngineReloadEvent; +import world.bentobox.aoneblock.AOneBlock; + +/** + * Handles CraftEngineReloadEvent which is fired when CraftEngine loads or reloads its data + */ +public class CraftEngineListener implements Listener { + + private final AOneBlock addon; + + public CraftEngineListener(AOneBlock addon) { + this.addon = addon; + } + + /** + * Handle CraftEngineReloadEvent and reload the addon when triggered + * @param e - CraftEngineReloadEvent + */ + @EventHandler + public void onReload(CraftEngineReloadEvent e) { + addon.loadData(); + } +} diff --git a/src/main/java/world/bentobox/aoneblock/listeners/HoloListener.java b/src/main/java/world/bentobox/aoneblock/listeners/HoloListener.java index 9a0cc5f4..5ceff030 100644 --- a/src/main/java/world/bentobox/aoneblock/listeners/HoloListener.java +++ b/src/main/java/world/bentobox/aoneblock/listeners/HoloListener.java @@ -15,13 +15,14 @@ import org.bukkit.util.Vector; import org.eclipse.jdt.annotation.NonNull; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import world.bentobox.aoneblock.AOneBlock; +import world.bentobox.bentobox.util.Util; import world.bentobox.aoneblock.dataobjects.OneBlockIslands; import world.bentobox.aoneblock.oneblocks.OneBlockPhase; import world.bentobox.bentobox.api.events.island.IslandDeleteEvent; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.bentobox.util.Util; /** * Handles Holographic elements @@ -89,7 +90,7 @@ protected void setUp(@NonNull Island island, @NonNull OneBlockIslands is, boolea */ protected void process(@NonNull Island i, @NonNull OneBlockIslands is, @NonNull OneBlockPhase phase) { String holoText = phase.getHologramLine(is.getBlockNumber()); - is.setHologram(holoText == null ? "" : Util.translateColorCodes(holoText)); + is.setHologram(holoText == null ? "" : holoText); updateHologram(i, is.getHologram()); } @@ -139,7 +140,7 @@ private void createHologram(Location pos, String text) { display.setAlignment(TextDisplay.TextAlignment.CENTER); display.setBillboard(Billboard.CENTER); display.setPersistent(true); - display.setText(text); + display.text(LegacyComponentSerializer.legacyAmpersand().deserialize(text)); activeHolograms.add(pos); } diff --git a/src/main/java/world/bentobox/aoneblock/listeners/StartSafetyListener.java b/src/main/java/world/bentobox/aoneblock/listeners/StartSafetyListener.java index fa752a68..e33a5899 100644 --- a/src/main/java/world/bentobox/aoneblock/listeners/StartSafetyListener.java +++ b/src/main/java/world/bentobox/aoneblock/listeners/StartSafetyListener.java @@ -59,7 +59,7 @@ public void onResetIsland(IslandResetEvent e) { @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onPlayerMove(PlayerMoveEvent e) { if (addon.inWorld(e.getPlayer().getWorld()) && newIslands.containsKey(e.getPlayer().getUniqueId()) - && e.getTo() != null && !e.getPlayer().isSneaking() + && !e.getPlayer().isSneaking() && (e.getFrom().getX() != e.getTo().getX() || e.getFrom().getZ() != e.getTo().getZ())) { // Do not allow x or z movement e.setTo(new Location(e.getFrom().getWorld(), e.getFrom().getX(), e.getTo().getY(), e.getFrom().getZ(), diff --git a/src/main/java/world/bentobox/aoneblock/listeners/WarningSounder.java b/src/main/java/world/bentobox/aoneblock/listeners/WarningSounder.java index 18445f45..b74111f2 100644 --- a/src/main/java/world/bentobox/aoneblock/listeners/WarningSounder.java +++ b/src/main/java/world/bentobox/aoneblock/listeners/WarningSounder.java @@ -35,14 +35,14 @@ public WarningSounder(AOneBlock addon) { /** * Mob aspects. */ - private Map MOB_ASPECTS; + private Map mobAspects; void play(@NonNull OneBlockIslands is, @NonNull Block block) { - if (MOB_ASPECTS == null) { + if (mobAspects == null) { initialize(); // Done to avoid static definition with Sound due to test issues } List opMob = is.getNearestMob(addon.getSettings().getMobWarning()); - opMob.stream().filter(MOB_ASPECTS::containsKey).map(MOB_ASPECTS::get).forEach(s -> { + opMob.stream().filter(mobAspects::containsKey).map(mobAspects::get).forEach(s -> { block.getWorld().playSound(block.getLocation(), s.sound(), 1F, 1F); block.getWorld().spawnParticle(Particle.DUST, block.getLocation().add(new Vector(0.5, 1.0, 0.5)), 10, 0.5, 0, 0.5, 1, new Particle.DustOptions(s.color(), 1)); @@ -84,7 +84,7 @@ private void initialize() { m.put(EntityType.ZOMBIE, new MobAspects(Sound.ENTITY_ZOMBIE_AMBIENT, Color.fromRGB(74, 99, 53))); m.put(EntityType.ZOMBIE_VILLAGER, new MobAspects(Sound.ENTITY_ZOMBIE_VILLAGER_AMBIENT, Color.fromRGB(111, 104, 90))); - MOB_ASPECTS = Collections.unmodifiableMap(m); + mobAspects = Collections.unmodifiableMap(m); } diff --git a/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockPhase.java b/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockPhase.java index af595537..2f56fdd6 100644 --- a/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockPhase.java +++ b/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockPhase.java @@ -296,7 +296,7 @@ public void setIconBlock(ItemStack iconBlock) { * @return collection of all the chests */ public Collection getChests() { - return chests.values().stream().flatMap(List::stream).collect(Collectors.toList()); + return chests.values().stream().flatMap(List::stream).toList(); } /** diff --git a/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlocksManager.java b/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlocksManager.java index 7f5ea0f1..012ac547 100644 --- a/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlocksManager.java +++ b/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlocksManager.java @@ -22,8 +22,10 @@ import org.apache.commons.lang3.math.NumberUtils; import org.bukkit.Material; import org.bukkit.NamespacedKey; -import org.bukkit.Registry; import org.bukkit.block.Biome; + +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.EntityType; @@ -64,6 +66,7 @@ public class OneBlocksManager { private static final String CUSTOM_BLOCKS = "custom-blocks"; private static final String PHASES = "phases"; private static final String GOTO_BLOCK = "gotoBlock"; + private static final String CHEST_WITH_PREFIX = "CHEST_WITH_"; private static final String START_COMMANDS = "start-commands"; private static final String END_COMMANDS = "end-commands"; private static final String END_COMMANDS_FIRST_TIME = "end-commands-first-time"; @@ -181,66 +184,58 @@ private void loadPhase(File phaseFile) throws IOException { void initBlock(String blockNumber, OneBlockPhase obPhase, ConfigurationSection phaseConfig) throws IOException { // Set name if (phaseConfig.contains(NAME, true)) { - if (obPhase.getPhaseName() != null) { - throw new IOException( - BLOCK + blockNumber + ": Phase name trying to be set to " + phaseConfig.getString(NAME) - + BUT_ALREADY_SET_TO + obPhase.getPhaseName() + ". Duplicate phase file?"); - } + checkNotDuplicate(obPhase.getPhaseName() != null, blockNumber, "Phase name", + phaseConfig.getString(NAME), obPhase.getPhaseName(), ". Duplicate phase file?"); obPhase.setPhaseName(phaseConfig.getString(NAME, blockNumber)); } // Set biome if (phaseConfig.contains(BIOME, true)) { - if (obPhase.getPhaseBiome() != null) { - throw new IOException(BLOCK + blockNumber + ": Biome trying to be set to " - + phaseConfig.getString(BIOME) + BUT_ALREADY_SET_TO + obPhase.getPhaseBiome() + DUPLICATE); - } + checkNotDuplicate(obPhase.getPhaseBiome() != null, blockNumber, "Biome", + phaseConfig.getString(BIOME), obPhase.getPhaseBiome(), DUPLICATE); obPhase.setPhaseBiome(getBiome(phaseConfig.getString(BIOME))); } // Set first block if (phaseConfig.contains(FIRST_BLOCK)) { - if (obPhase.getFirstBlock() != null) { - throw new IOException( - BLOCK + blockNumber + ": First block trying to be set to " + phaseConfig.getString(FIRST_BLOCK) - + BUT_ALREADY_SET_TO + obPhase.getFirstBlock() + DUPLICATE); - } + checkNotDuplicate(obPhase.getFirstBlock() != null, blockNumber, "First block", + phaseConfig.getString(FIRST_BLOCK), obPhase.getFirstBlock(), DUPLICATE); addFirstBlock(obPhase, phaseConfig.getString(FIRST_BLOCK)); } // Set icon if (phaseConfig.contains(ICON)) { ItemStack icon = ItemParser.parse(phaseConfig.getString(ICON)); - if (icon == null) { throw new IOException("ItemParser failed to parse icon: '" + phaseConfig.getString(ICON) + "' for phase " + obPhase.getFirstBlock() + ". Can you check if it is correct?"); } - obPhase.setIconBlock(icon); } // Add fixed blocks if (phaseConfig.contains(FIXED_BLOCKS)) { - if (!obPhase.getFixedBlocks().isEmpty()) { - throw new IOException(BLOCK + blockNumber + ": Fixed blocks trying to be set to " - + phaseConfig.getString(FIXED_BLOCKS) + BUT_ALREADY_SET_TO + obPhase.getFixedBlocks() - + DUPLICATE); - } + checkNotDuplicate(!obPhase.getFixedBlocks().isEmpty(), blockNumber, "Fixed blocks", + phaseConfig.getString(FIXED_BLOCKS), obPhase.getFixedBlocks(), DUPLICATE); addFixedBlocks(obPhase, phaseConfig.getConfigurationSection(FIXED_BLOCKS)); } // Add holograms if (phaseConfig.contains(HOLOGRAMS)) { - if (!obPhase.getHologramLines().isEmpty()) { - throw new IOException( - BLOCK + blockNumber + ": Hologram Lines trying to be set to " + phaseConfig.getString(HOLOGRAMS) - + BUT_ALREADY_SET_TO + obPhase.getHologramLines() + DUPLICATE); - } + checkNotDuplicate(!obPhase.getHologramLines().isEmpty(), blockNumber, "Hologram Lines", + phaseConfig.getString(HOLOGRAMS), obPhase.getHologramLines(), DUPLICATE); addHologramLines(obPhase, phaseConfig.getConfigurationSection(HOLOGRAMS)); } } + private void checkNotDuplicate(boolean alreadySet, String blockNumber, String field, + Object newValue, Object existingValue, String suffix) throws IOException { + if (alreadySet) { + throw new IOException(BLOCK + blockNumber + ": " + field + " trying to be set to " + + newValue + BUT_ALREADY_SET_TO + existingValue + suffix); + } + } + private void addFixedBlocks(OneBlockPhase obPhase, ConfigurationSection firstBlocksConfig) { if (firstBlocksConfig == null) { return; @@ -318,6 +313,13 @@ private void parseStringBlock(Map result, Configuration return; } + // Check for CHEST_WITH_X notation + String matUpper = mat.toUpperCase(Locale.ENGLISH); + if (matUpper.startsWith(CHEST_WITH_PREFIX)) { + parseChestWithItem(result, key, k, matUpper.substring(CHEST_WITH_PREFIX.length())); + return; + } + Optional customBlock = OneBlockCustomBlockCreator.create(mat); if (customBlock.isPresent()) { result.put(k, new OneBlockObject(customBlock.get(), 0)); @@ -331,6 +333,27 @@ private void parseStringBlock(Map result, Configuration } } + /** + * Parses a {@code CHEST_WITH_X} shorthand entry and adds it to the result map. + * The produced chest block will contain a single item of the specified material + * in slot 0. + * + * @param result the resulting fixed-blocks map + * @param key the raw YAML key (used in error messages) + * @param k the integer value of the key + * @param itemName the material name of the item to place in the chest + */ + private void parseChestWithItem(Map result, String key, int k, String itemName) { + Material item = Material.matchMaterial(itemName); + if (item == null) { + addon.logError("Fixed block key " + key + " CHEST_WITH item is invalid: " + itemName + ". Ignoring."); + return; + } + Map chestContents = new HashMap<>(); + chestContents.put(0, new ItemStack(item)); + result.put(k, new OneBlockObject(chestContents, Rarity.COMMON)); + } + private void addHologramLines(OneBlockPhase obPhase, ConfigurationSection fb) { if (fb == null) return; @@ -356,10 +379,11 @@ private Biome getBiome(String string) { return Biome.PLAINS; } NamespacedKey key = NamespacedKey.fromString(string.toLowerCase(Locale.ENGLISH)); - Biome result = Registry.BIOME.get(key); + var biomeRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.BIOME); + Biome result = biomeRegistry.get(key); if (result == null) { addon.logError("Biome " + string + " is invalid! Use one of these..."); - Registry.BIOME.stream().sorted(Comparator.comparing(biome -> biome.getKey().getKey())) + biomeRegistry.stream().sorted(Comparator.comparing(biome -> biome.getKey().getKey())) .forEach(biome -> addon.logError(biome.getKey().getKey())); return Biome.PLAINS; } @@ -455,33 +479,37 @@ void addMobs(OneBlockPhase obPhase, ConfigurationSection phase) throws IOExcepti } ConfigurationSection mobs = phase.getConfigurationSection(MOBS); for (String entity : mobs.getKeys(false)) { - String name = entity.toUpperCase(Locale.ENGLISH); - EntityType et = null; - // Pig zombie handling - if (name.equals("PIG_ZOMBIE") || name.equals("ZOMBIFIED_PIGLIN")) { - et = Enums.getIfPresent(EntityType.class, "ZOMBIFIED_PIGLIN") - .or(Enums.getIfPresent(EntityType.class, "PIG_ZOMBIE").or(EntityType.PIG)); - } else { - et = Enums.getIfPresent(EntityType.class, name).orNull(); - } + EntityType et = resolveEntityType(entity.toUpperCase(Locale.ENGLISH)); if (et == null) { - // Does not exist addon.logError("Bad entity type in " + obPhase.getPhaseName() + ": " + entity); addon.logError("Try one of these..."); addon.logError(Arrays.stream(EntityType.values()).filter(EntityType::isSpawnable) .filter(EntityType::isAlive).map(EntityType::name).collect(Collectors.joining(","))); return; } - if (et.isSpawnable() && et.isAlive()) { - if (mobs.getInt(entity) > 0) { - obPhase.addMob(et, mobs.getInt(entity)); - } else { - addon.logWarning("Bad entity weight for " + obPhase.getPhaseName() + ": " + entity - + ". Must be positive number above 1."); - } - } else { - addon.logError("Entity type is not spawnable " + obPhase.getPhaseName() + ": " + entity); - } + processMobEntry(obPhase, mobs, entity, et); + } + } + + private EntityType resolveEntityType(String name) { + // Pig zombie handling: accept both legacy and current name + if (name.equals("PIG_ZOMBIE") || name.equals("ZOMBIFIED_PIGLIN")) { + return Enums.getIfPresent(EntityType.class, "ZOMBIFIED_PIGLIN") + .or(Enums.getIfPresent(EntityType.class, "PIG_ZOMBIE").or(EntityType.PIG)); + } + return Enums.getIfPresent(EntityType.class, name).orNull(); + } + + private void processMobEntry(OneBlockPhase obPhase, ConfigurationSection mobs, String entity, EntityType et) { + if (!et.isSpawnable() || !et.isAlive()) { + addon.logError("Entity type is not spawnable " + obPhase.getPhaseName() + ": " + entity); + return; + } + if (mobs.getInt(entity) > 0) { + obPhase.addMob(et, mobs.getInt(entity)); + } else { + addon.logWarning("Bad entity weight for " + obPhase.getPhaseName() + ": " + entity + + ". Must be positive number above 1."); } } @@ -492,16 +520,8 @@ void addBlocks(OneBlockPhase obPhase, ConfigurationSection phase) { addMaterial(obPhase, material, Objects.toString(blocks.get(material))); } } else if (phase.isList(BLOCKS)) { - List> blocks = phase.getMapList(BLOCKS); - for (Map map : blocks) { - if (map.size() == 1) { - Map.Entry entry = map.entrySet().iterator().next(); - if (addMaterial(obPhase, Objects.toString(entry.getKey()), Objects.toString(entry.getValue()))) { - continue; - } - } - - addCustomBlockFromMap(obPhase, map); + for (Map map : phase.getMapList(BLOCKS)) { + processBlockMapEntry(obPhase, map); } } @@ -515,6 +535,16 @@ void addBlocks(OneBlockPhase obPhase, ConfigurationSection phase) { } } + private void processBlockMapEntry(OneBlockPhase obPhase, Map map) { + if (map.size() == 1) { + Map.Entry entry = map.entrySet().iterator().next(); + if (addMaterial(obPhase, Objects.toString(entry.getKey()), Objects.toString(entry.getValue()))) { + return; + } + } + addCustomBlockFromMap(obPhase, map); + } + private void addCustomBlockFromMap(OneBlockPhase obPhase, Map map) { int probability = Integer.parseInt(Objects.toString(map.get("probability"), "0")); Optional customBlock = OneBlockCustomBlockCreator.create(map); @@ -575,7 +605,7 @@ public OneBlockPhase getPhase(int blockCount) { */ public List getPhaseList() { return blockProbs.values().stream().map(OneBlockPhase::getPhaseName).filter(Objects::nonNull) - .map(n -> n.replace(" ", "_")).collect(Collectors.toList()); + .map(n -> n.replace(" ", "_")).toList(); } /** @@ -648,7 +678,7 @@ private boolean saveMainPhase(OneBlockPhase p) { phSec.set(FIRST_BLOCK, p.getFirstBlock().getMaterial().name()); } if (p.getPhaseBiome() != null) { - phSec.set(BIOME, p.getPhaseBiome().name()); + phSec.set(BIOME, p.getPhaseBiome().getKey().getKey()); } saveBlocks(phSec, p); saveEntities(phSec, p); @@ -721,7 +751,18 @@ private void saveEntities(ConfigurationSection phSec, OneBlockPhase phase) { private void saveBlocks(ConfigurationSection phSec, OneBlockPhase phase) { ConfigurationSection fixedBlocks = phSec.createSection(FIXED_BLOCKS); - phase.getFixedBlocks().forEach((k, v) -> fixedBlocks.set(String.valueOf(k), v.getMaterial().name())); + phase.getFixedBlocks().forEach((k, v) -> { + String value; + if (v.getChest() != null && v.getChest().size() == 1 && v.getChest().containsKey(0)) { + // Serialize as CHEST_WITH_X when there is exactly one item in slot 0 + value = CHEST_WITH_PREFIX + v.getChest().get(0).getType().name(); + } else if (v.getMaterial() != null) { + value = v.getMaterial().name(); + } else { + value = Material.CHEST.name(); + } + fixedBlocks.set(String.valueOf(k), value); + }); ConfigurationSection blocks = phSec.createSection(BLOCKS); phase.getBlocks().forEach((k, v) -> blocks.set(k.name(), v)); diff --git a/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/CraftEngineCustomBlock.java b/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/CraftEngineCustomBlock.java new file mode 100644 index 00000000..0c31d304 --- /dev/null +++ b/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/CraftEngineCustomBlock.java @@ -0,0 +1,48 @@ +package world.bentobox.aoneblock.oneblocks.customblock; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.bukkit.Material; +import org.bukkit.block.Block; + +import world.bentobox.aoneblock.AOneBlock; +import world.bentobox.aoneblock.oneblocks.OneBlockCustomBlock; +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.hooks.CraftEngineHook; + +public class CraftEngineCustomBlock implements OneBlockCustomBlock { + private final String blockId; + + public CraftEngineCustomBlock(String blockId) { + this.blockId = blockId; + } + + public static Optional fromId(String id) { + if (CraftEngineHook.exists(id)) { + return Optional.of(new CraftEngineCustomBlock(id)); + } + return Optional.empty(); + } + + public static Optional fromMap(Map map) { + return Optional + .ofNullable(Objects.toString(map.get("id"), null)) + .flatMap(CraftEngineCustomBlock::fromId); + } + + @Override + public void execute(AOneBlock addon, Block block) { + try { + block.setType(Material.AIR); + if (!CraftEngineHook.placeBlock(block.getLocation(), blockId)) { + BentoBox.getInstance().logError("Could not place CraftEngine block " + blockId); + block.setType(Material.STONE); + } + } catch (Exception e) { + BentoBox.getInstance().logError("Could not place CraftEngine block " + blockId + ": " + e.getMessage()); + block.setType(Material.STONE); + } + } +} diff --git a/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/MythicMobCustomBlock.java b/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/MythicMobCustomBlock.java index 515c72ef..cc52b659 100644 --- a/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/MythicMobCustomBlock.java +++ b/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/MythicMobCustomBlock.java @@ -113,7 +113,7 @@ public void execute(AOneBlock addon, Block block) { } MythicMobsHook hook = hookOpt.get(); - MythicMobRecord record = new MythicMobRecord( + MythicMobRecord mobRecord = new MythicMobRecord( mob, displayName != null ? displayName : mob, level, @@ -132,8 +132,8 @@ public void execute(AOneBlock addon, Block block) { // unnecessary for AOneBlock's synchronous block replace. Fall back to the // 3-arg overload (BentoBox >= 3.14.0, 40-tick delay) and finally the 2-arg // method on older BentoBox. MakeSpace still runs from the callback. - if (!invokeWithCallback(hook, record, spawnLoc, onSpawn)) { - hook.spawnMythicMob(record, spawnLoc); + if (!invokeWithCallback(hook, mobRecord, spawnLoc, onSpawn)) { + hook.spawnMythicMob(mobRecord, spawnLoc); } block.getWorld().playSound(block.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 1F, 2F); @@ -156,13 +156,13 @@ public void execute(AOneBlock addon, Block block) { * * @return true if either callback overload was invoked successfully */ - private boolean invokeWithCallback(MythicMobsHook hook, MythicMobRecord record, Location spawnLoc, + private boolean invokeWithCallback(MythicMobsHook hook, MythicMobRecord mobRecord, Location spawnLoc, Consumer onSpawn) { // Preferred: 4-arg overload with explicit zero delay. try { Method m = MythicMobsHook.class.getMethod("spawnMythicMob", MythicMobRecord.class, Location.class, Consumer.class, long.class); - m.invoke(hook, record, spawnLoc, onSpawn, 0L); + m.invoke(hook, mobRecord, spawnLoc, onSpawn, 0L); return true; } catch (NoSuchMethodException ignored) { // fall through to the 3-arg form @@ -176,7 +176,7 @@ private boolean invokeWithCallback(MythicMobsHook hook, MythicMobRecord record, try { Method m = MythicMobsHook.class.getMethod("spawnMythicMob", MythicMobRecord.class, Location.class, Consumer.class); - m.invoke(hook, record, spawnLoc, onSpawn); + m.invoke(hook, mobRecord, spawnLoc, onSpawn); return true; } catch (NoSuchMethodException e) { return false; diff --git a/src/main/java/world/bentobox/aoneblock/panels/PhasesPanel.java b/src/main/java/world/bentobox/aoneblock/panels/PhasesPanel.java index 5aff1d62..d0a9e72f 100644 --- a/src/main/java/world/bentobox/aoneblock/panels/PhasesPanel.java +++ b/src/main/java/world/bentobox/aoneblock/panels/PhasesPanel.java @@ -14,7 +14,6 @@ import java.util.Map; import java.util.stream.Collectors; -import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.event.inventory.ClickType; @@ -591,7 +590,7 @@ private static String insertNewlines(String input, int interval) { int lastAmpIndex = -2; while (index < input.length()) { - if (input.charAt(index) == ChatColor.COLOR_CHAR && index < (input.length() - 1)) { + if (input.charAt(index) == '\u00A7' && index < (input.length() - 1)) { lastAmpIndex = index; activeColor = input.charAt(index + 1); } @@ -609,7 +608,7 @@ private static String insertNewlines(String input, int interval) { result.append(input, index, breakPoint).append('\n'); if (lastAmpIndex >= 0) { // Append color code - result.append(ChatColor.COLOR_CHAR); + result.append('\u00A7'); result.append(activeColor); result.append(" "); } diff --git a/src/main/java/world/bentobox/aoneblock/requests/LocationStatsHandler.java b/src/main/java/world/bentobox/aoneblock/requests/LocationStatsHandler.java index f1708428..7192eb99 100644 --- a/src/main/java/world/bentobox/aoneblock/requests/LocationStatsHandler.java +++ b/src/main/java/world/bentobox/aoneblock/requests/LocationStatsHandler.java @@ -60,10 +60,9 @@ public Object handle(Map map) { } User user = User.getInstance((UUID)map.get(PLAYER)); - if (user == null || !user.isOnline()) { + if (!user.isOnline()) { return Collections.emptyMap(); } - // No null check required Map result = new HashMap<>(); result.put("count", addon.getPlaceholdersManager().getCountByLocation(user)); result.put("doneScale", addon.getPlaceholdersManager().getDoneScaleByLocation(user)); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 5708bea7..d339659a 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -82,6 +82,38 @@ world: RED: 0 BLUE: 0 GREEN: 128 + # Chest particle effects - particles that appear when a special chest spawns based on rarity. + # Valid particle names: https://jd.papermc.io/paper/1.21/org/bukkit/Particle.html + # Use NONE to disable particles for that rarity level. + # Note: The color setting is only used for DUST-type particles. + chest-particles: + # Particle type for UNCOMMON rarity chests. + uncommon-particle: DUST + # Color of UNCOMMON chest particles (only used for DUST-type particles). Default: yellow + uncommon-color: + ==: Color + ALPHA: 255 + RED: 255 + GREEN: 255 + BLUE: 0 + # Particle type for RARE rarity chests. + rare-particle: DUST + # Color of RARE chest particles (only used for DUST-type particles). Default: white + rare-color: + ==: Color + ALPHA: 255 + RED: 255 + GREEN: 255 + BLUE: 255 + # Particle type for EPIC rarity chests. + epic-particle: DUST + # Color of EPIC chest particles (only used for DUST-type particles). Default: fuchsia + epic-color: + ==: Color + ALPHA: 255 + RED: 255 + GREEN: 0 + BLUE: 255 # Clear blocks when spawning mobs. # Mobs break blocks when they spawn is to prevent players from building a box around the magic block, # having the mob spawn, and then die by suffocation, i.e., it's a cheat prevention. @@ -199,7 +231,7 @@ world: # World flags. These are boolean settings for various flags for this world flags: CREEPER_DAMAGE: true - OBSIDIAN_SCOOPING: true + OBSIDIAN_SCOOPING: false ISLAND_RESPAWN: true CREEPER_GRIEFING: false VISITOR_KEEP_INVENTORY: false diff --git a/src/main/resources/locales/cs.yml b/src/main/resources/locales/cs.yml index a05c1082..e5fd039e 100644 --- a/src/main/resources/locales/cs.yml +++ b/src/main/resources/locales/cs.yml @@ -33,6 +33,9 @@ aoneblock: color: RED style: SEGMENTED_20 not-active: '&c Boss Bar není pro tento ostrov aktivní' + actionbar: + status: "&a Fáze: &b [phase-name] &d | &a Bloky: &b [done] &d / &b [total] &d | &a Postup: &b [percent-done]" + not-active: "&c Action Bar není pro tento ostrov aktivní" commands: admin: setcount: @@ -71,6 +74,10 @@ aoneblock: description: přepíná fázový šéfový bar status_on: '&b Bossbar se otočil &a zapnul' status_off: '&b Bossbar se &c otočil' + actionbar: + description: přepíná action bar fáze + status_on: "&b Action Bar &a zapnut" + status_off: "&b Action Bar &c vypnut" setcount: parameters: description: nastavte počet bloků na dříve dokončenou hodnotu @@ -88,6 +95,7 @@ aoneblock: cooldown: '&c Další fáze bude dostupná za [number] sekund!' placeholders: infinite: Nekonečný + my-island-phase-default: Neznámá gui: titles: phases: '&0&l Jednoblokové fáze' diff --git a/src/main/resources/locales/de.yml b/src/main/resources/locales/de.yml index fadfec1d..a9dfa70c 100644 --- a/src/main/resources/locales/de.yml +++ b/src/main/resources/locales/de.yml @@ -33,6 +33,9 @@ aoneblock: color: RED style: SEGMENTED_20 not-active: '&c Boss Bar ist für diese Insel nicht aktiv' + actionbar: + status: "&a Phase: &b [phase-name] &d | &a Blöcke: &b [done] &d / &b [total] &d | &a Fortschritt: &b [percent-done]" + not-active: "&c Action Bar ist für diese Insel nicht aktiv" commands: admin: setcount: @@ -79,6 +82,10 @@ aoneblock: description: Phase Boss Bar umschalten status_on: '&b Bossbar &a eingeschaltet' status_off: '&b Bossbar &c ausgeschaltet' + actionbar: + description: "schaltet die Phasen-Aktionsleiste um" + status_on: "&b Action Bar &a eingeschaltet" + status_off: "&b Action Bar &c ausgeschaltet" setcount: parameters: description: Setzen Sie die Blockanzahl auf den zuvor abgeschlossenen Wert @@ -104,6 +111,7 @@ aoneblock: cooldown: '&c Die nächste Stufe ist in [number] Sekunden verfügbar!' placeholders: infinite: Unendlich + my-island-phase-default: Unbekannt gui: titles: phases: '&0&l OneBlock-Phasen' diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index f2d7c059..6ef260f6 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -102,6 +102,7 @@ aoneblock: cooldown: "&c Next phase will be available in [number] seconds!" placeholders: infinite: Infinite + my-island-phase-default: Unknown gui: titles: phases: '&0&l OneBlock Phases' diff --git a/src/main/resources/locales/es.yml b/src/main/resources/locales/es.yml index efc6e2fa..009e1664 100644 --- a/src/main/resources/locales/es.yml +++ b/src/main/resources/locales/es.yml @@ -33,6 +33,9 @@ aoneblock: color: RED style: SEGMENTED_20 not-active: '&c Boss Bar no está activo para esta isla' + actionbar: + status: "&a Fase: &b [phase-name] &d | &a Bloques: &b [done] &d / &b [total] &d | &a Progreso: &b [percent-done]" + not-active: "&c La barra de acción no está activa para esta isla" commands: admin: setcount: @@ -77,6 +80,10 @@ aoneblock: description: Barra de jefe de fase de alojamiento status_on: '&b Bossbar &a encendió' status_off: '&b Bossbar &a apagó' + actionbar: + description: alterna la barra de acción de fase + status_on: "&b Barra de acción &a activada" + status_off: "&b Barra de acción &c desactivada" setcount: parameters: description: Establece la cantidad de bloques a un valor previamente completado @@ -98,6 +105,7 @@ aoneblock: cooldown: '&c ¡La siguiente etapa estará disponible en [number] segundos!' placeholders: infinite: Infinito + my-island-phase-default: Desconocida gui: titles: phases: '&0&l Fases de OneBlock' diff --git a/src/main/resources/locales/fr.yml b/src/main/resources/locales/fr.yml index 24e868da..99679f81 100644 --- a/src/main/resources/locales/fr.yml +++ b/src/main/resources/locales/fr.yml @@ -33,6 +33,9 @@ aoneblock: color: RED style: SEGMENTED_20 not-active: '&c Boss Bar n''est pas actif pour cette île' + actionbar: + status: "&a Phase : &b [phase-name] &d | &a Blocs : &b [done] &d / &b [total] &d | &a Progression : &b [percent-done]" + not-active: "&c La barre d'action n'est pas active pour cette île" commands: admin: setcount: @@ -75,6 +78,10 @@ aoneblock: description: bascule la barre de boss de phase status_on: '&b Bossbar a &a activé' status_off: '&b Bossbar &a désactivé' + actionbar: + description: "active/désactive la barre d'action de phase" + status_on: "&b Barre d'action &a activée" + status_off: "&b Barre d'action &c désactivée" setcount: parameters: description: définir le nombre de blocs à la valeur précédemment terminée @@ -96,6 +103,7 @@ aoneblock: cooldown: '&c La prochaine étape sera disponible dans [number] secondes!' placeholders: infinite: Infini + my-island-phase-default: Inconnue gui: titles: phases: '&0&l Phases OneBlock' diff --git a/src/main/resources/locales/hr.yml b/src/main/resources/locales/hr.yml index 288a7993..1c4fd72e 100644 --- a/src/main/resources/locales/hr.yml +++ b/src/main/resources/locales/hr.yml @@ -33,6 +33,9 @@ aoneblock: color: RED style: SEGMENTED_20 not-active: '&c Boss bar nije aktivan za ovaj otok' + actionbar: + status: "&a Faza: &b [phase-name] &d | &a Blokovi: &b [done] &d / &b [total] &d | &a Napredak: &b [percent-done]" + not-active: "&c Akcijska traka nije aktivna za ovaj otok" commands: admin: setcount: @@ -73,6 +76,10 @@ aoneblock: description: prebacuje fazni boss bar status_on: '&b Bossbar se &a uključio' status_off: '&b Bossbar se &a isključio' + actionbar: + description: uključuje/isključuje akcijsku traku faze + status_on: "&b Akcijska traka &a uključena" + status_off: "&b Akcijska traka &c isključena" setcount: parameters: description: postaviti broj blokova na prethodno dovršenu vrijednost @@ -92,6 +99,7 @@ aoneblock: cooldown: '&c Sljedeća faza bit će dostupna za [number] sekundi!' placeholders: infinite: Beskonačno + my-island-phase-default: Nepoznato gui: titles: phases: '&0&l OneBlock faze' diff --git a/src/main/resources/locales/hu.yml b/src/main/resources/locales/hu.yml index 90c6d74f..1ed94f8a 100644 --- a/src/main/resources/locales/hu.yml +++ b/src/main/resources/locales/hu.yml @@ -34,6 +34,9 @@ aoneblock: color: RED style: SEGMENTED_20 not-active: '&c A Boss Bar nem aktív ezen a szigeten' + actionbar: + status: "&a Fázis: &b [phase-name] &d | &a Blokkok: &b [done] &d / &b [total] &d | &a Haladás: &b [percent-done]" + not-active: "&c Az akciósáv nem aktív ezen a szigeten" commands: admin: setcount: @@ -74,6 +77,10 @@ aoneblock: description: váltók fázisú főnök sáv status_on: '&b Bossbar &a bekapcsolt' status_off: '&b Bossbar &c kikapcsolt' + actionbar: + description: fázis akciósáv ki/bekapcsolása + status_on: "&b Akciósáv &a bekapcsolva" + status_off: "&b Akciósáv &c kikapcsolva" setcount: parameters: description: állítsa be a blokkszámot a korábban kitöltött értékre @@ -95,6 +102,7 @@ aoneblock: cooldown: '&c A következő szakasz [number] másodpercen belül elérhető lesz!' placeholders: infinite: Végtelen + my-island-phase-default: Ismeretlen gui: titles: phases: '&0&l OneBlock fázisok' diff --git a/src/main/resources/locales/id.yml b/src/main/resources/locales/id.yml index 3fbfdca8..9639d5e6 100644 --- a/src/main/resources/locales/id.yml +++ b/src/main/resources/locales/id.yml @@ -33,6 +33,9 @@ aoneblock: color: RED style: SEGMENTED_20 not-active: '&c Bos Bar tidak aktif untuk pulau ini' + actionbar: + status: "&a Fase: &b [phase-name] &d | &a Blok: &b [done] &d / &b [total] &d | &a Kemajuan: &b [percent-done]" + not-active: "&c Action Bar tidak aktif untuk pulau ini" commands: admin: setcount: @@ -71,6 +74,10 @@ aoneblock: description: Mengalogkan Bar Bos Fase status_on: '&b Bos bar &a dihidupkan' status_off: '&b Bos bar &c dimatikan' + actionbar: + description: mengaktifkan/menonaktifkan action bar fase + status_on: "&b Action Bar &a diaktifkan" + status_off: "&b Action Bar &c dinonaktifkan" setcount: parameters: description: Setel jumlah blok ke nilai yang sebelumnya selesai @@ -90,6 +97,7 @@ aoneblock: cooldown: Fase berikutnya akan tersedia dalam detik [number]! placeholders: infinite: Tak terbatas + my-island-phase-default: Tidak diketahui gui: titles: phases: OneBlock Phases diff --git a/src/main/resources/locales/it.yml b/src/main/resources/locales/it.yml index a1cefc05..9b0a70c6 100644 --- a/src/main/resources/locales/it.yml +++ b/src/main/resources/locales/it.yml @@ -33,6 +33,9 @@ aoneblock: color: RED style: SEGMENTED_20 not-active: '&c Boss Bar non è attivo per quest''isola' + actionbar: + status: "&a Fase: &b [phase-name] &d | &a Blocchi: &b [done] &d / &b [total] &d | &a Progresso: &b [percent-done]" + not-active: "&c La barra d'azione non è attiva per quest'isola" commands: admin: setcount: @@ -73,6 +76,10 @@ aoneblock: description: barra boss di fase di levetta status_on: '&b Bossbar si è &a acceso' status_off: '&b Bossbar si è &c spento' + actionbar: + description: "attiva/disattiva la barra d'azione della fase" + status_on: "&b Barra d'azione &a attivata" + status_off: "&b Barra d'azione &c disattivata" setcount: parameters: description: Imposta il conteggio dei blocchi sul valore precedentemente completato @@ -90,6 +97,7 @@ aoneblock: cooldown: '&c Next phase will be available in [number] seconds!' placeholders: infinite: Infinito + my-island-phase-default: Sconosciuta gui: titles: phases: '&0&l fasi di un blocco' diff --git a/src/main/resources/locales/ja.yml b/src/main/resources/locales/ja.yml index 53ca82cf..3e42c38a 100644 --- a/src/main/resources/locales/ja.yml +++ b/src/main/resources/locales/ja.yml @@ -33,6 +33,9 @@ aoneblock: color: RED style: SEGMENTED_20 not-active: '&c この島ではボスバーがアクティブではありません' + actionbar: + status: "&a フェーズ: &b [phase-name] &d | &a ブロック数: &b [done] &d / &b [total] &d | &a 進行状況: &b [percent-done]" + not-active: "&c この島ではアクションバーが有効ではありません" commands: admin: setcount: @@ -71,6 +74,10 @@ aoneblock: description: トグルフェーズボスバー status_on: '&b ボスバーが&a オン' status_off: '&b ボスバーが&aオフ' + actionbar: + description: フェーズアクションバーの切り替え + status_on: "&b アクションバーを &a オン &bにしました" + status_off: "&b アクションバーを &c オフ &bにしました" setcount: parameters: <カウント> description: ブロック数を以前に完了した値に設定する @@ -88,6 +95,7 @@ aoneblock: cooldown: '&c [number] 秒で次のステージへ!' placeholders: infinite: 無限 + my-island-phase-default: 不明 gui: titles: phases: '&0&l ワンブロックフェーズ' diff --git a/src/main/resources/locales/pl.yml b/src/main/resources/locales/pl.yml index ce9e85d4..002cac29 100644 --- a/src/main/resources/locales/pl.yml +++ b/src/main/resources/locales/pl.yml @@ -33,6 +33,9 @@ aoneblock: color: RED style: SEGMENTED_20 not-active: '&c Boss Bar nie jest aktywny dla tej wyspy' + actionbar: + status: "&a Faza: &b [phase-name] &d | &a Bloki: &b [done] &d / &b [total] &d | &a Postęp: &b [percent-done]" + not-active: "&c Pasek akcji nie jest aktywny dla tej wyspy" commands: admin: setcount: @@ -71,6 +74,10 @@ aoneblock: description: Przełącza fazę boss status_on: '&b Bossbar &a włączył' status_off: '&b Bossbar &a wyłączył' + actionbar: + description: przełącza pasek akcji fazy + status_on: "&b Pasek akcji &a włączony" + status_off: "&b Pasek akcji &c wyłączony" setcount: parameters: description: ustaw liczbę bloków na poprzednio uzupełnioną wartość @@ -90,6 +97,7 @@ aoneblock: cooldown: '&c Następny etap będzie dostępny za [number] sekund!' placeholders: infinite: Nieskończony + my-island-phase-default: Nieznana gui: titles: phases: '&0&l Fazy OneBlock' diff --git a/src/main/resources/locales/pt.yml b/src/main/resources/locales/pt.yml index 6951ed74..7617a2e1 100644 --- a/src/main/resources/locales/pt.yml +++ b/src/main/resources/locales/pt.yml @@ -33,6 +33,9 @@ aoneblock: color: RED style: SEGMENTED_20 not-active: '&c O Boss Bar não está ativo para esta ilha' + actionbar: + status: "&a Fase: &b [phase-name] &d | &a Blocos: &b [done] &d / &b [total] &d | &a Progresso: &b [percent-done]" + not-active: "&c A barra de ação não está ativa para esta ilha" commands: admin: setcount: @@ -73,6 +76,10 @@ aoneblock: description: Alterna o Boss Boss Bar status_on: '&b Bossbar &a ligado' status_off: '&b Bossbar &c desligado' + actionbar: + description: alterna a barra de ação de fase + status_on: "&b Barra de ação &a ativada" + status_off: "&b Barra de ação &c desativada" setcount: parameters: description: Defina a contagem de blocos para o valor previamente concluído @@ -90,6 +97,7 @@ aoneblock: cooldown: '&c A próxima fase estará disponível em [number] segundos!' placeholders: infinite: Infinito + my-island-phase-default: Desconhecida gui: titles: phases: Oneblock Fases diff --git a/src/main/resources/locales/ru.yml b/src/main/resources/locales/ru.yml index 84fa1491..a65464dc 100644 --- a/src/main/resources/locales/ru.yml +++ b/src/main/resources/locales/ru.yml @@ -97,6 +97,7 @@ aoneblock: cooldown: Следующая фаза станет доступна через [number] секунд! placeholders: infinite: Бесконечность + my-island-phase-default: Неизвестно gui: titles: phases: Фазы OneBlock diff --git a/src/main/resources/locales/tr.yml b/src/main/resources/locales/tr.yml index 4cac9fc0..f17feee6 100644 --- a/src/main/resources/locales/tr.yml +++ b/src/main/resources/locales/tr.yml @@ -33,6 +33,9 @@ aoneblock: color: RED style: SEGMENTED_20 not-active: '&c Patron Bar bu ada için aktif değil' + actionbar: + status: "&a Aşama: &b [phase-name] &d | &a Bloklar: &b [done] &d / &b [total] &d | &a İlerleme: &b [percent-done]" + not-active: "&c Bu ada için eylem çubuğu aktif değil" commands: admin: setcount: @@ -73,6 +76,10 @@ aoneblock: description: Faz patron çubuğunu değiştirir status_on: '&b Bossbar &a açıldı' status_off: '&b Bossbar &c kapandı' + actionbar: + description: aşama eylem çubuğunu açar/kapatır + status_on: "&b Eylem Çubuğu &a açıldı" + status_off: "&b Eylem Çubuğu &c kapatıldı" setcount: parameters: description: blok sayısını önceden tamamlanmış değere ayarla @@ -90,6 +97,7 @@ aoneblock: cooldown: '&c Bir sonraki aşama [number] saniye içinde hazır olacak!' placeholders: infinite: Sonsuz + my-island-phase-default: Bilinmiyor gui: titles: phases: '&0&l TekBlok Aşamaları' diff --git a/src/main/resources/locales/uk.yml b/src/main/resources/locales/uk.yml index 8e5bf5d2..194cde57 100644 --- a/src/main/resources/locales/uk.yml +++ b/src/main/resources/locales/uk.yml @@ -33,6 +33,9 @@ aoneblock: color: RED style: SEGMENTED_20 not-active: '&c Boss Bar не активний для цього острова' + actionbar: + status: "&a Фаза: &b [phase-name] &d | &a Блоки: &b [done] &d / &b [total] &d | &a Прогрес: &b [percent-done]" + not-active: "&c Панель дій не активна для цього острова" commands: admin: setcount: @@ -71,6 +74,10 @@ aoneblock: description: перемикає фазу боса status_on: '&b Bossbar &a увімкнув' status_off: '&b Bossbar &c вимкнувся' + actionbar: + description: перемикає панель дій фази + status_on: "&b Панель дій &a увімкнена" + status_off: "&b Панель дій &c вимкнена" setcount: parameters: description: встановити кількість блоків до попередньо завершеного значення @@ -92,6 +99,7 @@ aoneblock: cooldown: '&c Наступна фаза буде доступна через [number] секунд!' placeholders: infinite: Нескінченний + my-island-phase-default: Невідомо gui: titles: phases: '&0&l Фази одного блоку' diff --git a/src/main/resources/locales/vi.yml b/src/main/resources/locales/vi.yml index 479c6382..a1f63203 100644 --- a/src/main/resources/locales/vi.yml +++ b/src/main/resources/locales/vi.yml @@ -33,6 +33,9 @@ aoneblock: color: RED style: SEGMENTED_20 not-active: '&c Boss Bar không hoạt động cho hòn đảo này' + actionbar: + status: "&a Giai đoạn: &b [phase-name] &d | &a Khối: &b [done] &d / &b [total] &d | &a Tiến độ: &b [percent-done]" + not-active: "&c Thanh hành động không hoạt động cho hòn đảo này" commands: admin: setcount: @@ -73,6 +76,10 @@ aoneblock: description: bật thanh Boss giai đoạn status_on: '&b Bossbar &a bật lên' status_off: '&b Bossbar &c tắt' + actionbar: + description: bật/tắt thanh hành động giai đoạn + status_on: "&b Thanh hành động đã &a bật" + status_off: "&b Thanh hành động đã &c tắt" setcount: parameters: description: đặt số khối thành giá trị đã hoàn thành trước đó @@ -90,6 +97,7 @@ aoneblock: cooldown: Giai đoạn tiếp theo sẽ có sau [number] giây! placeholders: infinite: Vô hạn + my-island-phase-default: Không xác định gui: titles: phases: '&0&l Giai đoạn OneBlock' diff --git a/src/main/resources/locales/zh-CN.yml b/src/main/resources/locales/zh-CN.yml index 5dcdf849..08de70e4 100644 --- a/src/main/resources/locales/zh-CN.yml +++ b/src/main/resources/locales/zh-CN.yml @@ -32,6 +32,9 @@ aoneblock: color: RED style: SEGMENTED_20 not-active: '&c 老板酒吧对这个岛不活跃' + actionbar: + status: "&a 阶段: &b [phase-name] &d | &a 方块数: &b [done] &d / &b [total] &d | &a 进度: &b [percent-done]" + not-active: "&c 该岛屿的动作栏未激活" commands: admin: setcount: @@ -68,6 +71,10 @@ aoneblock: description: 切换相位栏 status_on: '&b Bossbar turned &a on' status_off: '&b Bossbar turned &c off' + actionbar: + description: 切换阶段动作栏 + status_on: "&b 动作栏已 &a 开启" + status_off: "&b 动作栏已 &c 关闭" setcount: parameters: description: 将块计数设置为先前完成的值 @@ -85,6 +92,7 @@ aoneblock: cooldown: '&c [number] 秒后即可进入下一阶段!' placeholders: infinite: 无限 + my-island-phase-default: 未知 gui: titles: phases: '&0&l OneBlock 阶段' diff --git a/src/main/resources/locales/zh-TW.yml b/src/main/resources/locales/zh-TW.yml index bf6f984b..b11bc7c2 100644 --- a/src/main/resources/locales/zh-TW.yml +++ b/src/main/resources/locales/zh-TW.yml @@ -32,6 +32,9 @@ aoneblock: color: RED style: SEGMENTED_20 not-active: '&c 老闆酒吧對這個島不活躍' + actionbar: + status: "&a 階段: &b [phase-name] &d | &a 方塊數: &b [done] &d / &b [total] &d | &a 進度: &b [percent-done]" + not-active: "&c 該島嶼的動作欄未啟用" commands: admin: setcount: @@ -68,6 +71,10 @@ aoneblock: description: 切換相位欄 status_on: '&b Bossbar&a 打開' status_off: '&b Bossbar&c 關閉' + actionbar: + description: 切換階段動作欄 + status_on: "&b 動作欄已 &a 開啟" + status_off: "&b 動作欄已 &c 關閉" setcount: parameters: <計數> description: 將區塊計數設定為之前完成的值 @@ -85,6 +92,7 @@ aoneblock: cooldown: '&c [number] 秒後即可進入下一階段!' placeholders: infinite: 無窮 + my-island-phase-default: 未知 gui: titles: phases: '&0&l OneBlock 階段' diff --git a/src/main/resources/phases/0_plains.yml b/src/main/resources/phases/0_plains.yml index 8190682e..bd21f744 100644 --- a/src/main/resources/phases/0_plains.yml +++ b/src/main/resources/phases/0_plains.yml @@ -13,7 +13,7 @@ 3: OAK_LOG 4: OAK_LOG 5: OAK_LOG - 50: SPONGE + 700: CHEST_WITH_WATER_BUCKET # Hologram Lines to Display # The First (Before Phase 1) Hologram is Located in your Locale. holograms: diff --git a/src/test/java/world/bentobox/aoneblock/AOneBlockTest.java b/src/test/java/world/bentobox/aoneblock/AOneBlockTest.java index daa96905..3ed47025 100644 --- a/src/test/java/world/bentobox/aoneblock/AOneBlockTest.java +++ b/src/test/java/world/bentobox/aoneblock/AOneBlockTest.java @@ -1,6 +1,7 @@ package world.bentobox.aoneblock; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -149,7 +150,7 @@ public void setUp() throws Exception { } - private void add(Path path, JarOutputStream tempJarOutputStream) throws FileNotFoundException, IOException { + private void add(Path path, JarOutputStream tempJarOutputStream) throws IOException { try (FileInputStream fis = new FileInputStream(path.toFile())) { byte[] buffer = new byte[1024]; int bytesRead = 0; @@ -249,6 +250,19 @@ void testGetOneBlocksIsland() { assertEquals(island.getUniqueId(), i.getUniqueId()); } + /** + * Test method for {@link world.bentobox.aoneblock.AOneBlock#loadData()} when + * oneBlockManager is null (addon not yet enabled). Should return false without + * throwing NullPointerException. + */ + @Test + void testLoadDataWhenManagerIsNull() { + // oneBlockManager is null before onEnable() is called + assertNull(addon.getOneBlockManager()); + // Should not throw NPE, should return false (no error) + assertFalse(addon.loadData()); + } + /** * Test method for * {@link world.bentobox.aoneblock.AOneBlock#getOneBlockManager()}. diff --git a/src/test/java/world/bentobox/aoneblock/CommonTestSetup.java b/src/test/java/world/bentobox/aoneblock/CommonTestSetup.java index 3c089541..0b61fc86 100644 --- a/src/test/java/world/bentobox/aoneblock/CommonTestSetup.java +++ b/src/test/java/world/bentobox/aoneblock/CommonTestSetup.java @@ -130,6 +130,7 @@ public abstract class CommonTestSetup { protected PlaceholdersManager phm; + @SuppressWarnings("java:S1130") // subclasses override and declare checked exceptions @BeforeEach public void setUp() throws Exception { // Processes the @Mock annotations and initializes the field @@ -206,6 +207,7 @@ public void setUp() throws Exception { when(island.getMemberSet()).thenReturn(ImmutableSet.of(uuid)); // Enable reporting from Flags class + @SuppressWarnings("deprecation") MetadataValue mdv = new FixedMetadataValue(plugin, "_why_debug"); when(mockPlayer.getMetadata(anyString())).thenReturn(Collections.singletonList(mdv)); @@ -228,9 +230,6 @@ public void setUp() throws Exception { // 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); @@ -302,8 +301,7 @@ public void checkSpigotMessage(String expectedMessage, int expectedOccurrences) * @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); + return new EntityExplodeEvent(entity, l, list, 0, org.bukkit.ExplosionResult.DESTROY); } public PlayerDeathEvent getPlayerDeathEvent(Player player, List drops, int droppedExp, int newExp, diff --git a/src/test/java/world/bentobox/aoneblock/PlaceholdersManagerTest.java b/src/test/java/world/bentobox/aoneblock/PlaceholdersManagerTest.java index 11d7690f..3413bf79 100644 --- a/src/test/java/world/bentobox/aoneblock/PlaceholdersManagerTest.java +++ b/src/test/java/world/bentobox/aoneblock/PlaceholdersManagerTest.java @@ -40,6 +40,7 @@ public void setUp() throws Exception { // User when(user.getLocation()).thenReturn(location); when(user.getTranslation("aoneblock.placeholders.infinite")).thenReturn("Infinite"); + when(user.getTranslation("aoneblock.placeholders.my-island-phase-default")).thenReturn("Unknown"); when(user.getWorld()).thenReturn(world); // Addon when(addon.getIslands()).thenReturn(im); @@ -103,7 +104,7 @@ void testGetPhase() { assertEquals("first", pm.getPhase(user)); when(im.getIsland(world, user)).thenReturn(null); when(im.getIslands(world, user)).thenReturn(List.of()); - assertEquals("", pm.getPhase(user)); + assertEquals("Unknown", pm.getPhase(user)); } /** @@ -117,7 +118,7 @@ void testGetCount() { assertEquals("1000", pm.getCount(user)); when(im.getIsland(world, user)).thenReturn(null); when(im.getIslands(world, user)).thenReturn(List.of()); - assertEquals("", pm.getCount(user)); + assertEquals("0", pm.getCount(user)); } /** @@ -202,7 +203,7 @@ void testGetPercentDone() { assertEquals("70%", pm.getPercentDone(user)); when(im.getIsland(world, user)).thenReturn(null); when(im.getIslands(world, user)).thenReturn(List.of()); - assertEquals("", pm.getPercentDone(user)); + assertEquals("0%", pm.getPercentDone(user)); } /** diff --git a/src/test/java/world/bentobox/aoneblock/commands/admin/AdminSanityCheckTest.java b/src/test/java/world/bentobox/aoneblock/commands/admin/AdminSanityCheckTest.java index 64f7a1ee..f2eba35d 100644 --- a/src/test/java/world/bentobox/aoneblock/commands/admin/AdminSanityCheckTest.java +++ b/src/test/java/world/bentobox/aoneblock/commands/admin/AdminSanityCheckTest.java @@ -4,7 +4,6 @@ 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.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/src/test/java/world/bentobox/aoneblock/commands/island/IslandRespawnBlockCommandTest.java b/src/test/java/world/bentobox/aoneblock/commands/island/IslandRespawnBlockCommandTest.java index 958164aa..60f21987 100644 --- a/src/test/java/world/bentobox/aoneblock/commands/island/IslandRespawnBlockCommandTest.java +++ b/src/test/java/world/bentobox/aoneblock/commands/island/IslandRespawnBlockCommandTest.java @@ -50,6 +50,7 @@ class IslandRespawnBlockCommandTest extends CommonTestSetup { /** * @throws java.lang.Exception */ + @Override @BeforeEach public void setUp() throws Exception { super.setUp(); @@ -75,6 +76,7 @@ public void setUp() throws Exception { /** * @throws java.lang.Exception */ + @Override @AfterEach public void tearDown() throws Exception { super.tearDown(); @@ -131,7 +133,6 @@ void testCanExecuteNoIsland() { */ @Test void testExecuteUserStringListOfString() { - // when(block.getType()).thenReturn(Material.STONE); rbc.execute(user, "", List.of()); verify(user).sendMessage("aoneblock.commands.respawn-block.block-respawned"); } diff --git a/src/test/java/world/bentobox/aoneblock/commands/island/IslandSetCountCommandTest.java b/src/test/java/world/bentobox/aoneblock/commands/island/IslandSetCountCommandTest.java index 973b180a..d1d024d7 100644 --- a/src/test/java/world/bentobox/aoneblock/commands/island/IslandSetCountCommandTest.java +++ b/src/test/java/world/bentobox/aoneblock/commands/island/IslandSetCountCommandTest.java @@ -113,7 +113,6 @@ public void setUp() throws Exception { when(ac.getAddon()).thenReturn(addon); // Islands - //when(plugin.getIslands()).thenReturn(im); when(im.getIsland(world, user)).thenReturn(island); when(im.hasIsland(world, user)).thenReturn(true); when(im.inTeam(world, uuid)).thenReturn(true); @@ -121,7 +120,6 @@ public void setUp() throws Exception { when(island.getRank(user)).thenReturn(RanksManager.MEMBER_RANK); // IWM - //when(plugin.getIWM()).thenReturn(iwm); when(iwm.getPermissionPrefix(any())).thenReturn("bskyblock."); // Settings diff --git a/src/test/java/world/bentobox/aoneblock/listeners/BlockListenerTest.java b/src/test/java/world/bentobox/aoneblock/listeners/BlockListenerTest.java index 91e7e83a..c22db089 100644 --- a/src/test/java/world/bentobox/aoneblock/listeners/BlockListenerTest.java +++ b/src/test/java/world/bentobox/aoneblock/listeners/BlockListenerTest.java @@ -73,6 +73,7 @@ public class BlockListenerTest extends CommonTestSetup { /** * @throws java.lang.Exception */ + @Override @SuppressWarnings("unchecked") @BeforeEach public void setUp() throws Exception { diff --git a/src/test/java/world/bentobox/aoneblock/listeners/BlockListenerTest2.java b/src/test/java/world/bentobox/aoneblock/listeners/BlockListenerTest2.java index 87c83296..5a3796fe 100644 --- a/src/test/java/world/bentobox/aoneblock/listeners/BlockListenerTest2.java +++ b/src/test/java/world/bentobox/aoneblock/listeners/BlockListenerTest2.java @@ -1,7 +1,10 @@ package world.bentobox.aoneblock.listeners; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -35,10 +38,8 @@ import org.bukkit.block.Chest; import org.bukkit.block.data.type.Leaves; import org.bukkit.block.data.Brushable; -import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Item; -import org.bukkit.event.Event; import org.bukkit.event.block.Action; import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.entity.EntityInteractEvent; @@ -82,6 +83,7 @@ * * @author tastybento */ +@SuppressWarnings("java:S3577") class BlockListenerTest2 extends CommonTestSetup { // Class under test @@ -140,6 +142,9 @@ public void setUp() throws Exception { when(addonSettings.getMobWarning()).thenReturn(0); when(addonSettings.isDropOnTop()).thenReturn(true); when(addonSettings.isClearBlocks()).thenReturn(false); + // Chest particle defaults + when(addonSettings.resolveChestParticle(any())).thenReturn(org.bukkit.Particle.DUST); + when(addonSettings.resolveChestColor(any())).thenReturn(org.bukkit.Color.YELLOW); // Player when(mockPlayer.getUniqueId()).thenReturn(UUID.randomUUID()); @@ -507,7 +512,7 @@ void testOnPlayerInteractBrushingFinishedWithLootTable() { bl.onPlayerInteract(e); verify(magicBlock).setType(Material.AIR); - verify(world).dropItemNaturally(eq(blockCenter), eq(lootItem)); + verify(world).dropItemNaturally(blockCenter, lootItem); } /** @@ -988,7 +993,6 @@ void testOnBlockBreakNoIslandAtLocation() { */ @Test void testOnBlockBreakByMinionNotArmorStand() { - Entity zombie = mock(Entity.class); EntityInteractEvent e = mock(EntityInteractEvent.class); when(e.getBlock()).thenReturn(magicBlock); when(e.getEntityType()).thenReturn(EntityType.ZOMBIE); @@ -1009,7 +1013,6 @@ void testOnBlockBreakByMinionNotArmorStand() { void testOnBlockBreakByMinionNotInWorld() { when(addon.inWorld(world)).thenReturn(false); - Entity armorStand = mock(Entity.class); EntityInteractEvent e = mock(EntityInteractEvent.class); when(e.getBlock()).thenReturn(magicBlock); when(e.getEntityType()).thenReturn(EntityType.ARMOR_STAND); @@ -1059,12 +1062,12 @@ void testGetIslandNotInCache() { OneBlockIslands result = bl.getIsland(island); // Should return a valid, non-null result - assertTrue(result != null); - assertTrue(result.getUniqueId().equals(island.getUniqueId())); + assertNotNull(result); + assertEquals(island.getUniqueId(), result.getUniqueId()); // Second call should return the same cached object OneBlockIslands cached = bl.getIsland(island); - assertTrue(result == cached); + assertSame(result, cached); } /** @@ -1078,7 +1081,7 @@ void testSaveIslandInCache() { bl.getIsland(island); var future = bl.saveIsland(island); - assertTrue(future != null); + assertNotNull(future); } /** diff --git a/src/test/java/world/bentobox/aoneblock/listeners/JoinLeaveListenerTest.java b/src/test/java/world/bentobox/aoneblock/listeners/JoinLeaveListenerTest.java index 7322ef23..04681d49 100644 --- a/src/test/java/world/bentobox/aoneblock/listeners/JoinLeaveListenerTest.java +++ b/src/test/java/world/bentobox/aoneblock/listeners/JoinLeaveListenerTest.java @@ -83,7 +83,7 @@ void testJoinLeaveListener() { */ @Test void testOnPlayerQuit() { - PlayerQuitEvent event = new PlayerQuitEvent(mockPlayer, "nothing"); + PlayerQuitEvent event = new PlayerQuitEvent(mockPlayer, (net.kyori.adventure.text.Component) null, null); jll.onPlayerQuit(event); verify(aob,never()).logError(anyString()); verify(bl).saveIsland(island); @@ -95,7 +95,7 @@ void testOnPlayerQuit() { @Test void testOnPlayerQuitNoIsland() { when(im.getIsland(world, ID)).thenReturn(null); - PlayerQuitEvent event = new PlayerQuitEvent(mockPlayer, "nothing"); + PlayerQuitEvent event = new PlayerQuitEvent(mockPlayer, (net.kyori.adventure.text.Component) null, null); jll.onPlayerQuit(event); verify(aob,never()).logError(anyString()); verify(bl, never()).saveIsland(island); @@ -107,7 +107,7 @@ void testOnPlayerQuitNoIsland() { @Test void testOnPlayerQuitSaveError() { when(bl.saveIsland(any())).thenReturn(CompletableFuture.completedFuture(Boolean.FALSE)); - PlayerQuitEvent event = new PlayerQuitEvent(mockPlayer, "nothing"); + PlayerQuitEvent event = new PlayerQuitEvent(mockPlayer, (net.kyori.adventure.text.Component) null, null); jll.onPlayerQuit(event); verify(aob).logError(anyString()); verify(bl).saveIsland(island); diff --git a/src/test/java/world/bentobox/aoneblock/listeners/MakeSpaceTest.java b/src/test/java/world/bentobox/aoneblock/listeners/MakeSpaceTest.java index 712a900e..82ec1105 100644 --- a/src/test/java/world/bentobox/aoneblock/listeners/MakeSpaceTest.java +++ b/src/test/java/world/bentobox/aoneblock/listeners/MakeSpaceTest.java @@ -33,7 +33,7 @@ /** * Tests for {@link MakeSpace}. */ -public class MakeSpaceTest extends CommonTestSetup { +class MakeSpaceTest extends CommonTestSetup { @Mock private AOneBlock addon; diff --git a/src/test/java/world/bentobox/aoneblock/oneblocks/OneBlocksManagerTest3.java b/src/test/java/world/bentobox/aoneblock/oneblocks/OneBlocksManagerTest3.java index 4bcc087c..e9720083 100644 --- a/src/test/java/world/bentobox/aoneblock/oneblocks/OneBlocksManagerTest3.java +++ b/src/test/java/world/bentobox/aoneblock/oneblocks/OneBlocksManagerTest3.java @@ -50,6 +50,7 @@ * @author tastybento * */ +@SuppressWarnings("java:S3577") public class OneBlocksManagerTest3 extends CommonTestSetup { private static File jFile; @@ -65,11 +66,6 @@ public class OneBlocksManagerTest3 extends CommonTestSetup { public static void beforeClass() throws IOException, InvalidConfigurationException { // Make the addon jar jFile = new File("addon.jar"); - // Copy over config file from src folder - /* - * Path fromPath = Paths.get("src/main/resources/config.yml"); Path path = - * Paths.get("config.yml"); Files.copy(fromPath, path); - */ // Dummy oneblocks.yml String oneblocks = """ '0': @@ -105,11 +101,6 @@ public static void beforeClass() throws IOException, InvalidConfigurationExcepti File obFile = new File(obFileDir, "0_plains.yml"); obFileDir.mkdirs(); oneBlocks.save(obFile); - /* - * // Copy over block config file from src folder fromPath = - * Paths.get("src/main/resources/oneblocks.yml"); path = - * Paths.get("oneblocks.yml"); Files.copy(fromPath, path); - */ try (JarOutputStream tempJarOutputStream = new JarOutputStream(new FileOutputStream(jFile))) { // Added the new files to the jar. try (FileInputStream fis = new FileInputStream(obFile)) { @@ -228,7 +219,7 @@ void testLoadPhases() throws NumberFormatException, IOException { * @throws NumberFormatException */ @Test - void testGetPhaseList() throws NumberFormatException, IOException, InvalidConfigurationException { + void testGetPhaseList() throws NumberFormatException, IOException { testLoadPhases(); List l = obm.getPhaseList(); assertEquals(2, l.size()); @@ -246,7 +237,7 @@ void testGetPhaseList() throws NumberFormatException, IOException, InvalidConfig * @throws NumberFormatException */ @Test - void testGetPhaseString() throws NumberFormatException, IOException, InvalidConfigurationException { + void testGetPhaseString() throws NumberFormatException, IOException { testLoadPhases(); assertFalse(obm.getPhase("sdf").isPresent()); assertTrue(obm.getPhase("Plains").isPresent()); @@ -276,7 +267,7 @@ void testSaveOneBlockConfig() throws NumberFormatException, IOException { * @throws NumberFormatException */ @Test - void testGetNextPhase() throws NumberFormatException, IOException, InvalidConfigurationException { + void testGetNextPhase() throws NumberFormatException, IOException { testLoadPhases(); OneBlockPhase plains = obm.getPhase("Plains").get(); OneBlockPhase underground = obm.getPhase("Underground").get(); @@ -310,7 +301,7 @@ void testCopyPhasesFromAddonJar() throws IOException { * @throws IOException */ @Test - @Disabled + @Disabled("TODO: fix initBlock test setup to work with new config structure") void testInitBlock() throws IOException { System.out.println(oneBlocks); obm.initBlock("0", obPhase, oneBlocks); @@ -536,4 +527,67 @@ void testGetPhaseBlocks() { assertEquals(-1, obm.getPhaseBlocks(obi)); } + /** + * Test that a valid {@code CHEST_WITH_X} entry in fixedBlocks produces a chest + * OneBlockObject whose inventory contains the specified item at slot 0. + */ + @Test + void testLoadPhases_fixedBlockChestWithItem() throws Exception { + String yaml = """ + name: Plains + biome: PLAINS + fixedBlocks: + 0: GRASS_BLOCK + 5: CHEST_WITH_WATER_BUCKET + blocks: + GRASS_BLOCK: 1000 + """; + YamlConfiguration cfg = new YamlConfiguration(); + cfg.loadFromString(yaml); + + // initBlock parses fixedBlocks and delegates to parseStringBlock -> parseChestWithItem + obm.initBlock("0", obPhase, cfg); + + assertNotNull(obPhase.getFixedBlocks(), "fixedBlocks should not be null"); + + // Slot 5 should be a chest containing a WATER_BUCKET + OneBlockObject chest = obPhase.getFixedBlocks().get(5); + assertNotNull(chest, "fixedBlocks should contain an entry at position 5"); + assertEquals(Material.CHEST, chest.getMaterial()); + assertNotNull(chest.getChest(), "Chest contents should not be null"); + assertFalse(chest.getChest().isEmpty(), "Chest should have contents"); + assertEquals(Material.WATER_BUCKET, chest.getChest().get(0).getType()); + + // Slot 0 should be a regular GRASS_BLOCK (set as firstBlock via addFixedBlocks) + assertEquals(Material.GRASS_BLOCK, obPhase.getFirstBlock().getMaterial()); + + verify(plugin, never()).logError(anyString()); + } + + /** + * Test that an invalid {@code CHEST_WITH_X} entry (unknown item) logs an error + * and is ignored — it must not appear in fixedBlocks. + */ + @Test + void testLoadPhases_fixedBlockChestWithInvalidItem() throws Exception { + String yaml = """ + name: Plains + biome: PLAINS + fixedBlocks: + 6: CHEST_WITH_INVALID_ITEM_XYZ + blocks: + GRASS_BLOCK: 1000 + """; + YamlConfiguration cfg = new YamlConfiguration(); + cfg.loadFromString(yaml); + + // initBlock parses fixedBlocks and delegates to parseStringBlock -> parseChestWithItem + obm.initBlock("0", obPhase, cfg); + + // The invalid CHEST_WITH entry should be silently skipped (logged but not added) + assertTrue(obPhase.getFixedBlocks().isEmpty(), + "fixedBlocks should be empty because the item name is invalid"); + verify(plugin).logError(org.mockito.ArgumentMatchers.contains("CHEST_WITH item is invalid")); + } + } diff --git a/src/test/java/world/bentobox/aoneblock/oneblocks/customblock/CraftEngineCustomBlockTest.java b/src/test/java/world/bentobox/aoneblock/oneblocks/customblock/CraftEngineCustomBlockTest.java new file mode 100644 index 00000000..cd35d318 --- /dev/null +++ b/src/test/java/world/bentobox/aoneblock/oneblocks/customblock/CraftEngineCustomBlockTest.java @@ -0,0 +1,30 @@ +package world.bentobox.aoneblock.oneblocks.customblock; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link CraftEngineCustomBlock#fromMap(Map)}. + *

+ * Because CraftEngine is an optional runtime dependency, the {@code fromId} + * path cannot be exercised without a live server. These tests cover the + * map-parsing entry point and the factory registration in + * {@link world.bentobox.aoneblock.oneblocks.OneBlockCustomBlockCreator}. + */ +class CraftEngineCustomBlockTest { + + @Test + void fromMapReturnsEmptyWhenIdMissing() { + Map map = new LinkedHashMap<>(); + map.put("type", "craftengine"); + // no "id" key + + var result = CraftEngineCustomBlock.fromMap(map); + + assertTrue(result.isEmpty(), "Should return empty when 'id' is missing"); + } +}