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.
|
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
|
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
|
flood-fills the ring, walks it in geometric order, and picks `gate.chevron-count`
|
||||||
further outward than their immediate ring-neighbours - the "elbow" points of the
|
(default 7) blocks evenly spaced around the ring's actual perimeter. Those blocks
|
||||||
shape, which is where a real Stargate's chevrons sit (an 11-wide octagonal ring like
|
swap to `gate.chevron-lit-material` (default glowstone) while dialing/open, and
|
||||||
the reference design naturally produces 7 of them: the top apex, the four shoulder
|
revert to whatever they looked like at rest when the gate closes - so a chevron can
|
||||||
elbows, and the two side bumps). Those blocks swap to `gate.chevron-lit-material`
|
rest as plain obsidian and blend invisibly into the frame until it lights up.
|
||||||
(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.
|
1. Build the ring.
|
||||||
2. Optionally place a button directly below where you'll put the sign - this becomes
|
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
|
- **Right-click** the sign: cycles the destination shown on line 3 among the other
|
||||||
gates on the same network.
|
gates on the same network.
|
||||||
- **Left-click** the sign: dials the shown destination - chevrons light in sequence,
|
- **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
|
then the interior becomes the event horizon (`gate.iris-open-material`, default
|
||||||
is just an empty ring you can see straight through) to either the event horizon
|
water) and travel opens up. It auto-closes after `dialing.open-seconds`.
|
||||||
(`gate.iris-open-material`, default water) or, if the iris shield is closed, to
|
- Travel is **one-way**: only the gate you dialed *from* can send you anywhere.
|
||||||
`gate.iris-shield-material` (default iron block) instead, blocking travel even
|
Walking into its event horizon teleports you to the destination, arriving just
|
||||||
though the gate is actively connected. It auto-closes after `dialing.open-seconds`,
|
clear of its iris (never inside it) and still facing whatever direction you were
|
||||||
switching back to the idle material either way.
|
already walking, so entering forward always means exiting forward. The
|
||||||
- Walking into the event horizon teleports you to the connected gate, arriving just
|
destination's horizon is visible but not walkable - stepping into it does nothing,
|
||||||
clear of its iris (never inside it - that would immediately teleport you right back)
|
same as a real Stargate only running one direction at a time.
|
||||||
and still facing whatever direction you were already walking, so entering forward
|
- The **iris shield** is a separate, physical thing from the wormhole connection.
|
||||||
always means exiting forward.
|
**Right-click a button placed directly below the sign** to toggle it. Closed means
|
||||||
- **Right-click a button placed directly below the sign** toggles the iris shield.
|
closed - `gate.iris-shield-material` (default bedrock), solid, blocking everything -
|
||||||
Closing it never needs anything extra. If the gate has a pin set (`/sg pin <code>`),
|
whether or not the gate is even connected right now. With the shield open, the
|
||||||
opening a closed shield prompts you to type the code in chat within
|
interior just reflects the connection state: `gate.iris-idle-material` (default air)
|
||||||
`gate.pin-timeout-seconds` before it'll open.
|
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
|
## Multi-world
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public class GateConfig {
|
|||||||
|
|
||||||
public final Set<Material> frameMaterials;
|
public final Set<Material> frameMaterials;
|
||||||
public final Material chevronLit;
|
public final Material chevronLit;
|
||||||
|
public final int chevronCount;
|
||||||
public final Material irisIdleMaterial;
|
public final Material irisIdleMaterial;
|
||||||
public final Material irisOpenMaterial;
|
public final Material irisOpenMaterial;
|
||||||
public final Material irisShieldMaterial;
|
public final Material irisShieldMaterial;
|
||||||
@@ -37,9 +38,10 @@ public class GateConfig {
|
|||||||
this.frameMaterials = materials;
|
this.frameMaterials = materials;
|
||||||
|
|
||||||
this.chevronLit = matOr(cfg.getString("gate.chevron-lit-material"), Material.GLOWSTONE, logger);
|
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.irisIdleMaterial = matOr(cfg.getString("gate.iris-idle-material"), Material.AIR, logger);
|
||||||
this.irisOpenMaterial = matOr(cfg.getString("gate.iris-open-material"), Material.WATER, 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.maxFrameBlocks = cfg.getInt("gate.max-frame-blocks", 300);
|
||||||
this.maxIrisBlocks = cfg.getInt("gate.max-iris-blocks", 200);
|
this.maxIrisBlocks = cfg.getInt("gate.max-iris-blocks", 200);
|
||||||
this.minFrameBlocks = cfg.getInt("gate.min-frame-blocks", 8);
|
this.minFrameBlocks = cfg.getInt("gate.min-frame-blocks", 8);
|
||||||
|
|||||||
+25
-17
@@ -43,7 +43,7 @@ public class GateManager {
|
|||||||
|
|
||||||
public void reloadConfig() {
|
public void reloadConfig() {
|
||||||
this.config = new GateConfig(plugin.getConfig(), plugin.getLogger());
|
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);
|
config.maxFrameBlocks, config.maxIrisBlocks, config.minFrameBlocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,18 +61,17 @@ public class GateManager {
|
|||||||
GateStructureScanner.ScanResult result = scanner.scanWithDiagnostics(linked);
|
GateStructureScanner.ScanResult result = scanner.scanWithDiagnostics(linked);
|
||||||
if (result.isSuccess()) {
|
if (result.isSuccess()) {
|
||||||
structure = result.structure;
|
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 {
|
} else {
|
||||||
plugin.getLogger().warning("[Stargate] Could not re-scan structure for gate '" + gate.getName()
|
plugin.getLogger().warning("[Stargate] Could not re-scan structure for gate '" + gate.getName()
|
||||||
+ "' - it may have been damaged (" + result.failureReason + ")");
|
+ "' - 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);
|
RuntimeGate rg = new RuntimeGate(gate, structure);
|
||||||
|
if (structure != null) applyIrisMaterial(rg);
|
||||||
gatesById.put(gate.getId(), rg);
|
gatesById.put(gate.getId(), rg);
|
||||||
if (gate.getServerId().equals(plugin.getServerId())) {
|
if (gate.getServerId().equals(plugin.getServerId())) {
|
||||||
gatesBySignBlock.put(signKey(gate.getSignWorld(), gate.getSignX(), gate.getSignY(), gate.getSignZ()), rg);
|
gatesBySignBlock.put(signKey(gate.getSignWorld(), gate.getSignX(), gate.getSignY(), gate.getSignZ()), rg);
|
||||||
@@ -178,9 +177,6 @@ public class GateManager {
|
|||||||
return LinkResult.fail(result.failureReason);
|
return LinkResult.fail(result.failureReason);
|
||||||
}
|
}
|
||||||
GateStructure structure = result.structure;
|
GateStructure structure = result.structure;
|
||||||
for (Block b : structure.getIris()) {
|
|
||||||
b.setType(config.irisIdleMaterial);
|
|
||||||
}
|
|
||||||
|
|
||||||
Location exit = computeExitLocation(structure, clickedBlock.getWorld(), facing);
|
Location exit = computeExitLocation(structure, clickedBlock.getWorld(), facing);
|
||||||
Gate gate = new Gate(UUID.randomUUID(), pending.name, pending.network, plugin.getServerId(),
|
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());
|
clickedBlock.getX(), clickedBlock.getY(), clickedBlock.getZ());
|
||||||
|
|
||||||
RuntimeGate rg = new RuntimeGate(gate, structure);
|
RuntimeGate rg = new RuntimeGate(gate, structure);
|
||||||
|
applyIrisMaterial(rg);
|
||||||
gatesById.put(gate.getId(), rg);
|
gatesById.put(gate.getId(), rg);
|
||||||
gatesBySignBlock.put(signKey(gate.getSignWorld(), gate.getSignX(), gate.getSignY(), gate.getSignZ()), rg);
|
gatesBySignBlock.put(signKey(gate.getSignWorld(), gate.getSignX(), gate.getSignY(), gate.getSignZ()), rg);
|
||||||
storage.saveGate(gate);
|
storage.saveGate(gate);
|
||||||
@@ -262,7 +259,9 @@ public class GateManager {
|
|||||||
|
|
||||||
Runnable finish = () -> {
|
Runnable finish = () -> {
|
||||||
openGate(from, to);
|
openGate(from, to);
|
||||||
|
from.setOutgoing(true);
|
||||||
openGate(to, from);
|
openGate(to, from);
|
||||||
|
to.setOutgoing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (chevrons.isEmpty()) {
|
if (chevrons.isEmpty()) {
|
||||||
@@ -313,12 +312,11 @@ public class GateManager {
|
|||||||
if (gate.getDialTask() != null) { gate.getDialTask().cancel(); gate.setDialTask(null); }
|
if (gate.getDialTask() != null) { gate.getDialTask().cancel(); gate.setDialTask(null); }
|
||||||
if (gate.getCloseTask() != null) { gate.getCloseTask().cancel(); gate.setCloseTask(null); }
|
if (gate.getCloseTask() != null) { gate.getCloseTask().cancel(); gate.setCloseTask(null); }
|
||||||
gate.setOpen(false);
|
gate.setOpen(false);
|
||||||
|
gate.setOutgoing(false);
|
||||||
RuntimeGate other = gate.getConnectedTo();
|
RuntimeGate other = gate.getConnectedTo();
|
||||||
gate.setConnectedTo(null);
|
gate.setConnectedTo(null);
|
||||||
if (gate.getStructure() != null) {
|
if (gate.getStructure() != null) {
|
||||||
for (Block b : gate.getStructure().getIris()) {
|
applyIrisMaterial(gate);
|
||||||
b.setType(config.irisIdleMaterial);
|
|
||||||
}
|
|
||||||
for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) {
|
for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) {
|
||||||
c.getBlock().setType(c.getRestingMaterial());
|
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) {
|
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()) {
|
for (Block b : gate.getStructure().getIris()) {
|
||||||
b.setType(mat);
|
b.setType(mat);
|
||||||
if (b.getBlockData() instanceof Levelled lvl) {
|
if (b.getBlockData() instanceof Levelled lvl) {
|
||||||
@@ -348,12 +356,12 @@ public class GateManager {
|
|||||||
|
|
||||||
// ---- Iris shield ----
|
// ---- 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) {
|
public boolean toggleIrisShield(RuntimeGate rg) {
|
||||||
boolean nowClosed = !rg.getGate().isIrisClosed();
|
boolean nowClosed = !rg.getGate().isIrisClosed();
|
||||||
rg.getGate().setIrisClosed(nowClosed);
|
rg.getGate().setIrisClosed(nowClosed);
|
||||||
saveGate(rg);
|
saveGate(rg);
|
||||||
if (rg.isOpen() && rg.getStructure() != null) {
|
if (rg.getStructure() != null) {
|
||||||
applyIrisMaterial(rg);
|
applyIrisMaterial(rg);
|
||||||
}
|
}
|
||||||
return nowClosed;
|
return nowClosed;
|
||||||
@@ -363,7 +371,7 @@ public class GateManager {
|
|||||||
public void openIrisShield(RuntimeGate rg) {
|
public void openIrisShield(RuntimeGate rg) {
|
||||||
rg.getGate().setIrisClosed(false);
|
rg.getGate().setIrisClosed(false);
|
||||||
saveGate(rg);
|
saveGate(rg);
|
||||||
if (rg.isOpen() && rg.getStructure() != null) {
|
if (rg.getStructure() != null) {
|
||||||
applyIrisMaterial(rg);
|
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
|
* 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
|
* 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"
|
* interior ("iris"), and a fixed number of chevrons evenly spaced around the ring's actual
|
||||||
* points, picked out geometrically, which is where a real Stargate's chevrons sit.
|
* geometric perimeter.
|
||||||
*/
|
*/
|
||||||
public class GateStructureScanner {
|
public class GateStructureScanner {
|
||||||
|
|
||||||
// Round/octagonal rings built the usual Minecraft way step diagonally at the corners - two
|
private static final BlockFace[] ORTHOGONAL = {
|
||||||
// frame blocks touching only edge-to-edge (or even just corner-to-corner), not face-to-face.
|
BlockFace.UP, BlockFace.DOWN, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST
|
||||||
// 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][]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class ScanResult {
|
public static final class ScanResult {
|
||||||
public final GateStructure structure; // null on failure
|
public final GateStructure structure; // null on failure
|
||||||
@@ -57,14 +42,16 @@ public class GateStructureScanner {
|
|||||||
|
|
||||||
private final Set<Material> frameMaterials;
|
private final Set<Material> frameMaterials;
|
||||||
private final Material chevronLitMaterial;
|
private final Material chevronLitMaterial;
|
||||||
|
private final int chevronCount;
|
||||||
private final int maxFrameBlocks;
|
private final int maxFrameBlocks;
|
||||||
private final int maxIrisBlocks;
|
private final int maxIrisBlocks;
|
||||||
private final int minFrameBlocks;
|
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) {
|
int maxFrameBlocks, int maxIrisBlocks, int minFrameBlocks) {
|
||||||
this.frameMaterials = frameMaterials;
|
this.frameMaterials = frameMaterials;
|
||||||
this.chevronLitMaterial = chevronLitMaterial;
|
this.chevronLitMaterial = chevronLitMaterial;
|
||||||
|
this.chevronCount = chevronCount;
|
||||||
this.maxFrameBlocks = maxFrameBlocks;
|
this.maxFrameBlocks = maxFrameBlocks;
|
||||||
this.maxIrisBlocks = maxIrisBlocks;
|
this.maxIrisBlocks = maxIrisBlocks;
|
||||||
this.minFrameBlocks = minFrameBlocks;
|
this.minFrameBlocks = minFrameBlocks;
|
||||||
@@ -84,6 +71,16 @@ public class GateStructureScanner {
|
|||||||
return ScanResult.fail("That block isn't listed in gate.frame-materials.");
|
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<>();
|
Set<Long> frameKeys = new HashSet<>();
|
||||||
List<Block> frameBlocks = new ArrayList<>();
|
List<Block> frameBlocks = new ArrayList<>();
|
||||||
Deque<Block> queue = new ArrayDeque<>();
|
Deque<Block> queue = new ArrayDeque<>();
|
||||||
@@ -99,7 +96,7 @@ public class GateStructureScanner {
|
|||||||
+ "Raise max-frame-blocks or make the frame materials more specific.");
|
+ "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]);
|
Block next = cur.getRelative(d[0], d[1], d[2]);
|
||||||
long k = key(next);
|
long k = key(next);
|
||||||
if (frameKeys.contains(k)) continue;
|
if (frameKeys.contains(k)) continue;
|
||||||
@@ -115,12 +112,6 @@ public class GateStructureScanner {
|
|||||||
+ minFrameBlocks + " (gate.min-frame-blocks).");
|
+ 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<>();
|
Set<Long> triedSeeds = new HashSet<>();
|
||||||
boolean triedAnyInterior = false;
|
boolean triedAnyInterior = false;
|
||||||
for (Block frameBlock : frameBlocks) {
|
for (Block frameBlock : frameBlocks) {
|
||||||
@@ -134,7 +125,7 @@ public class GateStructureScanner {
|
|||||||
triedAnyInterior = true;
|
triedAnyInterior = true;
|
||||||
List<Block> iris = floodInterior(candidate, frameKeys, inPlane);
|
List<Block> iris = floodInterior(candidate, frameKeys, inPlane);
|
||||||
if (iris != null && !iris.isEmpty()) {
|
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 + ").");
|
+ "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) {
|
private List<Block> floodInterior(Block seed, Set<Long> frameKeys, BlockFace[] inPlane) {
|
||||||
Set<Long> visited = new HashSet<>();
|
Set<Long> visited = new HashSet<>();
|
||||||
List<Block> interior = new ArrayList<>();
|
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
|
* Picks {@code chevronCount} chevrons evenly spaced around the ring's actual geometric
|
||||||
* chevrons sit - not evenly spaced by scan order. Works by walking the ring in geometric
|
* perimeter - walks the ring in order (each frame block should have exactly two ring
|
||||||
* order (each frame block should have exactly two ring neighbours) and marking local maxima
|
* neighbours) and takes evenly-spaced indices from that walk, not raw scan-discovery order.
|
||||||
* of distance from the interior's centroid, i.e. points that stick out further than their
|
|
||||||
* immediate neighbours on the ring.
|
|
||||||
*/
|
*/
|
||||||
private List<GateStructure.ChevronSlot> pickChevrons(List<Block> frameBlocks, List<Block> iris) {
|
private List<GateStructure.ChevronSlot> pickChevrons(List<Block> frameBlocks, BlockFace[] inPlane) {
|
||||||
// The ring is essentially 2D (one block thick); find which world axis is the thin one.
|
boolean horizontalIsX = false;
|
||||||
int minX = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE;
|
for (BlockFace f : inPlane) if (f.getModX() != 0) horizontalIsX = true;
|
||||||
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);
|
|
||||||
|
|
||||||
Map<Long, Block> plane = new HashMap<>();
|
Map<Long, Block> plane = new HashMap<>();
|
||||||
Map<Block, int[]> coords = new HashMap<>();
|
|
||||||
for (Block b : frameBlocks) {
|
for (Block b : frameBlocks) {
|
||||||
int col = horizontalIsX ? b.getX() : b.getZ();
|
int col = horizontalIsX ? b.getX() : b.getZ();
|
||||||
int row = b.getY();
|
int row = b.getY();
|
||||||
long k = (((long) row) << 32) ^ (col & 0xFFFFFFFFL);
|
long k = (((long) row) << 32) ^ (col & 0xFFFFFFFFL);
|
||||||
plane.putIfAbsent(k, b);
|
plane.putIfAbsent(k, b);
|
||||||
coords.put(b, new int[]{row, col});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Block> ordered = walkRing(frameBlocks, plane, horizontalIsX);
|
List<Block> ordered = walkRing(frameBlocks, plane, horizontalIsX);
|
||||||
if (ordered == null || ordered.size() < 4) {
|
List<Block> ring = (ordered != null && ordered.size() >= 4) ? ordered : frameBlocks;
|
||||||
return fallbackEvenlySpaced(frameBlocks);
|
|
||||||
}
|
|
||||||
|
|
||||||
double centroidRow, centroidCol;
|
int count = Math.max(0, Math.min(chevronCount, ring.size()));
|
||||||
if (!iris.isEmpty()) {
|
List<GateStructure.ChevronSlot> slots = new ArrayList<>(count);
|
||||||
long sumRow = 0, sumCol = 0;
|
if (count == 0) return slots;
|
||||||
for (Block b : iris) {
|
double step = ring.size() / (double) count;
|
||||||
sumRow += b.getY();
|
Set<Integer> chosen = new HashSet<>();
|
||||||
sumCol += horizontalIsX ? b.getX() : b.getZ();
|
for (int i = 0; i < count; i++) {
|
||||||
}
|
int idx = (int) Math.round(i * step) % ring.size();
|
||||||
centroidRow = (double) sumRow / iris.size();
|
while (chosen.contains(idx)) idx = (idx + 1) % ring.size();
|
||||||
centroidCol = (double) sumCol / iris.size();
|
chosen.add(idx);
|
||||||
} else {
|
Block b = ring.get(idx);
|
||||||
long sumRow = 0, sumCol = 0;
|
slots.add(new GateStructure.ChevronSlot(b, b.getType()));
|
||||||
for (Block b : frameBlocks) {
|
|
||||||
sumRow += b.getY();
|
|
||||||
sumCol += horizontalIsX ? b.getX() : b.getZ();
|
|
||||||
}
|
|
||||||
centroidRow = (double) sumRow / frameBlocks.size();
|
|
||||||
centroidCol = (double) sumCol / frameBlocks.size();
|
|
||||||
}
|
}
|
||||||
|
return slots;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final int[][] PLANE_8_OFFSETS = {
|
private static final int[][] PLANE_8_OFFSETS = {
|
||||||
@@ -302,18 +303,6 @@ public class GateStructureScanner {
|
|||||||
return ordered.size() == frameBlocks.size() ? ordered : null;
|
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) {
|
private long key(Block b) {
|
||||||
return (((long) b.getX() & 0x3FFFFFF) << 38) | (((long) (b.getY() + 512) & 0xFFF) << 26) | ((long) b.getZ() & 0x3FFFFFF);
|
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 final Gate gate;
|
||||||
private GateStructure structure; // null if structure could not be (re)scanned
|
private GateStructure structure; // null if structure could not be (re)scanned
|
||||||
private boolean open = false;
|
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 RuntimeGate connectedTo = null;
|
||||||
private BukkitTask closeTask;
|
private BukkitTask closeTask;
|
||||||
private BukkitTask dialTask;
|
private BukkitTask dialTask;
|
||||||
@@ -24,6 +25,8 @@ public class RuntimeGate {
|
|||||||
public void setStructure(GateStructure structure) { this.structure = structure; }
|
public void setStructure(GateStructure structure) { this.structure = structure; }
|
||||||
public boolean isOpen() { return open; }
|
public boolean isOpen() { return open; }
|
||||||
public void setOpen(boolean open) { this.open = 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 RuntimeGate getConnectedTo() { return connectedTo; }
|
||||||
public void setConnectedTo(RuntimeGate connectedTo) { this.connectedTo = connectedTo; }
|
public void setConnectedTo(RuntimeGate connectedTo) { this.connectedTo = connectedTo; }
|
||||||
public BukkitTask getCloseTask() { return closeTask; }
|
public BukkitTask getCloseTask() { return closeTask; }
|
||||||
|
|||||||
+7
-5
@@ -18,10 +18,12 @@ import java.util.Map;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Walking into an open gate's event horizon teleports the player to the connected gate,
|
* Walking into the DIALING gate's event horizon teleports the player to the connected gate,
|
||||||
* arriving just past the destination's iris (never inside it - that would immediately
|
* arriving just past its iris (never inside it) and facing the same absolute direction they
|
||||||
* re-trigger this same listener and bounce them straight back) and facing the same absolute
|
* were already walking, so "enter forward" always means "exit forward". Travel is strictly
|
||||||
* direction they were already walking, so "enter forward" always means "exit forward".
|
* 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 {
|
public class GateTeleportListener implements Listener {
|
||||||
|
|
||||||
@@ -51,7 +53,7 @@ public class GateTeleportListener implements Listener {
|
|||||||
Block standing = to.getBlock();
|
Block standing = to.getBlock();
|
||||||
List<RuntimeGate> gates = gateManager.all();
|
List<RuntimeGate> gates = gateManager.all();
|
||||||
for (RuntimeGate rg : gates) {
|
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()) {
|
for (Block iris : rg.getStructure().getIris()) {
|
||||||
if (iris.getX() == standing.getX() && iris.getY() == standing.getY() && iris.getZ() == standing.getZ()
|
if (iris.getX() == standing.getX() && iris.getY() == standing.getY() && iris.getZ() == standing.getZ()
|
||||||
&& iris.getWorld().equals(standing.getWorld())) {
|
&& iris.getWorld().equals(standing.getWorld())) {
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ gate:
|
|||||||
# Mix and match to match your build (the default matches an obsidian/gilded-blackstone
|
# 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
|
# 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
|
# 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:
|
frame-materials:
|
||||||
- OBSIDIAN
|
- OBSIDIAN
|
||||||
- GOLD_BLOCK
|
- GOLD_BLOCK
|
||||||
@@ -39,19 +42,22 @@ gate:
|
|||||||
- BIRCH_PLANKS
|
- BIRCH_PLANKS
|
||||||
- OAK_PLANKS
|
- OAK_PLANKS
|
||||||
# Chevrons aren't a separate material you place. The plugin flood-fills the ring,
|
# 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
|
# walks it in geometric order, and picks this many blocks, evenly spaced around the
|
||||||
# than their ring-neighbours - the "elbow" points of the shape, same as a real
|
# ring's actual perimeter (not raw scan order) - then swaps just those to
|
||||||
# Stargate's chevron placement - then swaps just those to chevron-lit-material while
|
# chevron-lit-material while dialing/open, restoring whatever they looked like at
|
||||||
# dialing/open, restoring whatever they looked like at rest when the gate closes.
|
# rest when the gate closes.
|
||||||
chevron-lit-material: GLOWSTONE
|
chevron-lit-material: GLOWSTONE
|
||||||
# The interior has three looks, matching SG-1: idle (not connected) is just empty
|
chevron-count: 7
|
||||||
# air - you can see straight through the ring. Connected, it's the event horizon
|
# The interior's look is driven by two independent things, matching SG-1:
|
||||||
# (iris-open-material) UNLESS the iris shield is closed, in which case it shows
|
# - The iris shield: a physical barrier toggled with a button placed directly below
|
||||||
# iris-shield-material instead and blocks all travel even though the gate is active.
|
# the control sign. Closed means closed - iris-shield-material, solid, blocking
|
||||||
# The shield is toggled with a button placed directly below the control sign.
|
# 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-idle-material: AIR
|
||||||
iris-open-material: WATER
|
iris-open-material: WATER
|
||||||
iris-shield-material: IRON_BLOCK
|
iris-shield-material: BEDROCK
|
||||||
max-frame-blocks: 300
|
max-frame-blocks: 300
|
||||||
max-iris-blocks: 200
|
max-iris-blocks: 200
|
||||||
min-frame-blocks: 8
|
min-frame-blocks: 8
|
||||||
|
|||||||
Reference in New Issue
Block a user