From 8932dfed29b2a7f0e0641ce281c310c65769c065 Mon Sep 17 00:00:00 2001 From: Michael Burgess Date: Sun, 9 Aug 2026 11:52:04 -0400 Subject: [PATCH] Fix chevron over-count, plane-lock diagonal frame connectivity, true one-way travel, iris shield independence Chevron placement: the "protruding elbow" heuristic marked every convex corner of a smooth ring as a chevron, which for a true octagon/circle is *every* corner - producing 8 chevrons instead of a deliberate subset. Replaced with a fixed, configurable count (gate.chevron-count, default 7) evenly spaced along the ring's actual geometric walk order. Frame flood-fill leaking into unrelated structures: the diagonal ("stepped ring") connectivity fix from two commits ago used a full 3D 26-neighbor search, which let the flood leak sideways through the ring's thickness into any other nearby structure sharing a frame material (e.g. gilded blackstone used decoratively on a backdrop wall), putting chevrons on it. The scan now bootstraps with a small orthogonal-only flood to find the ring's plane first, then only allows diagonal connectivity within that plane - never through depth. One-way travel, properly this time: RuntimeGate gained an `outgoing` flag, set true only on the gate that was actually dialed. The teleport listener now only matches outgoing gates, so the destination shows the event horizon but isn't walkable - stepping into it does nothing, and only the dialing side can send a player anywhere. Iris shield: it's a physical barrier independent of the wormhole, not tied to "connected" state. Closed always means iris-shield-material (default changed from IRON_BLOCK to BEDROCK per feedback), whether or not the gate is even dialed right now. Only when the shield is open does the idle/connected distinction (air vs event horizon) apply. applyIrisMaterial() centralizes this so link, restart re-scan, open/close, and the shield toggle all agree. --- README.md | 43 ++-- .../stargate/paper/gate/GateConfig.java | 4 +- .../stargate/paper/gate/GateManager.java | 42 ++-- .../paper/gate/GateStructureScanner.java | 185 ++++++++---------- .../stargate/paper/gate/RuntimeGate.java | 3 + .../paper/listener/GateTeleportListener.java | 12 +- stargate-paper/src/main/resources/config.yml | 28 +-- 7 files changed, 163 insertions(+), 154 deletions(-) diff --git a/README.md b/README.md index 33f6e9c..dabedec 100644 --- a/README.md +++ b/README.md @@ -29,14 +29,11 @@ is the ring's "thickness" and only looks for the interior within that plane, so 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 -shape, which is where a real Stargate's chevrons sit (an 11-wide octagonal ring like -the reference design naturally produces 7 of them: the top apex, the four shoulder -elbows, and the two side bumps). Those blocks swap to `gate.chevron-lit-material` -(default glowstone) while dialing/open, and revert to whatever they looked like at -rest when the gate closes - so a chevron can rest as plain obsidian and blend -invisibly into the frame until it lights up. +flood-fills the ring, walks it in geometric order, and picks `gate.chevron-count` +(default 7) blocks evenly spaced around the ring's actual perimeter. Those blocks +swap to `gate.chevron-lit-material` (default glowstone) while dialing/open, and +revert to whatever they looked like at rest when the gate closes - so a chevron can +rest as plain obsidian and blend invisibly into the frame until it lights up. 1. Build the ring. 2. Optionally place a button directly below where you'll put the sign - this becomes @@ -61,20 +58,22 @@ 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 switches from `gate.iris-idle-material` (default air - an idle gate - is just an empty ring you can see straight through) to either the event horizon - (`gate.iris-open-material`, default water) or, if the iris shield is closed, to - `gate.iris-shield-material` (default iron block) instead, blocking travel even - though the gate is actively connected. It auto-closes after `dialing.open-seconds`, - switching back to the idle material either way. -- Walking into the event horizon teleports you to the connected gate, arriving just - clear of its iris (never inside it - that would immediately teleport you right back) - and still facing whatever direction you were already walking, so entering forward - always means exiting forward. -- **Right-click a button placed directly below the sign** toggles the iris shield. - Closing it never needs anything extra. If the gate has a pin set (`/sg pin `), - opening a closed shield prompts you to type the code in chat within - `gate.pin-timeout-seconds` before it'll open. + then the interior becomes the event horizon (`gate.iris-open-material`, default + water) and travel opens up. It auto-closes after `dialing.open-seconds`. +- Travel is **one-way**: only the gate you dialed *from* can send you anywhere. + Walking into its event horizon teleports you to the destination, arriving just + clear of its iris (never inside it) and still facing whatever direction you were + already walking, so entering forward always means exiting forward. The + destination's horizon is visible but not walkable - stepping into it does nothing, + same as a real Stargate only running one direction at a time. +- The **iris shield** is a separate, physical thing from the wormhole connection. + **Right-click a button placed directly below the sign** to toggle it. Closed means + closed - `gate.iris-shield-material` (default bedrock), solid, blocking everything - + whether or not the gate is even connected right now. With the shield open, the + interior just reflects the connection state: `gate.iris-idle-material` (default air) + if idle, or the event horizon if actively dialed. Closing needs nothing extra; + if the gate has a pin set (`/sg pin `), opening a closed shield prompts you + to type the code in chat within `gate.pin-timeout-seconds` before it'll open. ## 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 de9bf40..b207162 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,6 +12,7 @@ public class GateConfig { public final Set frameMaterials; public final Material chevronLit; + public final int chevronCount; public final Material irisIdleMaterial; public final Material irisOpenMaterial; public final Material irisShieldMaterial; @@ -37,9 +38,10 @@ public class GateConfig { this.frameMaterials = materials; this.chevronLit = matOr(cfg.getString("gate.chevron-lit-material"), Material.GLOWSTONE, logger); + this.chevronCount = cfg.getInt("gate.chevron-count", 7); this.irisIdleMaterial = matOr(cfg.getString("gate.iris-idle-material"), Material.AIR, logger); this.irisOpenMaterial = matOr(cfg.getString("gate.iris-open-material"), Material.WATER, logger); - this.irisShieldMaterial = matOr(cfg.getString("gate.iris-shield-material"), Material.IRON_BLOCK, logger); + this.irisShieldMaterial = matOr(cfg.getString("gate.iris-shield-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 a6dfeea..4c9a68d 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 @@ -43,7 +43,7 @@ public class GateManager { public void reloadConfig() { this.config = new GateConfig(plugin.getConfig(), plugin.getLogger()); - this.scanner = new GateStructureScanner(config.frameMaterials, config.chevronLit, + this.scanner = new GateStructureScanner(config.frameMaterials, config.chevronLit, config.chevronCount, config.maxFrameBlocks, config.maxIrisBlocks, config.minFrameBlocks); } @@ -61,18 +61,17 @@ public class GateManager { GateStructureScanner.ScanResult result = scanner.scanWithDiagnostics(linked); if (result.isSuccess()) { structure = result.structure; - // a restart can't preserve "connected" runtime state, so every gate - // comes back idle - make sure the interior actually reflects that - for (Block b : structure.getIris()) { - b.setType(config.irisIdleMaterial); - } } else { plugin.getLogger().warning("[Stargate] Could not re-scan structure for gate '" + gate.getName() + "' - it may have been damaged (" + result.failureReason + ")"); } } } + // a restart can't preserve "connected" runtime state, so every gate comes back + // 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); gatesById.put(gate.getId(), rg); if (gate.getServerId().equals(plugin.getServerId())) { gatesBySignBlock.put(signKey(gate.getSignWorld(), gate.getSignX(), gate.getSignY(), gate.getSignZ()), rg); @@ -178,9 +177,6 @@ public class GateManager { return LinkResult.fail(result.failureReason); } GateStructure structure = result.structure; - for (Block b : structure.getIris()) { - b.setType(config.irisIdleMaterial); - } Location exit = computeExitLocation(structure, clickedBlock.getWorld(), facing); Gate gate = new Gate(UUID.randomUUID(), pending.name, pending.network, plugin.getServerId(), @@ -190,6 +186,7 @@ public class GateManager { clickedBlock.getX(), clickedBlock.getY(), clickedBlock.getZ()); RuntimeGate rg = new RuntimeGate(gate, structure); + applyIrisMaterial(rg); gatesById.put(gate.getId(), rg); gatesBySignBlock.put(signKey(gate.getSignWorld(), gate.getSignX(), gate.getSignY(), gate.getSignZ()), rg); storage.saveGate(gate); @@ -262,7 +259,9 @@ public class GateManager { Runnable finish = () -> { openGate(from, to); + from.setOutgoing(true); openGate(to, from); + to.setOutgoing(false); }; if (chevrons.isEmpty()) { @@ -313,12 +312,11 @@ public class GateManager { if (gate.getDialTask() != null) { gate.getDialTask().cancel(); gate.setDialTask(null); } if (gate.getCloseTask() != null) { gate.getCloseTask().cancel(); gate.setCloseTask(null); } gate.setOpen(false); + gate.setOutgoing(false); RuntimeGate other = gate.getConnectedTo(); gate.setConnectedTo(null); if (gate.getStructure() != null) { - for (Block b : gate.getStructure().getIris()) { - b.setType(config.irisIdleMaterial); - } + applyIrisMaterial(gate); for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) { c.getBlock().setType(c.getRestingMaterial()); } @@ -334,9 +332,19 @@ public class GateManager { } } - /** Sets the iris blocks to whatever the gate's current connected/shield state implies. */ + /** + * Sets the iris blocks to whatever the gate's current state implies. The shield is a + * physical barrier independent of the wormhole: closed means closed - solid, blocking + * everything - whether or not the gate is even connected right now. Only when the shield + * is open does the connected/idle distinction (event horizon vs plain air) matter. + */ private void applyIrisMaterial(RuntimeGate gate) { - Material mat = gate.getGate().isIrisClosed() ? config.irisShieldMaterial : config.irisOpenMaterial; + Material mat; + if (gate.getGate().isIrisClosed()) { + mat = config.irisShieldMaterial; + } else { + mat = gate.isOpen() ? config.irisOpenMaterial : config.irisIdleMaterial; + } for (Block b : gate.getStructure().getIris()) { b.setType(mat); if (b.getBlockData() instanceof Levelled lvl) { @@ -348,12 +356,12 @@ public class GateManager { // ---- Iris shield ---- - /** Flips the iris shield and, if the gate is currently connected, updates the blocks immediately. Returns the new closed-state. */ + /** Flips the iris shield and updates the blocks immediately. Returns the new closed-state. */ public boolean toggleIrisShield(RuntimeGate rg) { boolean nowClosed = !rg.getGate().isIrisClosed(); rg.getGate().setIrisClosed(nowClosed); saveGate(rg); - if (rg.isOpen() && rg.getStructure() != null) { + if (rg.getStructure() != null) { applyIrisMaterial(rg); } return nowClosed; @@ -363,7 +371,7 @@ public class GateManager { public void openIrisShield(RuntimeGate rg) { rg.getGate().setIrisClosed(false); saveGate(rg); - if (rg.isOpen() && rg.getStructure() != null) { + if (rg.getStructure() != null) { applyIrisMaterial(rg); } } 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 0039067..b39b9e0 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 @@ -16,29 +16,14 @@ import java.util.Set; /** * Flood-fill scanner that discovers a gate's physical structure starting from a punched * frame block: the connected frame ring (any size/shape the builder makes), the enclosed - * interior ("iris"), and - unlike naive even-spacing - the ring's actual protruding "elbow" - * points, picked out geometrically, which is where a real Stargate's chevrons sit. + * interior ("iris"), and a fixed number of chevrons evenly spaced around the ring's actual + * geometric perimeter. */ public class GateStructureScanner { - // 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 - // disconnected fragments instead of one structure. - private static final int[][] ALL_26_OFFSETS = buildAll26Offsets(); - - private static int[][] buildAll26Offsets() { - List offsets = new ArrayList<>(); - for (int dx = -1; dx <= 1; dx++) { - for (int dy = -1; dy <= 1; dy++) { - for (int dz = -1; dz <= 1; dz++) { - if (dx == 0 && dy == 0 && dz == 0) continue; - offsets.add(new int[]{dx, dy, dz}); - } - } - } - return offsets.toArray(new int[0][]); - } + private static final BlockFace[] ORTHOGONAL = { + BlockFace.UP, BlockFace.DOWN, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST + }; public static final class ScanResult { public final GateStructure structure; // null on failure @@ -57,14 +42,16 @@ public class GateStructureScanner { private final Set frameMaterials; private final Material chevronLitMaterial; + private final int chevronCount; private final int maxFrameBlocks; private final int maxIrisBlocks; private final int minFrameBlocks; - public GateStructureScanner(Set frameMaterials, Material chevronLitMaterial, + public GateStructureScanner(Set frameMaterials, Material chevronLitMaterial, int chevronCount, int maxFrameBlocks, int maxIrisBlocks, int minFrameBlocks) { this.frameMaterials = frameMaterials; this.chevronLitMaterial = chevronLitMaterial; + this.chevronCount = chevronCount; this.maxFrameBlocks = maxFrameBlocks; this.maxIrisBlocks = maxIrisBlocks; this.minFrameBlocks = minFrameBlocks; @@ -84,6 +71,16 @@ public class GateStructureScanner { return ScanResult.fail("That block isn't listed in gate.frame-materials."); } + // Diagonal ("stepped") connectivity must only ever apply within the ring's own flat + // plane - never through its thickness - or the flood can leak sideways into an + // unrelated nearby structure built from the same materials (a decorative backdrop + // wall, for instance). We don't know the plane yet, so bootstrap it with a small, + // orthogonal-only (face-to-face) flood first, which can't leak through depth at all, + // then lock diagonal moves to that plane for the real flood. + List bootstrap = orthogonalFlood(seed, 24); + BlockFace[] inPlane = inPlaneDirections(bootstrap); + int[][] planeOffsets = planeOffsetsFor(inPlane); + Set frameKeys = new HashSet<>(); List frameBlocks = new ArrayList<>(); Deque queue = new ArrayDeque<>(); @@ -99,7 +96,7 @@ public class GateStructureScanner { + "Raise max-frame-blocks or make the frame materials more specific."); } - for (int[] d : ALL_26_OFFSETS) { + for (int[] d : planeOffsets) { Block next = cur.getRelative(d[0], d[1], d[2]); long k = key(next); if (frameKeys.contains(k)) continue; @@ -115,12 +112,6 @@ 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) { @@ -134,7 +125,7 @@ public class GateStructureScanner { triedAnyInterior = true; List iris = floodInterior(candidate, frameKeys, inPlane); if (iris != null && !iris.isEmpty()) { - return ScanResult.ok(new GateStructure(frameBlocks, pickChevrons(frameBlocks, iris), iris)); + return ScanResult.ok(new GateStructure(frameBlocks, pickChevrons(frameBlocks, inPlane), iris)); } } } @@ -148,6 +139,54 @@ public class GateStructureScanner { + "gate.max-iris-blocks (" + maxIrisBlocks + ")."); } + /** Small orthogonal-only (face-to-face) flood used purely to sample the ring's shape before we know its plane. */ + private List orthogonalFlood(Block seed, int cap) { + Set visited = new HashSet<>(); + List found = new ArrayList<>(); + Deque queue = new ArrayDeque<>(); + queue.add(seed); + visited.add(key(seed)); + + while (!queue.isEmpty() && found.size() < cap) { + Block cur = queue.poll(); + found.add(cur); + for (BlockFace face : ORTHOGONAL) { + Block next = cur.getRelative(face); + long k = key(next); + if (visited.contains(k)) continue; + if (isFrameMaterial(next.getType())) { + visited.add(k); + queue.add(next); + } + } + } + return found; + } + + /** The 6 orthogonal offsets plus the 4 diagonals that stay within the given in-plane directions. */ + private int[][] planeOffsetsFor(BlockFace[] inPlane) { + List offsets = new ArrayList<>(); + for (BlockFace f : ORTHOGONAL) { + offsets.add(new int[]{f.getModX(), f.getModY(), f.getModZ()}); + } + // Only two of the three axes appear in inPlane (the other is the thickness axis); + // add the 4 corner combinations of those two axes. + boolean usesX = false, usesY = false, usesZ = false; + for (BlockFace f : inPlane) { + if (f.getModX() != 0) usesX = true; + if (f.getModY() != 0) usesY = true; + if (f.getModZ() != 0) usesZ = true; + } + for (int a = -1; a <= 1; a += 2) { + for (int b = -1; b <= 1; b += 2) { + if (usesX && usesY) offsets.add(new int[]{a, b, 0}); + else if (usesX && usesZ) offsets.add(new int[]{a, 0, b}); + else if (usesY && usesZ) offsets.add(new int[]{0, a, b}); + } + } + return offsets.toArray(new int[0][]); + } + private List floodInterior(Block seed, Set frameKeys, BlockFace[] inPlane) { Set visited = new HashSet<>(); List interior = new ArrayList<>(); @@ -194,76 +233,38 @@ public class GateStructureScanner { } /** - * 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 - * order (each frame block should have exactly two ring neighbours) and marking local maxima - * of distance from the interior's centroid, i.e. points that stick out further than their - * immediate neighbours on the ring. + * Picks {@code chevronCount} chevrons evenly spaced around the ring's actual geometric + * perimeter - walks the ring in order (each frame block should have exactly two ring + * neighbours) and takes evenly-spaced indices from that walk, not raw scan-discovery order. */ - private List pickChevrons(List frameBlocks, List iris) { - // The ring is essentially 2D (one block thick); find which world axis is the thin one. - int minX = Integer.MAX_VALUE, maxX = 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()); - minZ = Math.min(minZ, b.getZ()); maxZ = Math.max(maxZ, b.getZ()); - } - boolean horizontalIsX = (maxX - minX) >= (maxZ - minZ); + private List pickChevrons(List frameBlocks, BlockFace[] inPlane) { + boolean horizontalIsX = false; + for (BlockFace f : inPlane) if (f.getModX() != 0) horizontalIsX = true; Map plane = new HashMap<>(); - Map coords = new HashMap<>(); for (Block b : frameBlocks) { int col = horizontalIsX ? b.getX() : b.getZ(); int row = b.getY(); long k = (((long) row) << 32) ^ (col & 0xFFFFFFFFL); plane.putIfAbsent(k, b); - coords.put(b, new int[]{row, col}); } List ordered = walkRing(frameBlocks, plane, horizontalIsX); - if (ordered == null || ordered.size() < 4) { - return fallbackEvenlySpaced(frameBlocks); - } + List ring = (ordered != null && ordered.size() >= 4) ? ordered : frameBlocks; - double centroidRow, centroidCol; - if (!iris.isEmpty()) { - long sumRow = 0, sumCol = 0; - for (Block b : iris) { - sumRow += b.getY(); - sumCol += horizontalIsX ? b.getX() : b.getZ(); - } - centroidRow = (double) sumRow / iris.size(); - centroidCol = (double) sumCol / iris.size(); - } else { - long sumRow = 0, sumCol = 0; - for (Block b : frameBlocks) { - sumRow += b.getY(); - sumCol += horizontalIsX ? b.getX() : b.getZ(); - } - centroidRow = (double) sumRow / frameBlocks.size(); - centroidCol = (double) sumCol / frameBlocks.size(); + int count = Math.max(0, Math.min(chevronCount, ring.size())); + List slots = new ArrayList<>(count); + if (count == 0) return slots; + double step = ring.size() / (double) count; + Set chosen = new HashSet<>(); + for (int i = 0; i < count; i++) { + int idx = (int) Math.round(i * step) % ring.size(); + while (chosen.contains(idx)) idx = (idx + 1) % ring.size(); + chosen.add(idx); + Block b = ring.get(idx); + slots.add(new GateStructure.ChevronSlot(b, b.getType())); } - - int n = ordered.size(); - double[] dist = new double[n]; - for (int i = 0; i < n; i++) { - int[] rc = coords.get(ordered.get(i)); - double dr = rc[0] - centroidRow; - double dc = rc[1] - centroidCol; - dist[i] = dr * dr + dc * dc; - } - - List chevrons = new ArrayList<>(); - for (int i = 0; i < n; i++) { - double prev = dist[(i - 1 + n) % n]; - double next = dist[(i + 1) % n]; - if (dist[i] > prev && dist[i] >= next) { - Block b = ordered.get(i); - chevrons.add(new GateStructure.ChevronSlot(b, b.getType())); - } - } - - return chevrons.isEmpty() ? fallbackEvenlySpaced(frameBlocks) : chevrons; + return slots; } private static final int[][] PLANE_8_OFFSETS = { @@ -302,18 +303,6 @@ public class GateStructureScanner { return ordered.size() == frameBlocks.size() ? ordered : null; } - private List fallbackEvenlySpaced(List frameBlocks) { - int count = Math.min(6, frameBlocks.size()); - List slots = new ArrayList<>(count); - if (count == 0) return slots; - double step = frameBlocks.size() / (double) count; - for (int i = 0; i < count; i++) { - Block b = frameBlocks.get((int) Math.round(i * step) % frameBlocks.size()); - slots.add(new GateStructure.ChevronSlot(b, b.getType())); - } - return slots; - } - private long key(Block b) { return (((long) b.getX() & 0x3FFFFFF) << 38) | (((long) (b.getY() + 512) & 0xFFF) << 26) | ((long) b.getZ() & 0x3FFFFFF); } diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/RuntimeGate.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/RuntimeGate.java index 05c4a51..87d269e 100644 --- a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/RuntimeGate.java +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/RuntimeGate.java @@ -9,6 +9,7 @@ public class RuntimeGate { private final Gate gate; private GateStructure structure; // null if structure could not be (re)scanned private boolean open = false; + private boolean outgoing = false; // true only for the gate that was dialed - travel is one-way private RuntimeGate connectedTo = null; private BukkitTask closeTask; private BukkitTask dialTask; @@ -24,6 +25,8 @@ public class RuntimeGate { public void setStructure(GateStructure structure) { this.structure = structure; } public boolean isOpen() { return open; } public void setOpen(boolean open) { this.open = open; } + public boolean isOutgoing() { return outgoing; } + public void setOutgoing(boolean outgoing) { this.outgoing = outgoing; } public RuntimeGate getConnectedTo() { return connectedTo; } public void setConnectedTo(RuntimeGate connectedTo) { this.connectedTo = connectedTo; } public BukkitTask getCloseTask() { return closeTask; } diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/GateTeleportListener.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/GateTeleportListener.java index d2bb688..146c18c 100644 --- a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/GateTeleportListener.java +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/GateTeleportListener.java @@ -18,10 +18,12 @@ import java.util.Map; import java.util.UUID; /** - * Walking into an open gate's event horizon teleports the player to the connected gate, - * arriving just past the destination's iris (never inside it - that would immediately - * re-trigger this same listener and bounce them straight back) and facing the same absolute - * direction they were already walking, so "enter forward" always means "exit forward". + * Walking into the DIALING gate's event horizon teleports the player to the connected gate, + * arriving just past its iris (never inside it) and facing the same absolute direction they + * were already walking, so "enter forward" always means "exit forward". Travel is strictly + * one-way: only the gate that was dialed (outgoing = true) can send anyone anywhere - stepping + * into the receiving gate's horizon does nothing, matching the wormhole only running one + * direction at a time. */ public class GateTeleportListener implements Listener { @@ -51,7 +53,7 @@ public class GateTeleportListener implements Listener { Block standing = to.getBlock(); List gates = gateManager.all(); for (RuntimeGate rg : gates) { - if (!rg.isOpen() || rg.getStructure() == null || rg.getConnectedTo() == null) continue; + if (!rg.isOpen() || !rg.isOutgoing() || rg.getStructure() == null || rg.getConnectedTo() == null) continue; for (Block iris : rg.getStructure().getIris()) { if (iris.getX() == standing.getX() && iris.getY() == standing.getY() && iris.getZ() == standing.getZ() && iris.getWorld().equals(standing.getWorld())) { diff --git a/stargate-paper/src/main/resources/config.yml b/stargate-paper/src/main/resources/config.yml index 1e7427a..de88596 100644 --- a/stargate-paper/src/main/resources/config.yml +++ b/stargate-paper/src/main/resources/config.yml @@ -31,7 +31,10 @@ gate: # Mix and match to match your build (the default matches an obsidian/gilded-blackstone # ring). The ring can be any size or shape, as long as it's a single connected loop # made only of these materials with a fully sealed hollow interior - no fixed template - # or size is required. + # or size is required. "Connected" includes diagonal stepping (round rings need that + # at their corners), but only within the ring's own flat plane - a nearby structure + # built from the same materials won't get pulled in just because it touches the ring + # through its thickness. frame-materials: - OBSIDIAN - GOLD_BLOCK @@ -39,19 +42,22 @@ gate: - BIRCH_PLANKS - OAK_PLANKS # Chevrons aren't a separate material you place. The plugin flood-fills the ring, - # walks it in geometric order, and picks out the blocks that stick further outward - # than their ring-neighbours - the "elbow" points of the shape, same as a real - # 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. + # walks it in geometric order, and picks this many blocks, evenly spaced around the + # ring's actual perimeter (not raw scan order) - 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: GLOWSTONE - # The interior has three looks, matching SG-1: idle (not connected) is just empty - # air - you can see straight through the ring. Connected, it's the event horizon - # (iris-open-material) UNLESS the iris shield is closed, in which case it shows - # iris-shield-material instead and blocks all travel even though the gate is active. - # The shield is toggled with a button placed directly below the control sign. + chevron-count: 7 + # The interior's look is driven by two independent things, matching SG-1: + # - The iris shield: a physical barrier toggled with a button placed directly below + # the control sign. Closed means closed - iris-shield-material, solid, blocking + # everything - REGARDLESS of whether a wormhole is even active right now. + # - Whether the shield is open: then it's iris-open-material (the event horizon) + # if the gate is actively connected, or iris-idle-material (just empty air, you + # see straight through) if it's just sitting there dialed to nothing. iris-idle-material: AIR iris-open-material: WATER - iris-shield-material: IRON_BLOCK + iris-shield-material: BEDROCK max-frame-blocks: 300 max-iris-blocks: 200 min-frame-blocks: 8