Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project

Boxed is a BentoBox GameModeAddon for Minecraft (Paper) where each player is confined to a small expandable box. Completing Minecraft advancements grows the box. Built against `bentobox` 3.13.0, Paper API 1.21.11, Java 21.

## Build / Test

- Build (default goal is `clean package`): `mvn clean package`
- Run all tests: `mvn test`
- Run a single test class: `mvn test -Dtest=AdvancementsManagerTest`
- Run a single test method: `mvn test -Dtest=AdvancementsManagerTest#testMethodName`
- Jacoco coverage report is generated as part of the build (`target/site/jacoco/`).

The `build.version` property in `pom.xml` is the canonical version. The `ci` and `master` Maven profiles activate from Jenkins env vars (`BUILD_NUMBER`, `GIT_BRANCH=origin/master`) to compute the final artifact name; don't hand-edit `revision`.

Surefire is configured with a long list of `--add-opens` JVM args required by Mockito's inline mock maker on Java 21+ — if you add new tests that touch JDK internals, extend that `argLine` in `pom.xml` rather than fighting module access errors locally. The `maven-compiler-plugin` runs with `<fork>true</fork>` to work around a JDK 25 in-process javac NPE (`this.hashes is null`).

## Architecture

Boxed is a BentoBox addon, not a standalone plugin. `Boxed` extends `GameModeAddon` and is loaded by BentoBox at runtime. `BoxedPladdon` is the Paper plugin-loader shim so Paper recognises the jar.

### The two-world "seed + game" generator model

This is the core concept and touches almost everything:

1. **Seed world** (`<worldname>/seed`, and `/seed_nether`): a real vanilla-ish world generated once using `BoxedSeedChunkGenerator` + `SeedBiomeGenerator` / `NetherSeedBiomeGenerator`. It's where Minecraft's normal terrain + structures (villages, shipwrecks, fortresses, etc.) actually get generated by the server.
2. **Game world** (`<worldname>`, `<worldname>_nether`): the world players actually play in. It uses `BoxedChunkGenerator` (a subclass of `AbstractBoxedChunkGenerator`) which does **not** generate terrain from scratch — instead, `Boxed.copyChunks(...)` pre-reads every chunk inside `islandDistance` from the seed world during `createWorlds()` and stores them in the chunk generator. When the game world asks for a chunk, the generator serves back the pre-captured copy.

This is why first boot is extremely slow and RAM-hungry (see `README.md` warnings): the entire seed region is force-loaded up front. Any change to world generation, structure handling, or world naming must respect both worlds and the copy step in `Boxed.copyChunks()` / `createOverWorld()` / `createNether()`. The `generatorMaps` / `generatorMap` fields in `Boxed.java` route world names → generators for `getDefaultWorldGenerator` (used by Multiverse and similar world-management plugins) and for the hook in `allLoaded()` that calls `WorldManagementHook.registerWorld`.

`isUsesNewChunkGeneration()` returns `true`, which tells BentoBox this addon uses the modern chunk-generation API.

### Advancements drive box size

`AdvancementsManager` is the other key subsystem. Box growth is data-driven from `advancements.yml`: each advancement key maps to an integer "box growth" increment. `AdvancementListener` watches for player advancement events and asks the manager to update the island's protection-range. Per-island state lives in `objects/IslandAdvancements.java` (a BentoBox `DataObject` persisted via its database layer). `AdvancementsManager.save()` is called in `onDisable()` — any new cached state it holds should be flushed there too.

Because advancements are per-world in vanilla but Boxed runs multiple worlds on one server, the **InvSwitcher** addon is required at runtime to keep advancements separate between worlds. The code logs a warning if InvSwitcher/Border aren't installed but does not hard-fail.

### Structures

`NewAreaListener` plus `objects/IslandStructures.java`, `BoxedJigsawBlock`, `BoxedStructureBlock`, and `ToBePlacedStructures` handle placing/tracking vanilla structures inside player boxes. `AdminPlaceStructureCommand` is the admin-side hook for manual placement. If you're adding structure logic, both the "captured from seed world" pathway and the "placed into player box" pathway need to stay in sync.

### Flags

