From 190f875d234ad1c6979314df91583e03dcc03353 Mon Sep 17 00:00:00 2001 From: Michael Burgess Date: Sun, 9 Aug 2026 11:19:55 -0400 Subject: [PATCH] Fix interior flood leaking out the front/back of free-standing rings A one-block-thick ring standing free (nothing built behind it, like every reference screenshot) has open air directly in front of and behind the interior. The iris/interior flood-fill searched all 6 directions, so it always escaped through that open air and flooded the surrounding world instead of staying inside the ring - reliably producing "interior isn't fully enclosed" regardless of how sound the ring actually was. Fixed by detecting which world axis the ring is thinnest along (same idea already used for chevron placement) and restricting both the interior candidate search and the flood itself to the other two axes, so it can only explore within the ring's own plane. Also fixed iris material semantics per feedback: the interior is no longer just air when idle. gate.iris-material is now split into iris-closed-material (default BEDROCK - solid, blocks all passage) and iris-open-material (default WATER, the event horizon), applied at link time, on open/close, and after a restart re-scan. --- README.md | 11 +++++- .../stargate/paper/gate/GateConfig.java | 6 ++- .../stargate/paper/gate/GateManager.java | 12 +++++- .../paper/gate/GateStructureScanner.java | 39 +++++++++++++++---- stargate-paper/src/main/resources/config.yml | 7 +++- 5 files changed, 59 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fa28659..3f9a7b6 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,11 @@ planks) with a fully sealed hollow interior. No gaps; the interior must be enclo corners (blocks touching only edge-to-edge or corner-to-corner still count), so a circular build doesn't need every block to share a full face with the next. +Build it free-standing, one block thick, in a single flat plane (a vertical ring +facing a direction, like all the examples above) - the plugin figures out which axis +is the ring's "thickness" and only looks for the interior within that plane, so it +doesn't try to flood-fill out through the open air in front of and behind the gate. + Chevrons aren't a separate material you place. When the gate is linked, the plugin flood-fills the ring, walks it in geometric order, and picks out the blocks that stick further outward than their immediate ring-neighbours - the "elbow" points of the @@ -53,8 +58,10 @@ material, unsealed interior, too far from the sign, etc. - **Right-click** the sign: cycles the destination shown on line 3 among the other gates on the same network. - **Left-click** the sign: dials the shown destination — chevrons light in sequence, - then the interior fills with `gate.iris-material` (default water). Walk into it to - teleport. It auto-closes after `dialing.open-seconds`. + then the interior switches from `gate.iris-closed-material` (default bedrock - solid, + nothing can pass through a closed/idle gate) to `gate.iris-open-material` (default + water, the "event horizon"). Walk into it to teleport. It auto-closes after + `dialing.open-seconds`, switching the interior back to bedrock. ## Multi-world diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateConfig.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateConfig.java index 6c23e68..773e0c7 100644 --- a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateConfig.java +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateConfig.java @@ -12,7 +12,8 @@ public class GateConfig { public final Set frameMaterials; public final Material chevronLit; - public final Material irisMaterial; + public final Material irisOpenMaterial; + public final Material irisClosedMaterial; public final int maxFrameBlocks; public final int maxIrisBlocks; public final int minFrameBlocks; @@ -34,7 +35,8 @@ public class GateConfig { this.frameMaterials = materials; this.chevronLit = matOr(cfg.getString("gate.chevron-lit-material"), Material.GILDED_BLACKSTONE, logger); - this.irisMaterial = matOr(cfg.getString("gate.iris-material"), Material.WATER, logger); + this.irisOpenMaterial = matOr(cfg.getString("gate.iris-open-material"), Material.WATER, logger); + this.irisClosedMaterial = matOr(cfg.getString("gate.iris-closed-material"), Material.BEDROCK, logger); this.maxFrameBlocks = cfg.getInt("gate.max-frame-blocks", 300); this.maxIrisBlocks = cfg.getInt("gate.max-iris-blocks", 200); this.minFrameBlocks = cfg.getInt("gate.min-frame-blocks", 8); 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 fee370d..65cea14 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 @@ -57,6 +57,11 @@ public class GateManager { GateStructureScanner.ScanResult result = scanner.scanWithDiagnostics(linked); if (result.isSuccess()) { structure = result.structure; + // a restart can't preserve "open" runtime state, so every gate comes + // back closed - make sure the iris actually reflects that + for (Block b : structure.getIris()) { + b.setType(config.irisClosedMaterial); + } } else { plugin.getLogger().warning("[Stargate] Could not re-scan structure for gate '" + gate.getName() + "' - it may have been damaged (" + result.failureReason + ")"); @@ -169,6 +174,9 @@ public class GateManager { return LinkResult.fail(result.failureReason); } GateStructure structure = result.structure; + for (Block b : structure.getIris()) { + b.setType(config.irisClosedMaterial); + } Location exit = computeExitLocation(structure, clickedBlock.getWorld(), facing); Gate gate = new Gate(UUID.randomUUID(), pending.name, pending.network, plugin.getServerId(), @@ -281,7 +289,7 @@ public class GateManager { gate.setConnectedTo(connectedTo); if (gate.getStructure() != null) { for (Block b : gate.getStructure().getIris()) { - b.setType(config.irisMaterial); + b.setType(config.irisOpenMaterial); if (b.getBlockData() instanceof Levelled lvl) { lvl.setLevel(0); b.setBlockData(lvl); @@ -311,7 +319,7 @@ public class GateManager { gate.setConnectedTo(null); if (gate.getStructure() != null) { for (Block b : gate.getStructure().getIris()) { - b.setType(org.bukkit.Material.AIR); + b.setType(config.irisClosedMaterial); } for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) { c.getBlock().setType(c.getRestingMaterial()); diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateStructureScanner.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateStructureScanner.java index f3032c0..0039067 100644 --- a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateStructureScanner.java +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateStructureScanner.java @@ -21,10 +21,6 @@ import java.util.Set; */ public class GateStructureScanner { - private static final BlockFace[] NEIGHBORS = { - BlockFace.UP, BlockFace.DOWN, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST - }; - // Round/octagonal rings built the usual Minecraft way step diagonally at the corners - two // frame blocks touching only edge-to-edge (or even just corner-to-corner), not face-to-face. // The frame search has to follow those too, or a stepped ring gets treated as several @@ -119,10 +115,16 @@ public class GateStructureScanner { + minFrameBlocks + " (gate.min-frame-blocks)."); } + // A free-standing, one-block-thick ring has nothing behind or in front of it, so + // interior candidates must stay confined to the ring's own plane - otherwise "inside" + // immediately leaks out the front/back into the open world. Find the thin axis (the + // one frame blocks barely vary along) and only search/flood the other two. + BlockFace[] inPlane = inPlaneDirections(frameBlocks); + Set triedSeeds = new HashSet<>(); boolean triedAnyInterior = false; for (Block frameBlock : frameBlocks) { - for (BlockFace face : NEIGHBORS) { + for (BlockFace face : inPlane) { Block candidate = frameBlock.getRelative(face); long ck = key(candidate); if (frameKeys.contains(ck) || triedSeeds.contains(ck)) continue; @@ -130,7 +132,7 @@ public class GateStructureScanner { if (candidate.getType().isSolid()) continue; triedAnyInterior = true; - List iris = floodInterior(candidate, frameKeys); + List iris = floodInterior(candidate, frameKeys, inPlane); if (iris != null && !iris.isEmpty()) { return ScanResult.ok(new GateStructure(frameBlocks, pickChevrons(frameBlocks, iris), iris)); } @@ -146,7 +148,7 @@ public class GateStructureScanner { + "gate.max-iris-blocks (" + maxIrisBlocks + ")."); } - private List floodInterior(Block seed, Set frameKeys) { + private List floodInterior(Block seed, Set frameKeys, BlockFace[] inPlane) { Set visited = new HashSet<>(); List interior = new ArrayList<>(); Deque queue = new ArrayDeque<>(); @@ -158,7 +160,7 @@ public class GateStructureScanner { interior.add(cur); if (interior.size() > maxIrisBlocks) return null; - for (BlockFace face : NEIGHBORS) { + for (BlockFace face : inPlane) { Block next = cur.getRelative(face); long k = key(next); if (visited.contains(k) || frameKeys.contains(k)) continue; @@ -170,6 +172,27 @@ public class GateStructureScanner { return interior; } + /** The 4 directions that stay within the ring's own plane, excluding the axis the ring is thinnest along. */ + private BlockFace[] inPlaneDirections(List frameBlocks) { + int minX = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE; + int minY = Integer.MAX_VALUE, maxY = Integer.MIN_VALUE; + int minZ = Integer.MAX_VALUE, maxZ = Integer.MIN_VALUE; + for (Block b : frameBlocks) { + minX = Math.min(minX, b.getX()); maxX = Math.max(maxX, b.getX()); + minY = Math.min(minY, b.getY()); maxY = Math.max(maxY, b.getY()); + minZ = Math.min(minZ, b.getZ()); maxZ = Math.max(maxZ, b.getZ()); + } + int rangeX = maxX - minX, rangeY = maxY - minY, rangeZ = maxZ - minZ; + + if (rangeX <= rangeY && rangeX <= rangeZ) { + return new BlockFace[]{BlockFace.UP, BlockFace.DOWN, BlockFace.NORTH, BlockFace.SOUTH}; + } else if (rangeZ <= rangeY && rangeZ <= rangeX) { + return new BlockFace[]{BlockFace.UP, BlockFace.DOWN, BlockFace.EAST, BlockFace.WEST}; + } else { + return new BlockFace[]{BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST}; + } + } + /** * Picks chevrons at the ring's actual protruding "elbow" points, the way a real Stargate's * chevrons sit - not evenly spaced by scan order. Works by walking the ring in geometric diff --git a/stargate-paper/src/main/resources/config.yml b/stargate-paper/src/main/resources/config.yml index 6306dbc..12d4ccf 100644 --- a/stargate-paper/src/main/resources/config.yml +++ b/stargate-paper/src/main/resources/config.yml @@ -44,8 +44,11 @@ gate: # Stargate's chevron placement - then swaps just those to chevron-lit-material while # dialing/open, restoring whatever they looked like at rest when the gate closes. chevron-lit-material: GILDED_BLACKSTONE - # Material poured into the interior (the "event horizon") while the gate is open. - iris-material: WATER + # The interior ("iris") reflects whether the gate is usable. Closed/idle, it's solid + # bedrock - nothing can walk or fall through it. Only while open does it become + # iris-open-material (the "event horizon") and let players/items pass. + iris-open-material: WATER + iris-closed-material: BEDROCK max-frame-blocks: 300 max-iris-blocks: 200 min-frame-blocks: 8