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
* frame block: the connected frame ring (any size/shape the builder makes), the enclosed
* interior ("iris"), and a fixed number of chevrons evenly spaced around the ring's actual
* geometric perimeter.
* frame block: the connected frame ring (any size/shape or orientation the builder makes -
* vertical against a wall, horizontal flat on the ground, whatever), the enclosed interior
* ("iris"), and a fixed number of chevrons evenly spaced around the ring's actual geometric
* perimeter.
*/
public class GateStructureScanner {
@@ -25,6 +26,26 @@ public class GateStructureScanner {
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 final GateStructure structure; // null on failure
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.");
}
// 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);
List<Block> bootstrap = diagonalBootstrapFlood(seed, 40);
BlockFace[] inPlane = inPlaneDirections(bootstrap);
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) {
return ScanResult.fail("Only found " + frameBlocks.size() + " connected frame block(s), need at least "
+ minFrameBlocks + " (gate.min-frame-blocks).");
@@ -139,8 +158,8 @@ 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) {
/** Samples the ring's real shape (diagonal-inclusive) purely to determine its plane before the real, plane-locked flood. */
private List<Block> diagonalBootstrapFlood(Block seed, int cap) {
Set<Long> visited = new HashSet<>();
List<Block> found = new ArrayList<>();
Deque<Block> queue = new ArrayDeque<>();
@@ -150,8 +169,8 @@ public class GateStructureScanner {
while (!queue.isEmpty() && found.size() < cap) {
Block cur = queue.poll();
found.add(cur);
for (BlockFace face : ORTHOGONAL) {
Block next = cur.getRelative(face);
for (int[] d : ALL_26_OFFSETS) {
Block next = cur.getRelative(d[0], d[1], d[2]);
long k = key(next);
if (visited.contains(k)) continue;
if (isFrameMaterial(next.getType())) {
@@ -169,8 +188,6 @@ public class GateStructureScanner {
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;
@@ -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
* 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, BlockFace[] inPlane) {
boolean horizontalIsX = false;
for (BlockFace f : inPlane) if (f.getModX() != 0) horizontalIsX = true;
Map<Long, Block> plane = new HashMap<>();
for (Block b : frameBlocks) {
int col = horizontalIsX ? b.getX() : b.getZ();
int row = b.getY();
long k = (((long) row) << 32) ^ (col & 0xFFFFFFFFL);
int[] rc = project(b, inPlane);
long k = (((long) rc[0]) << 32) ^ (rc[1] & 0xFFFFFFFFL);
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;
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. */
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<>();
for (Block b : frameBlocks) {
int col = horizontalIsX ? b.getX() : b.getZ();
int row = b.getY();
int[] rc = project(b, inPlane);
List<Block> neighbors = new ArrayList<>();
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);
if (n != null && n != b) neighbors.add(n);
}