From 5350cf15b710dd56a3a7b9de40344081cb31f588 Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Sun, 16 Aug 2026 14:48:56 +0200 Subject: [PATCH 1/2] Smoke on sticky pistons whose pull cannot be delivered anywhere A blocked pipe system is invisible from the outside: pulses arrive, nothing moves, and finding the stuck piston means watching every one of them at the moment a pulse happens. A pull that is refused everywhere now marks the piston blocked and a repeating task keeps it smoking, so a stuck system can be found by looking at it. The next pull that delivers clears the flag, and the flag is dropped when the piston or its source container is removed. Gated by 'full-pipe-smoke' (default on). --- .../sk89q/craftbook/mechanics/pipe/Pipes.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java b/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java index 3b6c9f4aa..d0a0315df 100644 --- a/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java +++ b/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java @@ -22,7 +22,10 @@ import com.sk89q.worldedit.world.block.BlockStateHolder; import com.sk89q.worldedit.world.block.BlockTypes; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.World; import org.bukkit.Tag; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; @@ -38,15 +41,18 @@ import org.bukkit.event.block.SignChangeEvent; import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; +import org.bukkit.scheduler.BukkitTask; import org.bukkit.util.Vector; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.Deque; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; public class Pipes extends AbstractCraftBookMechanic { @@ -378,6 +384,11 @@ private void startPipe(Block block, List items, boolean request) { break; } + int pulledAmount = 0; + for (ItemStack pulled : items) + if (pulled != null) + pulledAmount += pulled.getAmount(); + PipeSuckEvent event = new PipeSuckEvent(block, new ArrayList<>(items), fac); Bukkit.getPluginManager().callEvent(event); items.clear(); @@ -387,6 +398,23 @@ private void startPipe(Block block, List items, boolean request) { searchNearbyPipes(block, visitedPipes, items); } + // A pull refused everywhere marks the piston blocked; any pull that + // delivers something clears it again. + if (pipeBlockedSmoke && pulledAmount > 0) { + int undelivered = 0; + for (ItemStack left : items) + if (left != null) + undelivered += left.getAmount(); + if (undelivered >= pulledAmount) { + if (blockedPistons.size() > MAX_BLOCKED_PISTONS) + blockedPistons.clear(); + blockedPistons.add(block.getLocation()); + spawnBlockedSmoke(block); + } else { + blockedPistons.remove(block.getLocation()); + } + } + if (!items.isEmpty()) { if (facType == Material.CRAFTER) leftovers.addAll(InventoryUtil.addItemsToCrafter((Crafter) fac.getState(), items.toArray(new ItemStack[items.size()]))); @@ -507,10 +535,85 @@ public void onPipeRequest(PipeRequestEvent event) { } } + /** Pistons whose last pull was refused everywhere; kept smoking by the task. */ + private final Set blockedPistons = new HashSet<>(); + private final Map lastSmoke = new HashMap<>(); + private BukkitTask smokeTask; + private static final int MAX_BLOCKED_PISTONS = 4096; + + @Override + public boolean enable() { + smokeTask = Bukkit.getScheduler().runTaskTimer(CraftBookPlugin.inst(), this::smokeBlockedPistons, 12L, 12L); + return true; + } + + @Override + public void disable() { + if (smokeTask != null) { + smokeTask.cancel(); + smokeTask = null; + } + blockedPistons.clear(); + lastSmoke.clear(); + } + + /** + * Keeps every blocked piston visibly smoking between pulses. Blocked is state, + * set by a pull that was refused everywhere and cleared by the next pull that + * delivers, so a player can find a stuck system by looking at it instead of + * having to catch the moment a pulse happens. + */ + private void smokeBlockedPistons() { + if (!pipeBlockedSmoke || blockedPistons.isEmpty()) + return; + Iterator iterator = blockedPistons.iterator(); + while (iterator.hasNext()) { + Location loc = iterator.next(); + World world = loc.getWorld(); + if (world == null || !world.isChunkLoaded(loc.getBlockX() >> 4, loc.getBlockZ() >> 4)) + continue; + Block piston = world.getBlockAt(loc); + if (piston.getType() != Material.STICKY_PISTON) { + iterator.remove(); + continue; + } + // No container in front means there is no system left to be blocked: + // only a pull attempt clears the flag, and none can happen again, so + // the flag would smoke forever after the source is removed. + Block sourceBlock = piston.getRelative(((Piston) piston.getBlockData()).getFacing()); + if (!InventoryUtil.doesBlockHaveInventory(sourceBlock)) { + iterator.remove(); + continue; + } + world.spawnParticle(Particle.LARGE_SMOKE, + loc.getBlockX() + 0.5, loc.getBlockY() + 1.2, loc.getBlockZ() + 0.5, + 4, 0.15, 0.1, 0.15, 0.01); + } + } + + /** + * A puff at pulse time, throttled per piston so a fast clock reads as a steady + * chimney rather than a particle storm. + */ + private void spawnBlockedSmoke(Block piston) { + Location loc = piston.getLocation(); + long now = System.currentTimeMillis(); + Long last = lastSmoke.get(loc); + if (last != null && now - last < 600L) + return; + if (lastSmoke.size() > MAX_BLOCKED_PISTONS) + lastSmoke.clear(); + lastSmoke.put(loc, now); + piston.getWorld().spawnParticle(Particle.LARGE_SMOKE, + piston.getX() + 0.5, piston.getY() + 1.2, piston.getZ() + 0.5, + 5, 0.15, 0.1, 0.15, 0.01); + } + private boolean pipesDiagonal; private BlockStateHolder pipeInsulator; private boolean pipeStackPerPull; private boolean pipeRequireSign; + private boolean pipeBlockedSmoke; @Override public void loadConfiguration (YAMLProcessor config, String path) { @@ -526,5 +629,8 @@ public void loadConfiguration (YAMLProcessor config, String path) { config.setComment(path + "require-sign", "Requires pipes to have a [Pipe] sign connected to them. This is the only way to require permissions to make pipes."); pipeRequireSign = config.getBoolean(path + "require-sign", false); + + config.setComment(path + "full-pipe-smoke", "Show smoke particles on a sticky piston whose last pull could not be delivered anywhere, so players can see where a pipe system is stuck."); + pipeBlockedSmoke = config.getBoolean(path + "full-pipe-smoke", true); } } \ No newline at end of file From 7ca6c3b5bb451b068b06d4e49bd779d2939902f3 Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:35:10 +0200 Subject: [PATCH 2/2] Clear the blocked flag when the source runs empty Emptying the container by hand left the piston smoking forever, since the flag only cleared on a delivering pull. A pulse that finds nothing to pull now clears it too, a pipe with nothing to move is not stuck. --- .../sk89q/craftbook/mechanics/pipe/Pipes.java | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java b/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java index d0a0315df..fb2e95626 100644 --- a/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java +++ b/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java @@ -399,17 +399,22 @@ private void startPipe(Block block, List items, boolean request) { } // A pull refused everywhere marks the piston blocked; any pull that - // delivers something clears it again. - if (pipeBlockedSmoke && pulledAmount > 0) { - int undelivered = 0; - for (ItemStack left : items) - if (left != null) - undelivered += left.getAmount(); - if (undelivered >= pulledAmount) { - if (blockedPistons.size() > MAX_BLOCKED_PISTONS) - blockedPistons.clear(); - blockedPistons.add(block.getLocation()); - spawnBlockedSmoke(block); + // delivers something clears it again, and so does an empty source, + // since a pipe with nothing to move is not stuck. + if (pipeBlockedSmoke) { + if (pulledAmount > 0) { + int undelivered = 0; + for (ItemStack left : items) + if (left != null) + undelivered += left.getAmount(); + if (undelivered >= pulledAmount) { + if (blockedPistons.size() > MAX_BLOCKED_PISTONS) + blockedPistons.clear(); + blockedPistons.add(block.getLocation()); + spawnBlockedSmoke(block); + } else { + blockedPistons.remove(block.getLocation()); + } } else { blockedPistons.remove(block.getLocation()); }