diff --git a/.github/workflows/modrinth-publish.yml b/.github/workflows/modrinth-publish.yml index 72b93e6..4b4213c 100644 --- a/.github/workflows/modrinth-publish.yml +++ b/.github/workflows/modrinth-publish.yml @@ -38,6 +38,14 @@ jobs: paper spigot game-versions: |- - 1.21.4 1.21.5 + 1.21.6 + 1.21.7 + 1.21.8 + 1.21.9 + 1.21.10 + 1.21.11 + 26.1 + 26.1.1 + 26.1.2 files: /home/runner/work/Boxed/Boxed/target/Boxed-${{ github.event.release.tag_name }}.jar diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..c2c0488 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,30 @@ +# Boxed — .github/workflows/publish.yml +# Publishes the jar attached to a GitHub release to CurseForge and Hangar via the +# shared BentoBoxWorld/.github reusable workflow. Downloads the release asset instead of +# rebuilding from source. The reusable workflow is pinned to a commit SHA (Sonar +# githubactions:S7637). workflow_dispatch lets you (re)publish a given version. + +name: Publish release to CurseForge and Hangar + +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: "Version to publish (e.g. 1.2.3)" + required: true + type: string + +jobs: + publish: + uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@1f91a0edf72e8c86d671b3b8fdd3121ac6fb88e1 # master + with: + use_release_asset: "true" # publish the jar attached to the release; do not rebuild + hangar_slug: "Boxed" # blank = skip Hangar + curseforge_id: "1515492" + game_versions: "26.2,26.1.2,26.1.1,26.1,1.21.11,1.21.10,1.21.9,1.21.8,1.21.7,1.21.6,1.21.5" + version: ${{ inputs.version }} # empty on release events -> falls back to the release tag + secrets: + HANGAR_API_KEY: ${{ secrets.HANGAR_API_KEY }} + CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 1eb92cc..6991635 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project -Boxed is a BentoBox GameModeAddon for Minecraft (Paper) where each player is confined to a small expandable box. Completing Minecraft advancements grows the box. Built against `bentobox` 3.13.0, Paper API 1.21.11, Java 21. +Boxed is a BentoBox GameModeAddon for Minecraft (Paper) where each player is confined to a small expandable box. Completing Minecraft advancements grows the box. Built against `bentobox` 3.17.0, Paper API 1.21.11, Java 21. ## Build / Test @@ -31,8 +31,6 @@ This is the core concept and touches almost everything: This is why first boot is extremely slow and RAM-hungry (see `README.md` warnings): the entire seed region is force-loaded up front. Any change to world generation, structure handling, or world naming must respect both worlds and the copy step in `Boxed.copyChunks()` / `createOverWorld()` / `createNether()`. The `generatorMaps` / `generatorMap` fields in `Boxed.java` route world names → generators for `getDefaultWorldGenerator` (used by Multiverse and similar world-management plugins) and for the hook in `allLoaded()` that calls `WorldManagementHook.registerWorld`. -`isUsesNewChunkGeneration()` returns `true`, which tells BentoBox this addon uses the modern chunk-generation API. - ### Advancements drive box size `AdvancementsManager` is the other key subsystem. Box growth is data-driven from `advancements.yml`: each advancement key maps to an integer "box growth" increment. `AdvancementListener` watches for player advancement events and asks the manager to update the island's protection-range. Per-island state lives in `objects/IslandAdvancements.java` (a BentoBox `DataObject` persisted via its database layer). `AdvancementsManager.save()` is called in `onDisable()` — any new cached state it holds should be flushed there too. diff --git a/README.md b/README.md index e4fc994..e7f81d4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A game mode where you are boxed into a tiny space that only expands by completin ## BentoBox Requirements -* Requires BentoBox 1.23.0 or later (Snapshots can be downloaded here: [https://ci.bentobox.world](https://ci.bentobox.world)) +* Requires BentoBox 3.17.0 or later (Snapshots can be downloaded here: [https://ci.bentobox.world](https://ci.bentobox.world)). BentoBox 3.16.1+ is needed for native region-file cleanup of deleted boxes — see [Reclaiming Disk Space](#reclaiming-disk-space-deleted-island-chunks). * InvSwitcher - keeps advancements, inventory, etc. separate between worlds on a server. * Border - shows the box @@ -52,6 +52,8 @@ Each player will have a land of their own to explore up to the limit of the isla *World Seed* The world seed is used to generate the lands. It is recommended to keep this value. If you change it the land may be very different. Note that changing the seed mid-game requires a full reset of your databases and worlds. +Want to use your own terrain or a custom-built map instead? See [Using a Custom Map as the Seed](#using-a-custom-map-as-the-seed). + *Key Boxed-specific settings:* | Setting | Default | Description | @@ -127,6 +129,82 @@ To undo the last placed structure: `/boxadmin place undo` When a structure is placed via this command while standing in a player box, it is automatically saved to `structures.yml` and will be placed in all future boxes. +## Using a Custom Map as the Seed + +Boxed does **not** have an "import map" button, but because of how it works you can still get your own custom terrain into player boxes. This section explains how. + +### How seeding actually works + +Boxed uses two worlds: + +1. A hidden **seed world** — `/seed` (overworld) and `/seed_nether` (nether). This is a normal world generated from the numeric world seed. +2. The **game world** — `` and `_nether` — that players actually play in. + +On **every server start**, Boxed reads the chunks around the centre of the seed world and copies them into the game world's generator. Each player's box is then served a copy of that captured terrain. + +Two facts make custom maps possible: + +* The copy reads the seed world **from disk**, live, on every boot — so anything you change in the seed world flows into newly generated boxes. +* The game world **copies the seed world's biomes as-is**. There is no biome remapping when a chunk is copied, so whatever biomes are in the seed world are what players get. + +Two limits to keep in mind: + +* The seed-world centre is fixed at **x = 0, z = 0** (y ≈ 64). Your custom content must be built around 0,0. +* Only a square of radius **island distance** (the `world.island-distance` value, default 320 → a 640 × 640 area) around 0,0 is copied. Anything outside that is just the surrounding seas and is never used. +* Once a game-world chunk has been generated and saved to disk, it is loaded from disk and is **no longer** taken from the seed world. So changing the seed world only affects boxes/areas that have **not yet been generated**. To push changes into already-explored areas you must regenerate those game-world chunks (see methods below). + +> **Always back up your worlds before trying any of this.** These are unsupported, manual techniques. + +### Method 1 — Match an existing world's seed (easiest) + +If you just want the same *terrain* as a world you already like, set Boxed's seed to that world's numeric seed. + +1. Find the seed of the world you like (`/seed` in that world). +2. In `config.yml` set: + ```yaml + world: + generator: + seed: + ``` +3. Optionally enable vanilla structures with `world.allow-structures: true`. +4. Start with **fresh** Boxed worlds (the seed cannot be changed mid-game — delete the Boxed worlds and database, or set this up before first boot). + +Notes: this reproduces vanilla terrain for that seed. Boxed still applies its own biome overlay (`biomes.yml`) to the box area, and structures only appear if `world.allow-structures` is enabled. This does **not** let you import hand-built creations — for that use Method 2 or 3. + +### Method 2 — Edit the seed world by hand + +You can go into the seed world and build, paste, or remove whatever you like. + +1. Use a world-management plugin (e.g. Multiverse) to teleport into `/seed`. +2. Build or WorldEdit/paste your changes **around 0,0**, within the island-distance radius. +3. Restart the server. On boot, Boxed re-copies the seed world, so your edits appear in any box that is generated **after** the restart. + +To apply your edits to boxes/areas that already exist, you must regenerate those game-world chunks while leaving the seed world untouched — for example by deleting the relevant region files in `` / `_nether`, or by resetting/deleting the affected islands so BentoBox reaps their region files (see [Reclaiming Disk Space](#reclaiming-disk-space-deleted-island-chunks)). When the chunks regenerate they are re-copied from the seed world, which is the source of truth. + +### Method 3 — Drop in your own world as the seed + +This replaces the generated seed world with a world you built or downloaded. + +1. Build or obtain a normal (vanilla) Minecraft world. Arrange the terrain you want **centred on 0,0**, covering at least the island-distance radius (default 320 blocks in every direction from 0,0). +2. Stop the server. +3. Copy your world's region files into Boxed's seed world folders, replacing what's there: + * Overworld: your `region/` → `/seed/region/` + * Nether: your `DIM-1/region/` → `/seed_nether/region/` +4. Delete the game-world folders so they regenerate from your new seed world on next boot: + * `` and `_nether` + * Leave the seed worlds (`/seed`, `/seed_nether`) in place. +5. Start the server. Boxed copies your imported chunks — terrain **and biomes** — into the freshly generated game world. + +Biomes: because the game world copies the seed world's biomes unchanged, your imported biomes carry over automatically. The `biomes.yml` overlay only affects chunks that Boxed *generates*; it does not touch the pre-existing chunks you dropped in, so your map's biomes are preserved as-is. + +### Tips and gotchas + +* The copy radius is `world.island-distance`. If you increase it, your custom content must cover the larger area. +* BentoBox's native region cleanup never touches the seed worlds, so your custom terrain is safe. If you run a third-party chunk-cleaner like Regionerator, exempt the seed worlds (see [Reclaiming Disk Space](#reclaiming-disk-space-deleted-island-chunks)) or your custom seed terrain may be deleted and boxes will stop matching. +* The first boot after importing is slow and RAM-hungry, just like a normal first start (see the warning near the top of this file). +* All boxes share the same captured terrain, so every player gets the same custom map layout. + + ## Flags Boxed registers two flags unique to this gamemode. @@ -163,27 +241,47 @@ To find out how to add custom advancements to your server, watch the tutorial vi Download the official [Boxed DataPack](https://github.com/BentoBoxWorld/BoxedDataPack) for extra custom advancements. -## Using Regionerator +## Reclaiming Disk Space (Deleted Island Chunks) + +**As of BentoBox 3.16.1, BentoBox reclaims disk space natively — you no longer need a third-party plugin for this.** (The region-file housekeeping sweep was added in 3.15.0; admin deletes were routed through it in 3.16.1.) + +### How it works now -*Note: This plugin is designed to delete unused regions of your world! Make sure you take backups if you use it! Use at your own risk!* +When an island is deleted or reset, BentoBox does **not** wipe blocks immediately. Instead it *soft-deletes* the island: the owner is cleared, the box is locked, and the island is flagged `deletable` in the database. The actual world data is reclaimed later by deleting the `.mca` region files from disk — far cheaper than regenerating chunks block-by-block. -[Regionerator](https://github.com/Jikoo/Regionerator) is a plugin that gradually deletes unused chunks to keep world sizes low. It supports BentoBox and respects box boundaries. It can be used to delete box chunks so that they can be regenerated. As Boxed uses seed worlds to copy from, these can appear to be unused by Regionerator and deleted, which means that startup becomes very slow. To avoid this, set the seed worlds as exempt from its deletions by adding these entries to the `worlds` section of the Regionerator config file: +A background **housekeeping sweep** does the reaping automatically. It is on by default: + +| Setting (BentoBox `config.yml`) | Default | Description | +|---------|---------|-------------| +| `island.deletion.housekeeping.deleted-sweep.enabled` | `true` | Reaps region files for islands BentoBox itself soft-deleted (e.g. `/box reset`, admin delete). Never touches active or unvisited islands. | +| `island.deletion.housekeeping.deleted-sweep.interval-hours` | `24` | How often the deleted-island sweep runs. | +| `island.deletion.housekeeping.age-sweep.enabled` | `false` | Opt-in: also reaps region files older than `min-age-days`, even if never reset. Use to reclaim abandoned boxes. | +| `island.deletion.housekeeping.age-sweep.min-age-days` | `60` | Minimum age before the age-based sweep will reap a region. | + +A region is only reaped if **every** island overlapping it is deletable — a single active neighbour protects the whole region. To reap immediately instead of waiting for the next sweep, run: + +``` +/boxadmin purge deleted +``` + +### Your seed worlds are safe + +The sweep only scans gamemode island worlds and only the region files belonging to **deletable islands**. Boxed's seed worlds (`boxed_world/seed`, `boxed_world/seed_nether`) contain no islands, so they are never scanned and never reaped — your captured seed terrain is left intact. No exemption configuration is required. + +> The old advice to set `deletion.keep-previous-island-on-reset` no longer applies — that setting is ignored in current BentoBox; deletion is handled by the housekeeping sweep described above. + +### Regionerator (legacy / optional) + +You can still run [Regionerator](https://github.com/Jikoo/Regionerator) if you prefer, but it is no longer necessary for Boxed. If you do use it, exempt the seed worlds so it doesn't delete the terrain Boxed copies from (which would make startup very slow). Add these entries to the `worlds` section of the Regionerator config: ```yaml worlds: - boxed_world/seed_base: - days-till-flag-expires: -1 boxed_world/seed: days-till-flag-expires: -1 + boxed_world/seed_nether: + days-till-flag-expires: -1 default: days-till-flag-expires: 0 ``` -To get the most out of Regionerator, change the BentoBox `config.yml` to *not* delete chunks when an island is removed. This leaves deletion up to Regionerator and it will clean up the chunks if the unused area is large enough. Set `keep-previous-island-on-reset: true`: - -```yaml -deletion: - keep-previous-island-on-reset: true -``` - diff --git a/pom.xml b/pom.xml index 89e3181..d34bba0 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ 4.110.0 1.21.11-R0.1-SNAPSHOT - 3.13.0 + 3.17.0 ${build.version}-SNAPSHOT diff --git a/src/main/java/world/bentobox/boxed/AdvancementsManager.java b/src/main/java/world/bentobox/boxed/AdvancementsManager.java index 2540103..1b0644f 100644 --- a/src/main/java/world/bentobox/boxed/AdvancementsManager.java +++ b/src/main/java/world/bentobox/boxed/AdvancementsManager.java @@ -62,18 +62,6 @@ public AdvancementsManager(Boxed addon) { addon.logError("advancements.yml cannot be found! " + e.getLocalizedMessage()); } } - /* - // DEBUG - lists all advancements to console - int scoreTotal = 0; - Iterator ad = Bukkit.getServer().advancementIterator(); - while (ad.hasNext()) { - Advancement a = ad.next(); - int score = getScore(a); - BentoBox.getInstance().logDebug(" 'minecraft:" + a.getKey().getKey() + "': " + score); - scoreTotal += score; - } - BentoBox.getInstance().logDebug("Sum total = " + scoreTotal); - */ } /** diff --git a/src/main/java/world/bentobox/boxed/Boxed.java b/src/main/java/world/bentobox/boxed/Boxed.java index 8255fb5..c82949f 100644 --- a/src/main/java/world/bentobox/boxed/Boxed.java +++ b/src/main/java/world/bentobox/boxed/Boxed.java @@ -186,11 +186,7 @@ public void createWorlds() { if (settings.isNetherGenerate()) { createNether(worldName); } - /* - // Make the end if it does not exist - if (settings.isEndGenerate()) { - //TODO - */ + // The End is not supported yet } private void createNether(String worldName) { @@ -400,9 +396,4 @@ public void allLoaded() { public AdvancementsManager getAdvManager() { return advManager; } - - @Override - public boolean isUsesNewChunkGeneration() { - return true; - } } diff --git a/src/main/java/world/bentobox/boxed/PlaceholdersManager.java b/src/main/java/world/bentobox/boxed/PlaceholdersManager.java index c14eb47..2e73485 100644 --- a/src/main/java/world/bentobox/boxed/PlaceholdersManager.java +++ b/src/main/java/world/bentobox/boxed/PlaceholdersManager.java @@ -33,12 +33,11 @@ public String getCount(User user) { * @return string of advancement count */ public String getCountByLocation(User user) { - if (user != null && user.getUniqueId() != null && user.getLocation() != null) { - return addon.getIslands().getIslandAt(user.getLocation()) - .map(i -> String.valueOf(addon.getAdvManager().getIsland(i).getAdvancements().size())).orElse(""); - } else { + if (user == null || user.getUniqueId() == null) { return ""; } + return addon.getIslands().getIslandAt(user.getLocation()) + .map(i -> String.valueOf(addon.getAdvManager().getIsland(i).getAdvancements().size())).orElse(""); } diff --git a/src/main/java/world/bentobox/boxed/Settings.java b/src/main/java/world/bentobox/boxed/Settings.java index 06240b7..b0f5cd7 100644 --- a/src/main/java/world/bentobox/boxed/Settings.java +++ b/src/main/java/world/bentobox/boxed/Settings.java @@ -441,18 +441,24 @@ public class Settings implements WorldSettings { // Deaths @ConfigComment("Whether deaths are counted or not.") + @ConfigComment("If false, BentoBox does not count deaths and the Level addon does not record deaths against areas.") @ConfigEntry(path = "area.deaths.counted") private boolean deathsCounted = true; @ConfigComment("Maximum number of deaths to count. The death count can be used by add-ons.") + @ConfigComment("Since Level 2.29.0 this also caps how many deaths each member can contribute to an area's death penalty.") @ConfigEntry(path = "area.deaths.max") private int deathsMax = 10; - @ConfigComment("When a player joins a team, reset their death count") + @ConfigComment("When a player joins a team, reset their death count.") + @ConfigComment("This only affects BentoBox's own per-player death count, used by the %boxed_deaths% placeholder.") + @ConfigComment("Since Level 2.29.0, area levels use per-area death tracking and are not affected by this setting.") @ConfigEntry(path = "area.deaths.team-join-reset") private boolean teamJoinDeathReset = true; - @ConfigComment("Reset player death count when they start a new area or reset an area") + @ConfigComment("Reset player death count when they start a new area or reset an area.") + @ConfigComment("This only affects BentoBox's own per-player death count.") + @ConfigComment("Since Level 2.29.0 the Level addon clears an area's own death record automatically when it is reset or deleted.") @ConfigEntry(path = "area.deaths.reset-on-new-area") private boolean deathsResetOnNewIsland = true; @@ -1777,6 +1783,7 @@ public void setIgnoreAdvancements(boolean ignoreAdvancements) { /** * @return the concurrentIslands */ + @Override public int getConcurrentIslands() { if (concurrentIslands <= 0) { return BentoBox.getInstance().getSettings().getIslandNumber(); @@ -1794,6 +1801,7 @@ public void setConcurrentIslands(int concurrentIslands) { /** * @return the disallowTeamMemberIslands */ + @Override public boolean isDisallowTeamMemberIslands() { return disallowTeamMemberIslands; } diff --git a/src/main/java/world/bentobox/boxed/commands/AdminPlaceStructureCommand.java b/src/main/java/world/bentobox/boxed/commands/AdminPlaceStructureCommand.java index a6a492e..03342a7 100644 --- a/src/main/java/world/bentobox/boxed/commands/AdminPlaceStructureCommand.java +++ b/src/main/java/world/bentobox/boxed/commands/AdminPlaceStructureCommand.java @@ -16,7 +16,6 @@ import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.NamespacedKey; -import org.bukkit.block.BlockState; import org.bukkit.block.data.BlockData; import org.bukkit.block.structure.Mirror; import org.bukkit.block.structure.StructureRotation; @@ -119,9 +118,7 @@ public boolean canExecute(User user, String label, List args) { } // Next come the coordinates - there must be at least 3 of them - if ((!args.get(1).equals("~") && !Util.isInteger(args.get(1), true)) - || (!args.get(2).equals("~") && !Util.isInteger(args.get(2), true)) - || (!args.get(3).equals("~") && !Util.isInteger(args.get(3), true))) { + if (!isCoordinate(args.get(1)) || !isCoordinate(args.get(2)) || !isCoordinate(args.get(3))) { user.sendMessage("boxed.commands.boxadmin.place.use-integers"); return false; } @@ -132,10 +129,7 @@ public boolean canExecute(User user, String label, List args) { } // Handle rotation - sr = Enums.getIfPresent(StructureRotation.class, args.get(4).toUpperCase(Locale.ENGLISH)).orNull(); - if (sr == null) { - user.sendMessage("boxed.commands.boxadmin.place.unknown-rotation"); - Arrays.stream(StructureRotation.values()).map(StructureRotation::name).forEach(user::sendRawMessage); + if (!parseRotation(user, args.get(4))) { return false; } @@ -144,24 +138,67 @@ public boolean canExecute(User user, String label, List args) { } // Handle mirror - mirror = Enums.getIfPresent(Mirror.class, args.get(5).toUpperCase(Locale.ENGLISH)).orNull(); + if (!parseMirror(user, args.get(5))) { + return false; + } + + // Handle NO_MOBS + return args.size() < 7 || parseNoMobs(user, args.get(6)); + } + + /** + * @param arg command argument + * @return true if the argument is a relative marker (~) or an integer + */ + private static boolean isCoordinate(String arg) { + return arg.equals("~") || Util.isInteger(arg, true); + } + + /** + * Parses the rotation argument into {@link #sr} + * @param user user to inform if the rotation is unknown + * @param arg rotation argument + * @return true if the rotation is valid + */ + private boolean parseRotation(User user, String arg) { + sr = Enums.getIfPresent(StructureRotation.class, arg.toUpperCase(Locale.ENGLISH)).orNull(); + if (sr == null) { + user.sendMessage("boxed.commands.boxadmin.place.unknown-rotation"); + Arrays.stream(StructureRotation.values()).map(StructureRotation::name).forEach(user::sendRawMessage); + return false; + } + return true; + } + + /** + * Parses the mirror argument into {@link #mirror} + * @param user user to inform if the mirror is unknown + * @param arg mirror argument + * @return true if the mirror is valid + */ + private boolean parseMirror(User user, String arg) { + mirror = Enums.getIfPresent(Mirror.class, arg.toUpperCase(Locale.ENGLISH)).orNull(); if (mirror == null) { user.sendMessage("boxed.commands.boxadmin.place.unknown-mirror"); Arrays.stream(Mirror.values()).map(Mirror::name).forEach(user::sendRawMessage); return false; } + return true; + } - if (args.size() == 7) { - if (args.get(6).toUpperCase(Locale.ENGLISH).equals("NO_MOBS")) { - noMobs = true; - } else { - user.sendMessage("boxed.commands.boxadmin.place.unknown", TextVariables.LABEL, args.get(6).toUpperCase(Locale.ENGLISH)); - return false; - } + /** + * Parses the NO_MOBS argument into {@link #noMobs} + * @param user user to inform if the argument is unknown + * @param arg argument + * @return true if the argument is valid + */ + private boolean parseNoMobs(User user, String arg) { + if (arg.toUpperCase(Locale.ENGLISH).equals("NO_MOBS")) { + noMobs = true; + return true; } - - // Syntax is okay - return true; + user.sendMessage("boxed.commands.boxadmin.place.unknown", TextVariables.LABEL, arg.toUpperCase(Locale.ENGLISH)); + return false; } @Override @@ -193,7 +230,7 @@ public boolean execute(User user, String label, List args) { .removeJigsaw(new StructureRecord(tag.getKey(), tag.getKey(), spot, sr, mirror, noMobs, removedBlocks)); placedStructures.push(new StructureRecord(tag.getKey(), tag.getKey(), spot, sr, mirror, noMobs, removedBlocks)); // Track the placement - boolean result = saveStructure(spot, tag, user, sr, mirror); + boolean result = saveStructure(spot, tag, sr, mirror); if (result) { user.sendMessage("boxed.commands.boxadmin.place.saved"); } else { @@ -202,7 +239,7 @@ public boolean execute(User user, String label, List args) { return result; } - private boolean saveStructure(Location spot, NamespacedKey tag, User user, StructureRotation sr2, Mirror mirror2) { + private boolean saveStructure(Location spot, NamespacedKey tag, StructureRotation sr2, Mirror mirror2) { return getAddon().getIslands().getIslandAt(spot).map(i -> { int xx = spot.getBlockX() - i.getCenter().getBlockX(); int zz = spot.getBlockZ() - i.getCenter().getBlockZ(); @@ -218,8 +255,7 @@ private boolean saveStructure(Location spot, NamespacedKey tag, User user, Struc config.set(spot.getWorld().getEnvironment().name().toLowerCase(Locale.ENGLISH) + "." + xx + "," + spot.getBlockY() + "," + zz, v.toString()); config.save(structures); } catch (IOException | InvalidConfigurationException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + getAddon().logError("Could not save structure to " + STRUCTURE_FILE + ": " + e.getMessage()); return false; } return true; @@ -261,13 +297,13 @@ private boolean undoLastPlacement(User user) { Collections.emptyList() // No entity transformers ); lastRecord.removedBlocks().clear(); - removeStructure(lastRecord.location(), tag, user); // Remove from config + removeStructure(lastRecord.location()); // Remove from config user.sendMessage("boxed.commands.boxadmin.place.undo-success"); return true; } - private boolean removeStructure(Location spot, NamespacedKey tag, User user) { + private boolean removeStructure(Location spot) { return getAddon().getIslands().getIslandAt(spot).map(i -> { int xx = spot.getBlockX() - i.getCenter().getBlockX(); int zz = spot.getBlockZ() - i.getCenter().getBlockZ(); @@ -282,7 +318,7 @@ private boolean removeStructure(Location spot, NamespacedKey tag, User user) { return true; } } catch (IOException | InvalidConfigurationException e) { - e.printStackTrace(); + getAddon().logError("Could not remove structure from " + STRUCTURE_FILE + ": " + e.getMessage()); } return false; }).orElse(false); diff --git a/src/main/java/world/bentobox/boxed/generators/biomes/AbstractCopyBiomeProvider.java b/src/main/java/world/bentobox/boxed/generators/biomes/AbstractCopyBiomeProvider.java index 2742599..f57c5cc 100644 --- a/src/main/java/world/bentobox/boxed/generators/biomes/AbstractCopyBiomeProvider.java +++ b/src/main/java/world/bentobox/boxed/generators/biomes/AbstractCopyBiomeProvider.java @@ -2,14 +2,14 @@ import java.util.List; -import org.bukkit.Registry; -import org.bukkit.World.Environment; import org.bukkit.block.Biome; import org.bukkit.generator.BiomeProvider; import org.bukkit.generator.WorldInfo; import org.bukkit.util.Vector; import org.eclipse.jdt.annotation.Nullable; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; import world.bentobox.bentobox.BentoBox; import world.bentobox.boxed.Boxed; import world.bentobox.boxed.generators.chunks.AbstractBoxedChunkGenerator; @@ -27,7 +27,7 @@ public abstract class AbstractCopyBiomeProvider extends BiomeProvider { protected final int dist; - protected AbstractCopyBiomeProvider(Boxed boxed, Environment env, Biome defaultBiome) { + protected AbstractCopyBiomeProvider(Boxed boxed, Biome defaultBiome) { this.addon = boxed; this.defaultBiome = defaultBiome; dist = addon.getSettings().getIslandDistance(); @@ -35,11 +35,10 @@ protected AbstractCopyBiomeProvider(Boxed boxed, Environment env, Biome defaultB @Override public Biome getBiome(WorldInfo worldInfo, int x, int y, int z) { - int chunkX = x >> 4; - int chunkZ = z >> 4; - chunkX = AbstractBoxedChunkGenerator.repeatCalc(chunkX); - chunkZ = AbstractBoxedChunkGenerator.repeatCalc(chunkZ); - @Nullable ChunkStore c = addon.getChunkGenerator(worldInfo.getEnvironment()).getChunk(chunkX, chunkZ); + AbstractBoxedChunkGenerator gen = addon.getChunkGenerator(worldInfo.getEnvironment()); + int chunkX = gen.repeatCalc(x >> 4); + int chunkZ = gen.repeatCalc(z >> 4); + @Nullable ChunkStore c = gen.getChunk(chunkX, chunkZ); if (c != null) { int xx = Math.floorMod(x, 16); @@ -54,7 +53,7 @@ public Biome getBiome(WorldInfo worldInfo, int x, int y, int z) { @Override public List getBiomes(WorldInfo worldInfo) { // Return all of them for now! - return Registry.BIOME.stream().filter(b -> !b.equals(Biome.CUSTOM)).toList(); + return RegistryAccess.registryAccess().getRegistry(RegistryKey.BIOME).stream().toList(); } } diff --git a/src/main/java/world/bentobox/boxed/generators/biomes/AbstractSeedBiomeProvider.java b/src/main/java/world/bentobox/boxed/generators/biomes/AbstractSeedBiomeProvider.java index 5ed1f8f..1e536ba 100644 --- a/src/main/java/world/bentobox/boxed/generators/biomes/AbstractSeedBiomeProvider.java +++ b/src/main/java/world/bentobox/boxed/generators/biomes/AbstractSeedBiomeProvider.java @@ -7,10 +7,11 @@ import java.util.Locale; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.SortedMap; import java.util.TreeMap; -import org.bukkit.Registry; +import org.bukkit.NamespacedKey; import org.bukkit.World.Environment; import org.bukkit.block.Biome; import org.bukkit.block.BlockFace; @@ -21,6 +22,8 @@ import org.bukkit.util.Vector; import org.eclipse.jdt.annotation.NonNull; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; import world.bentobox.boxed.Boxed; /** @@ -31,6 +34,11 @@ */ public abstract class AbstractSeedBiomeProvider extends BiomeProvider { + /** + * Config marker for "no custom biome here, use the vanilla one" + */ + private static final String CUSTOM_BIOME = "CUSTOM"; + private static final Map ENV_MAP; static { @@ -103,7 +111,7 @@ public static Biome getBiome(int humidity, double weirdness) { */ private enum MiddleBiome { X00(0, 0, Biome.SNOWY_PLAINS, Biome.ICE_SPIKES), X01(0, 1, Biome.PLAINS, Biome.PLAINS), - X02(0, 2, Biome.FLOWER_FOREST, Biome.SUNFLOWER_PLAINS), x03(0, 3, Biome.SAVANNA, Biome.SAVANNA), + X02(0, 2, Biome.FLOWER_FOREST, Biome.SUNFLOWER_PLAINS), X03(0, 3, Biome.SAVANNA, Biome.SAVANNA), X04(0, 4, Biome.DESERT, Biome.DESERT), X10(1, 0, Biome.SNOWY_PLAINS, Biome.SNOWY_PLAINS), X11(1, 1, Biome.PLAINS, Biome.PLAINS), @@ -153,25 +161,25 @@ public static Biome getBiome(int humidity, int temperature, double weirdness) { * Plateau biomes by temperature and humidity zones */ private enum PlateauBiome { - X00(0, 0, Biome.SNOWY_PLAINS, Biome.ICE_SPIKES), x01(0, 1, Biome.MEADOW, Biome.CHERRY_GROVE), - X02(0, 2, Biome.MEADOW, Biome.CHERRY_GROVE), x03(0, 3, Biome.SAVANNA_PLATEAU, Biome.SAVANNA_PLATEAU), + X00(0, 0, Biome.SNOWY_PLAINS, Biome.ICE_SPIKES), X01(0, 1, Biome.MEADOW, Biome.CHERRY_GROVE), + X02(0, 2, Biome.MEADOW, Biome.CHERRY_GROVE), X03(0, 3, Biome.SAVANNA_PLATEAU, Biome.SAVANNA_PLATEAU), X04(0, 4, Biome.BADLANDS, Biome.ERODED_BADLANDS), - X10(1, 0, Biome.SNOWY_PLAINS, Biome.SNOWY_PLAINS), x11(1, 1, Biome.MEADOW, Biome.MEADOW), - X12(1, 2, Biome.MEADOW, Biome.CHERRY_GROVE), x13(1, 3, Biome.SAVANNA_PLATEAU, Biome.SAVANNA_PLATEAU), + X10(1, 0, Biome.SNOWY_PLAINS, Biome.SNOWY_PLAINS), X11(1, 1, Biome.MEADOW, Biome.MEADOW), + X12(1, 2, Biome.MEADOW, Biome.CHERRY_GROVE), X13(1, 3, Biome.SAVANNA_PLATEAU, Biome.SAVANNA_PLATEAU), X14(1, 4, Biome.BADLANDS, Biome.ERODED_BADLANDS), - X20(2, 0, Biome.SNOWY_PLAINS, Biome.SNOWY_TAIGA), x21(2, 1, Biome.FOREST, Biome.MEADOW), - X22(2, 2, Biome.MEADOW, Biome.BIRCH_FOREST), x23(2, 3, Biome.FOREST, Biome.FOREST), + X20(2, 0, Biome.SNOWY_PLAINS, Biome.SNOWY_TAIGA), X21(2, 1, Biome.FOREST, Biome.MEADOW), + X22(2, 2, Biome.MEADOW, Biome.BIRCH_FOREST), X23(2, 3, Biome.FOREST, Biome.FOREST), X24(2, 4, Biome.BADLANDS, Biome.BADLANDS), - X30(3, 0, Biome.SNOWY_TAIGA, Biome.SNOWY_TAIGA), x31(3, 1, Biome.TAIGA, Biome.MEADOW), - X32(3, 2, Biome.MEADOW, Biome.BIRCH_FOREST), x33(3, 3, Biome.FOREST, Biome.FOREST), + X30(3, 0, Biome.SNOWY_TAIGA, Biome.SNOWY_TAIGA), X31(3, 1, Biome.TAIGA, Biome.MEADOW), + X32(3, 2, Biome.MEADOW, Biome.BIRCH_FOREST), X33(3, 3, Biome.FOREST, Biome.FOREST), X34(3, 4, Biome.WOODED_BADLANDS, Biome.WOODED_BADLANDS), X40(4, 0, Biome.SNOWY_TAIGA, Biome.SNOWY_TAIGA), X41(4, 1, Biome.OLD_GROWTH_SPRUCE_TAIGA, Biome.OLD_GROWTH_PINE_TAIGA), - X42(4, 2, Biome.DARK_FOREST, Biome.DARK_FOREST), x43(4, 3, Biome.JUNGLE, Biome.JUNGLE), + X42(4, 2, Biome.DARK_FOREST, Biome.DARK_FOREST), X43(4, 3, Biome.JUNGLE, Biome.JUNGLE), X44(4, 4, Biome.WOODED_BADLANDS, Biome.WOODED_BADLANDS),; private int temp; @@ -256,16 +264,17 @@ public static Biome getBiome(int h, int t, double we) { } /** - * Continental location by continentalness + * Continental location by continentalness. The outer bands are open-ended because + * vanilla noise can fall outside the nominal [-1.2, 1.0] range. */ - private enum ContLoc { - MUSHROOM_FIELDS(-1.2, -1.05), + enum ContLoc { + MUSHROOM_FIELDS(Double.NEGATIVE_INFINITY, -1.05), DEEP_OCEAN(-1.05, -0.455), OCEAN(-0.455, -0.19), COAST(-0.19, -0.11), NEAR_INLAND(-0.11, 0.03), MID_INLAND(0.03, 0.3), - FAR_INLAND(0.3, 10.0); + FAR_INLAND(0.3, Double.POSITIVE_INFINITY); private double min; private double max; @@ -275,7 +284,7 @@ private enum ContLoc { this.max = max; } - public static ContLoc getCont(double continentalness) { + static ContLoc getCont(double continentalness) { for (ContLoc c : ContLoc.values()) { if (continentalness >= c.min && continentalness < c.max) { return c; @@ -501,6 +510,113 @@ private int getTemp(double temp) { }; } + /* + * Shared building blocks for the erosion / ridge biome tables below. + * Temperature and humidity are levels 0-4, erosion is a level 0-6. + */ + + /** + * Erosion level 0 peaks: jagged/frozen peaks when cold, stony peaks when temperate, badlands when hot. + */ + private static @NonNull Biome peaksBiome(int humidity, int temperature, double weirdness) { + if (temperature <= 2) { + return weirdness < 0 ? Biome.JAGGED_PEAKS : Biome.FROZEN_PEAKS; + } + if (temperature == 3) { + return Biome.STONY_PEAKS; + } + return BadlandBiome.getBiome(humidity, weirdness); + } + + /** + * Snowy slopes when dry, grove when humid. + */ + private static @NonNull Biome slopesOrGrove(int humidity) { + return humidity < 2 ? Biome.SNOWY_SLOPES : Biome.GROVE; + } + + /** + * Middle biomes unless hot, in which case badlands. + */ + private static @NonNull Biome middleOrBadlands(int humidity, int temperature, double weirdness) { + return temperature < 4 ? MiddleBiome.getBiome(humidity, temperature, weirdness) + : BadlandBiome.getBiome(humidity, weirdness); + } + + /** + * Cold (temperature 0-2): slopes or grove, otherwise plateau biomes. + */ + private static @NonNull Biome coldSlopesOrPlateau(int humidity, int temperature, double weirdness) { + return temperature < 3 ? slopesOrGrove(humidity) : PlateauBiome.getBiome(humidity, temperature, weirdness); + } + + /** + * Frozen (temperature 0): slopes or grove, otherwise plateau biomes. + */ + private static @NonNull Biome frozenSlopesOrPlateau(int humidity, int temperature, double weirdness) { + return temperature == 0 ? slopesOrGrove(humidity) : PlateauBiome.getBiome(humidity, temperature, weirdness); + } + + /** + * Frozen (temperature 0): slopes or grove, otherwise middle biomes / badlands. + */ + private static @NonNull Biome frozenSlopesOrMiddle(int humidity, int temperature, double weirdness) { + return temperature == 0 ? slopesOrGrove(humidity) : middleOrBadlands(humidity, temperature, weirdness); + } + + /** + * Rivers: frozen when temperature is 0. + */ + private static @NonNull Biome riverBiome(int temperature) { + return temperature == 0 ? Biome.FROZEN_RIVER : Biome.RIVER; + } + + /** + * Swamps for temperate, mangrove swamps for hot. Only valid for temperature > 0. + */ + private static @NonNull Biome swampOrMangrove(int temperature) { + return temperature <= 2 ? Biome.SWAMP : Biome.MANGROVE_SWAMP; + } + + /** + * High erosion fall-through: middle biomes when frozen, otherwise swamps. + */ + private static @NonNull Biome swampBiome(int humidity, int temperature, double weirdness) { + return temperature == 0 ? MiddleBiome.getBiome(humidity, temperature, weirdness) : swampOrMangrove(temperature); + } + + /** + * High erosion fall-through in valleys: frozen river when frozen, otherwise swamps. + */ + private static @NonNull Biome valleySwampBiome(int temperature) { + return temperature == 0 ? Biome.FROZEN_RIVER : swampOrMangrove(temperature); + } + + /** + * Whether erosion level 5 with positive weirdness and a hot temperature yields windswept savanna. + */ + private static boolean isWindsweptSavanna(int temperature, double weirdness) { + return weirdness > 0 && temperature > 2; + } + + /** + * Erosion level 5 special cases shared by most ridge types: negative weirdness with a cold temperature + * or high humidity gives shattered (or middle) biomes, positive weirdness with a hot temperature gives + * windswept savanna. + * @param shattered true to use shattered biomes for the negative-weirdness case, false to use middle biomes + * @return the special-case biome, or empty if the caller's default applies + */ + private static Optional erosionFiveBiome(int humidity, int temperature, double weirdness, boolean shattered) { + if (weirdness < 0 && (temperature <= 1 || humidity == 4)) { + return Optional.of(shattered ? ShatteredBiome.getBiome(humidity, temperature, weirdness) + : MiddleBiome.getBiome(humidity, temperature, weirdness)); + } + if (isWindsweptSavanna(temperature, weirdness)) { + return Optional.of(Biome.WINDSWEPT_SAVANNA); + } + return Optional.empty(); + } + private @NonNull Biome farInlandBiome(int humidity, int temperature, int erosion, double weirdness) { return switch (Ridges.getRidge(convertToY(weirdness))) { case HIGH -> getFarInlandHighBiome(humidity, temperature, erosion, weirdness); @@ -512,308 +628,86 @@ private int getTemp(double temp) { } private @NonNull Biome getFarInlandValleysBiome(int humidity, int temperature, int erosion, double weirdness) { - if (erosion >= 0 && erosion < 6) { - if (temperature > 0D) { - return Biome.RIVER; - } else { - return Biome.FROZEN_RIVER; - } - } - // e == 6 - if (temperature == 0) { - return Biome.FROZEN_RIVER; - } - if (temperature == 1 || temperature == 2) { - return Biome.SWAMP; - } - return Biome.MANGROVE_SWAMP; - } - - private @NonNull Biome getValleysNearInlandBiome(int humidity, int temperature, int erosion, double weirdness) { - return getFarInlandValleysBiome(humidity, temperature, erosion, weirdness); + return erosion < 6 ? riverBiome(temperature) : valleySwampBiome(temperature); } + /** + * Peaks biomes. Shared by far inland, near inland and coast. + */ private @NonNull Biome getFarInlandPeaksBiome(int humidity, int temperature, int erosion, double weirdness) { - if (erosion == 0) { - if (temperature >= 0 && temperature <= 2) { - if (weirdness < 0) { - return Biome.JAGGED_PEAKS; - } else { - return Biome.FROZEN_PEAKS; - } - } else if (temperature == 3) { - return Biome.STONY_PEAKS; - } - return BadlandBiome.getBiome(humidity, weirdness); - } else if (erosion == 1) { - if (temperature == 0 && (humidity == 0 || humidity == 1)) { - return Biome.SNOWY_SLOPES; - } else if (temperature == 0 && humidity > 1) { - return Biome.GROVE; - } else if (temperature < 4) { - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - return BadlandBiome.getBiome(humidity, weirdness); - } else if (erosion >= 2 && erosion <= 4) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 5) { - if (weirdness < 0 && (temperature == 0 || temperature == 1 || humidity == 4)) { - // Shattered biomes - return ShatteredBiome.getBiome(humidity, temperature, weirdness); - } - if (weirdness > 0 && (temperature > 2 && temperature <= 4 && humidity >= 0 && humidity <= 4)) { - return Biome.WINDSWEPT_SAVANNA; - } - } - // middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); + return switch (erosion) { + case 0 -> peaksBiome(humidity, temperature, weirdness); + case 1 -> frozenSlopesOrMiddle(humidity, temperature, weirdness); + case 5 -> erosionFiveBiome(humidity, temperature, weirdness, true) + .orElseGet(() -> MiddleBiome.getBiome(humidity, temperature, weirdness)); + default -> MiddleBiome.getBiome(humidity, temperature, weirdness); + }; } private @NonNull Biome getFarInlandMidBiome(int humidity, int temperature, int erosion, double weirdness) { - if (erosion == 0) { - if (temperature < 3 && (humidity == 0 || humidity == 1)) { - return Biome.SNOWY_SLOPES; - } - if (temperature < 3 && (humidity == 2 || humidity == 3 || humidity == 4)) { - return Biome.GROVE; - } - return PlateauBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 1) { - if (temperature == 0 && (humidity == 0 || humidity == 1)) { - return Biome.SNOWY_SLOPES; - } - if (temperature == 0 && (humidity == 2 || humidity == 3 || humidity == 4)) { - return Biome.GROVE; - } - return PlateauBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 2) { - return PlateauBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 3) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 5) { - if (weirdness < 0 && (temperature == 0 || temperature == 1) || humidity == 4) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - if (weirdness > 0 && (temperature > 2 && temperature <= 4 && humidity >= 0 && humidity <= 4)) { - return Biome.WINDSWEPT_SAVANNA; - } - } - if (temperature == 0) { - // Middle Biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - if (temperature == 1 || temperature == 2) { - return Biome.SWAMP; - } - return Biome.MANGROVE_SWAMP; + return switch (erosion) { + case 0 -> coldSlopesOrPlateau(humidity, temperature, weirdness); + case 1 -> frozenSlopesOrPlateau(humidity, temperature, weirdness); + case 2 -> PlateauBiome.getBiome(humidity, temperature, weirdness); + case 3 -> MiddleBiome.getBiome(humidity, temperature, weirdness); + case 5 -> erosionFiveBiome(humidity, temperature, weirdness, false) + .orElseGet(() -> swampBiome(humidity, temperature, weirdness)); + default -> swampBiome(humidity, temperature, weirdness); + }; } + /** + * Low biomes. Shared by far inland and near inland. + */ private @NonNull Biome getFarInlandLowBiome(int humidity, int temperature, int erosion, double weirdness) { - if (erosion >= 0 && erosion < 2) { - if (temperature < 4) { - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - return BadlandBiome.getBiome(humidity, weirdness); - } else if (erosion >= 2 && erosion < 5) { - // Middle Biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 5) { - if (weirdness < 0 && (temperature == 0 || temperature == 1 || humidity == 4)) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - if (weirdness > 0 && (temperature > 2 && temperature <= 4 && humidity >= 0 && humidity <= 4)) { - return Biome.WINDSWEPT_SAVANNA; - } - } - if (temperature == 0) { - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } else if (temperature == 1 || temperature == 2) { - return Biome.SWAMP; - } - return Biome.MANGROVE_SWAMP; + return switch (erosion) { + case 0, 1 -> middleOrBadlands(humidity, temperature, weirdness); + case 2, 3, 4 -> MiddleBiome.getBiome(humidity, temperature, weirdness); + case 5 -> erosionFiveBiome(humidity, temperature, weirdness, false) + .orElseGet(() -> swampBiome(humidity, temperature, weirdness)); + default -> swampBiome(humidity, temperature, weirdness); + }; } private @NonNull Biome getFarInlandHighBiome(int humidity, int temperature, int erosion, double weirdness) { - if (erosion == 0) { - if (temperature < 3 && (humidity == 0 || humidity == 1)) { - return Biome.SNOWY_SLOPES; - } - if (temperature < 3 && (humidity == 2 || humidity == 3 || humidity == 4)) { - return Biome.GROVE; - } - return PlateauBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 1) { - if (temperature == 0 && (humidity == 0 || humidity == 1)) { - return Biome.SNOWY_SLOPES; - } - if (temperature == 0 && (humidity == 2 || humidity == 3 || humidity == 4)) { - return Biome.GROVE; - } - if (temperature > 0 && temperature < 4) { - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - return BadlandBiome.getBiome(humidity, weirdness); - } else if (erosion == 2 || erosion == 3 || erosion == 4) { - return PlateauBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 4 || erosion == 6) { - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - return ShatteredBiome.getBiome(humidity, temperature, weirdness); + return switch (erosion) { + case 0 -> coldSlopesOrPlateau(humidity, temperature, weirdness); + case 1 -> frozenSlopesOrMiddle(humidity, temperature, weirdness); + case 2, 3, 4 -> PlateauBiome.getBiome(humidity, temperature, weirdness); + case 6 -> MiddleBiome.getBiome(humidity, temperature, weirdness); + default -> ShatteredBiome.getBiome(humidity, temperature, weirdness); + }; } private @NonNull Biome nearInlandBiome(int humidity, int temperature, int erosion, double weirdness) { return switch (Ridges.getRidge(convertToY(weirdness))) { case HIGH -> getHighNearInlandBiome(humidity, temperature, erosion, weirdness); - case LOW -> getLowNearInlandBiome(humidity, temperature, erosion, weirdness); + case LOW -> getFarInlandLowBiome(humidity, temperature, erosion, weirdness); case MID -> getMidNearInlandBiome(humidity, temperature, erosion, weirdness); - case PEAKS -> getPeaksNearInlandBiome(humidity, temperature, erosion, weirdness); - default -> getValleysNearInlandBiome(humidity, temperature, erosion, weirdness); + case PEAKS -> getFarInlandPeaksBiome(humidity, temperature, erosion, weirdness); + default -> getFarInlandValleysBiome(humidity, temperature, erosion, weirdness); }; } - private @NonNull Biome getPeaksNearInlandBiome(int humidity, int temperature, int erosion, double weirdness) { - if (erosion == 0) { - if (temperature >= 0 && temperature <= 2) { - if (weirdness < 0) { - return Biome.JAGGED_PEAKS; - } else { - return Biome.FROZEN_PEAKS; - } - } else if (temperature == 3) { - return Biome.STONY_PEAKS; - } - return BadlandBiome.getBiome(humidity, weirdness); - - } else if (erosion == 1) { - if (temperature == 0 && (humidity == 0 || humidity == 1)) { - return Biome.SNOWY_SLOPES; - } else if (temperature == 0 && humidity > 1) { - return Biome.GROVE; - } else if (temperature < 4) { - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - return BadlandBiome.getBiome(humidity, weirdness); - } else if (erosion >= 2 && erosion <= 4) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 5) { - if (weirdness < 0 && (temperature == 0 || temperature == 1 || humidity == 4)) { - // Shattered biomes - return ShatteredBiome.getBiome(humidity, temperature, weirdness); - } - if (weirdness > 0 && (temperature > 2 && temperature <= 4 && humidity >= 0 && humidity <= 4)) { - return Biome.WINDSWEPT_SAVANNA; - } - } - // middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - private @NonNull Biome getMidNearInlandBiome(int humidity, int temperature, int erosion, double weirdness) { - if (erosion == 0) { - if (temperature < 3 && (humidity == 0 || humidity == 1)) { - return Biome.SNOWY_SLOPES; - } - if (temperature < 3 && (humidity == 2 || humidity == 3 || humidity == 4)) { - return Biome.GROVE; - } - return PlateauBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 1) { - if (temperature == 0 && (humidity == 0 || humidity == 1)) { - return Biome.SNOWY_SLOPES; - } - if (temperature == 0 && (humidity == 2 || humidity == 3 || humidity == 4)) { - return Biome.GROVE; - } - if (temperature > 0 && temperature < 4) { - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - return BadlandBiome.getBiome(humidity, weirdness); - } else if (erosion >= 2 && erosion <= 4) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 5) { - if (weirdness < 0 && (temperature == 0 || temperature == 1) || humidity == 4) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - if (weirdness > 0 && (temperature > 2 && temperature <= 4 && humidity >= 0 && humidity <= 4)) { - return Biome.WINDSWEPT_SAVANNA; - } - } - if (temperature == 0) { - // Middle Biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - if (temperature == 1 || temperature == 2) { - return Biome.SWAMP; - } - return Biome.MANGROVE_SWAMP; - } - - private @NonNull Biome getLowNearInlandBiome(int humidity, int temperature, int erosion, double weirdness) { - if (erosion >= 0 && erosion < 2) { - if (temperature < 4) { - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - return BadlandBiome.getBiome(humidity, weirdness); - } else if (erosion >= 2 && erosion < 5) { - // Middle Biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 5) { - if (weirdness < 0 && (temperature == 0 || temperature == 1 || humidity == 4)) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - if (weirdness > 0 && (temperature > 2 && temperature <= 4 && humidity >= 0 && humidity <= 4)) { - return Biome.WINDSWEPT_SAVANNA; - } - } - if (temperature == 0) { - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } else if (temperature == 1 || temperature == 2) { - return Biome.SWAMP; - } - return Biome.MANGROVE_SWAMP; + return switch (erosion) { + case 0 -> coldSlopesOrPlateau(humidity, temperature, weirdness); + case 1 -> frozenSlopesOrMiddle(humidity, temperature, weirdness); + case 2, 3, 4 -> MiddleBiome.getBiome(humidity, temperature, weirdness); + case 5 -> erosionFiveBiome(humidity, temperature, weirdness, false) + .orElseGet(() -> swampBiome(humidity, temperature, weirdness)); + default -> swampBiome(humidity, temperature, weirdness); + }; } private @NonNull Biome getHighNearInlandBiome(int humidity, int temperature, int erosion, double weirdness) { - if (erosion == 0) { - if (temperature < 3 && (humidity == 0 || humidity == 1)) { - return Biome.SNOWY_SLOPES; - } - if (temperature < 3 && (humidity == 2 || humidity == 3 || humidity == 4)) { - return Biome.GROVE; - } - return PlateauBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 1) { - if (temperature == 0 && (humidity == 0 || humidity == 1)) { - return Biome.SNOWY_SLOPES; - } - if (temperature == 0 && (humidity == 2 || humidity == 3 || humidity == 4)) { - return Biome.GROVE; - } - if (temperature > 0 && temperature < 4) { - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - return BadlandBiome.getBiome(humidity, weirdness); - } else if (erosion >= 2 && erosion <= 4) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 5) { - if (weirdness < 0 && (temperature == 0 || temperature == 1) || humidity == 4) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - if (weirdness > 0 && (temperature > 2 && temperature <= 4 && humidity >= 0 && humidity <= 4)) { - return Biome.WINDSWEPT_SAVANNA; - } - } - return MiddleBiome.getBiome(humidity, temperature, weirdness); + return switch (erosion) { + case 0 -> coldSlopesOrPlateau(humidity, temperature, weirdness); + case 1 -> frozenSlopesOrMiddle(humidity, temperature, weirdness); + case 5 -> erosionFiveBiome(humidity, temperature, weirdness, false) + .orElseGet(() -> MiddleBiome.getBiome(humidity, temperature, weirdness)); + default -> MiddleBiome.getBiome(humidity, temperature, weirdness); + }; } private @NonNull Biome midInlandBiome(int humidity, int temperature, int erosion, double weirdness) { @@ -827,297 +721,105 @@ private int getTemp(double temp) { } private @NonNull Biome getValleysMidInlandBiome(int humidity, int temperature, int erosion, double weirdness) { - if (erosion == 0 || erosion == 1) { - if (temperature < 4) { - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } else { - return BadlandBiome.getBiome(humidity, weirdness); - } - } - if (erosion >= 2 && erosion <= 5) { - if (temperature == 0) { - return Biome.FROZEN_RIVER; - } else { - return Biome.RIVER; - } - } - if (temperature == 0) { - return Biome.FROZEN_RIVER; - } - if (temperature == 1 || temperature == 2) { - return Biome.SWAMP; - } - return Biome.MANGROVE_SWAMP; + return switch (erosion) { + case 0, 1 -> middleOrBadlands(humidity, temperature, weirdness); + case 2, 3, 4, 5 -> riverBiome(temperature); + default -> valleySwampBiome(temperature); + }; } private @NonNull Biome getPeaksMidInlandBiome(int humidity, int temperature, int erosion, double weirdness) { - if (erosion == 0 || erosion == 1) { - if (temperature >= 0 && temperature <= 2) { - if (weirdness < 0) { - return Biome.JAGGED_PEAKS; - } else { - return Biome.FROZEN_PEAKS; - } - } else if (temperature == 3) { - return Biome.STONY_PEAKS; - } - return BadlandBiome.getBiome(humidity, weirdness); - - } else if (erosion == 2) { - return PlateauBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 3) { - if (temperature < 4) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - return BadlandBiome.getBiome(humidity, weirdness); - } else if (erosion == 4 || erosion == 6) { - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - return ShatteredBiome.getBiome(humidity, temperature, weirdness); + return switch (erosion) { + case 0, 1 -> peaksBiome(humidity, temperature, weirdness); + case 2 -> PlateauBiome.getBiome(humidity, temperature, weirdness); + case 3 -> middleOrBadlands(humidity, temperature, weirdness); + case 4, 6 -> MiddleBiome.getBiome(humidity, temperature, weirdness); + default -> ShatteredBiome.getBiome(humidity, temperature, weirdness); + }; } private @NonNull Biome getMidMidInlandBiome(int humidity, int temperature, int erosion, double weirdness) { - if (erosion == 0) { - if (temperature < 3 && (humidity == 0 || humidity == 1)) { - return Biome.SNOWY_SLOPES; - } - if (temperature < 3 && (humidity == 2 || humidity == 3 || humidity == 4)) { - return Biome.GROVE; - } - return PlateauBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 1) { - if (temperature == 0 && (humidity == 0 || humidity == 1)) { - return Biome.SNOWY_SLOPES; - } - if (temperature == 0 && (humidity == 2 || humidity == 3 || humidity == 4)) { - return Biome.GROVE; - } - if (temperature > 0 && temperature < 4) { - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - return BadlandBiome.getBiome(humidity, weirdness); - } else if (erosion == 2 || erosion == 3) { - if (temperature < 4) { - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - return BadlandBiome.getBiome(humidity, weirdness); - } else if (erosion == 4) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 5) { - return ShatteredBiome.getBiome(humidity, temperature, weirdness); - } - if (temperature == 0) { - // Middle Biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - if (temperature == 1 || temperature == 2) { - return Biome.SWAMP; - } - return Biome.MANGROVE_SWAMP; + return switch (erosion) { + case 0 -> coldSlopesOrPlateau(humidity, temperature, weirdness); + case 1 -> frozenSlopesOrMiddle(humidity, temperature, weirdness); + case 2, 3 -> middleOrBadlands(humidity, temperature, weirdness); + case 4 -> MiddleBiome.getBiome(humidity, temperature, weirdness); + case 5 -> ShatteredBiome.getBiome(humidity, temperature, weirdness); + default -> swampBiome(humidity, temperature, weirdness); + }; } private @NonNull Biome getLowMidInlandBiome(int humidity, int temperature, int erosion, double weirdness) { - if (erosion == 0 || erosion == 1) { - if (temperature == 0 && (humidity == 0 || humidity == 1)) { - return Biome.SNOWY_SLOPES; - } else if (temperature == 0 && humidity > 1) { - return Biome.GROVE; - } else if (temperature < 4) { - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - return BadlandBiome.getBiome(humidity, weirdness); - } - if (erosion == 2 || erosion == 3) { - if (temperature < 4) { - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - return BadlandBiome.getBiome(humidity, weirdness); - } - // e == 6 - if (temperature == 0) { - // Middle Biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - if (temperature == 1 || temperature == 2) { - return Biome.SWAMP; - } - return Biome.MANGROVE_SWAMP; + return switch (erosion) { + case 0, 1 -> frozenSlopesOrMiddle(humidity, temperature, weirdness); + case 2, 3 -> middleOrBadlands(humidity, temperature, weirdness); + default -> swampBiome(humidity, temperature, weirdness); + }; } private @NonNull Biome getHighMidInlandBiome(int humidity, int temperature, int erosion, double weirdness) { - if (erosion == 0) { - if (temperature < 3 && weirdness < 0D) { - return Biome.JAGGED_PEAKS; - } - if (temperature < 3 && weirdness > 0.0D) { - return Biome.FROZEN_PEAKS; - } - if (temperature == 3) { - return Biome.STONY_PEAKS; - } - return BadlandBiome.getBiome(humidity, weirdness); - } else if (erosion == 1) { - if (temperature < 3 && (humidity == 0 || humidity == 1)) { - return Biome.SNOWY_SLOPES; - } - if (temperature < 3 && (humidity == 2 || humidity == 3 || humidity == 4)) { - return Biome.GROVE; - } - return PlateauBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 2) { - return PlateauBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 3) { - if (temperature < 4) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - return BadlandBiome.getBiome(humidity, weirdness); - } else if (erosion == 4) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 5) { - return ShatteredBiome.getBiome(humidity, temperature, weirdness); - } - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - - private @NonNull Biome coastBiome(int humidity, int temperature, int erosion, double weirdness) { - return switch (Ridges.getRidge(convertToY(weirdness))) { - case HIGH -> getHighCoastBionme(humidity, temperature, erosion, weirdness); - case LOW -> getLowCoastBionme(humidity, temperature, erosion, weirdness); - case MID -> getMidCoastBionme(humidity, temperature, erosion, weirdness); - case PEAKS -> getPeaksCoastBionme(humidity, temperature, erosion, weirdness); - default -> getValleysCoastBionme(humidity, temperature, erosion, weirdness); + return switch (erosion) { + case 0 -> highMidInlandPeaks(humidity, temperature, weirdness); + case 1 -> coldSlopesOrPlateau(humidity, temperature, weirdness); + case 2 -> PlateauBiome.getBiome(humidity, temperature, weirdness); + case 3 -> middleOrBadlands(humidity, temperature, weirdness); + case 5 -> ShatteredBiome.getBiome(humidity, temperature, weirdness); + default -> MiddleBiome.getBiome(humidity, temperature, weirdness); }; } - private @NonNull Biome getValleysCoastBionme(int humidity, int temperature, int erosion, double weirdness) { - if (temperature > 0D) { - return Biome.RIVER; + /** + * Erosion 0 peaks for the mid inland high ridge. Unlike {@link #peaksBiome} a weirdness of exactly zero + * with a cold temperature falls through to badlands. + */ + private static @NonNull Biome highMidInlandPeaks(int humidity, int temperature, double weirdness) { + if (temperature < 3 && weirdness < 0D) { + return Biome.JAGGED_PEAKS; } - return Biome.FROZEN_RIVER; - } - - private @NonNull Biome getPeaksCoastBionme(int humidity, int temperature, int erosion, double weirdness) { - if (erosion == 0) { - if (temperature >= 0 && temperature <= 2) { - if (weirdness < 0) { - return Biome.JAGGED_PEAKS; - } else { - return Biome.FROZEN_PEAKS; - } - } else if (temperature == 3) { - return Biome.STONY_PEAKS; - } - return BadlandBiome.getBiome(humidity, weirdness); - - } else if (erosion == 1) { - if (temperature == 0 && (humidity == 0 || humidity == 1)) { - return Biome.SNOWY_SLOPES; - } else if (temperature == 0 && humidity > 1) { - return Biome.GROVE; - } else if (temperature < 4) { - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - return BadlandBiome.getBiome(humidity, weirdness); - } else if (erosion >= 2 && erosion <= 4) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 5) { - if (weirdness < 0 && (temperature == 0 || temperature == 1 || humidity == 4)) { - // Shattered biomes - return ShatteredBiome.getBiome(humidity, temperature, weirdness); - } - if (weirdness > 0 && (temperature > 2 && temperature <= 4 && humidity >= 0 && humidity <= 4)) { - return Biome.WINDSWEPT_SAVANNA; - } + if (temperature < 3 && weirdness > 0D) { + return Biome.FROZEN_PEAKS; } - // middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); + if (temperature == 3) { + return Biome.STONY_PEAKS; + } + return BadlandBiome.getBiome(humidity, weirdness); } - private @NonNull Biome getMidCoastBionme(int humidity, int temperature, int erosion, double weirdness) { - if (erosion > 0 && erosion < 3) { - return Biome.STONY_SHORE; - } else if (erosion == 3) { - // Middle Biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 4) { - if (weirdness < 0) { - // Beach Biomes - return getBeachBiome(temperature); - } else { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - } else if (erosion == 5) { - if (weirdness < 0) { - // Beach Biomes - return getBeachBiome(temperature); - } - if (weirdness < 0 && (temperature == 0 || temperature == 1 || humidity == 4)) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - if (weirdness > 0 && (temperature > 2 && temperature <= 4 && humidity >= 0 && humidity <= 4)) { - return Biome.WINDSWEPT_SAVANNA; - } + private @NonNull Biome coastBiome(int humidity, int temperature, int erosion, double weirdness) { + return switch (Ridges.getRidge(convertToY(weirdness))) { + case HIGH -> getHighCoastBiome(humidity, temperature, erosion, weirdness); + case LOW -> getLowCoastBiome(humidity, temperature, erosion, weirdness); + case MID -> getMidCoastBiome(humidity, temperature, erosion, weirdness); + case PEAKS -> getFarInlandPeaksBiome(humidity, temperature, erosion, weirdness); + default -> riverBiome(temperature); + }; + } - } else if (erosion == 6) { - if (weirdness < 0D) { - // Beach Biomes - return getBeachBiome(temperature); - } else { - // Middle Biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - } - // Else Beach biomes - return getBeachBiome(temperature); + private @NonNull Biome getMidCoastBiome(int humidity, int temperature, int erosion, double weirdness) { + return switch (erosion) { + case 1, 2 -> Biome.STONY_SHORE; + case 3 -> MiddleBiome.getBiome(humidity, temperature, weirdness); + case 4, 6 -> weirdness < 0 ? getBeachBiome(temperature) : MiddleBiome.getBiome(humidity, temperature, weirdness); + case 5 -> isWindsweptSavanna(temperature, weirdness) ? Biome.WINDSWEPT_SAVANNA : getBeachBiome(temperature); + default -> getBeachBiome(temperature); + }; } - private @NonNull Biome getLowCoastBionme(int humidity, int temperature, int erosion, double weirdness) { - if (erosion >= 0 && erosion < 3) { - return Biome.STONY_SHORE; - } else if (erosion >= 3 && erosion < 5) { - // Beach Biomes - return getBeachBiome(temperature); - } else if (erosion == 5) { - if (weirdness < 0) { - // Beach Biomes - return getBeachBiome(temperature); - } - if (weirdness < 0 && (temperature == 0 || temperature == 1 || humidity == 4)) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - if (weirdness > 0 && (temperature > 2 && temperature <= 4 && humidity >= 0 && humidity <= 4)) { - return Biome.WINDSWEPT_SAVANNA; - } - } - // Else Beach biomes - return getBeachBiome(temperature); + private @NonNull Biome getLowCoastBiome(int humidity, int temperature, int erosion, double weirdness) { + return switch (erosion) { + case 0, 1, 2 -> Biome.STONY_SHORE; + case 5 -> isWindsweptSavanna(temperature, weirdness) ? Biome.WINDSWEPT_SAVANNA : getBeachBiome(temperature); + default -> getBeachBiome(temperature); + }; } - private @NonNull Biome getHighCoastBionme(int humidity, int temperature, int erosion, double weirdness) { - if (erosion >= 0 && erosion < 5) { - // Middle Biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } else if (erosion == 5) { - if (weirdness < 0 && (temperature == 0 || temperature == 1 || humidity == 4)) { - // Middle biomes - return MiddleBiome.getBiome(humidity, temperature, weirdness); - } - if (weirdness > 0 && (temperature > 2 && temperature <= 4 && humidity >= 0 && humidity <= 4)) { - return Biome.WINDSWEPT_SAVANNA; - } + private @NonNull Biome getHighCoastBiome(int humidity, int temperature, int erosion, double weirdness) { + if (erosion == 5) { + return erosionFiveBiome(humidity, temperature, weirdness, false) + .orElseGet(() -> MiddleBiome.getBiome(humidity, temperature, weirdness)); } - // Middle Biomes return MiddleBiome.getBiome(humidity, temperature, weirdness); } - Biome getBeachBiome(int t) { return switch (t) { case 0 -> Biome.SNOWY_BEACH; @@ -1177,9 +879,8 @@ private Biome getMappedBiome(WorldInfo worldInfo, int x, int y, int z, BiomePara result = getQuadrantBiome(BlockFace.SOUTH_EAST, d); } - if (result == null || result.equals(Biome.CUSTOM)) { + if (result == null) { result = getVanillaBiome(worldInfo, x, y, z, biomeParameterPoint, null); - } // Caves @@ -1194,7 +895,7 @@ private Biome getMappedBiome(WorldInfo worldInfo, int x, int y, int z, BiomePara @Override public List getBiomes(WorldInfo worldInfo) { // Return all of them for now! - return Registry.BIOME.stream().filter(b -> !b.equals(Biome.CUSTOM)).toList(); + return RegistryAccess.registryAccess().getRegistry(RegistryKey.BIOME).stream().toList(); } /** @@ -1210,26 +911,37 @@ private SortedMap loadQuad(YamlConfiguration config, String secto } for (String ring : config.getStringList(sector)) { String[] split = ring.split(":"); - if (split.length == 2) { - try { - double d = Double.parseDouble(split[0]); - Biome biome = Biome.valueOf(split[1].toUpperCase(Locale.ENGLISH)); - if (biome == null) { - addon.logError(split[1].toUpperCase(Locale.ENGLISH) + " is an unknown biome on this server."); - result.put(d, Biome.CUSTOM); - } else { - // A biome of null means that no alternative biome should be applied - result.put(d, biome); - } - } catch (Exception e) { - addon.logError(sector + ": " + split[0] - + " does not seem to be a double. For integers add a .0 to the end"); - } - } else { + if (split.length != 2) { addon.logError(ring + " must be in the format ratio:biome where ratio is a double."); + continue; + } + try { + double d = Double.parseDouble(split[0]); + // A biome of null means that no alternative biome should be applied + result.put(d, parseBiome(split[1])); + } catch (NumberFormatException e) { + addon.logError(sector + ": " + split[0] + + " does not seem to be a double. For integers add a .0 to the end"); } } return result; } + /** + * Looks up a biome by config name. + * @param name biome name from the config, e.g. PLAINS or minecraft:plains. CUSTOM means "no biome". + * @return the biome, or null if the name is CUSTOM or the biome is unknown on this server + */ + private Biome parseBiome(String name) { + if (CUSTOM_BIOME.equalsIgnoreCase(name)) { + return null; + } + NamespacedKey key = NamespacedKey.fromString(name.toLowerCase(Locale.ENGLISH)); + Biome biome = key == null ? null : RegistryAccess.registryAccess().getRegistry(RegistryKey.BIOME).get(key); + if (biome == null) { + addon.logError(name.toUpperCase(Locale.ENGLISH) + " is an unknown biome on this server."); + } + return biome; + } + } diff --git a/src/main/java/world/bentobox/boxed/generators/biomes/BoxedBiomeGenerator.java b/src/main/java/world/bentobox/boxed/generators/biomes/BoxedBiomeGenerator.java index b7d664b..87d4c5f 100644 --- a/src/main/java/world/bentobox/boxed/generators/biomes/BoxedBiomeGenerator.java +++ b/src/main/java/world/bentobox/boxed/generators/biomes/BoxedBiomeGenerator.java @@ -1,6 +1,5 @@ package world.bentobox.boxed.generators.biomes; -import org.bukkit.World.Environment; import org.bukkit.block.Biome; import world.bentobox.boxed.Boxed; @@ -13,7 +12,7 @@ public class BoxedBiomeGenerator extends AbstractCopyBiomeProvider { public BoxedBiomeGenerator(Boxed boxed) { - super(boxed, Environment.NORMAL, Biome.OCEAN); + super(boxed, Biome.OCEAN); } } \ No newline at end of file diff --git a/src/main/java/world/bentobox/boxed/generators/biomes/BoxedNetherBiomeGenerator.java b/src/main/java/world/bentobox/boxed/generators/biomes/BoxedNetherBiomeGenerator.java index 504c220..b3cf4df 100644 --- a/src/main/java/world/bentobox/boxed/generators/biomes/BoxedNetherBiomeGenerator.java +++ b/src/main/java/world/bentobox/boxed/generators/biomes/BoxedNetherBiomeGenerator.java @@ -1,6 +1,5 @@ package world.bentobox.boxed.generators.biomes; -import org.bukkit.World.Environment; import org.bukkit.block.Biome; import world.bentobox.boxed.Boxed; @@ -13,7 +12,7 @@ public class BoxedNetherBiomeGenerator extends AbstractCopyBiomeProvider { public BoxedNetherBiomeGenerator(Boxed boxed) { - super(boxed, Environment.NETHER, Biome.BASALT_DELTAS); + super(boxed, Biome.BASALT_DELTAS); } } \ No newline at end of file diff --git a/src/main/java/world/bentobox/boxed/generators/chunks/AbstractBoxedChunkGenerator.java b/src/main/java/world/bentobox/boxed/generators/chunks/AbstractBoxedChunkGenerator.java index 766580a..025c7fd 100644 --- a/src/main/java/world/bentobox/boxed/generators/chunks/AbstractBoxedChunkGenerator.java +++ b/src/main/java/world/bentobox/boxed/generators/chunks/AbstractBoxedChunkGenerator.java @@ -25,7 +25,10 @@ public abstract class AbstractBoxedChunkGenerator extends ChunkGenerator { protected final Boxed addon; - protected static int size; + /** + * Half-width of the repeating seed region in chunks + */ + protected final int size; protected final Map, ChunkStore> chunks = new HashMap<>(); public record ChunkStore(ChunkSnapshot snapshot, List bpEnts, List chests, Map chunkBiomes) {} @@ -33,12 +36,9 @@ public record EntityData(Vector relativeLoc, BlueprintEntity entity) {} public record ChestData(Vector relativeLoc, BlueprintBlock chest) {} - //private final WorldRef wordRefNether; - - public AbstractBoxedChunkGenerator(Boxed addon) { + protected AbstractBoxedChunkGenerator(Boxed addon) { this.addon = addon; size = (int)(addon.getSettings().getIslandDistance() / 16D); // Size is chunks - } /** @@ -51,7 +51,8 @@ public void setChunk(int x, int z, Chunk chunk) { Map chunkBiomes = new HashMap<>(); for (int xx = 0; xx < 16; xx+=4) { for (int zz = 0; zz < 16; zz+=4) { - for (int yy = chunk.getWorld().getMinHeight(); yy < chunk.getWorld().getMaxHeight(); yy+=4) { // TODO: every 4th yy? + // Biomes are stored per 4x4x4 cell, so only sample every 4th block + for (int yy = chunk.getWorld().getMinHeight(); yy < chunk.getWorld().getMaxHeight(); yy+=4) { chunkBiomes.put(new Vector(xx, yy, zz), chunk.getBlock(xx, yy, zz).getBiome()); } } @@ -82,20 +83,22 @@ public boolean canSpawn(World world, int x, int z) /** - * Calculates the repeating value for a given size + * Maps a chunk coordinate back into this generator's repeating seed region + * @param chunkCoord chunk coord + * @return mapped chunk coord in the range [-size, size) + */ + public int repeatCalc(int chunkCoord) { + return repeatCalc(chunkCoord, size); + } + + /** + * Maps a chunk coordinate back into a repeating region of the given half-width * @param chunkCoord chunk coord - * @return mapped chunk coord + * @param size half-width of the region in chunks + * @return mapped chunk coord in the range [-size, size) */ - public static int repeatCalc(int chunkCoord) { + public static int repeatCalc(int chunkCoord, int size) { return Math.floorMod(chunkCoord + size, size*2) - size; - /* - int xx; - if (chunkCoord > 0) { - xx = Math.floorMod(chunkCoord + size, size*2) - size; - } else { - xx = Math.floorMod(chunkCoord - size, -size*2) + size; - } - return xx;*/ } /** @@ -119,19 +122,16 @@ public boolean shouldGenerateSurface() { @Override public boolean shouldGenerateCaves() { return false; - //return this.addon.getSettings().isGenerateCaves(); } @Override public boolean shouldGenerateDecorations() { return false; - //return this.addon.getSettings().isGenerateDecorations(); } @Override public boolean shouldGenerateMobs() { return true; - //return this.addon.getSettings().isGenerateMobs(); } @Override diff --git a/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java b/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java index 3344f84..c404602 100644 --- a/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java +++ b/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java @@ -1,6 +1,5 @@ package world.bentobox.boxed.generators.chunks; -import java.util.Map; import java.util.Objects; import java.util.Random; @@ -45,34 +44,38 @@ public BoxedBlockPopulator(Boxed addon) { @Override public void populate(WorldInfo worldInfo, Random random, int chunkX, int chunkZ, LimitedRegion limitedRegion) { - Map, ChunkStore> chunks = addon.getChunkGenerator(worldInfo.getEnvironment()).getChunks(); + AbstractBoxedChunkGenerator gen = addon.getChunkGenerator(worldInfo.getEnvironment()); World world = Bukkit.getWorld(worldInfo.getUID()); - int xx = BoxedChunkGenerator.repeatCalc(chunkX); - int zz = BoxedChunkGenerator.repeatCalc(chunkZ); - Pair coords = new Pair<>(xx, zz); - if (chunks.containsKey(coords)) { - ChunkStore data = chunks.get(coords); - // Paste entities - data.bpEnts().forEach(e -> { - Location l = getLoc(world, e.relativeLoc().clone(), chunkX, chunkZ); - if (limitedRegion.isInRegion(l)) { - Entity ent = limitedRegion.spawnEntity(l, e.entity().getType()); - e.entity().configureEntity(ent); - } - }); - // Fill chests - limitedRegion.getTileEntities().forEach(te -> { - int teX = BoxedChunkGenerator.repeatCalc(te.getX() >> 4); - int teZ = BoxedChunkGenerator.repeatCalc(te.getZ() >> 4); - if (teX == xx && teZ == zz) { - for (ChestData cd : data.chests()) { - Location chestLoc = getLoc(world, cd.relativeLoc().clone(), chunkX, chunkZ); - if (limitedRegion.isInRegion(chestLoc) && te.getLocation().equals(chestLoc)) { - this.setBlockState(te, cd.chest()); - } - } - } - }); + int xx = gen.repeatCalc(chunkX); + int zz = gen.repeatCalc(chunkZ); + ChunkStore data = gen.getChunks().get(new Pair<>(xx, zz)); + if (data == null) { + return; + } + // Paste entities + data.bpEnts().forEach(e -> { + Location l = getLoc(world, e.relativeLoc().clone(), chunkX, chunkZ); + if (limitedRegion.isInRegion(l)) { + Entity ent = limitedRegion.spawnEntity(l, e.entity().getType()); + e.entity().configureEntity(ent); + } + }); + // Fill chests + limitedRegion.getTileEntities().stream() + .filter(te -> gen.repeatCalc(te.getX() >> 4) == xx && gen.repeatCalc(te.getZ() >> 4) == zz) + .forEach(te -> fillChest(world, limitedRegion, data, te, chunkX, chunkZ)); + } + + /** + * Applies the stored chest data that matches this tile entity's location, if any + */ + private void fillChest(World world, LimitedRegion limitedRegion, ChunkStore data, BlockState te, int chunkX, + int chunkZ) { + for (ChestData cd : data.chests()) { + Location chestLoc = getLoc(world, cd.relativeLoc().clone(), chunkX, chunkZ); + if (limitedRegion.isInRegion(chestLoc) && te.getLocation().equals(chestLoc)) { + this.setBlockState(te, cd.chest()); + } } } diff --git a/src/main/java/world/bentobox/boxed/generators/chunks/BoxedChunkGenerator.java b/src/main/java/world/bentobox/boxed/generators/chunks/BoxedChunkGenerator.java index 48547ed..82da84d 100644 --- a/src/main/java/world/bentobox/boxed/generators/chunks/BoxedChunkGenerator.java +++ b/src/main/java/world/bentobox/boxed/generators/chunks/BoxedChunkGenerator.java @@ -87,44 +87,59 @@ protected List getTileEnts(Chunk chunk) { private List setEntities(Collection entities) { List bpEnts = new ArrayList<>(); for (LivingEntity entity: entities) { - BlueprintEntity bpe = new BlueprintEntity(); - bpe.setType(entity.getType()); - bpe.setCustomName(entity.getCustomName()); - if (entity instanceof Villager villager) { - setVillager(villager, bpe); - } - if (entity instanceof Colorable c) { - if (c.getColor() != null) { - bpe.setColor(c.getColor()); - } - } - if (entity instanceof Tameable tameable) { - bpe.setTamed(tameable.isTamed()); - } - if (entity instanceof ChestedHorse chestedHorse) { - bpe.setChest(chestedHorse.isCarryingChest()); - } - // Only set if child. Most animals are adults - if (entity instanceof Ageable ageable && !ageable.isAdult()) { - bpe.setAdult(false); - } - if (entity instanceof AbstractHorse horse) { - bpe.setDomestication(horse.getDomestication()); - bpe.setInventory(new HashMap<>()); - for (int i = 0; i < horse.getInventory().getSize(); i++) { - ItemStack item = horse.getInventory().getItem(i); - if (item != null) { - bpe.getInventory().put(i, item); - } - } - } + bpEnts.add(new EntityData(getLocInChunk(entity.getLocation()), toBlueprintEntity(entity))); + } + return bpEnts; + } + + /** + * Captures the entity's state as a blueprint entity + * @param entity living entity + * @return blueprint entity + */ + private BlueprintEntity toBlueprintEntity(LivingEntity entity) { + BlueprintEntity bpe = new BlueprintEntity(); + bpe.setType(entity.getType()); + bpe.setCustomName(entity.getCustomName()); + if (entity instanceof Villager villager) { + setVillager(villager, bpe); + } + if (entity instanceof Colorable c && c.getColor() != null) { + bpe.setColor(c.getColor()); + } + if (entity instanceof Tameable tameable) { + bpe.setTamed(tameable.isTamed()); + } + if (entity instanceof ChestedHorse chestedHorse) { + bpe.setChest(chestedHorse.isCarryingChest()); + } + // Only set if child. Most animals are adults + if (entity instanceof Ageable ageable && !ageable.isAdult()) { + bpe.setAdult(false); + } + if (entity instanceof AbstractHorse horse) { + setHorse(horse, bpe); + } + if (entity instanceof Horse horse) { + bpe.setStyle(horse.getStyle()); + } + return bpe; + } - if (entity instanceof Horse horse) { - bpe.setStyle(horse.getStyle()); + /** + * Set the horse domestication and inventory + * @param horse - horse + * @param bpe - Blueprint Entity + */ + private void setHorse(AbstractHorse horse, BlueprintEntity bpe) { + bpe.setDomestication(horse.getDomestication()); + bpe.setInventory(new HashMap<>()); + for (int i = 0; i < horse.getInventory().getSize(); i++) { + ItemStack item = horse.getInventory().getItem(i); + if (item != null) { + bpe.getInventory().put(i, item); } - bpEnts.add(new EntityData(getLocInChunk(entity.getLocation()), bpe)); } - return bpEnts; } // Get the location in the chunk @@ -212,8 +227,6 @@ public void generateNoise(WorldInfo worldInfo, Random r, int chunkX, int chunkZ, ChunkStore chunk = this.getChunk(xx,zz); if (chunk == null) { // This should never be needed because islands should abut each other - //cd.setRegion(0, minY, 0, 16, 0, 16, Material.WATER); - //BentoBox.getInstance().logError("No chunks found for " + xx + " " + zz); return; } // Copy the chunk diff --git a/src/main/java/world/bentobox/boxed/generators/chunks/BoxedSeedChunkGenerator.java b/src/main/java/world/bentobox/boxed/generators/chunks/BoxedSeedChunkGenerator.java index 6ba8dab..84d6a18 100644 --- a/src/main/java/world/bentobox/boxed/generators/chunks/BoxedSeedChunkGenerator.java +++ b/src/main/java/world/bentobox/boxed/generators/chunks/BoxedSeedChunkGenerator.java @@ -52,7 +52,6 @@ public boolean shouldGenerateNoise() { @Override public boolean shouldGenerateSurface() { return true; - // return this.addon.getSettings().isGenerateSurface(); } @Override @@ -73,18 +72,17 @@ public boolean shouldGenerateMobs() { @Override public boolean shouldGenerateStructures() { return true; - //return env.equals(Environment.NETHER); // We allow structures in the Nether } @Override protected List getEnts(Chunk chunk) { // These won't be stored - return null; + return List.of(); } @Override protected List getTileEnts(Chunk chunk) { // These won't be stored - return null; + return List.of(); } } diff --git a/src/main/java/world/bentobox/boxed/listeners/AdvancementListener.java b/src/main/java/world/bentobox/boxed/listeners/AdvancementListener.java index a6ac578..9d92889 100644 --- a/src/main/java/world/bentobox/boxed/listeners/AdvancementListener.java +++ b/src/main/java/world/bentobox/boxed/listeners/AdvancementListener.java @@ -10,6 +10,7 @@ import org.bukkit.Bukkit; import org.bukkit.GameMode; +import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.Server; @@ -91,15 +92,16 @@ public void onAdvancement(PlayerAdvancementDoneEvent e) { return; } // Check if player is in the Boxed worlds - if (addon.inWorld(e.getPlayer().getWorld())) { + Player player = e.getPlayer(); + if (addon.inWorld(player.getWorld())) { // Only allow members or higher to get advancements in a box - if (addon.getSettings().isDenyVisitorAdvancements() && !addon.getIslands().getIslandAt(e.getPlayer().getLocation()).map(i -> i.getMemberSet().contains(e.getPlayer().getUniqueId())).orElse(false)) { + if (addon.getSettings().isDenyVisitorAdvancements() && !isMemberAt(player)) { // Remove advancement from player e.getAdvancement().getCriteria().forEach(c -> - e.getPlayer().getAdvancementProgress(e.getAdvancement()).revokeCriteria(c)); - User u = User.getInstance(e.getPlayer()); - if (u != null && addon.getAdvManager().getScore(e.getAdvancement().getKey().getKey()) > 0) { - u.notify("boxed.adv-disallowed", TextVariables.NAME, e.getPlayer().getName(), TextVariables.DESCRIPTION, this.keyToString(u, e.getAdvancement().getKey())); + player.getAdvancementProgress(e.getAdvancement()).revokeCriteria(c)); + User u = User.getInstance(player); + if (addon.getAdvManager().getScore(e.getAdvancement().getKey().getKey()) > 0) { + u.notify("boxed.adv-disallowed", TextVariables.NAME, player.getName(), TextVariables.DESCRIPTION, this.keyToString(u, e.getAdvancement().getKey())); } return; } @@ -113,6 +115,20 @@ public void onAdvancement(PlayerAdvancementDoneEvent e) { } } + /** + * @param player player + * @return true if the player is a member (or higher) of the box they are standing in + */ + private boolean isMemberAt(Player player) { + Location loc = player.getLocation(); + if (loc == null) { + // OfflinePlayer#getLocation() is nullable; an online player always has one + return false; + } + return addon.getIslands().getIslandAt(loc).map(i -> i.getMemberSet().contains(player.getUniqueId())) + .orElse(false); + } + private void tellTeam(User user, NamespacedKey key, int score) { Island island = addon.getIslands().getIsland(addon.getOverWorld(), user); if (island == null) { @@ -253,7 +269,7 @@ public void onPlayerEnterWorld(PlayerChangedWorldEvent e) { @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onTeamJoinTime(TeamJoinedEvent e) { User user = User.getInstance(e.getPlayerUUID()); - if (user != null && addon.getSettings().isOnJoinResetAdvancements() && user.isOnline() + if (addon.getSettings().isOnJoinResetAdvancements() && user.isOnline() && addon.getOverWorld().equals(Util.getWorld(user.getWorld()))) { // Clear and set advancements clearAndSetAdv(user, addon.getSettings().isOnJoinResetAdvancements(), addon.getSettings().getOnJoinGrantAdvancements()); @@ -270,11 +286,10 @@ public void onTeamJoinTime(TeamJoinedEvent e) { public void onTeamLeaveTime(TeamLeaveEvent e) { if (addon.getSettings().isIgnoreAdvancements()) return; User user = User.getInstance(e.getPlayerUUID()); - if (user != null && addon.getSettings().isOnJoinResetAdvancements() && user.isOnline() + if (addon.getSettings().isOnJoinResetAdvancements() && user.isOnline() && addon.getOverWorld().equals(Util.getWorld(user.getWorld()))) { // Clear and set advancements clearAndSetAdv(user, addon.getSettings().isOnLeaveResetAdvancements(), addon.getSettings().getOnLeaveGrantAdvancements()); - } } diff --git a/src/main/java/world/bentobox/boxed/listeners/EnderPearlListener.java b/src/main/java/world/bentobox/boxed/listeners/EnderPearlListener.java index bff58c5..bd4eee0 100644 --- a/src/main/java/world/bentobox/boxed/listeners/EnderPearlListener.java +++ b/src/main/java/world/bentobox/boxed/listeners/EnderPearlListener.java @@ -37,6 +37,8 @@ public class EnderPearlListener implements Listener { /** * @param addon addon */ + private static final String NO_TELEPORT_OUTSIDE = "boxed.general.errors.no-teleport-outside"; + public EnderPearlListener(Boxed addon) { this.addon = addon; } @@ -48,7 +50,7 @@ public void onPlayerTeleport(PlayerTeleportEvent e) { return; // Allow the teleport this one time } if (!addon.inWorld(e.getFrom()) || !e.getPlayer().getGameMode().equals(GameMode.SURVIVAL) - || (e.getTo() != null && !addon.inWorld(e.getTo())) + || !addon.inWorld(e.getTo()) || addon.getIslands().getSpawn(e.getFrom().getWorld()).map(spawn -> spawn.onIsland(e.getTo())).orElse(false) ) { return; @@ -56,16 +58,14 @@ public void onPlayerTeleport(PlayerTeleportEvent e) { User u = User.getInstance(e.getPlayer()); // If the to-location is outside the box, cancel it - if (e.getTo() != null) { - addon.getIslands().getIslandAt(e.getTo()).ifPresent(i -> { - if (!i.onIsland(e.getTo())) { - u.sendMessage("boxed.general.errors.no-teleport-outside"); - addon.logWarning(e.getPlayer().getName() + " tried to teleport outside of their box from " - + e.getFrom() + " to " + e.getTo()); - e.setCancelled(true); - } - }); - } + addon.getIslands().getIslandAt(e.getTo()).ifPresent(i -> { + if (!i.onIsland(e.getTo())) { + u.sendMessage(NO_TELEPORT_OUTSIDE); + addon.logWarning(e.getPlayer().getName() + " tried to teleport outside of their box from " + + e.getFrom() + " to " + e.getTo()); + e.setCancelled(true); + } + }); } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) @@ -80,46 +80,50 @@ public void onEnderPearlLand(ProjectileHitEvent e) { // Moving box is allowed Location l = e.getHitBlock().getRelative(BlockFace.UP).getLocation(); World w = e.getHitBlock().getWorld(); - if (e.getEntity() instanceof EnderPearl ep && ep.getShooter() instanceof Player player) { - User u = User.getInstance(player); - // Check if enderpearl is inside or outside the box - // Get user's box - Island is = addon.getIslands().getIsland(w, u); - if (is == null) { - return; // Nothing to do - } + if (!(e.getEntity() instanceof EnderPearl ep) || !(ep.getShooter() instanceof Player player)) { + return; + } + User u = User.getInstance(player); + // Check if enderpearl is inside or outside the box + // Get user's box + Island is = addon.getIslands().getIsland(w, u); + if (is == null) { + return; // Nothing to do + } + // Get the box that the player is in and check that it is their box + addon.getIslands().getIslandAt(u.getLocation()) + .filter(fromIsland -> is.getUniqueId().equals(fromIsland.getUniqueId())) + .ifPresent(fromIsland -> handlePearlLanding(e, u, fromIsland, l)); + } - // Get the box that the player is in - addon.getIslands().getIslandAt(u.getLocation()).ifPresent(fromIsland -> { - // Check that it is their box - if (!is.getUniqueId().equals(fromIsland.getUniqueId())) { - return; + /** + * Handles an ender pearl thrown by a player from inside their own box. + * @param e projectile hit event + * @param u the thrower + * @param fromIsland the thrower's box + * @param l where the pearl landed + */ + private void handlePearlLanding(ProjectileHitEvent e, User u, Island fromIsland, Location l) { + // Find where the pearl landed + addon.getIslands().getIslandAt(l).ifPresentOrElse(toIsland -> { + if (fromIsland.getUniqueId().equals(toIsland.getUniqueId())) { + if (!toIsland.onIsland(l)) { + // Moving is allowed + moveBox(u, fromIsland, l); + Util.teleportAsync(u.getPlayer(), l, TeleportCause.ENDER_PEARL); } - // Find where the pearl landed - addon.getIslands().getIslandAt(l).ifPresentOrElse(toIsland -> { - if (fromIsland.getUniqueId().equals(toIsland.getUniqueId())) { - if (!toIsland.onIsland(l)) { - // Moving is allowed - moveBox(u, fromIsland, l); - Util.teleportAsync(player, l, TeleportCause.ENDER_PEARL); - } - } else { - // Different box. This is never allowed. Cancel the throw - e.setCancelled(true); - u.sendMessage("boxed.general.errors.no-teleport-outside"); - addon.logWarning("Enderpearl: " + player.getName() + " tried to teleport between boxes from " - + fromIsland.getCenter() + " to " + toIsland.getCenter()); - } - }, () -> { - // No box. This is never allowed. Cancel the throw - e.setCancelled(true); - u.sendMessage("boxed.general.errors.no-teleport-outside"); - addon.logWarning("Enderpearl: " + player.getName() + " tried to teleport between boxes from " - + fromIsland.getCenter() + " to some place outside"); - }); - - }); - } + } else { + // Different box. This is never allowed. Cancel the throw + cancelThrow(e, u, fromIsland, toIsland.getCenter()); + } + }, () -> cancelThrow(e, u, fromIsland, "some place outside")); // No box. This is never allowed. Cancel the throw + } + + private void cancelThrow(ProjectileHitEvent e, User u, Island fromIsland, Object destination) { + e.setCancelled(true); + u.sendMessage(NO_TELEPORT_OUTSIDE); + addon.logWarning("Enderpearl: " + u.getName() + " tried to teleport between boxes from " + + fromIsland.getCenter() + " to " + destination); } diff --git a/src/main/java/world/bentobox/boxed/listeners/NewAreaListener.java b/src/main/java/world/bentobox/boxed/listeners/NewAreaListener.java index 18577da..615bdf7 100644 --- a/src/main/java/world/bentobox/boxed/listeners/NewAreaListener.java +++ b/src/main/java/world/bentobox/boxed/listeners/NewAreaListener.java @@ -30,7 +30,6 @@ import org.bukkit.block.structure.StructureRotation; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -101,14 +100,12 @@ public class NewAreaListener implements Listener { private boolean pasting = true; private static final Gson gson = new Gson(); private static final String TODO = "ToDo"; + private static final String MINECRAFT = "minecraft:"; private static final String COULD_NOT_LOAD = "Could not load "; // Database handler for structure data private final Database handler; private final Database toPlace; - private static String bukkitVersion = "v" + Bukkit.getBukkitVersion().replace('.', '_').replace('-', '_'); - private static String pluginPackageName; - /** * Constructor for NewAreaListener. * Initializes structure files, databases, and starts the structure printer. @@ -117,7 +114,6 @@ public class NewAreaListener implements Listener { */ public NewAreaListener(Boxed addon) { this.addon = addon; - pluginPackageName = addon.getClass().getPackage().getName(); // Save the default structures file from the jar addon.saveResource("structures.yml", false); // Load the config @@ -140,13 +136,13 @@ private void runStructurePrinter() { // Run through all the structures in the Jar and register them with the server for (String js : JAR_STRUCTURES) { addon.saveResource("structures/" + js + ".nbt", false); - File structureFile = new File(addon.getDataFolder(), "structures/" + js + ".nbt"); + File nbtFile = new File(addon.getDataFolder(), "structures/" + js + ".nbt"); try { - Structure s = Bukkit.getStructureManager().loadStructure(structureFile); - Bukkit.getStructureManager().registerStructure(NamespacedKey.fromString("minecraft:boxed/" + js), s); + Structure s = Bukkit.getStructureManager().loadStructure(nbtFile); + Bukkit.getStructureManager().registerStructure(NamespacedKey.fromString(MINECRAFT + "boxed/" + js), s); addon.log("Loaded " + js + ".nbt"); } catch (IOException e) { - addon.logError("Error trying to load " + structureFile.getAbsolutePath()); + addon.logError("Error trying to load " + nbtFile.getAbsolutePath()); addon.getPlugin().logStacktrace(e); } } @@ -266,26 +262,27 @@ public void onPlayerMove(PlayerMoveEvent e) { return; } // Check where the player is - addon.getIslands().getIslandAt(e.getTo()).ifPresent(island -> { - // See if island is in cache - final String islandId = island.getUniqueId(); - IslandStructures is = getIslandStructData(islandId); - // Check if player is in any of the structures - Map structures = e.getTo().getWorld().getEnvironment().equals(Environment.NETHER) - ? is.getNetherStructureBoundingBoxMap() - : is.getStructureBoundingBoxMap(); - for (Map.Entry en : structures.entrySet()) { - if (en.getKey().contains(e.getTo().toVector())) { - for (String s : STRUCTURES) { - if (en.getValue().startsWith(s)) { - giveAdvFromCriteria(e.getPlayer(), s); - } - } - // STRUCTURES.stream().filter(en.getValue()::startsWith).forEach(s -> - // giveAdvFromCriteria(e.getPlayer(), s)); - } + addon.getIslands().getIslandAt(e.getTo()).ifPresent(island -> checkStructures(e.getPlayer(), island, e.getTo())); + } + + /** + * Awards structure advancements if the location is inside any of the island's structures. + * @param player player to award + * @param island island the player is on + * @param to location the player has moved to + */ + private void checkStructures(Player player, Island island, Location to) { + // See if island is in cache + IslandStructures is = getIslandStructData(island.getUniqueId()); + // Check if player is in any of the structures + Map structures = to.getWorld().getEnvironment().equals(Environment.NETHER) + ? is.getNetherStructureBoundingBoxMap() + : is.getStructureBoundingBoxMap(); + for (Map.Entry en : structures.entrySet()) { + if (en.getKey().contains(to.toVector())) { + STRUCTURES.stream().filter(en.getValue()::startsWith).forEach(s -> giveAdvFromCriteria(player, s)); } - }); + } } /** @@ -361,11 +358,11 @@ public void onIslandDeleted(IslandDeleteEvent event) { islandStructureCache.remove(deletedIslandId); // Remove from active build queue so we don't paste into a deleted island - itemsToBuild.removeIf(record -> event.getIsland().inIslandSpace(record.location())); + itemsToBuild.removeIf(rec -> event.getIsland().inIslandSpace(rec.location())); // Remove from in-memory pending structures for (List records : pending.values()) { - records.removeIf(record -> event.getIsland().inIslandSpace(record.location())); + records.removeIf(rec -> event.getIsland().inIslandSpace(rec.location())); } pending.values().removeIf(List::isEmpty); @@ -373,7 +370,7 @@ public void onIslandDeleted(IslandDeleteEvent event) { Map, List> readyToBuild = loadToDos().getReadyToBuild(); boolean dbChanged = false; for (List records : readyToBuild.values()) { - if (records.removeIf(record -> event.getIsland().inIslandSpace(record.location()))) { + if (records.removeIf(rec -> event.getIsland().inIslandSpace(rec.location()))) { dbChanged = true; } } @@ -443,7 +440,7 @@ private void place(ConfigurationSection section, Location center, Environment en // Check the structure exists Structure structure = Bukkit.getStructureManager() - .loadStructure(NamespacedKey.fromString("minecraft:" + name)); + .loadStructure(NamespacedKey.fromString(MINECRAFT + name)); if (structure == null) { BentoBox.getInstance().logError(COULD_NOT_LOAD + name); return; @@ -456,17 +453,15 @@ private void place(ConfigurationSection section, Location center, Environment en int z = Integer.parseInt(coords[2].strip()) + center.getBlockZ(); Location location = new Location(world, x, y, z); // Structure will be placed at location - readyToBuild.computeIfAbsent(new Pair<>(x >> 4, z >> 4), k -> new ArrayList<>()) - .add(new StructureRecord(name, "minecraft:" + name, location, rotation, mirror, noMobs, - Collections.emptyMap())); - this.itemsToBuild - .add(new StructureRecord(name, "minecraft:" + name, location, rotation, mirror, noMobs, - Collections.emptyMap())); + StructureRecord sr = new StructureRecord(name, MINECRAFT + name, location, rotation, mirror, noMobs, + Collections.emptyMap()); + readyToBuild.computeIfAbsent(new Pair<>(x >> 4, z >> 4), k -> new ArrayList<>()).add(sr); + this.itemsToBuild.add(sr); } else { addon.logError("Structure file syntax error: " + vector + ": " + Arrays.toString(coords)); } } - // Load any todo's and add the ones from this new island to the list + // Load the pending structures and add the ones from this new island to the list ToBePlacedStructures tbd = this.loadToDos(); Map, List> mergedMap = tbd.getReadyToBuild(); readyToBuild.forEach((key, value) -> mergedMap.merge(key, value, (list1, list2) -> { @@ -512,26 +507,38 @@ public static BoundingBox removeJigsaw(StructureRecord item) { }; BoundingBox bb = BoundingBox.of(loc, otherCorner); + boolean underwater = key.contains("underwater_ruin"); for (int x = (int) bb.getMinX(); x <= bb.getMaxX(); x++) { for (int y = (int) bb.getMinY(); y <= bb.getMaxY(); y++) { for (int z = (int) bb.getMinZ(); z <= bb.getMaxZ(); z++) { - Block b = loc.getWorld().getBlockAt(x, y, z); - if (b.getType().equals(Material.JIGSAW)) { - // I would like to read the data from the block and do something with it! - processJigsaw(b, structureRotation, !item.noMobs()); - } else if (b.getType().equals(Material.STRUCTURE_BLOCK)) { - processStructureBlock(b); - } - // Set water blocks for underwater ruins - if (key.contains("underwater_ruin") && b.getType().equals(Material.AIR)) { - b.setType(Material.WATER); - } + processBlock(loc.getWorld().getBlockAt(x, y, z), structureRotation, !item.noMobs(), underwater); } } } return bb; } + /** + * Processes a single block of a pasted structure: resolves jigsaw and structure blocks and + * fills air with water for underwater ruins. + * @param b block + * @param structureRotation the structure's rotation + * @param pasteMobs whether mobs should be spawned from jigsaw blocks + * @param underwater whether the structure is an underwater ruin + */ + private static void processBlock(Block b, StructureRotation structureRotation, boolean pasteMobs, + boolean underwater) { + if (b.getType().equals(Material.JIGSAW)) { + processJigsaw(b, structureRotation, pasteMobs); + } else if (b.getType().equals(Material.STRUCTURE_BLOCK)) { + processStructureBlock(b); + } + // Set water blocks for underwater ruins + if (underwater && b.getType().equals(Material.AIR)) { + b.setType(Material.WATER); + } + } + /** * Processes a structure block, possibly spawning entities or filling chests with loot. * @@ -551,13 +558,11 @@ private static void processStructureBlock(Block b) { Block downBlock = b.getRelative(BlockFace.DOWN); if (downBlock.getType().equals(Material.CHEST)) { Chest chest = (Chest) downBlock.getState(); - // TODO: for now just give treasure + // Only buried treasure loot is supported at the moment chest.setLootTable(LootTables.BURIED_TREASURE.getLootTable()); chest.update(); - if (chest.getBlockData() instanceof Waterlogged wl) { - if (wl.isWaterlogged()) { - b.setType(Material.WATER); - } + if (chest.getBlockData() instanceof Waterlogged wl && wl.isWaterlogged()) { + b.setType(Material.WATER); } } } @@ -624,10 +629,7 @@ private static void spawnMob(Block b, BoxedJigsawBlock bjb) { } // Spawn it if (type != null) { - Entity e = b.getWorld().spawnEntity(b.getRelative(BlockFace.UP).getLocation(), type); - if (e != null) { - e.setPersistent(true); - } + b.getWorld().spawnEntity(b.getRelative(BlockFace.UP).getLocation(), type).setPersistent(true); } } diff --git a/src/main/java/world/bentobox/boxed/objects/BoxedStructureBlock.java b/src/main/java/world/bentobox/boxed/objects/BoxedStructureBlock.java index 0d1a835..65d1905 100644 --- a/src/main/java/world/bentobox/boxed/objects/BoxedStructureBlock.java +++ b/src/main/java/world/bentobox/boxed/objects/BoxedStructureBlock.java @@ -12,8 +12,12 @@ * */ public class BoxedStructureBlock { - //{author:"LadyAgnes",ignoreEntities:1b,integrity:1.0f,metadata:"drowned",mirror:"NONE",mode:"DATA",name:"",posX:0,posY:1,posZ:0,powered:0b,rotation:"NONE",seed:0L,showair:0b - //,showboundingbox:1b,sizeX:0,sizeY:0,sizeZ:0} + /* + * Example NBT payload this class is deserialized from: + * author "LadyAgnes", ignoreEntities 1b, integrity 1.0f, metadata "drowned", mirror "NONE", mode "DATA", + * name "", posX 0, posY 1, posZ 0, powered 0b, rotation "NONE", seed 0L, showair 0b, showboundingbox 1b, + * sizeX 0, sizeY 0, sizeZ 0 + */ @Expose private String author; @Expose diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index d8951dd..50f4aa0 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -474,12 +474,18 @@ area: require-confirmation: true deaths: # Whether deaths are counted or not. + # If false, BentoBox does not count deaths and the Level addon does not record deaths against areas. counted: true # Maximum number of deaths to count. The death count can be used by add-ons. + # Since Level 2.29.0 this also caps how many deaths each member can contribute to an area's death penalty. max: 10 - # When a player joins a team, reset their death count + # When a player joins a team, reset their death count. + # This only affects BentoBox's own per-player death count, used by the %boxed_deaths% placeholder. + # Since Level 2.29.0, area levels use per-area death tracking and are not affected by this setting. team-join-reset: true - # Reset player death count when they start a new area or reset an area + # Reset player death count when they start a new area or reset an area. + # This only affects BentoBox's own per-player death count. + # Since Level 2.29.0 the Level addon clears an area's own death record automatically when it is reset or deleted. reset-on-new-area: true protection: # Geo restrict mobs. diff --git a/src/test/java/world/bentobox/boxed/AdvancementsManagerTest.java b/src/test/java/world/bentobox/boxed/AdvancementsManagerTest.java index a36eb7a..62ab53e 100644 --- a/src/test/java/world/bentobox/boxed/AdvancementsManagerTest.java +++ b/src/test/java/world/bentobox/boxed/AdvancementsManagerTest.java @@ -61,11 +61,11 @@ class AdvancementsManagerTest extends CommonTestSetup { private AbstractDatabaseHandler h; @SuppressWarnings("unchecked") - @Override + /** + * Runs after {@link CommonTestSetup#setUp()} (JUnit runs superclass @BeforeEach methods first) + */ @BeforeEach - public void setUp() throws Exception { - super.setUp(); - + void setUpManager() throws Exception { // Database static mock (local — CommonTestSetup does not handle this one) h = mock(AbstractDatabaseHandler.class); mockedDatabaseSetup = Mockito.mockStatic(DatabaseSetup.class); diff --git a/src/test/java/world/bentobox/boxed/CommonTestSetup.java b/src/test/java/world/bentobox/boxed/CommonTestSetup.java index 0962436..26fc7d6 100644 --- a/src/test/java/world/bentobox/boxed/CommonTestSetup.java +++ b/src/test/java/world/bentobox/boxed/CommonTestSetup.java @@ -21,22 +21,16 @@ import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.entity.Player.Spigot; -import org.bukkit.event.entity.EntityExplodeEvent; -import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.inventory.ItemFactory; -import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; import org.bukkit.metadata.FixedMetadataValue; import org.bukkit.metadata.MetadataValue; import org.bukkit.plugin.PluginManager; import org.bukkit.scheduler.BukkitScheduler; import org.bukkit.util.Vector; -import org.eclipse.jdt.annotation.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.mockbukkit.mockbukkit.MockBukkit; @@ -131,7 +125,7 @@ public abstract class CommonTestSetup { @BeforeEach - public void setUp() throws Exception { + public void setUp() { // Processes the @Mock annotations and initializes the field closeable = MockitoAnnotations.openMocks(this); server = MockBukkit.mock(); @@ -223,14 +217,13 @@ public void setUp() throws Exception { //Util mockedUtil = Mockito.mockStatic(Util.class, Mockito.CALLS_REAL_METHODS); - mockedUtil.when(() -> Util.getWorld(any())).thenReturn(mock(World.class)); + World utilWorld = mock(World.class); + mockedUtil.when(() -> Util.getWorld(any())).thenReturn(utilWorld); Util.setPlugin(plugin); // Util mockedUtil.when(() -> Util.findFirstMatchingEnum(any(), any())).thenCallRealMethod(); - // Util translate color codes (used in user translate methods) - //mockedUtil.when(() -> translateColorCodes(anyString())).thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); - + // Server & Scheduler mockedBukkit.when(Bukkit::getScheduler).thenReturn(sch); @@ -287,8 +280,8 @@ public void checkSpigotMessage(String expectedMessage, int expectedOccurrences) List capturedMessages = captor.getAllValues(); // Count the number of occurrences of the expectedMessage in the captured messages - long actualOccurrences = capturedMessages.stream().map(component -> component.toLegacyText()) // Convert each TextComponent to plain text - .filter(messageText -> messageText.contains(expectedMessage)) // Check if the message contains the expected text + long actualOccurrences = capturedMessages.stream() + .filter(component -> component.toLegacyText().contains(expectedMessage)) // Check if the message contains the expected text .count(); // Count how many times the expected message appears // Assert that the number of occurrences matches the expectedOccurrences @@ -296,23 +289,4 @@ public void checkSpigotMessage(String expectedMessage, int expectedOccurrences) actualOccurrences, "Expected message occurrence mismatch: " + expectedMessage); } - /** - * Get the exploded event - * @param entity - * @param l - * @param list - * @return - */ - public EntityExplodeEvent getExplodeEvent(Entity entity, Location l, List list) { - //return new EntityExplodeEvent(entity, l, list, 0, null); - return new EntityExplodeEvent(entity, l, list, 0, null); - } - - public PlayerDeathEvent getPlayerDeathEvent(Player player, List drops, int droppedExp, int newExp, - int newTotalExp, int newLevel, @Nullable String deathMessage) { - //Technically this null is not allowed, but it works right now - return new PlayerDeathEvent(player, null, drops, droppedExp, newExp, - newTotalExp, newLevel, deathMessage); - } - } diff --git a/src/test/java/world/bentobox/boxed/PlaceholdersManagerTest.java b/src/test/java/world/bentobox/boxed/PlaceholdersManagerTest.java index 3eb15ec..78e33f6 100644 --- a/src/test/java/world/bentobox/boxed/PlaceholdersManagerTest.java +++ b/src/test/java/world/bentobox/boxed/PlaceholdersManagerTest.java @@ -37,7 +37,7 @@ class PlaceholdersManagerTest extends CommonTestSetup { @Override @BeforeEach - public void setUp() throws Exception { + public void setUp() { super.setUp(); uuid = UUID.randomUUID(); diff --git a/src/test/java/world/bentobox/boxed/SettingsTest.java b/src/test/java/world/bentobox/boxed/SettingsTest.java index fb8aca4..200b1b2 100644 --- a/src/test/java/world/bentobox/boxed/SettingsTest.java +++ b/src/test/java/world/bentobox/boxed/SettingsTest.java @@ -26,7 +26,7 @@ class SettingsTest extends CommonTestSetup { @Override @BeforeEach - public void setUp() throws Exception { + public void setUp() { super.setUp(); s = new Settings(); } diff --git a/src/test/java/world/bentobox/boxed/commands/AdminPlaceStructureCommandTest.java b/src/test/java/world/bentobox/boxed/commands/AdminPlaceStructureCommandTest.java index 1a64b38..b648dfb 100644 --- a/src/test/java/world/bentobox/boxed/commands/AdminPlaceStructureCommandTest.java +++ b/src/test/java/world/bentobox/boxed/commands/AdminPlaceStructureCommandTest.java @@ -38,7 +38,7 @@ class AdminPlaceStructureCommandTest extends CommonTestSetup { private User user; @BeforeEach - public void setUpCommand() { + void setUpCommand() { addon = mock(Boxed.class); CompositeCommand parent = mock(CompositeCommand.class); diff --git a/src/test/java/world/bentobox/boxed/generators/biomes/ContLocTest.java b/src/test/java/world/bentobox/boxed/generators/biomes/ContLocTest.java new file mode 100644 index 0000000..88a8187 --- /dev/null +++ b/src/test/java/world/bentobox/boxed/generators/biomes/ContLocTest.java @@ -0,0 +1,38 @@ +package world.bentobox.boxed.generators.biomes; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import world.bentobox.boxed.generators.biomes.AbstractSeedBiomeProvider.ContLoc; + +/** + * Tests {@link ContLoc#getCont(double)} band lookup, including values outside the nominal + * continentalness range that vanilla noise can produce. + */ +class ContLocTest { + + @Test + void testBelowNominalRangeIsMushroomFields() { + // Value seen in the wild that used to throw "contloc out of spec" + assertEquals(ContLoc.MUSHROOM_FIELDS, ContLoc.getCont(-1.2687000036239624)); + assertEquals(ContLoc.MUSHROOM_FIELDS, ContLoc.getCont(-100D)); + } + + @Test + void testAboveNominalRangeIsFarInland() { + assertEquals(ContLoc.FAR_INLAND, ContLoc.getCont(10D)); + assertEquals(ContLoc.FAR_INLAND, ContLoc.getCont(100D)); + } + + @Test + void testBandBoundaries() { + assertEquals(ContLoc.MUSHROOM_FIELDS, ContLoc.getCont(-1.2)); + assertEquals(ContLoc.DEEP_OCEAN, ContLoc.getCont(-1.05)); + assertEquals(ContLoc.OCEAN, ContLoc.getCont(-0.455)); + assertEquals(ContLoc.COAST, ContLoc.getCont(-0.19)); + assertEquals(ContLoc.NEAR_INLAND, ContLoc.getCont(-0.11)); + assertEquals(ContLoc.MID_INLAND, ContLoc.getCont(0.03)); + assertEquals(ContLoc.FAR_INLAND, ContLoc.getCont(0.3)); + } +} diff --git a/src/test/java/world/bentobox/boxed/generators/biomes/CopyBiomeProviderTest.java b/src/test/java/world/bentobox/boxed/generators/biomes/CopyBiomeProviderTest.java index 2217520..3eafe0c 100644 --- a/src/test/java/world/bentobox/boxed/generators/biomes/CopyBiomeProviderTest.java +++ b/src/test/java/world/bentobox/boxed/generators/biomes/CopyBiomeProviderTest.java @@ -1,7 +1,7 @@ package world.bentobox.boxed.generators.biomes; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -19,7 +19,6 @@ import world.bentobox.boxed.Boxed; import world.bentobox.boxed.CommonTestSetup; import world.bentobox.boxed.Settings; -import world.bentobox.boxed.WhiteBox; import world.bentobox.boxed.generators.chunks.AbstractBoxedChunkGenerator; import world.bentobox.boxed.generators.chunks.AbstractBoxedChunkGenerator.ChunkStore; @@ -36,7 +35,7 @@ class CopyBiomeProviderTest extends CommonTestSetup { private Boxed addon; @BeforeEach - public void setUpProvider() { + void setUpProvider() { addon = mock(Boxed.class); Settings settings = mock(Settings.class); when(addon.getSettings()).thenReturn(settings); @@ -48,8 +47,9 @@ public void setUpProvider() { worldInfo = mock(WorldInfo.class); when(worldInfo.getEnvironment()).thenReturn(Environment.NORMAL); - // 400 / 16 = 25 chunks half-width. repeatCalc needs size > 0. - WhiteBox.setInternalState(AbstractBoxedChunkGenerator.class, "size", 25); + // 400 / 16 = 25 chunks half-width. + when(chunkGen.repeatCalc(anyInt())) + .thenAnswer(inv -> AbstractBoxedChunkGenerator.repeatCalc(inv.getArgument(0), 25)); gen = new BoxedBiomeGenerator(addon); } @@ -78,7 +78,7 @@ void testReturnsDefaultBiomeAndWarnsWhenChunkMissing() { assertEquals(Biome.OCEAN, gen.getBiome(worldInfo, 3, 64, 5)); // The missing snapshot is logged as a warning - verify(plugin).logWarning(eq("Snapshot at 0 0 is not stored")); + verify(plugin).logWarning("Snapshot at 0 0 is not stored"); } @Test diff --git a/src/test/java/world/bentobox/boxed/generators/chunks/RepeatCalcTest.java b/src/test/java/world/bentobox/boxed/generators/chunks/RepeatCalcTest.java index a665aa7..7962bd9 100644 --- a/src/test/java/world/bentobox/boxed/generators/chunks/RepeatCalcTest.java +++ b/src/test/java/world/bentobox/boxed/generators/chunks/RepeatCalcTest.java @@ -4,18 +4,22 @@ import org.junit.jupiter.api.Test; -import world.bentobox.boxed.WhiteBox; - /** - * Tests {@link AbstractBoxedChunkGenerator#repeatCalc(int)} - the function that + * Tests {@link AbstractBoxedChunkGenerator#repeatCalc(int, int)} - the function that * maps an arbitrary chunk coordinate back into the repeating seed region * {@code [-size, size)}. This is what makes the small captured seed area tile * infinitely across the game world. */ class RepeatCalcTest { + private int size; + private void setSize(int size) { - WhiteBox.setInternalState(AbstractBoxedChunkGenerator.class, "size", size); + this.size = size; + } + + private int repeatCalc(int c) { + return AbstractBoxedChunkGenerator.repeatCalc(c, size); } @Test @@ -23,7 +27,7 @@ void testIdentityWithinRange() { setSize(5); // Coordinates already inside [-size, size) are returned unchanged for (int c = -5; c < 5; c++) { - assertEquals(c, AbstractBoxedChunkGenerator.repeatCalc(c), "coord " + c); + assertEquals(c, repeatCalc(c), "coord " + c); } } @@ -31,26 +35,26 @@ void testIdentityWithinRange() { void testWrapsAboveRange() { setSize(5); // size maps back to -size, and it keeps wrapping with period 2*size - assertEquals(-5, AbstractBoxedChunkGenerator.repeatCalc(5)); - assertEquals(-4, AbstractBoxedChunkGenerator.repeatCalc(6)); - assertEquals(0, AbstractBoxedChunkGenerator.repeatCalc(10)); - assertEquals(4, AbstractBoxedChunkGenerator.repeatCalc(14)); - assertEquals(-1, AbstractBoxedChunkGenerator.repeatCalc(19)); + assertEquals(-5, repeatCalc(5)); + assertEquals(-4, repeatCalc(6)); + assertEquals(0, repeatCalc(10)); + assertEquals(4, repeatCalc(14)); + assertEquals(-1, repeatCalc(19)); } @Test void testWrapsBelowRange() { setSize(5); - assertEquals(-5, AbstractBoxedChunkGenerator.repeatCalc(-5)); - assertEquals(0, AbstractBoxedChunkGenerator.repeatCalc(-10)); - assertEquals(-1, AbstractBoxedChunkGenerator.repeatCalc(-11)); + assertEquals(-5, repeatCalc(-5)); + assertEquals(0, repeatCalc(-10)); + assertEquals(-1, repeatCalc(-11)); } @Test void testResultAlwaysWithinRange() { setSize(8); for (int c = -100; c <= 100; c++) { - int r = AbstractBoxedChunkGenerator.repeatCalc(c); + int r = repeatCalc(c); assertEquals(true, r >= -8 && r < 8, "coord " + c + " mapped out of range to " + r); } } @@ -58,10 +62,10 @@ void testResultAlwaysWithinRange() { @Test void testDifferentSize() { setSize(1); - // With size 1 the region is just {-1, 0} - assertEquals(0, AbstractBoxedChunkGenerator.repeatCalc(0)); - assertEquals(-1, AbstractBoxedChunkGenerator.repeatCalc(-1)); - assertEquals(-1, AbstractBoxedChunkGenerator.repeatCalc(1)); - assertEquals(0, AbstractBoxedChunkGenerator.repeatCalc(2)); + // With size 1 the region only contains the two chunks -1 and 0 + assertEquals(0, repeatCalc(0)); + assertEquals(-1, repeatCalc(-1)); + assertEquals(-1, repeatCalc(1)); + assertEquals(0, repeatCalc(2)); } } diff --git a/src/test/java/world/bentobox/boxed/listeners/AdvancementListenerTest.java b/src/test/java/world/bentobox/boxed/listeners/AdvancementListenerTest.java index e36dc70..6072283 100644 --- a/src/test/java/world/bentobox/boxed/listeners/AdvancementListenerTest.java +++ b/src/test/java/world/bentobox/boxed/listeners/AdvancementListenerTest.java @@ -1,11 +1,13 @@ package world.bentobox.boxed.listeners; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -46,6 +48,7 @@ import world.bentobox.boxed.Boxed; import world.bentobox.boxed.CommonTestSetup; import world.bentobox.boxed.Settings; +import world.bentobox.boxed.objects.IslandAdvancements; /** * @author tastybento @@ -76,7 +79,7 @@ class AdvancementListenerTest extends CommonTestSetup { @Override @BeforeEach - public void setUp() throws Exception { + public void setUp() { super.setUp(); // Local User mock (parent only cached mockPlayer, not our local player mock) @@ -219,26 +222,34 @@ private PlayerPortalEvent portalEvent(TeleportCause cause) { @Test void testOnPortalNetherNoException() { - // With null netherAdvancement fields, giveAdv short-circuits — we just assert no throw. - listener.onPortal(portalEvent(TeleportCause.NETHER_PORTAL)); + // With null netherAdvancement fields, giveAdv short-circuits — assert no throw and that the cause was inspected. + PlayerPortalEvent e = portalEvent(TeleportCause.NETHER_PORTAL); + assertDoesNotThrow(() -> listener.onPortal(e)); + verify(e).getCause(); } @Test void testOnPortalEndNoException() { - listener.onPortal(portalEvent(TeleportCause.END_PORTAL)); + PlayerPortalEvent e = portalEvent(TeleportCause.END_PORTAL); + assertDoesNotThrow(() -> listener.onPortal(e)); + verify(e, atLeastOnce()).getCause(); } @Test void testOnPortalNotSurvival() { when(player.getGameMode()).thenReturn(GameMode.CREATIVE); - // should early return — no NPE even though we don't stub the cause - listener.onPortal(portalEvent(TeleportCause.NETHER_PORTAL)); + // should early return before the cause is ever inspected + PlayerPortalEvent e = portalEvent(TeleportCause.NETHER_PORTAL); + listener.onPortal(e); + verify(e, never()).getCause(); } @Test void testOnPortalNotInWorld() { when(addon.inWorld(world)).thenReturn(false); - listener.onPortal(portalEvent(TeleportCause.NETHER_PORTAL)); + PlayerPortalEvent e = portalEvent(TeleportCause.NETHER_PORTAL); + listener.onPortal(e); + verify(e, never()).getCause(); } // ---------- syncAdvancements ---------- @@ -261,8 +272,8 @@ void testSyncAdvancementsNoIsland() { void testSyncAdvancementsSizeIncreased() { when(advManager.checkIslandSize(island)).thenReturn(3); // Return a non-null IslandAdvancements stub for grantAdv's iteration - when(advManager.getIsland(island)) - .thenReturn(mock(world.bentobox.boxed.objects.IslandAdvancements.class)); + IslandAdvancements islandAdvancements = mock(IslandAdvancements.class); + when(advManager.getIsland(island)).thenReturn(islandAdvancements); listener.syncAdvancements(user); verify(user).sendMessage(eq("boxed.size-changed"), anyString(), eq("3")); verify(player).playSound(eq(playerLocation), eq(Sound.ENTITY_PLAYER_LEVELUP), org.mockito.ArgumentMatchers.anyFloat(), org.mockito.ArgumentMatchers.anyFloat()); @@ -271,8 +282,8 @@ void testSyncAdvancementsSizeIncreased() { @Test void testSyncAdvancementsSizeDecreased() { when(advManager.checkIslandSize(island)).thenReturn(-2); - when(advManager.getIsland(island)) - .thenReturn(mock(world.bentobox.boxed.objects.IslandAdvancements.class)); + IslandAdvancements islandAdvancements = mock(IslandAdvancements.class); + when(advManager.getIsland(island)).thenReturn(islandAdvancements); listener.syncAdvancements(user); verify(user).sendMessage(eq("boxed.size-decreased"), anyString(), eq("2")); } diff --git a/src/test/java/world/bentobox/boxed/listeners/EnderPearlListenerTest.java b/src/test/java/world/bentobox/boxed/listeners/EnderPearlListenerTest.java index 90ce454..735fdee 100644 --- a/src/test/java/world/bentobox/boxed/listeners/EnderPearlListenerTest.java +++ b/src/test/java/world/bentobox/boxed/listeners/EnderPearlListenerTest.java @@ -81,7 +81,7 @@ class EnderPearlListenerTest extends CommonTestSetup { @Override @BeforeEach - public void setUp() throws Exception { + public void setUp() { super.setUp(); // Local static mock for User (parent already wired User.setPlugin + one cached mockPlayer) @@ -167,7 +167,7 @@ void testEnderPearlListener() { */ @Test void testOnPlayerTeleportNotAllowed() { - PlayerTeleportEvent e = new PlayerTeleportEvent(player, from, to, TeleportCause.CHORUS_FRUIT); + PlayerTeleportEvent e = new PlayerTeleportEvent(player, from, to, TeleportCause.CONSUMABLE_EFFECT); epl.onPlayerTeleport(e); assertTrue(e.isCancelled()); verify(user).sendMessage("boxed.general.errors.no-teleport-outside"); @@ -179,7 +179,7 @@ void testOnPlayerTeleportNotAllowed() { @Test void testOnPlayerTeleportNotSurvival() { when(player.getGameMode()).thenReturn(GameMode.CREATIVE); - PlayerTeleportEvent e = new PlayerTeleportEvent(player, from, to, TeleportCause.CHORUS_FRUIT); + PlayerTeleportEvent e = new PlayerTeleportEvent(player, from, to, TeleportCause.CONSUMABLE_EFFECT); epl.onPlayerTeleport(e); assertFalse(e.isCancelled()); verify(user, never()).sendMessage("boxed.general.errors.no-teleport-outside"); @@ -191,7 +191,7 @@ void testOnPlayerTeleportNotSurvival() { @Test void testOnPlayerTeleportNullTo() { when(player.getGameMode()).thenReturn(GameMode.CREATIVE); - PlayerTeleportEvent e = new PlayerTeleportEvent(player, from, null, TeleportCause.CHORUS_FRUIT); + PlayerTeleportEvent e = new PlayerTeleportEvent(player, from, null, TeleportCause.CONSUMABLE_EFFECT); epl.onPlayerTeleport(e); assertFalse(e.isCancelled()); verify(user, never()).sendMessage("boxed.general.errors.no-teleport-outside"); @@ -203,7 +203,7 @@ void testOnPlayerTeleportNullTo() { @Test void testOnPlayerTeleportToSpawn() { when(spawn.onIsland(any())).thenReturn(true); - PlayerTeleportEvent e = new PlayerTeleportEvent(player, from, to, TeleportCause.CHORUS_FRUIT); + PlayerTeleportEvent e = new PlayerTeleportEvent(player, from, to, TeleportCause.CONSUMABLE_EFFECT); epl.onPlayerTeleport(e); assertFalse(e.isCancelled()); verify(user, never()).sendMessage("boxed.general.errors.no-teleport-outside"); @@ -216,7 +216,7 @@ void testOnPlayerTeleportToSpawn() { void testOnPlayerTeleportNotInWorldAllowed() { when(addon.inWorld(any(World.class))).thenReturn(false); when(addon.inWorld(any(Location.class))).thenReturn(false); - PlayerTeleportEvent e = new PlayerTeleportEvent(player, from, to, TeleportCause.CHORUS_FRUIT); + PlayerTeleportEvent e = new PlayerTeleportEvent(player, from, to, TeleportCause.CONSUMABLE_EFFECT); epl.onPlayerTeleport(e); assertFalse(e.isCancelled()); verify(user, never()).sendMessage("boxed.general.errors.no-teleport-outside");