From 67330744ebc0982e0c287488afbb4dace5f8ca5e Mon Sep 17 00:00:00 2001 From: Michael Burgess Date: Sun, 9 Aug 2026 13:18:40 -0400 Subject: [PATCH] Actively contain the event horizon so water can never flow out Scan-time enclosure checking can catch most gaps, but Minecraft's own fluid physics will still spread a water source block into any adjacent open space it finds - a diagonal/stepped ring corner can have a path that isn't a "leak" by the scanner's flood-fill rules (which only ever move orthogonally) but vanilla water flow still takes, since flow is also orthogonal-only and can find edge cases the scanner's enclosure proof didn't need to rule out. Rather than trying to prove every possible ring geometry is leak-proof at scan time, GateManager now tracks every iris block of every locally scanned gate (registered at link time, on restart re-scan, and unregistered on destroy), and a new BlockFromToEvent listener cancels any fluid spread whose source is one of those blocks. This is the same technique real portal-style plugins use: containment enforced by the plugin at the physics level, not by proving the shape is sound in advance. Note: this only stops future leaking. Water that already escaped before this fix needs to be manually cleaned up once. --- README.md | 5 ++- .../stargate/paper/StargatePlugin.java | 1 + .../stargate/paper/gate/GateManager.java | 32 ++++++++++++++++++- .../GateWaterContainmentListener.java | 31 ++++++++++++++++++ 4 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/GateWaterContainmentListener.java diff --git a/README.md b/README.md index d215f5c..6d58ce8 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,10 @@ material, unsealed interior, too far from the sign, etc. gates on the same network. - **Left-click** the sign: dials the shown destination - chevrons light in sequence, then the interior becomes the event horizon (`gate.iris-open-material`, default - water) and travel opens up. It auto-closes after `dialing.open-seconds`. + water) and travel opens up. It auto-closes after `dialing.open-seconds`. That water + is contained to exactly the iris blocks - the plugin actively cancels any attempt + by Minecraft's own fluid physics to spread it further, so it can't leak out through + a gap or an odd ring shape and flood the ground around the gate. - **Shift + left-click** the sign: destroys the gate on the spot (same permission check as `/sg destroy` - owner or `stargate.admin`), breaking the sign as if you'd mined it. Quicker than looking at the sign and typing the command. diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/StargatePlugin.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/StargatePlugin.java index 2c479e5..9635be7 100644 --- a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/StargatePlugin.java +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/StargatePlugin.java @@ -52,6 +52,7 @@ public class StargatePlugin extends JavaPlugin { getServer().getPluginManager().registerEvents(new StructureProtectListener(gateManager), this); getServer().getPluginManager().registerEvents(new dev.skywalker3200.stargate.paper.listener.GateTeleportListener(this, gateManager, crossServerBridge), this); getServer().getPluginManager().registerEvents(new dev.skywalker3200.stargate.paper.listener.GateIrisButtonListener(this, gateManager), this); + getServer().getPluginManager().registerEvents(new dev.skywalker3200.stargate.paper.listener.GateWaterContainmentListener(gateManager), this); StargateCommand command = new StargateCommand(this, gateManager); getCommand("stargate").setExecutor(command); diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateManager.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateManager.java index 6133ff9..e0c8d15 100644 --- a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateManager.java +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateManager.java @@ -34,6 +34,11 @@ public class GateManager { private final Map gatesById = new HashMap<>(); private final Map gatesBySignBlock = new HashMap<>(); // "world,x,y,z" -> gate + // Every iris block of every locally-scanned gate, regardless of open/closed - used to stop + // Minecraft's own fluid physics from spreading the event horizon water past exactly these + // blocks, no matter what shape the ring is. Scan-time enclosure checking can't guarantee + // that on its own for every possible geometry, so this is the actual containment. + private final Set irisKeys = new HashSet<>(); public GateManager(StargatePlugin plugin, GateStorage storage) { this.plugin = plugin; @@ -52,6 +57,7 @@ public class GateManager { public void loadAll() { gatesById.clear(); gatesBySignBlock.clear(); + irisKeys.clear(); for (Gate gate : storage.loadAll()) { GateStructure structure = null; if (gate.getServerId().equals(plugin.getServerId())) { @@ -71,7 +77,10 @@ public class GateManager { // idle - applyIrisMaterial() still shows the shield if it was left closed, since // that's independent of the wormhole being active RuntimeGate rg = new RuntimeGate(gate, structure); - if (structure != null) applyIrisMaterial(rg); + if (structure != null) { + applyIrisMaterial(rg); + registerIrisKeys(structure); + } gatesById.put(gate.getId(), rg); if (gate.getServerId().equals(plugin.getServerId())) { gatesBySignBlock.put(signKey(gate.getSignWorld(), gate.getSignX(), gate.getSignY(), gate.getSignZ()), rg); @@ -231,6 +240,7 @@ public class GateManager { RuntimeGate rg = new RuntimeGate(gate, structure); applyIrisMaterial(rg); + registerIrisKeys(structure); gatesById.put(gate.getId(), rg); gatesBySignBlock.put(signKey(gate.getSignWorld(), gate.getSignX(), gate.getSignY(), gate.getSignZ()), rg); storage.saveGate(gate); @@ -241,11 +251,31 @@ public class GateManager { public void destroyGate(RuntimeGate rg) { closeGate(rg); + unregisterIrisKeys(rg.getStructure()); gatesById.remove(rg.getGate().getId()); gatesBySignBlock.remove(signKey(rg.getGate().getSignWorld(), rg.getGate().getSignX(), rg.getGate().getSignY(), rg.getGate().getSignZ())); storage.deleteGate(rg.getGate().getId()); } + private String blockKey(Block b) { + return b.getWorld().getName() + "," + b.getX() + "," + b.getY() + "," + b.getZ(); + } + + private void registerIrisKeys(GateStructure structure) { + if (structure == null) return; + for (Block b : structure.getIris()) irisKeys.add(blockKey(b)); + } + + private void unregisterIrisKeys(GateStructure structure) { + if (structure == null) return; + for (Block b : structure.getIris()) irisKeys.remove(blockKey(b)); + } + + /** True if this block is a gate's iris - used to stop water from ever flowing out of one. */ + public boolean isIrisBlock(Block b) { + return irisKeys.contains(blockKey(b)); + } + public void saveGate(RuntimeGate rg) { storage.saveGate(rg.getGate()); } diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/GateWaterContainmentListener.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/GateWaterContainmentListener.java new file mode 100644 index 0000000..7d4e48d --- /dev/null +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/GateWaterContainmentListener.java @@ -0,0 +1,31 @@ +package dev.skywalker3200.stargate.paper.listener; + +import dev.skywalker3200.stargate.paper.gate.GateManager; +import org.bukkit.Material; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockFromToEvent; + +/** + * Stops the event horizon from ever spreading past exactly the blocks the plugin placed. + * Scan-time enclosure checking can catch most gaps, but Minecraft's own fluid physics will + * still spread water into any adjacent open space it finds - a stepped/diagonal ring corner + * can have a path scan-time checks don't consider a "leak" but vanilla water flow still takes. + * This is the actual containment: cancel any flow whose source is a registered iris block. + */ +public class GateWaterContainmentListener implements Listener { + + private final GateManager gateManager; + + public GateWaterContainmentListener(GateManager gateManager) { + this.gateManager = gateManager; + } + + @EventHandler(ignoreCancelled = true) + public void onFlow(BlockFromToEvent event) { + if (event.getBlock().getType() != Material.WATER) return; + if (gateManager.isIrisBlock(event.getBlock())) { + event.setCancelled(true); + } + } +}