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.
This commit is contained in:
Michael Burgess
2026-08-09 11:52:04 -04:00
parent b822389387
commit 8932dfed29
7 changed files with 163 additions and 154 deletions
+21 -22
View File
@@ -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 <code>`),
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 <code>`), opening a closed shield prompts you
to type the code in chat within `gate.pin-timeout-seconds` before it'll open.
## Multi-world
@@ -12,6 +12,7 @@ public class GateConfig {
public final Set<Material> 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);
@@ -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);
}
}
@@ -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<int[]> 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<Material> frameMaterials;
private final Material chevronLitMaterial;
private final int chevronCount;
private final int maxFrameBlocks;
private final int maxIrisBlocks;
private final int minFrameBlocks;
public GateStructureScanner(Set<Material> frameMaterials, Material chevronLitMaterial,
public GateStructureScanner(Set<Material> 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<Block> bootstrap = orthogonalFlood(seed, 24);
BlockFace[] inPlane = inPlaneDirections(bootstrap);
int[][] planeOffsets = planeOffsetsFor(inPlane);
Set<Long> frameKeys = new HashSet<>();
List<Block> frameBlocks = new ArrayList<>();
Deque<Block> 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<Long> triedSeeds = new HashSet<>();
boolean triedAnyInterior = false;
for (Block frameBlock : frameBlocks) {
@@ -134,7 +125,7 @@ public class GateStructureScanner {
triedAnyInterior = true;
List<Block> 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<Block> orthogonalFlood(Block seed, int cap) {
Set<Long> visited = new HashSet<>();
List<Block> found = new ArrayList<>();
Deque<Block> 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<int[]> 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<Block> floodInterior(Block seed, Set<Long> frameKeys, BlockFace[] inPlane) {
Set<Long> visited = new HashSet<>();
List<Block> 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<GateStructure.ChevronSlot> pickChevrons(List<Block> frameBlocks, List<Block> 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<GateStructure.ChevronSlot> pickChevrons(List<Block> frameBlocks, BlockFace[] inPlane) {
boolean horizontalIsX = false;
for (BlockFace f : inPlane) if (f.getModX() != 0) horizontalIsX = true;
Map<Long, Block> plane = new HashMap<>();
Map<Block, int[]> 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<Block> ordered = walkRing(frameBlocks, plane, horizontalIsX);
if (ordered == null || ordered.size() < 4) {
return fallbackEvenlySpaced(frameBlocks);
}
List<Block> 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<GateStructure.ChevronSlot> slots = new ArrayList<>(count);
if (count == 0) return slots;
double step = ring.size() / (double) count;
Set<Integer> 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<GateStructure.ChevronSlot> 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<GateStructure.ChevronSlot> fallbackEvenlySpaced(List<Block> frameBlocks) {
int count = Math.min(6, frameBlocks.size());
List<GateStructure.ChevronSlot> 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);
}
@@ -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; }
@@ -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<RuntimeGate> 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())) {
+17 -11
View File
@@ -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