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:
+52
-27
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user