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:
@@ -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);
|
||||
|
||||
+25
-17
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+87
-98
@@ -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; }
|
||||
|
||||
+7
-5
@@ -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())) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user