From 962cddfda81636c47d66eb6129410795b3b79154 Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 3 Jun 2026 08:08:03 -0700 Subject: [PATCH 01/16] Update Modrinth game-versions to current MC range Brings the supported-versions list in line with the other addons (1.21.5-1.21.11, 26.1, 26.1.1, 26.1.2). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/modrinth-publish.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 From 104efe6a68bd29254c523efabb26418e3a02a046 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 12 Jun 2026 08:27:50 -0700 Subject: [PATCH 02/16] Document using a custom map as the seed world Adds a README section explaining the three practical ways to get custom terrain into player boxes (match an existing world's seed, edit the seed world by hand, or drop in your own world as the seed), plus a cross-link from the World Seed note. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/README.md b/README.md index e4fc994..9e7ec0e 100644 --- a/README.md +++ b/README.md @@ -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 letting [Regionerator](#using-regionerator) clean up unused chunks so they regenerate. The seed world is the source of truth; the game world is rebuilt from it. + +### 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. +* Keep the seed worlds exempt from chunk-cleaning plugins (see [Using Regionerator](#using-regionerator)) 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. From 2d933b4614ce6fe248a9964b4509ee0facfe9af6 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 12 Jun 2026 08:36:59 -0700 Subject: [PATCH 03/16] Document native BentoBox region deletion; demote Regionerator BentoBox 3.16.1 reclaims island disk space natively via the housekeeping region-file sweep (added 3.15.0), so Regionerator is no longer required. Replace the "Using Regionerator" section with "Reclaiming Disk Space", covering the deleted-sweep/age-sweep config keys, /boxadmin purge deleted, and the fact that the sweep never touches Boxed's seed worlds. Note that deletion.keep-previous-island-on-reset is now ignored, keep Regionerator as a legacy/optional path, and update the custom-map cross-links. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 48 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 9e7ec0e..5819e09 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ You can go into the seed world and build, paste, or remove whatever you like. 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 letting [Regionerator](#using-regionerator) clean up unused chunks so they regenerate. The seed world is the source of truth; the game world is rebuilt from it. +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 @@ -200,7 +200,7 @@ Biomes: because the game world copies the seed world's biomes unchanged, your im ### Tips and gotchas * The copy radius is `world.island-distance`. If you increase it, your custom content must cover the larger area. -* Keep the seed worlds exempt from chunk-cleaning plugins (see [Using Regionerator](#using-regionerator)) or your custom seed terrain may be deleted and boxes will stop matching. +* 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. @@ -241,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) -*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!* +**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.) -[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: +### How it works now + +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. + +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 -``` - From b3c34f6fd58537156e76310df54547c43af99ebf Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 12 Jun 2026 08:40:36 -0700 Subject: [PATCH 04/16] Bump BentoBox dependency to 3.17.0 Update bentobox.version 3.13.0 -> 3.17.0 so the build matches the latest release and the native region-file deletion behaviour now documented in the README. Update the README BentoBox requirement accordingly. All 167 tests pass against 3.17.0. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5819e09..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 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 From 8fe4aa9a233a014abaa0dbbb6d791e0909f2f5a1 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 12 Jun 2026 17:00:25 -0700 Subject: [PATCH 05/16] Update CLAUDE.md to BentoBox 3.17.0 Keep the project note in sync with the pom dependency bump. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1eb92cc..beb40d5 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 From 65384270d4a8400331e4853116fa25b05fd1ed10 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 27 Jun 2026 17:26:41 -0700 Subject: [PATCH 06/16] Add files via upload --- .github/workflows/publish.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..00b05d8 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,19 @@ +# Boxed — .github/workflows/publish.yml +# CurseForge-first rollout. Hangar left blank; add its slug later to enable Hangar. + +name: Publish release + +on: + release: + types: [published] + +jobs: + publish: + uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@main + with: + hangar_slug: "" + curseforge_id: "1515492" + game_versions: "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" + secrets: + HANGAR_API_KEY: ${{ secrets.HANGAR_API_KEY }} + CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} From 1c2989b3a2b29f8dc288057131008d1e3ecb6d33 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sun, 28 Jun 2026 09:01:32 -0700 Subject: [PATCH 07/16] ci: add CurseForge publish workflow (publishes the release asset) Publish the jar attached to each GitHub release to CurseForge via the shared BentoBoxWorld/.github reusable workflow, downloading the release asset instead of rebuilding from source. Mirrors the BentoBox core setup. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/publish.yml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 00b05d8..84386cb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,19 +1,30 @@ # Boxed — .github/workflows/publish.yml -# CurseForge-first rollout. Hangar left blank; add its slug later to enable Hangar. +# Publishes the jar attached to a GitHub release to CurseForge (and Hangar once a +# slug is set) via the shared BentoBoxWorld/.github reusable workflow. It downloads +# the release asset rather than rebuilding from source, so a dependency-repo outage +# can't block publishing. Includes workflow_dispatch to (re)publish a given version. -name: Publish release +name: Publish release to CurseForge 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@main + uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@master with: - hangar_slug: "" + use_release_asset: "true" # publish the jar attached to the release; do not rebuild + hangar_slug: "" # set the Hangar project slug to enable Hangar publishing curseforge_id: "1515492" - game_versions: "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" + 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 }} From fc8a7546bdb86252e590ede741436663df690908 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sun, 28 Jun 2026 11:16:38 -0700 Subject: [PATCH 08/16] ci: pin reusable workflow to SHA and enable Hangar publish Pin bentoboxworld/.github publish-platforms.yml to commit e0c5d98f5e6ef9ea7c9a28afad05f4c07bcde898 (Sonar githubactions:S7637) and set the Hangar slug so releases publish to Hangar alongside CurseForge. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/publish.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 84386cb..d619e9c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,10 +1,10 @@ # Boxed — .github/workflows/publish.yml -# Publishes the jar attached to a GitHub release to CurseForge (and Hangar once a -# slug is set) via the shared BentoBoxWorld/.github reusable workflow. It downloads -# the release asset rather than rebuilding from source, so a dependency-repo outage -# can't block publishing. Includes workflow_dispatch to (re)publish a given version. +# 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 +name: Publish release to CurseForge and Hangar on: release: @@ -18,10 +18,10 @@ on: jobs: publish: - uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@master + uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@e0c5d98f5e6ef9ea7c9a28afad05f4c07bcde898 # master with: use_release_asset: "true" # publish the jar attached to the release; do not rebuild - hangar_slug: "" # set the Hangar project slug to enable Hangar publishing + 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 From 95152267cb5ce940dcdcd38977ea975ff1e372d5 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sun, 28 Jun 2026 11:50:58 -0700 Subject: [PATCH 09/16] ci: bump pinned reusable workflow to 71bf927bce32586216baa6995f21852d944b98b9 Picks up the split into independent CurseForge and Hangar jobs so neither platform can block the other. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d619e9c..c1ab401 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ on: jobs: publish: - uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@e0c5d98f5e6ef9ea7c9a28afad05f4c07bcde898 # master + uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@71bf927bce32586216baa6995f21852d944b98b9 # master with: use_release_asset: "true" # publish the jar attached to the release; do not rebuild hangar_slug: "Boxed" # blank = skip Hangar From 53061cfb8ad7b222f0ced29e103f572fa986f244 Mon Sep 17 00:00:00 2001 From: tastybento Date: Tue, 30 Jun 2026 23:15:07 -0700 Subject: [PATCH 10/16] ci: bump pinned reusable workflow to fe4b1f0 --- .github/workflows/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c1ab401..2999860 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ on: jobs: publish: - uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@71bf927bce32586216baa6995f21852d944b98b9 # master + uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@fe4b1f03c19f4fd4212020a06a07a7097923adec # master with: use_release_asset: "true" # publish the jar attached to the release; do not rebuild hangar_slug: "Boxed" # blank = skip Hangar @@ -27,4 +27,4 @@ jobs: 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 }} + CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} \ No newline at end of file From 46b2903c5290f0ee045b6033552a6c7a1f78c40e Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 3 Jul 2026 11:54:10 -0700 Subject: [PATCH 11/16] ci: bump pinned publish-platforms.yml to ca2dcd1 --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2999860..3b5501a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ on: jobs: publish: - uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@fe4b1f03c19f4fd4212020a06a07a7097923adec # master + uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@ca2dcd167e8db4e0f671a976080744dda43801a6 # master with: use_release_asset: "true" # publish the jar attached to the release; do not rebuild hangar_slug: "Boxed" # blank = skip Hangar From 6411884c9f6d737730ff0e4c94fac58b156f93ad Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 29 Jul 2026 09:08:16 -0700 Subject: [PATCH 12/16] ci: bump publish-platforms pin to the multipart-JSON fix The pinned SHA predates BentoBoxWorld/.github#13, which moved the CurseForge metadata and Hangar versionUpload JSON out of inline curl -F values and into files. curl treats a ';' in an inline -F value as the start of a type= attribute, so a release body containing a semicolon truncates the JSON and both platforms reject the upload. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SAQ3YQkcfRoCZEwa6SPJcp --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3b5501a..c2c0488 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ on: jobs: publish: - uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@ca2dcd167e8db4e0f671a976080744dda43801a6 # master + 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 From 06564083baaa0b47d198709acfaa38c1cd8a88ae Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 14 Aug 2026 21:17:05 -0700 Subject: [PATCH 13/16] Fix open SonarCloud issues (blockers through info) Blockers - AdvancementListenerTest: add assertions to the four onPortal tests. Critical - AbstractSeedBiomeProvider: rename enum constants to UPPER_CASE, replace the deeply nested erosion/ridge if-chains with switch expressions over shared helpers (peaks, slopes/grove, plateau, swamp, erosion-5 cases), and drop the three duplicate near-inland/coast methods. Also fixes a missing-parentheses precedence bug in the erosion-5 humidity check. - NewAreaListener: MINECRAFT constant, split onPlayerMove/removeJigsaw, remove unused static fields, rename `record` lambda vars, drop dead null check and commented code, merge nested if. - EnderPearlListener: NO_TELEPORT_OUTSIDE constant, extract pearl landing handling, remove always-true getTo() null checks. - AdminPlaceStructureCommand: split canExecute into parse helpers, drop unused params/import, log instead of printStackTrace. - BoxedBlockPopulator / BoxedChunkGenerator: extract helpers. - AbstractBoxedChunkGenerator: `size` is now an instance field with an instance repeatCalc(int) plus a pure static repeatCalc(int, int); callers use the generator instance. Constructor is protected. Major / minor / info - Replace deprecated Registry.BIOME / Biome.CUSTOM / Biome.valueOf with RegistryAccess + NamespacedKey lookups; unknown/CUSTOM biomes map to null ("use vanilla") as before. - Remove deprecated isUsesNewChunkGeneration override (no consumer left in BentoBox 3.16.1+); update CLAUDE.md. - Settings: add missing @Override annotations. - BoxedSeedChunkGenerator: return empty lists instead of null. - Remove commented-out code and TODO comments across the codebase. - Tests: drop redundant public modifiers, useless eq(), unused helper methods, deprecated CHORUS_FRUIT/PlayerDeathEvent usages, and the superfluous throws clause on CommonTestSetup.setUp(). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RgbmBTXKJzbhGKMuGKfaBm --- CLAUDE.md | 2 - .../bentobox/boxed/AdvancementsManager.java | 12 - src/main/java/world/bentobox/boxed/Boxed.java | 11 +- .../bentobox/boxed/PlaceholdersManager.java | 7 +- .../java/world/bentobox/boxed/Settings.java | 2 + .../commands/AdminPlaceStructureCommand.java | 88 +- .../biomes/AbstractCopyBiomeProvider.java | 17 +- .../biomes/AbstractSeedBiomeProvider.java | 853 ++++++------------ .../biomes/BoxedBiomeGenerator.java | 3 +- .../biomes/BoxedNetherBiomeGenerator.java | 3 +- .../chunks/AbstractBoxedChunkGenerator.java | 40 +- .../chunks/BoxedBlockPopulator.java | 58 +- .../chunks/BoxedChunkGenerator.java | 87 +- .../chunks/BoxedSeedChunkGenerator.java | 6 +- .../boxed/listeners/AdvancementListener.java | 29 +- .../boxed/listeners/EnderPearlListener.java | 102 ++- .../boxed/listeners/NewAreaListener.java | 120 +-- .../boxed/objects/BoxedStructureBlock.java | 8 +- .../boxed/AdvancementsManagerTest.java | 8 +- .../world/bentobox/boxed/CommonTestSetup.java | 35 +- .../boxed/PlaceholdersManagerTest.java | 2 +- .../world/bentobox/boxed/SettingsTest.java | 2 +- .../AdminPlaceStructureCommandTest.java | 2 +- .../biomes/CopyBiomeProviderTest.java | 12 +- .../generators/chunks/RepeatCalcTest.java | 42 +- .../listeners/AdvancementListenerTest.java | 24 +- .../listeners/EnderPearlListenerTest.java | 12 +- 27 files changed, 666 insertions(+), 921 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index beb40d5..6991635 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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/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..420269c 100644 --- a/src/main/java/world/bentobox/boxed/Settings.java +++ b/src/main/java/world/bentobox/boxed/Settings.java @@ -1777,6 +1777,7 @@ public void setIgnoreAdvancements(boolean ignoreAdvancements) { /** * @return the concurrentIslands */ + @Override public int getConcurrentIslands() { if (concurrentIslands <= 0) { return BentoBox.getInstance().getSettings().getIslandNumber(); @@ -1794,6 +1795,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..18697d0 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; @@ -501,6 +509,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 +627,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 +720,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 +878,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 +894,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 +910,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..bba45b6 100644 --- a/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java +++ b/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java @@ -45,34 +45,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..5977df0 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,16 @@ 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(); + 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 +265,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 +282,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/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..8dea3bc 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(); @@ -228,9 +222,7 @@ public void setUp() throws Exception { // Util mockedUtil.when(() -> Util.findFirstMatchingEnum(any(), any())).thenCallRealMethod(); - // Util translate color codes (used in user translate methods) - //mockedUtil.when(() -> translateColorCodes(anyString())).thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); - + // Server & Scheduler mockedBukkit.when(Bukkit::getScheduler).thenReturn(sch); @@ -287,8 +279,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 +288,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/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..5d508c5 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; @@ -76,7 +78,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 +221,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 ---------- 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"); From 285f5ef3115baa5218309d821b36a47ab3759a78 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 14 Aug 2026 21:39:39 -0700 Subject: [PATCH 14/16] Address remaining SonarCloud findings on PR #133 - Extract inline mock creations to local variables (S9016). - Guard OfflinePlayer#getLocation() nullability before getIslandAt (S2637). - Remove unused java.util.Map import in BoxedBlockPopulator (S1128). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RgbmBTXKJzbhGKMuGKfaBm --- .../boxed/generators/chunks/BoxedBlockPopulator.java | 1 - .../bentobox/boxed/listeners/AdvancementListener.java | 4 ++++ src/test/java/world/bentobox/boxed/CommonTestSetup.java | 3 ++- .../boxed/listeners/AdvancementListenerTest.java | 9 +++++---- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java b/src/main/java/world/bentobox/boxed/generators/chunks/BoxedBlockPopulator.java index bba45b6..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; diff --git a/src/main/java/world/bentobox/boxed/listeners/AdvancementListener.java b/src/main/java/world/bentobox/boxed/listeners/AdvancementListener.java index 5977df0..9d92889 100644 --- a/src/main/java/world/bentobox/boxed/listeners/AdvancementListener.java +++ b/src/main/java/world/bentobox/boxed/listeners/AdvancementListener.java @@ -121,6 +121,10 @@ public void onAdvancement(PlayerAdvancementDoneEvent e) { */ 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); } diff --git a/src/test/java/world/bentobox/boxed/CommonTestSetup.java b/src/test/java/world/bentobox/boxed/CommonTestSetup.java index 8dea3bc..26fc7d6 100644 --- a/src/test/java/world/bentobox/boxed/CommonTestSetup.java +++ b/src/test/java/world/bentobox/boxed/CommonTestSetup.java @@ -217,7 +217,8 @@ public void setUp() { //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 diff --git a/src/test/java/world/bentobox/boxed/listeners/AdvancementListenerTest.java b/src/test/java/world/bentobox/boxed/listeners/AdvancementListenerTest.java index 5d508c5..6072283 100644 --- a/src/test/java/world/bentobox/boxed/listeners/AdvancementListenerTest.java +++ b/src/test/java/world/bentobox/boxed/listeners/AdvancementListenerTest.java @@ -48,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 @@ -271,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()); @@ -281,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")); } From 7c58bb7056929e535f49ad10d832c20a9dfcacdc Mon Sep 17 00:00:00 2001 From: tastybento Date: Mon, 7 Sep 2026 10:49:03 -0700 Subject: [PATCH 15/16] docs: clarify which deaths settings affect Level 2.29.0 island levels Level 2.29.0 tracks deaths per island itself. Update the config comments so admins can see that team-join-reset and reset-on-new only affect BentoBox's own death counter and placeholder, while counted and max still gate what Level records. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01SxX9oscerhv1nsozPbT5aX --- src/main/java/world/bentobox/boxed/Settings.java | 10 ++++++++-- src/main/resources/config.yml | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/world/bentobox/boxed/Settings.java b/src/main/java/world/bentobox/boxed/Settings.java index 420269c..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; 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. From 2664b712b4bd651e56d1df4bb88e8611c0648f7d Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 25 Sep 2026 09:10:04 +0100 Subject: [PATCH 16/16] Fix "contloc out of spec" crash when continentalness < -1.2 Vanilla continentalness noise can fall below the nominal -1.2 lower bound (e.g. when an explorer map in an unpacked loot chest searches for structures far outside the seed area), which made ContLoc.getCont throw and caused BentoBox to skip Boxed during enable. Make both outer bands open-ended. Fixes #135 Co-Authored-By: Claude Opus 5.5 --- .../biomes/AbstractSeedBiomeProvider.java | 11 +++--- .../boxed/generators/biomes/ContLocTest.java | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 src/test/java/world/bentobox/boxed/generators/biomes/ContLocTest.java 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 18697d0..1e536ba 100644 --- a/src/main/java/world/bentobox/boxed/generators/biomes/AbstractSeedBiomeProvider.java +++ b/src/main/java/world/bentobox/boxed/generators/biomes/AbstractSeedBiomeProvider.java @@ -264,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; @@ -283,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; 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)); + } +}