Fix ring-orientation detection, which was breaking horizontal (and some vertical) gates

The plane-lock fix from a couple rounds back bootstrapped its axis
detection with an orthogonal-only (face-to-face) flood, reasoning
that couldn't leak through depth. But on a stepped/diagonal ring, an
orthogonal-only sample from the seed is just a short straight run -
which has zero variance in TWO axes, not only the true thickness
one. inPlaneDirections()'s tie-breaking (checks X thinnest, then Z,
falls back to Y) then frequently picked the wrong axis: a horizontal
ring's true thin axis (Y) was never even reachable once a straight
run made X or Z look equally "thin" by coincidence. Whether a given
gate linked successfully came down to which direction the seed's
first few connected blocks happened to run in - explaining the
inconsistent failures across otherwise-identical builds.

Fixed by making the bootstrap diagonal-inclusive again (safe now that
frame-materials defaults to just obsidian, so bootstrap leak risk is
minimal) so it actually samples the ring's real 2D footprint before
deciding the plane, then re-deriving the plane once more from the
full, properly-flooded frame set as a second safety net.

Also generalized pickChevrons()/walkRing(), which hardcoded
`row = block.getY()` - a vertical-ring assumption that's meaningless
for a horizontal ring where Y is constant across the whole thing.
Replaced with project(), which picks the correct pair of plane axes
for whatever orientation the ring actually has.
This commit is contained in:
Michael Burgess
2026-08-09 12:11:16 -04:00
parent 980f73e7d2
commit 0c74b68cbe
@@ -15,9 +15,10 @@ 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 or orientation the builder makes -
* interior ("iris"), and a fixed number of chevrons evenly spaced around the ring's actual * vertical against a wall, horizontal flat on the ground, whatever), the enclosed interior
* geometric perimeter. * ("iris"), and a fixed number of chevrons evenly spaced around the ring's actual geometric
* perimeter.
*/ */
public class GateStructureScanner { public class GateStructureScanner {
@@ -25,6 +26,26 @@ public class GateStructureScanner {
BlockFace.UP, BlockFace.DOWN, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST BlockFace.UP, BlockFace.DOWN, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST
}; };
// Used only to sample enough of the ring's real shape to determine its plane. A ring's
// corners commonly step diagonally, so an orthogonal-only sample would just be a short
// straight run - which has zero variance in TWO axes, not just the true thickness one,
// and reliably fools axis detection. Diagonal connectivity here is fine: it's discarded
// after orientation is known, and the real frame flood below stays plane-locked.
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
public final String failureReason; // null on success public final String failureReason; // null on success
@@ -71,13 +92,7 @@ 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 List<Block> bootstrap = diagonalBootstrapFlood(seed, 40);
// 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); BlockFace[] inPlane = inPlaneDirections(bootstrap);
int[][] planeOffsets = planeOffsetsFor(inPlane); int[][] planeOffsets = planeOffsetsFor(inPlane);
@@ -107,6 +122,10 @@ public class GateStructureScanner {
} }
} }
// The bootstrap only sampled a corner of the ring; re-derive the plane from the full,
// properly-flooded frame set in case the bootstrap's smaller sample was ambiguous.
inPlane = inPlaneDirections(frameBlocks);
if (frameBlocks.size() < minFrameBlocks) { if (frameBlocks.size() < minFrameBlocks) {
return ScanResult.fail("Only found " + frameBlocks.size() + " connected frame block(s), need at least " return ScanResult.fail("Only found " + frameBlocks.size() + " connected frame block(s), need at least "
+ minFrameBlocks + " (gate.min-frame-blocks)."); + minFrameBlocks + " (gate.min-frame-blocks).");
@@ -139,8 +158,8 @@ 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. */ /** Samples the ring's real shape (diagonal-inclusive) purely to determine its plane before the real, plane-locked flood. */
private List<Block> orthogonalFlood(Block seed, int cap) { private List<Block> diagonalBootstrapFlood(Block seed, int cap) {
Set<Long> visited = new HashSet<>(); Set<Long> visited = new HashSet<>();
List<Block> found = new ArrayList<>(); List<Block> found = new ArrayList<>();
Deque<Block> queue = new ArrayDeque<>(); Deque<Block> queue = new ArrayDeque<>();
@@ -150,8 +169,8 @@ public class GateStructureScanner {
while (!queue.isEmpty() && found.size() < cap) { while (!queue.isEmpty() && found.size() < cap) {
Block cur = queue.poll(); Block cur = queue.poll();
found.add(cur); found.add(cur);
for (BlockFace face : ORTHOGONAL) { for (int[] d : ALL_26_OFFSETS) {
Block next = cur.getRelative(face); Block next = cur.getRelative(d[0], d[1], d[2]);
long k = key(next); long k = key(next);
if (visited.contains(k)) continue; if (visited.contains(k)) continue;
if (isFrameMaterial(next.getType())) { if (isFrameMaterial(next.getType())) {
@@ -169,8 +188,6 @@ public class GateStructureScanner {
for (BlockFace f : ORTHOGONAL) { for (BlockFace f : ORTHOGONAL) {
offsets.add(new int[]{f.getModX(), f.getModY(), f.getModZ()}); 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; boolean usesX = false, usesY = false, usesZ = false;
for (BlockFace f : inPlane) { for (BlockFace f : inPlane) {
if (f.getModX() != 0) usesX = true; if (f.getModX() != 0) usesX = true;
@@ -232,24 +249,33 @@ public class GateStructureScanner {
} }
} }
/** Projects a block onto the ring's 2D plane (whatever orientation it is) as (row, col). */
private int[] project(Block b, BlockFace[] inPlane) {
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;
}
if (usesX && usesY) return new int[]{b.getY(), b.getX()}; // thickness axis is Z (a vertical ring facing N/S)
if (usesX && usesZ) return new int[]{b.getX(), b.getZ()}; // thickness axis is Y (a horizontal ring)
return new int[]{b.getY(), b.getZ()}; // thickness axis is X (a vertical ring facing E/W)
}
/** /**
* Picks {@code chevronCount} chevrons evenly spaced around the ring's actual geometric * 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 * 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. * neighbours) and takes evenly-spaced indices from that walk, not raw scan-discovery order.
*/ */
private List<GateStructure.ChevronSlot> pickChevrons(List<Block> frameBlocks, BlockFace[] inPlane) { 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<Long, Block> plane = new HashMap<>();
for (Block b : frameBlocks) { for (Block b : frameBlocks) {
int col = horizontalIsX ? b.getX() : b.getZ(); int[] rc = project(b, inPlane);
int row = b.getY(); long k = (((long) rc[0]) << 32) ^ (rc[1] & 0xFFFFFFFFL);
long k = (((long) row) << 32) ^ (col & 0xFFFFFFFFL);
plane.putIfAbsent(k, b); plane.putIfAbsent(k, b);
} }
List<Block> ordered = walkRing(frameBlocks, plane, horizontalIsX); List<Block> ordered = walkRing(frameBlocks, plane, inPlane);
List<Block> ring = (ordered != null && ordered.size() >= 4) ? ordered : frameBlocks; List<Block> ring = (ordered != null && ordered.size() >= 4) ? ordered : frameBlocks;
int count = Math.max(0, Math.min(chevronCount, ring.size())); int count = Math.max(0, Math.min(chevronCount, ring.size()));
@@ -272,14 +298,13 @@ public class GateStructureScanner {
}; };
/** Walks the ring in geometric order assuming each frame block has exactly two in-plane neighbours (orthogonal or diagonal, for stepped rings); null if that assumption fails. */ /** Walks the ring in geometric order assuming each frame block has exactly two in-plane neighbours (orthogonal or diagonal, for stepped rings); null if that assumption fails. */
private List<Block> walkRing(List<Block> frameBlocks, Map<Long, Block> plane, boolean horizontalIsX) { private List<Block> walkRing(List<Block> frameBlocks, Map<Long, Block> plane, BlockFace[] inPlane) {
Map<Block, List<Block>> adjacency = new HashMap<>(); Map<Block, List<Block>> adjacency = new HashMap<>();
for (Block b : frameBlocks) { for (Block b : frameBlocks) {
int col = horizontalIsX ? b.getX() : b.getZ(); int[] rc = project(b, inPlane);
int row = b.getY();
List<Block> neighbors = new ArrayList<>(); List<Block> neighbors = new ArrayList<>();
for (int[] d : PLANE_8_OFFSETS) { for (int[] d : PLANE_8_OFFSETS) {
long k = (((long) (row + d[0])) << 32) ^ ((col + d[1]) & 0xFFFFFFFFL); long k = (((long) (rc[0] + d[0])) << 32) ^ ((rc[1] + d[1]) & 0xFFFFFFFFL);
Block n = plane.get(k); Block n = plane.get(k);
if (n != null && n != b) neighbors.add(n); if (n != null && n != b) neighbors.add(n);
} }