diff --git a/.claude/rules/build-toolchain.md b/.claude/rules/build-toolchain.md index 32e57de13..4ea3a1740 100644 --- a/.claude/rules/build-toolchain.md +++ b/.claude/rules/build-toolchain.md @@ -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:` (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:` (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. diff --git a/src/main/java/world/bentobox/bentobox/listeners/flags/protection/BucketListener.java b/src/main/java/world/bentobox/bentobox/listeners/flags/protection/BucketListener.java index d2c954d1f..01ba3a172 100644 --- a/src/main/java/world/bentobox/bentobox/listeners/flags/protection/BucketListener.java +++ b/src/main/java/world/bentobox/bentobox/listeners/flags/protection/BucketListener.java @@ -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(); /** diff --git a/src/main/java/world/bentobox/bentobox/listeners/flags/protection/EntityInteractListener.java b/src/main/java/world/bentobox/bentobox/listeners/flags/protection/EntityInteractListener.java index 55d104c92..8f28c7991 100644 --- a/src/main/java/world/bentobox/bentobox/listeners/flags/protection/EntityInteractListener.java +++ b/src/main/java/world/bentobox/bentobox/listeners/flags/protection/EntityInteractListener.java @@ -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) diff --git a/src/main/java/world/bentobox/bentobox/util/Util.java b/src/main/java/world/bentobox/bentobox/util/Util.java index 663f579cb..0de0cfd32 100644 --- a/src/main/java/world/bentobox/bentobox/util/Util.java +++ b/src/main/java/world/bentobox/bentobox/util/Util.java @@ -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(); /** diff --git a/src/test/java/world/bentobox/bentobox/listeners/flags/protection/BucketListenerTest.java b/src/test/java/world/bentobox/bentobox/listeners/flags/protection/BucketListenerTest.java index 6a04f7fcb..8535cf808 100644 --- a/src/test/java/world/bentobox/bentobox/listeners/flags/protection/BucketListenerTest.java +++ b/src/test/java/world/bentobox/bentobox/listeners/flags/protection/BucketListenerTest.java @@ -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()); } } diff --git a/src/test/java/world/bentobox/bentobox/listeners/flags/protection/EntityInteractListenerTest.java b/src/test/java/world/bentobox/bentobox/listeners/flags/protection/EntityInteractListenerTest.java index 2cd33e60b..0c6470887 100644 --- a/src/test/java/world/bentobox/bentobox/listeners/flags/protection/EntityInteractListenerTest.java +++ b/src/test/java/world/bentobox/bentobox/listeners/flags/protection/EntityInteractListenerTest.java @@ -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()); } } diff --git a/src/test/java/world/bentobox/bentobox/util/UtilTest.java b/src/test/java/world/bentobox/bentobox/util/UtilTest.java index caa7f3708..175d7e918 100644 --- a/src/test/java/world/bentobox/bentobox/util/UtilTest.java +++ b/src/test/java/world/bentobox/bentobox/util/UtilTest.java @@ -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}. - *

- * 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 ----