Fix interior flood leaking out the front/back of free-standing rings
A one-block-thick ring standing free (nothing built behind it, like every reference screenshot) has open air directly in front of and behind the interior. The iris/interior flood-fill searched all 6 directions, so it always escaped through that open air and flooded the surrounding world instead of staying inside the ring - reliably producing "interior isn't fully enclosed" regardless of how sound the ring actually was. Fixed by detecting which world axis the ring is thinnest along (same idea already used for chevron placement) and restricting both the interior candidate search and the flood itself to the other two axes, so it can only explore within the ring's own plane. Also fixed iris material semantics per feedback: the interior is no longer just air when idle. gate.iris-material is now split into iris-closed-material (default BEDROCK - solid, blocks all passage) and iris-open-material (default WATER, the event horizon), applied at link time, on open/close, and after a restart re-scan.
This commit is contained in:
@@ -23,6 +23,11 @@ planks) with a fully sealed hollow interior. No gaps; the interior must be enclo
|
|||||||
corners (blocks touching only edge-to-edge or corner-to-corner still count), so a
|
corners (blocks touching only edge-to-edge or corner-to-corner still count), so a
|
||||||
circular build doesn't need every block to share a full face with the next.
|
circular build doesn't need every block to share a full face with the next.
|
||||||
|
|
||||||
|
Build it free-standing, one block thick, in a single flat plane (a vertical ring
|
||||||
|
facing a direction, like all the examples above) - the plugin figures out which axis
|
||||||
|
is the ring's "thickness" and only looks for the interior within that plane, so it
|
||||||
|
doesn't try to flood-fill out through the open air in front of and behind the gate.
|
||||||
|
|
||||||
Chevrons aren't a separate material you place. When the gate is linked, the plugin
|
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 out the blocks that stick
|
||||||
further outward than their immediate ring-neighbours - the "elbow" points of the
|
further outward than their immediate ring-neighbours - the "elbow" points of the
|
||||||
@@ -53,8 +58,10 @@ material, unsealed interior, too far from the sign, etc.
|
|||||||
- **Right-click** the sign: cycles the destination shown on line 3 among the other
|
- **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 fills with `gate.iris-material` (default water). Walk into it to
|
then the interior switches from `gate.iris-closed-material` (default bedrock - solid,
|
||||||
teleport. It auto-closes after `dialing.open-seconds`.
|
nothing can pass through a closed/idle gate) to `gate.iris-open-material` (default
|
||||||
|
water, the "event horizon"). Walk into it to teleport. It auto-closes after
|
||||||
|
`dialing.open-seconds`, switching the interior back to bedrock.
|
||||||
|
|
||||||
## Multi-world
|
## Multi-world
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ public class GateConfig {
|
|||||||
|
|
||||||
public final Set<Material> frameMaterials;
|
public final Set<Material> frameMaterials;
|
||||||
public final Material chevronLit;
|
public final Material chevronLit;
|
||||||
public final Material irisMaterial;
|
public final Material irisOpenMaterial;
|
||||||
|
public final Material irisClosedMaterial;
|
||||||
public final int maxFrameBlocks;
|
public final int maxFrameBlocks;
|
||||||
public final int maxIrisBlocks;
|
public final int maxIrisBlocks;
|
||||||
public final int minFrameBlocks;
|
public final int minFrameBlocks;
|
||||||
@@ -34,7 +35,8 @@ public class GateConfig {
|
|||||||
this.frameMaterials = materials;
|
this.frameMaterials = materials;
|
||||||
|
|
||||||
this.chevronLit = matOr(cfg.getString("gate.chevron-lit-material"), Material.GILDED_BLACKSTONE, logger);
|
this.chevronLit = matOr(cfg.getString("gate.chevron-lit-material"), Material.GILDED_BLACKSTONE, logger);
|
||||||
this.irisMaterial = matOr(cfg.getString("gate.iris-material"), Material.WATER, logger);
|
this.irisOpenMaterial = matOr(cfg.getString("gate.iris-open-material"), Material.WATER, logger);
|
||||||
|
this.irisClosedMaterial = matOr(cfg.getString("gate.iris-closed-material"), Material.BEDROCK, logger);
|
||||||
this.maxFrameBlocks = cfg.getInt("gate.max-frame-blocks", 300);
|
this.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);
|
||||||
|
|||||||
+10
-2
@@ -57,6 +57,11 @@ 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 "open" runtime state, so every gate comes
|
||||||
|
// back closed - make sure the iris actually reflects that
|
||||||
|
for (Block b : structure.getIris()) {
|
||||||
|
b.setType(config.irisClosedMaterial);
|
||||||
|
}
|
||||||
} else {
|
} 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 + ")");
|
||||||
@@ -169,6 +174,9 @@ 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.irisClosedMaterial);
|
||||||
|
}
|
||||||
|
|
||||||
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(),
|
||||||
@@ -281,7 +289,7 @@ public class GateManager {
|
|||||||
gate.setConnectedTo(connectedTo);
|
gate.setConnectedTo(connectedTo);
|
||||||
if (gate.getStructure() != null) {
|
if (gate.getStructure() != null) {
|
||||||
for (Block b : gate.getStructure().getIris()) {
|
for (Block b : gate.getStructure().getIris()) {
|
||||||
b.setType(config.irisMaterial);
|
b.setType(config.irisOpenMaterial);
|
||||||
if (b.getBlockData() instanceof Levelled lvl) {
|
if (b.getBlockData() instanceof Levelled lvl) {
|
||||||
lvl.setLevel(0);
|
lvl.setLevel(0);
|
||||||
b.setBlockData(lvl);
|
b.setBlockData(lvl);
|
||||||
@@ -311,7 +319,7 @@ public class GateManager {
|
|||||||
gate.setConnectedTo(null);
|
gate.setConnectedTo(null);
|
||||||
if (gate.getStructure() != null) {
|
if (gate.getStructure() != null) {
|
||||||
for (Block b : gate.getStructure().getIris()) {
|
for (Block b : gate.getStructure().getIris()) {
|
||||||
b.setType(org.bukkit.Material.AIR);
|
b.setType(config.irisClosedMaterial);
|
||||||
}
|
}
|
||||||
for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) {
|
for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) {
|
||||||
c.getBlock().setType(c.getRestingMaterial());
|
c.getBlock().setType(c.getRestingMaterial());
|
||||||
|
|||||||
+31
-8
@@ -21,10 +21,6 @@ import java.util.Set;
|
|||||||
*/
|
*/
|
||||||
public class GateStructureScanner {
|
public class GateStructureScanner {
|
||||||
|
|
||||||
private static final BlockFace[] NEIGHBORS = {
|
|
||||||
BlockFace.UP, BlockFace.DOWN, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST
|
|
||||||
};
|
|
||||||
|
|
||||||
// Round/octagonal rings built the usual Minecraft way step diagonally at the corners - two
|
// 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.
|
// 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
|
// The frame search has to follow those too, or a stepped ring gets treated as several
|
||||||
@@ -119,10 +115,16 @@ public class GateStructureScanner {
|
|||||||
+ minFrameBlocks + " (gate.min-frame-blocks).");
|
+ 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) {
|
||||||
for (BlockFace face : NEIGHBORS) {
|
for (BlockFace face : inPlane) {
|
||||||
Block candidate = frameBlock.getRelative(face);
|
Block candidate = frameBlock.getRelative(face);
|
||||||
long ck = key(candidate);
|
long ck = key(candidate);
|
||||||
if (frameKeys.contains(ck) || triedSeeds.contains(ck)) continue;
|
if (frameKeys.contains(ck) || triedSeeds.contains(ck)) continue;
|
||||||
@@ -130,7 +132,7 @@ public class GateStructureScanner {
|
|||||||
if (candidate.getType().isSolid()) continue;
|
if (candidate.getType().isSolid()) continue;
|
||||||
|
|
||||||
triedAnyInterior = true;
|
triedAnyInterior = true;
|
||||||
List<Block> iris = floodInterior(candidate, frameKeys);
|
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, iris), iris));
|
||||||
}
|
}
|
||||||
@@ -146,7 +148,7 @@ public class GateStructureScanner {
|
|||||||
+ "gate.max-iris-blocks (" + maxIrisBlocks + ").");
|
+ "gate.max-iris-blocks (" + maxIrisBlocks + ").");
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Block> floodInterior(Block seed, Set<Long> frameKeys) {
|
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<>();
|
||||||
Deque<Block> queue = new ArrayDeque<>();
|
Deque<Block> queue = new ArrayDeque<>();
|
||||||
@@ -158,7 +160,7 @@ public class GateStructureScanner {
|
|||||||
interior.add(cur);
|
interior.add(cur);
|
||||||
if (interior.size() > maxIrisBlocks) return null;
|
if (interior.size() > maxIrisBlocks) return null;
|
||||||
|
|
||||||
for (BlockFace face : NEIGHBORS) {
|
for (BlockFace face : inPlane) {
|
||||||
Block next = cur.getRelative(face);
|
Block next = cur.getRelative(face);
|
||||||
long k = key(next);
|
long k = key(next);
|
||||||
if (visited.contains(k) || frameKeys.contains(k)) continue;
|
if (visited.contains(k) || frameKeys.contains(k)) continue;
|
||||||
@@ -170,6 +172,27 @@ public class GateStructureScanner {
|
|||||||
return interior;
|
return interior;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The 4 directions that stay within the ring's own plane, excluding the axis the ring is thinnest along. */
|
||||||
|
private BlockFace[] inPlaneDirections(List<Block> frameBlocks) {
|
||||||
|
int minX = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE;
|
||||||
|
int minY = Integer.MAX_VALUE, maxY = Integer.MIN_VALUE;
|
||||||
|
int minZ = Integer.MAX_VALUE, maxZ = Integer.MIN_VALUE;
|
||||||
|
for (Block b : frameBlocks) {
|
||||||
|
minX = Math.min(minX, b.getX()); maxX = Math.max(maxX, b.getX());
|
||||||
|
minY = Math.min(minY, b.getY()); maxY = Math.max(maxY, b.getY());
|
||||||
|
minZ = Math.min(minZ, b.getZ()); maxZ = Math.max(maxZ, b.getZ());
|
||||||
|
}
|
||||||
|
int rangeX = maxX - minX, rangeY = maxY - minY, rangeZ = maxZ - minZ;
|
||||||
|
|
||||||
|
if (rangeX <= rangeY && rangeX <= rangeZ) {
|
||||||
|
return new BlockFace[]{BlockFace.UP, BlockFace.DOWN, BlockFace.NORTH, BlockFace.SOUTH};
|
||||||
|
} else if (rangeZ <= rangeY && rangeZ <= rangeX) {
|
||||||
|
return new BlockFace[]{BlockFace.UP, BlockFace.DOWN, BlockFace.EAST, BlockFace.WEST};
|
||||||
|
} else {
|
||||||
|
return new BlockFace[]{BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Picks chevrons at the ring's actual protruding "elbow" points, the way a real Stargate's
|
* 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
|
* chevrons sit - not evenly spaced by scan order. Works by walking the ring in geometric
|
||||||
|
|||||||
@@ -44,8 +44,11 @@ gate:
|
|||||||
# Stargate's chevron placement - then swaps just those to chevron-lit-material while
|
# 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.
|
# dialing/open, restoring whatever they looked like at rest when the gate closes.
|
||||||
chevron-lit-material: GILDED_BLACKSTONE
|
chevron-lit-material: GILDED_BLACKSTONE
|
||||||
# Material poured into the interior (the "event horizon") while the gate is open.
|
# The interior ("iris") reflects whether the gate is usable. Closed/idle, it's solid
|
||||||
iris-material: WATER
|
# bedrock - nothing can walk or fall through it. Only while open does it become
|
||||||
|
# iris-open-material (the "event horizon") and let players/items pass.
|
||||||
|
iris-open-material: WATER
|
||||||
|
iris-closed-material: BEDROCK
|
||||||
max-frame-blocks: 300
|
max-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