diff --git a/pom.xml b/pom.xml
index 725f292..70bd7f0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -67,7 +67,7 @@
-LOCAL
- 1.24.0
+ 1.25.0
BentoBoxWorld_AOneBlock
bentobox-world
diff --git a/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java b/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java
index c066b98..8cb522c 100644
--- a/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java
+++ b/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java
@@ -527,12 +527,18 @@ private void processNextBlock(Cancellable e, Island i, Player player, Block bloc
* @param nextBlock - next block object containing entity info
*/
private void handleEntitySpawn(Cancellable e, Island i, Player player, Block block, OneBlockObject nextBlock) {
- if (!(e instanceof EntitySpawnEvent)) {
+ boolean cancelled = !(e instanceof EntitySpawnEvent);
+ if (cancelled) {
e.setCancelled(true);
}
spawnEntity(nextBlock, block);
+ // Cancelling BlockBreakEvent at HIGHEST priority leaves the client with a
+ // mispredicted "block gone" state; resync the actual block to clear it.
+ if (cancelled && player != null) {
+ player.sendBlockChange(block.getLocation(), block.getBlockData());
+ }
Bukkit.getPluginManager().callEvent(new MagicBlockEntityEvent(i,
- player == null ? null : player.getUniqueId(),
+ player == null ? null : player.getUniqueId(),
block, nextBlock.getEntityType()));
}
@@ -886,7 +892,14 @@ private void fillChest(@NonNull OneBlockObject nextBlock, @NonNull Block block)
return;
}
Color color = addon.getSettings().resolveChestColor(rarity);
- Object particleData = Particle.DUST.equals(particle) ? new Particle.DustOptions(color, 1) : null;
+ Object particleData = null;
+ if (Particle.DUST.equals(particle)) {
+ particleData = new Particle.DustOptions(color, 1);
+ } else if (!Void.class.equals(particle.getDataType())) {
+ // Particle requires typed data we can't provide — skip rather than crash
+ addon.logWarning("Chest particle " + particle.name() + " requires extra data and cannot be used. Use DUST or a void-data particle.");
+ return;
+ }
block.getWorld().spawnParticle(particle, block.getLocation().add(new Vector(0.5, 1.0, 0.5)), 50, 0.5,
0, 0.5, 1, particleData);
}
diff --git a/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/CraftEngineCustomBlock.java b/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/CraftEngineCustomBlock.java
index 0c31d30..bd5b022 100644
--- a/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/CraftEngineCustomBlock.java
+++ b/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/CraftEngineCustomBlock.java
@@ -1,7 +1,6 @@
package world.bentobox.aoneblock.oneblocks.customblock;
import java.util.Map;
-import java.util.Objects;
import java.util.Optional;
import org.bukkit.Material;
@@ -26,10 +25,29 @@ public static Optional fromId(String id) {
return Optional.empty();
}
+ /**
+ * Checks whether {@code id} is a syntactically valid namespaced key
+ * ({@code namespace:key} with both parts non-blank).
+ */
+ static boolean isValidNamespacedKey(String id) {
+ int colon = id.indexOf(':');
+ if (colon <= 0 || colon == id.length() - 1) {
+ return false;
+ }
+ String namespace = id.substring(0, colon);
+ String key = id.substring(colon + 1);
+ return !namespace.isBlank() && !key.isBlank();
+ }
+
public static Optional fromMap(Map, ?> map) {
- return Optional
- .ofNullable(Objects.toString(map.get("id"), null))
- .flatMap(CraftEngineCustomBlock::fromId);
+ Object raw = map.get("id");
+ if (!(raw instanceof String id)) {
+ return Optional.empty();
+ }
+ if (id.isBlank() || !isValidNamespacedKey(id)) {
+ return Optional.empty();
+ }
+ return Optional.of(new CraftEngineCustomBlock(id));
}
@Override
diff --git a/src/main/resources/phases/8500_plenty.yml b/src/main/resources/phases/8500_plenty.yml
index 758153f..0615f37 100644
--- a/src/main/resources/phases/8500_plenty.yml
+++ b/src/main/resources/phases/8500_plenty.yml
@@ -45,6 +45,10 @@
DIRT_PATH: 1
LIGHT_GRAY_TERRACOTTA: 1
SUSPICIOUS_SAND: 1
+ custom-blocks:
+ - type: block
+ data: 'minecraft:bee_nest[honey_level=0,facing=north]{bees:[{entity_data:{id:"minecraft:bee"},min_ticks_in_hive:600,ticks_in_hive:0},{entity_data:{id:"minecraft:bee"},min_ticks_in_hive:600,ticks_in_hive:0},{entity_data:{id:"minecraft:bee"},min_ticks_in_hive:600,ticks_in_hive:0}]}'
+ probability: 1
mobs:
COW: 1
DONKEY: 1
diff --git a/src/test/java/world/bentobox/aoneblock/oneblocks/customblock/CraftEngineCustomBlockTest.java b/src/test/java/world/bentobox/aoneblock/oneblocks/customblock/CraftEngineCustomBlockTest.java
index cd35d31..2d91929 100644
--- a/src/test/java/world/bentobox/aoneblock/oneblocks/customblock/CraftEngineCustomBlockTest.java
+++ b/src/test/java/world/bentobox/aoneblock/oneblocks/customblock/CraftEngineCustomBlockTest.java
@@ -1,5 +1,7 @@
package world.bentobox.aoneblock.oneblocks.customblock;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.LinkedHashMap;
@@ -27,4 +29,122 @@ void fromMapReturnsEmptyWhenIdMissing() {
assertTrue(result.isEmpty(), "Should return empty when 'id' is missing");
}
+
+ @Test
+ void fromMapReturnsEmptyWhenIdIsNull() {
+ Map map = new LinkedHashMap<>();
+ map.put("type", "craftengine");
+ map.put("id", null);
+
+ var result = CraftEngineCustomBlock.fromMap(map);
+
+ assertTrue(result.isEmpty(), "Should return empty when 'id' is null");
+ }
+
+ @Test
+ void fromMapReturnsEmptyWhenIdIsBlank() {
+ Map map = new LinkedHashMap<>();
+ map.put("type", "craftengine");
+ map.put("id", " ");
+
+ var result = CraftEngineCustomBlock.fromMap(map);
+
+ assertTrue(result.isEmpty(), "Should return empty when 'id' is blank");
+ }
+
+ @Test
+ void fromMapReturnsEmptyWhenIdIsNonStringValue() {
+ Map map = new LinkedHashMap<>();
+ map.put("type", "craftengine");
+ map.put("id", 42); // integer, not a String
+
+ var result = CraftEngineCustomBlock.fromMap(map);
+
+ assertTrue(result.isEmpty(), "Should return empty when 'id' is not a String");
+ }
+
+ @Test
+ void fromMapReturnsEmptyWhenIdHasNoColon() {
+ Map map = new LinkedHashMap<>();
+ map.put("type", "craftengine");
+ map.put("id", "nocolon");
+
+ var result = CraftEngineCustomBlock.fromMap(map);
+
+ assertTrue(result.isEmpty(), "Should return empty when 'id' has no colon");
+ }
+
+ @Test
+ void fromMapReturnsEmptyWhenIdHasEmptyNamespace() {
+ Map map = new LinkedHashMap<>();
+ map.put("type", "craftengine");
+ map.put("id", ":key");
+
+ var result = CraftEngineCustomBlock.fromMap(map);
+
+ assertTrue(result.isEmpty(), "Should return empty when namespace part is empty");
+ }
+
+ @Test
+ void fromMapReturnsEmptyWhenIdHasEmptyKey() {
+ Map map = new LinkedHashMap<>();
+ map.put("type", "craftengine");
+ map.put("id", "namespace:");
+
+ var result = CraftEngineCustomBlock.fromMap(map);
+
+ assertTrue(result.isEmpty(), "Should return empty when key part is empty");
+ }
+
+ /**
+ * {@code fromMap} must succeed even when CraftEngine has not yet loaded its
+ * block registry (i.e. without calling {@code CraftEngineHook.exists}).
+ * This prevents false "Bad custom block" errors during the initial server
+ * start-up phase that occurs before CraftEngine fires its reload event.
+ */
+ @Test
+ void fromMapReturnsPresentWhenIdProvided() {
+ Map map = new LinkedHashMap<>();
+ map.put("type", "craftengine");
+ map.put("id", "oneblock:common_loot_block");
+ map.put("probability", 300);
+
+ var result = CraftEngineCustomBlock.fromMap(map);
+
+ assertTrue(result.isPresent(), "Should return a block when 'id' is present, regardless of CraftEngine load state");
+ assertInstanceOf(CraftEngineCustomBlock.class, result.get());
+ }
+
+ // --- isValidNamespacedKey helper tests ---
+
+ @Test
+ void isValidNamespacedKeyReturnsTrueForValidKey() {
+ assertTrue(CraftEngineCustomBlock.isValidNamespacedKey("ns:key"));
+ assertTrue(CraftEngineCustomBlock.isValidNamespacedKey("oneblock:common_loot_block"));
+ }
+
+ @Test
+ void isValidNamespacedKeyReturnsFalseForMissingColon() {
+ assertFalse(CraftEngineCustomBlock.isValidNamespacedKey("nocolon"));
+ }
+
+ @Test
+ void isValidNamespacedKeyReturnsFalseForLeadingColon() {
+ assertFalse(CraftEngineCustomBlock.isValidNamespacedKey(":key"));
+ }
+
+ @Test
+ void isValidNamespacedKeyReturnsFalseForTrailingColon() {
+ assertFalse(CraftEngineCustomBlock.isValidNamespacedKey("ns:"));
+ }
+
+ @Test
+ void isValidNamespacedKeyReturnsFalseForBlankNamespace() {
+ assertFalse(CraftEngineCustomBlock.isValidNamespacedKey(" :key"));
+ }
+
+ @Test
+ void isValidNamespacedKeyReturnsFalseForBlankKey() {
+ assertFalse(CraftEngineCustomBlock.isValidNamespacedKey("ns: "));
+ }
}