From 281fbe69a10b6effc0146c201d5dad9a5259c9d2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:35:25 +0000 Subject: [PATCH 1/9] Initial plan From 90f1555b03376a2862b1ac58291ef969bad4f40f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:45:13 +0000 Subject: [PATCH 2/9] fix: prevent false Bad custom block errors for CraftEngine blocks on initial load CraftEngineCustomBlock.fromMap() no longer delegates to fromId() (which calls CraftEngineHook.exists()). When a YAML entry explicitly declares type: craftengine the block type is already unambiguous, so the CraftEngineCustomBlock is created directly from the id field without requiring CraftEngine's block registry to be populated yet. The fromId() path (used by the short-form creator) keeps the exists() check so it can still distinguish CraftEngine IDs from vanilla material names. Fixes: invalid runtime error on startup when CraftEngine fires its reload event after AOneBlock has already attempted to load phases. Agent-Logs-Url: https://github.com/BentoBoxWorld/AOneBlock/sessions/7e16e2d1-e9ec-4551-b155-ebb10c178074 Co-authored-by: tastybento <4407265+tastybento@users.noreply.github.com> --- .../customblock/CraftEngineCustomBlock.java | 8 +++++--- .../CraftEngineCustomBlockTest.java | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) 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..e71c703 100644 --- a/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/CraftEngineCustomBlock.java +++ b/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/CraftEngineCustomBlock.java @@ -27,9 +27,11 @@ public static Optional fromId(String id) { } public static Optional fromMap(Map map) { - return Optional - .ofNullable(Objects.toString(map.get("id"), null)) - .flatMap(CraftEngineCustomBlock::fromId); + String id = Objects.toString(map.get("id"), null); + if (id == null) { + return Optional.empty(); + } + return Optional.of(new CraftEngineCustomBlock(id)); } @Override 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..4383f96 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,6 @@ package world.bentobox.aoneblock.oneblocks.customblock; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.LinkedHashMap; @@ -27,4 +28,23 @@ void fromMapReturnsEmptyWhenIdMissing() { assertTrue(result.isEmpty(), "Should return empty when 'id' is missing"); } + + /** + * {@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()); + } } From 052df36be55aa5cf3eb7011ca6c3421f48849af7 Mon Sep 17 00:00:00 2001 From: tastybento Date: Tue, 28 Apr 2026 17:04:55 -0700 Subject: [PATCH 3/9] Bump build version from 1.24.0 to 1.24.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 725f292..fb4b7a4 100644 --- a/pom.xml +++ b/pom.xml @@ -67,7 +67,7 @@ -LOCAL - 1.24.0 + 1.24.1 BentoBoxWorld_AOneBlock bentobox-world From 4582269cab5bde4d9401a7dcaea68cd76e3c71cf Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 2 May 2026 17:29:24 -0700 Subject: [PATCH 4/9] feat: drop populated bee nest in Plenty phase Adds a bee_nest custom block (3 bees, honey_level 0) to the Plenty phase block pool with weight 1, matching the existing HONEY_BLOCK and HONEYCOMB_BLOCK density. Closes the honey-farming gap players hit in Plenty: although the phase already drops honeycomb blocks, honey blocks, and honey bottles, there was no way to obtain a hive to farm them. Uses the block-data custom block path, which falls back to /setblock for NBT-bearing data so the nest spawns pre-populated with bees. Refs #512 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/resources/phases/8500_plenty.yml | 4 ++++ 1 file changed, 4 insertions(+) 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 From 26364341f4706aeb3b4bcb9517a1ce0696301787 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 2 May 2026 17:30:13 -0700 Subject: [PATCH 5/9] fix: resync magic block to client after entity-spawn cancel When a magic-block roll produces a mob, handleEntitySpawn cancels the triggering BlockBreakEvent at EventPriority.HIGHEST. The client has already played the "block disappearing" animation by then and predicted the block as gone, but never receives a corrective block-update packet, so the magic block appears transparent until the chunk resyncs (e.g. on relog). Send a block change to the mining player immediately after the cancel so the client repaints the actual server-side block state. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../bentobox/aoneblock/listeners/BlockListener.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java b/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java index c066b98..d001f39 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())); } From c1997cca36c878c6d24bcf1f754b15835f04dbed Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 2 May 2026 17:31:58 -0700 Subject: [PATCH 6/9] Update build version from 1.24.1 to 1.25.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fb4b7a4..70bd7f0 100644 --- a/pom.xml +++ b/pom.xml @@ -67,7 +67,7 @@ -LOCAL - 1.24.1 + 1.25.0 BentoBoxWorld_AOneBlock bentobox-world From 0aa6af0f5b36ce099e6c296f8de0c1b854817256 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 2 May 2026 17:34:43 -0700 Subject: [PATCH 7/9] fix: skip chest particles that require typed data instead of crashing Particle.DUST is the only particle type for which we can construct the required DustOptions data. Previously, any other particle type fell to particleData = null, and spawnParticle would throw IllegalArgumentException when the configured particle required non-void data (e.g. ITEM, BLOCK, ENTITY_EFFECT). Detect particles with non-void getDataType(), log a warning naming the configured particle, and return early instead of spawning. DUST and particles with Void data still work as before. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../bentobox/aoneblock/listeners/BlockListener.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java b/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java index d001f39..8cb522c 100644 --- a/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java +++ b/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java @@ -892,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); } From a905ba1fa1709a1f3c371e91951ef4119ed99ea4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 01:56:34 +0000 Subject: [PATCH 8/9] Initial plan From ade466e8d4468fbe4bb3a7b8899e63224ebb90e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 02:00:23 +0000 Subject: [PATCH 9/9] fix: validate CraftEngine id format in fromMap (non-blank, namespace:key) Agent-Logs-Url: https://github.com/BentoBoxWorld/AOneBlock/sessions/f42227f2-922c-49bc-9ac2-a3a093110e4f Co-authored-by: tastybento <4407265+tastybento@users.noreply.github.com> --- .../customblock/CraftEngineCustomBlock.java | 22 +++- .../CraftEngineCustomBlockTest.java | 100 ++++++++++++++++++ 2 files changed, 119 insertions(+), 3 deletions(-) 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 e71c703..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,9 +25,26 @@ 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) { - String id = Objects.toString(map.get("id"), null); - if (id == null) { + 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)); 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 4383f96..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,6 @@ 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; @@ -29,6 +30,72 @@ 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}). @@ -47,4 +114,37 @@ void fromMapReturnsPresentWhenIdProvided() { 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: ")); + } }