Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .claude/rules/build-toolchain.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ Supporting Minecraft 26.x forced a chain of build changes — keep these in mind

- **Java 25.** The 26.x `paper-api` is Java 25 bytecode and its Gradle metadata requires consumers to target Java 25, so BentoBox now compiles to Java 25 (`javaVersion = "25"`, `options.release = 25`). **Addons that compile against BentoBox must also move to Java 25.**
- **paperweight `2.0.0-SNAPSHOT`.** All 26.x dev bundles are dev-bundle *data version 8*, which no released paperweight (`<= 2.0.0-beta.21`) can read. The snapshot is resolved via a `pluginManagement` block in `settings.gradle.kts` pointing at Paper's repo. The paperweight tool launcher is pinned to Java 25 (the 26.1+ paperclip patch step requires it). Revisit once a stable paperweight reads data-version-8 bundles.
- **Compile target vs. runtime support.** `paperVersion` is the latest **stable 26.1.2** dev bundle, not 26.2 — because MockBukkit has no 26.2 build and its registry mock throws on 26.2's new `minecraft:sulfur_cube_archetype` registry. Minecraft **26.2 is supported at runtime** (see `ServerCompatibility` and the Modrinth `game-versions` list); 26.2-only blocks/entities are referenced via `Enums.getIfPresent(...)` by name, never a compile-time symbol. The forward "compile against literal 26.2" work is parked in a draft PR until MockBukkit ships a 26.2 build.
- **MockBukkit coordinate.** Tests use `org.mockbukkit.mockbukkit:mockbukkit-v26.1.2:<ver>` (from Paper's repo), which **must match `paperVersion`'s MC line** — a mismatched MockBukkit fails every test at init with `InternalDataLoadException` (it validates the live API's registries against its bundled per-version data). When bumping the MC version, bump both together.
- **Compile target.** `paperVersion` is the latest **stable 26.2** dev bundle, so 26.2 symbols (`EntityType.SULFUR_CUBE`, new materials) are available at compile time. This became possible when MockBukkit 4.116.1 shipped its `mockbukkit-v26.2` artifact (earlier MockBukkit threw on 26.2's new `minecraft:sulfur_cube_archetype` registry).
- **MockBukkit coordinate.** Tests use `org.mockbukkit.mockbukkit:mockbukkit-v26.2:<ver>` (from Paper's repo), which **must match `paperVersion`'s MC line** — a mismatched MockBukkit fails every test at init with `InternalDataLoadException` (it validates the live API's registries against its bundled per-version data). When bumping the MC version, bump both together.
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,9 @@ public class BucketListener extends FlagListener {

/**
* The Sulfur Cube entity type (Minecraft 26.2), resolved at runtime so the code still
* compiles against earlier API versions. {@code null} when absent. Non-final so tests can
* inject a stand-in type (the JVM constant-folds {@code static final} fields).
* compiles against earlier API versions. {@code null} when absent.
*/
@SuppressWarnings("java:S3008") // non-final by design; see Javadoc (test injection)
private static EntityType SULFUR_CUBE = Enums.getIfPresent(EntityType.class, "SULFUR_CUBE")
private static final EntityType SULFUR_CUBE = Enums.getIfPresent(EntityType.class, "SULFUR_CUBE")
.orNull();

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,9 @@ public class EntityInteractListener extends FlagListener {

/**
* The Sulfur Cube entity type (Minecraft 26.2), resolved at runtime so the code still
* compiles against earlier API versions. {@code null} when absent. Non-final so tests can
* inject a stand-in type (the JVM constant-folds {@code static final} fields).
* compiles against earlier API versions. {@code null} when absent.
*/
@SuppressWarnings("java:S3008") // non-final by design; see Javadoc (test injection)
private static EntityType SULFUR_CUBE = Enums.getIfPresent(EntityType.class, "SULFUR_CUBE")
private static final EntityType SULFUR_CUBE = Enums.getIfPresent(EntityType.class, "SULFUR_CUBE")
.orNull();

@EventHandler(priority = EventPriority.LOW, ignoreCancelled=true)
Expand Down
6 changes: 1 addition & 5 deletions src/main/java/world/bentobox/bentobox/util/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,8 @@ public class Util {
* The Sulfur Cube entity type (Minecraft 26.2), resolved at runtime so the code still
* compiles against earlier API versions where the constant does not exist. {@code null}
* when absent, in which case the {@code ==} comparisons against it are simply false.
* Intentionally non-final: assigned once at class load, but left non-final so tests can
* inject a stand-in type (the JVM constant-folds {@code static final} fields, defeating
* reflective injection).
*/
@SuppressWarnings("java:S3008") // non-final by design; see Javadoc (test injection)
private static EntityType SULFUR_CUBE = Enums.getIfPresent(EntityType.class, "SULFUR_CUBE")
private static final EntityType SULFUR_CUBE = Enums.getIfPresent(EntityType.class, "SULFUR_CUBE")
.orNull();

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,68 +252,40 @@ void testOnTropicalFishScoopingFishWaterBucketNotAllowed() {

/**
* A Sulfur Cube (Minecraft 26.2) is picked up with an empty bucket; this is blocked by the
* BUCKET flag when not allowed. The 26.2 EntityType constant is absent in the test API, so
* MAGMA_CUBE is injected as a stand-in for SULFUR_CUBE.
* BUCKET flag when not allowed.
*/
@Test
void testOnSulfurCubeBucketingNotAllowed() throws Exception {
void testOnSulfurCubeBucketingNotAllowed() {
when(island.isAllowed(any(), any())).thenReturn(false);
EntityType standIn = EntityType.MAGMA_CUBE;
Object previous = getStaticField(BucketListener.class, "SULFUR_CUBE");
setStaticField(BucketListener.class, "SULFUR_CUBE", standIn);
try {
Entity cube = mock(Entity.class);
when(cube.getLocation()).thenReturn(location);
when(cube.getType()).thenReturn(standIn);
PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(mockPlayer, cube);
PlayerInventory inv = mock(PlayerInventory.class);
ItemStack item = mock(ItemStack.class);
when(item.getType()).thenReturn(Material.BUCKET);
when(inv.getItemInMainHand()).thenReturn(item);
when(mockPlayer.getInventory()).thenReturn(inv);
l.onTropicalFishScooping(e);
assertTrue(e.isCancelled());
verify(notifier).notify(any(), eq("protection.protected"));
} finally {
setStaticField(BucketListener.class, "SULFUR_CUBE", previous);
}
Entity cube = mock(Entity.class);
when(cube.getLocation()).thenReturn(location);
when(cube.getType()).thenReturn(EntityType.SULFUR_CUBE);
PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(mockPlayer, cube);
PlayerInventory inv = mock(PlayerInventory.class);
ItemStack item = mock(ItemStack.class);
when(item.getType()).thenReturn(Material.BUCKET);
when(inv.getItemInMainHand()).thenReturn(item);
when(mockPlayer.getInventory()).thenReturn(inv);
l.onTropicalFishScooping(e);
assertTrue(e.isCancelled());
verify(notifier).notify(any(), eq("protection.protected"));
}

/**
* Bucketing a Sulfur Cube is allowed when the island permits the BUCKET flag.
*/
@Test
void testOnSulfurCubeBucketingAllowed() throws Exception {
EntityType standIn = EntityType.MAGMA_CUBE;
Object previous = getStaticField(BucketListener.class, "SULFUR_CUBE");
setStaticField(BucketListener.class, "SULFUR_CUBE", standIn);
try {
Entity cube = mock(Entity.class);
when(cube.getLocation()).thenReturn(location);
when(cube.getType()).thenReturn(standIn);
PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(mockPlayer, cube);
PlayerInventory inv = mock(PlayerInventory.class);
ItemStack item = mock(ItemStack.class);
when(item.getType()).thenReturn(Material.BUCKET);
when(inv.getItemInMainHand()).thenReturn(item);
when(mockPlayer.getInventory()).thenReturn(inv);
l.onTropicalFishScooping(e);
assertFalse(e.isCancelled());
} finally {
setStaticField(BucketListener.class, "SULFUR_CUBE", previous);
}
}

private static Object getStaticField(Class<?> clazz, String name) throws Exception {
java.lang.reflect.Field f = clazz.getDeclaredField(name);
f.setAccessible(true);
return f.get(null);
}

@SuppressWarnings("java:S3011")
private static void setStaticField(Class<?> clazz, String name, Object value) throws Exception {
java.lang.reflect.Field f = clazz.getDeclaredField(name);
f.setAccessible(true);
f.set(null, value);
void testOnSulfurCubeBucketingAllowed() {
Entity cube = mock(Entity.class);
when(cube.getLocation()).thenReturn(location);
when(cube.getType()).thenReturn(EntityType.SULFUR_CUBE);
PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(mockPlayer, cube);
PlayerInventory inv = mock(PlayerInventory.class);
ItemStack item = mock(ItemStack.class);
when(item.getType()).thenReturn(Material.BUCKET);
when(inv.getItemInMainHand()).thenReturn(item);
when(mockPlayer.getInventory()).thenReturn(inv);
l.onTropicalFishScooping(e);
assertFalse(e.isCancelled());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -399,66 +399,38 @@ void testOnPlayerInteractEntityCopperGolemNameTagNoInteraction() {

/**
* Giving a block to a Sulfur Cube (Minecraft 26.2) for it to absorb is treated as placing a
* block and must be blocked by the PLACE_BLOCKS flag when not allowed. The 26.2 EntityType
* constant is absent in the test API, so MAGMA_CUBE is injected as a stand-in for SULFUR_CUBE.
* block and must be blocked by the PLACE_BLOCKS flag when not allowed.
*/
@Test
void testOnPlayerInteractEntitySulfurCubeBlockAbsorptionNotAllowed() throws Exception {
EntityType standIn = EntityType.MAGMA_CUBE;
Object previous = getStaticField(EntityInteractListener.class, "SULFUR_CUBE");
setStaticField(EntityInteractListener.class, "SULFUR_CUBE", standIn);
try {
clickedEntity = mock(Entity.class);
when(clickedEntity.getLocation()).thenReturn(location);
when(clickedEntity.getType()).thenReturn(standIn);
ItemStack block = mock(ItemStack.class);
when(block.getType()).thenReturn(Material.STONE);
when(inv.getItemInMainHand()).thenReturn(block);
PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(mockPlayer, clickedEntity, hand);
eil.onPlayerInteractEntity(e);
verify(notifier).notify(any(), eq("protection.protected"));
assertTrue(e.isCancelled());
} finally {
setStaticField(EntityInteractListener.class, "SULFUR_CUBE", previous);
}
void testOnPlayerInteractEntitySulfurCubeBlockAbsorptionNotAllowed() {
clickedEntity = mock(Entity.class);
when(clickedEntity.getLocation()).thenReturn(location);
when(clickedEntity.getType()).thenReturn(EntityType.SULFUR_CUBE);
ItemStack block = mock(ItemStack.class);
when(block.getType()).thenReturn(Material.STONE);
when(inv.getItemInMainHand()).thenReturn(block);
PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(mockPlayer, clickedEntity, hand);
eil.onPlayerInteractEntity(e);
verify(notifier).notify(any(), eq("protection.protected"));
assertTrue(e.isCancelled());
}

/**
* Giving a block to a Sulfur Cube is allowed when the island permits PLACE_BLOCKS.
*/
@Test
void testOnPlayerInteractEntitySulfurCubeBlockAbsorptionAllowed() throws Exception {
void testOnPlayerInteractEntitySulfurCubeBlockAbsorptionAllowed() {
when(island.isAllowed(any(User.class), any())).thenReturn(true);
EntityType standIn = EntityType.MAGMA_CUBE;
Object previous = getStaticField(EntityInteractListener.class, "SULFUR_CUBE");
setStaticField(EntityInteractListener.class, "SULFUR_CUBE", standIn);
try {
clickedEntity = mock(Entity.class);
when(clickedEntity.getLocation()).thenReturn(location);
when(clickedEntity.getType()).thenReturn(standIn);
ItemStack block = mock(ItemStack.class);
when(block.getType()).thenReturn(Material.STONE);
when(inv.getItemInMainHand()).thenReturn(block);
PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(mockPlayer, clickedEntity, hand);
eil.onPlayerInteractEntity(e);
verify(notifier, never()).notify(any(), eq("protection.protected"));
assertFalse(e.isCancelled());
} finally {
setStaticField(EntityInteractListener.class, "SULFUR_CUBE", previous);
}
}

private static Object getStaticField(Class<?> clazz, String name) throws Exception {
java.lang.reflect.Field f = clazz.getDeclaredField(name);
f.setAccessible(true);
return f.get(null);
}

@SuppressWarnings("java:S3011")
private static void setStaticField(Class<?> clazz, String name, Object value) throws Exception {
java.lang.reflect.Field f = clazz.getDeclaredField(name);
f.setAccessible(true);
f.set(null, value);
clickedEntity = mock(Entity.class);
when(clickedEntity.getLocation()).thenReturn(location);
when(clickedEntity.getType()).thenReturn(EntityType.SULFUR_CUBE);
ItemStack block = mock(ItemStack.class);
when(block.getType()).thenReturn(Material.STONE);
when(inv.getItemInMainHand()).thenReturn(block);
PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(mockPlayer, clickedEntity, hand);
eil.onPlayerInteractEntity(e);
verify(notifier, never()).notify(any(), eq("protection.protected"));
assertFalse(e.isCancelled());
}

}
41 changes: 9 additions & 32 deletions src/test/java/world/bentobox/bentobox/util/UtilTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,40 +63,17 @@ void testSameWorldNullSafe() {
}

/**
* Sulfur Cube (Minecraft 26.2) is slime-like but passive. When the SULFUR_CUBE entity type is
* present, {@link Util#isPassiveEntity(org.bukkit.entity.Entity)} must classify it as passive
* and {@link Util#isHostileEntity(org.bukkit.entity.Entity)} must not treat it as hostile,
* even though it implements {@link Slime}.
* <p>
* The 26.2 EntityType constant does not exist in the test API, so an existing slime-like type
* (MAGMA_CUBE) is injected into Util's resolved SULFUR_CUBE field as a stand-in.
* Sulfur Cube (Minecraft 26.2) is slime-like but passive:
* {@link Util#isPassiveEntity(org.bukkit.entity.Entity)} must classify it as passive and
* {@link Util#isHostileEntity(org.bukkit.entity.Entity)} must not treat it as hostile, even
* though it implements {@link Slime}.
*/
@Test
void testSulfurCubeClassifiedAsPassiveNotHostile() throws Exception {
EntityType standIn = EntityType.MAGMA_CUBE;
Object previous = getStaticField(Util.class, "SULFUR_CUBE");
setStaticField(Util.class, "SULFUR_CUBE", standIn);
try {
Slime sulfurCube = mock(Slime.class);
when(sulfurCube.getType()).thenReturn(standIn);
assertTrue(Util.isPassiveEntity(sulfurCube));
assertFalse(Util.isHostileEntity(sulfurCube));
} finally {
setStaticField(Util.class, "SULFUR_CUBE", previous);
}
}

private static Object getStaticField(Class<?> clazz, String name) throws Exception {
java.lang.reflect.Field f = clazz.getDeclaredField(name);
f.setAccessible(true);
return f.get(null);
}

@SuppressWarnings("java:S3011")
private static void setStaticField(Class<?> clazz, String name, Object value) throws Exception {
java.lang.reflect.Field f = clazz.getDeclaredField(name);
f.setAccessible(true);
f.set(null, value);
void testSulfurCubeClassifiedAsPassiveNotHostile() {
Slime sulfurCube = mock(Slime.class);
when(sulfurCube.getType()).thenReturn(EntityType.SULFUR_CUBE);
assertTrue(Util.isPassiveEntity(sulfurCube));
assertFalse(Util.isHostileEntity(sulfurCube));
}

// ---- blockFaceToFloat ----
Expand Down
Loading