`Boxed.MOVE_BOX` (protection flag, owner-only by default) and `Boxed.ALLOW_MOVE_BOX` (world setting) gate the enderpearl-box-teleport feature implemented in `EnderPearlListener`. They are scoped to this game mode only via `setGameModes(...)` in `onEnable()`, and `MOVE_BOX` is conditionally registered/unregistered depending on `ALLOW_MOVE_BOX` — keep that conditional registration intact if you touch flag setup.

### Resources packaged at build time

`pom.xml` filters `src/main/resources` but copies `structures/*.nbt`, `locales/*.yml`, and `blueprints/*.blu|*.json` unfiltered into the jar at specific targetPaths. New resource types need a matching `<resource>` block or they won't ship.

## Testing notes

Tests use JUnit 5 (Jupiter) + Mockito 5 `mockStatic` + MockBukkit (`v1.21-SNAPSHOT` via jitpack.io). There is no PowerMock and no custom `ServerMocks` helper. All test classes extend `CommonTestSetup`, which:

- Calls `MockBukkit.mock()` and registers `Mockito.mockStatic(Bukkit.class, RETURNS_DEEP_STUBS)` — use the inherited `mockedBukkit` field to stub `Bukkit.*` calls rather than creating your own.
- Exposes ready-made `@Mock` fields (`plugin`, `mockPlayer`, `world`, `location`, `iwm`, `im`, `island`, `pim`, `itemFactory`, `inv`, `notifier`, `fm`, `spigot`, `hooksManager`, `bm`, `sch`, `lm`, `phm`) plus the MockBukkit `server` and `mockedUtil` statics. Don't re-declare these in subclasses.
- Forces `org.bukkit.Tag.LEAVES` to initialise **before** the static Bukkit mock is installed — if you touch tag-related code and see stale deep-stubs across tests, broaden that list in `CommonTestSetup`.

A small `WhiteBox` helper (reflection-based private static field setter) replaces PowerMock's `Whitebox.setInternalState`. Any test that needs its own `MockedStatic<...>` (e.g. `DatabaseSetup`, `User`) must create it **after** `super.setUp()` and close it via `closeOnDemand()` **before** `super.tearDown()` — the parent's `Mockito.framework().clearInlineMocks()` in tearDown will otherwise corrupt the local static.

Jacoco excludes `org/bukkit/Material*` to avoid "class too large to mock" failures; keep that exclusion if you rearrange the build section.
127 changes: 114 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A game mode where you are boxed into a tiny space that only expands by completin
* Border - shows the box

### Warning!!
Boxed requires **a lot of RAM** and can take up to **10 minutes** to boot up for the first time as it pre-generates the worlds. After the initial start, it will start up much quicker. With 12GB of RAM running on a fast ARM-based system, it takes ~ 8 minutes for the first boot. If you do not have enough RAM then weird things will happen to you server including strange errors about chunks and things like that. To dedicate enough RAM to your JVM, use the correct flags during startup. Here is my `start.sh` for running on Paper 1.19.4:
Boxed requires **a lot of RAM** and can take up to **10 minutes** to boot up for the first time as it pre-generates the worlds. After the initial start, it will start up much quicker. With 12GB of RAM running on a fast ARM-based system, it takes ~ 8 minutes for the first boot. If you do not have enough RAM then weird things will happen to your server including strange errors about chunks and things like that. To dedicate enough RAM to your JVM, use the correct flags during startup. Here is my `start.sh` for running on Paper 1.19.4:
```
#!/bin/sh
java -Xms12G -Xmx12G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 -XX:G1MixedGCLiveThresholdPercent=90 -XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:+PerfDisableSharedMem -XX:MaxTenuringThreshold=1 -XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M -XX:G1ReservePercent=20 -XX:InitiatingHeapOccupancyPercent=15 -Dusing.aikars.flags=https://mcflags.emc.gs -Daikars.new.flags=true -jar paper-1.19.4.jar nogui
Expand All @@ -20,14 +20,14 @@ java -Xms12G -Xmx12G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMill
### Quick Start

1. Place Boxed addon into the BentoBox addons folder along with InvSwitcher and Border (use the latest versions!).
2. (Optional) Installed the Datapack for custom advancements - https://github.com/BentoBoxWorld/BoxedDataPack/
4. Restart the server - new worlds will be created. This will take a while!
5. Login
6. Type `/boxed` to start.
7. Turn off advancement announcements `/gamerule announceAdvancements false` otherwise there is a lot of spam from the server when players get advancements.
2. (Optional) Install the Datapack for custom advancements - https://github.com/BentoBoxWorld/BoxedDataPack/
3. Restart the server - new worlds will be created. This will take a while!
4. Login.
5. Type `/boxed` to start.
6. Turn off advancement announcements with `/gamerule announceAdvancements false` to avoid spam, or set `boxed.broadcast-advancements: true` in `config.yml` to have Boxed broadcast them instead.


* You will start by a tree. The is a chest with some handy items in it. (This is the island blueprint)
* You will start next to a tree. There is a chest with some handy items in it. (This is the island blueprint)
* The only area you can operate on is your box that shows as a border.
* To make your box bigger, complete advancements.
* Check your progress with the Advancements screen, (L-key).
Expand All @@ -47,20 +47,44 @@ java -Xms12G -Xmx12G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMill
### config.yml
The config is very similar to BSkyBlock, AcidIsland, etc.

Each player will have a land of their own to explore up to the limit of the island distance value. The default is 400, so the land will be 800 x 800 blocks. The land is semi-random, but each player will get roughly the same layout (see the biomes config). Structures such as villages, broken nether gates, shipwrecks, etc. are random and so some players may get them, others not. In a future version, switching off structures will be a config option. Strongholds are switched off and do not exist. Each player's land is surrounded by seas of different temperatures. If the border is not solid, then players can theoretically explore other lands.
Each player will have a land of their own to explore up to the limit of the island distance value. The default is 320, so the land will be 640 x 640 blocks. The land is semi-random, but each player will get roughly the same layout (see the biomes config). Structures such as villages, broken nether gates, shipwrecks, etc. are random and so some players may get them, others not. Strongholds are switched off and do not exist. Each player's land is surrounded by seas of different temperatures. If the border is not solid, then players can theoretically explore other lands.

*World Seed*
The world seed is what it is used to generate the lands. I recommend keeping this value. If you change it the land may be very different.
The world seed is used to generate the lands. It is recommended to keep this value. If you change it the land may be very different. Note that changing the seed mid-game requires a full reset of your databases and worlds.

*Key Boxed-specific settings:*

| Setting | Default | Description |
|---------|---------|-------------|
| `boxed.ignore-advancements` | `false` | If `true`, advancements will not change the size of the box. |
| `boxed.broadcast-advancements` | `false` | If `true`, Boxed will broadcast new advancements. Recommended: set the game rule `/gamerule announceAdvancements false` and use this setting instead. |
| `boxed.deny-visitor-advancements` | `true` | If `true`, visitors cannot earn advancements. Note: visitors will still receive other rewards such as experience. |
| `world.allow-structures` | `false` | Allow vanilla structures to generate in the seed world. |

### Blueprint

There is one blueprint "island" that is used to generate the tree, chest and blocks below down to y = 5. The default height of the surface is about y = 65, so the blueprint has to be about 60 blocks tall. If you make any good blueprints, please share them!

### advancements.yml
This file contains all the advancements and how much your box should grow if you get one. The file can contain custom advancements if you have them. The default is for most recipe advancements to give nothing.
This file contains all the advancements and how much your box should grow if you get one. The file can contain custom advancements if you have them.

There are settings at the top of the file:

| Setting | Default | Description |
|---------|---------|-------------|
| `settings.default-root-increase` | `0` | Score applied when a root (tab-opening) advancement is earned. Typically left at 0 to avoid rewarding players simply for unlocking a new tab. |
| `settings.unknown-advancement-increase` | `1` | Default box increase for any advancement not listed in this file. Useful for custom advancements added via a data pack — you don't need to list every new advancement manually. |
| `settings.unknown-recipe-increase` | `0` | Default box increase for recipe advancements not listed in this file. |
| `settings.automatic-scoring` | `true` | If `true`, uses a proprietary algorithm to automatically score advancements. If `false`, each advancement must be scored manually. |

Example:
```
```yaml
# Lists how many blocks the box will increase when advancement occurs
settings:
default-root-increase: 0
unknown-advancement-increase: 1
unknown-recipe-increase: 0
automatic-scoring: true
advancements:
'minecraft:adventure/adventuring_time': 1
'minecraft:adventure/arbalistic': 1
Expand All @@ -69,7 +93,7 @@ advancements:
'minecraft:adventure/honey_block_slide': 1
'minecraft:adventure/kill_a_mob': 1
...
```
```

### biomes.yml
The player's land has biomes and they are defined here. It's not possible to define where the biomes are right now, only what affect they have on the terrain.
Expand All @@ -81,8 +105,85 @@ Setting ocean biomes to higher height numbers will result in the ocean floor bei

A lot of these numbers are rough guesses right now and if you come up with better values, please share them!

### structures.yml
This file records which Minecraft structures should be placed in each new player's box when their area is first created. Structures are stored relative to the island center and are placed in overworld (`normal`) and nether sections.

Admins can place structures in-game using the `/boxadmin place` command:

```
/boxadmin place <structure> [x y z] [ROTATION] [MIRROR] [NO_MOBS]
```

| Argument | Description |
|----------|-------------|
| `<structure>` | Minecraft structure name (tab-complete to see available structures) |
| `[x y z]` | Coordinates where the structure should be placed. Use `~` for the current position. |
| `[ROTATION]` | Optional rotation: `NONE`, `CLOCKWISE_90`, `CLOCKWISE_180`, `COUNTERCLOCKWISE_90` |
| `[MIRROR]` | Optional mirror: `NONE`, `LEFT_RIGHT`, `FRONT_BACK` |
| `[NO_MOBS]` | Optional flag to suppress mob spawning from this structure |

To undo the last placed structure: `/boxadmin place undo`

When a structure is placed via this command while standing in a player box, it is automatically saved to `structures.yml` and will be placed in all future boxes.


## Flags

Boxed registers two flags unique to this gamemode.

### ALLOW_MOVE_BOX (World Setting)
Controls whether box-moving via ender pearl is enabled at all in this world. This is a world-level toggle visible in the BentoBox admin settings.

* **Type:** World Setting
* **Default:** Enabled

### MOVE_BOX (Protection Flag)
Controls which rank of island member is allowed to move the box by throwing ender pearls from within it. Only shown and active when `ALLOW_MOVE_BOX` is enabled.

* **Type:** Protection (Island Setting)
* **Default:** Owner only
* **Icon:** Composter

Players can find this setting under `/box settings` (look for the Composter icon).


## Placeholders

The following PlaceholderAPI placeholders are registered by Boxed:

| Placeholder | Description |
|-------------|-------------|
| `%boxed_island_advancements%` | The number of advancements earned by the player's island (based on the player's island membership). |
| `%boxed_visited_island_advancements%` | The number of advancements earned by the island the player is currently standing on. |


## Custom Advancements
To find out how to add custom advacements to your server, watch the tutorial video [here](https://www.youtube.com/watch?v=zNzQvIbweQs)!
To find out how to add custom advancements to your server, watch the tutorial video [here](https://www.youtube.com/watch?v=zNzQvIbweQs)!

Download the official [Boxed DataPack](https://github.com/BentoBoxWorld/BoxedDataPack) for extra custom advancements.


## Using Regionerator

*Note: This plugin is designed to delete unused regions of your world! Make sure you take backups if you use it! Use at your own risk!*

[Regionerator](https://github.com/Jikoo/Regionerator) is a plugin that gradually deletes unused chunks to keep world sizes low. It supports BentoBox and respects box boundaries. It can be used to delete box chunks so that they can be regenerated. As Boxed uses seed worlds to copy from, these can appear to be unused by Regionerator and deleted, which means that startup becomes very slow. To avoid this, set the seed worlds as exempt from its deletions by adding these entries to the `worlds` section of the Regionerator config file:

```yaml
worlds:
boxed_world/seed_base:
days-till-flag-expires: -1
boxed_world/seed:
days-till-flag-expires: -1
default:
days-till-flag-expires: 0
```

To get the most out of Regionerator, change the BentoBox `config.yml` to *not* delete chunks when an island is removed. This leaves deletion up to Regionerator and it will clean up the chunks if the unused area is large enough. Set `keep-previous-island-on-reset: true`:

```yaml
deletion:
keep-previous-island-on-reset: true
```


Loading
Loading