diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateManager.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateManager.java index 0863bca..d7d56f0 100644 --- a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateManager.java +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateManager.java @@ -108,15 +108,30 @@ public class GateManager { .collect(Collectors.toList()); } + private String lastCreateFailureReason; + + /** Set only when {@link #createGate} just returned null - explains why the scan failed. */ + public String getLastCreateFailureReason() { + return lastCreateFailureReason; + } + public RuntimeGate createGate(Block signBlock, String network, String name, UUID owner, EnumSet flags) { return createGate(signBlock, network, name, owner, flags, null); } public RuntimeGate createGate(Block signBlock, String network, String name, UUID owner, EnumSet flags, String fixedDestination) { + lastCreateFailureReason = null; Block attached = attachedFrameBlock(signBlock); - if (attached == null) return null; - GateStructure structure = scanner.scan(attached); - if (structure == null) return null; + if (attached == null) { + lastCreateFailureReason = "Couldn't determine which block the sign is attached to."; + return null; + } + GateStructureScanner.ScanResult result = scanner.scanWithDiagnostics(attached); + if (!result.isSuccess()) { + lastCreateFailureReason = result.failureReason; + return null; + } + GateStructure structure = result.structure; Location exit = computeExitLocation(structure, signBlock); Gate gate = new Gate(UUID.randomUUID(), name, network, plugin.getServerId(), diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateStructureScanner.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateStructureScanner.java index 5c86d1a..367670e 100644 --- a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateStructureScanner.java +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateStructureScanner.java @@ -22,6 +22,22 @@ public class GateStructureScanner { BlockFace.UP, BlockFace.DOWN, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST }; + /** Result of a scan: either a successful structure, or a reason a player-facing message can be built from. */ + public static final class ScanResult { + public final GateStructure structure; // null on failure + public final String failureReason; // null on success + + private ScanResult(GateStructure structure, String failureReason) { + this.structure = structure; + this.failureReason = failureReason; + } + + static ScanResult ok(GateStructure s) { return new ScanResult(s, null); } + static ScanResult fail(String reason) { return new ScanResult(null, reason); } + + public boolean isSuccess() { return structure != null; } + } + private final Set frameMaterials; private final Material chevronUnlit; private final Material chevronLit; @@ -47,36 +63,49 @@ public class GateStructureScanner { return m == chevronUnlit || m == chevronLit; } - /** - * Scans outward from the given seed block (the block the sign is attached to). - * Returns null if no valid enclosed structure is found. - */ + /** Convenience wrapper for callers that only care about success/failure, not why. */ public GateStructure scan(Block seed) { - if (!isFrameMaterial(seed.getType())) { - // seed itself may be the wall block behind a sign that's part of a bigger build; - // try its direct neighbors for the actual frame block. + return scanWithDiagnostics(seed).structure; + } + + /** + * Scans outward from the given seed block (the block the sign is attached to) and reports + * exactly why it failed if it did, instead of just returning null. + */ + public ScanResult scanWithDiagnostics(Block seed) { + Block frameSeed = seed; + if (!isFrameMaterial(frameSeed.getType())) { for (BlockFace face : NEIGHBORS) { - Block b = seed.getRelative(face); + Block b = frameSeed.getRelative(face); if (isFrameMaterial(b.getType())) { - seed = b; + frameSeed = b; break; } } } - if (!isFrameMaterial(seed.getType())) return null; + if (!isFrameMaterial(frameSeed.getType())) { + return ScanResult.fail("The sign isn't touching a block listed in gate.frame-materials " + + "(nor is it touching the chevron material). Check the sign is mounted directly on the frame, " + + "and that every block type in your ring is listed in config.yml."); + } Set frameKeys = new HashSet<>(); List frameBlocks = new ArrayList<>(); List chevronBlocks = new ArrayList<>(); Deque queue = new ArrayDeque<>(); - queue.add(seed); - frameKeys.add(key(seed)); + queue.add(frameSeed); + frameKeys.add(key(frameSeed)); while (!queue.isEmpty()) { Block cur = queue.poll(); frameBlocks.add(cur); if (isChevronMaterial(cur.getType())) chevronBlocks.add(cur); - if (frameBlocks.size() > maxFrameBlocks) return null; + if (frameBlocks.size() > maxFrameBlocks) { + return ScanResult.fail("The connected frame has more than gate.max-frame-blocks (" + maxFrameBlocks + + ") blocks. Either it's too big, or a frame material is leaking into a much larger structure " + + "(e.g. planks used elsewhere in your build touching the ring). Raise max-frame-blocks or " + + "make the frame materials more specific."); + } for (BlockFace face : NEIGHBORS) { Block next = cur.getRelative(face); @@ -89,12 +118,17 @@ public class GateStructureScanner { } } - if (frameBlocks.size() < minFrameBlocks) return null; + if (frameBlocks.size() < minFrameBlocks) { + return ScanResult.fail("Only found " + frameBlocks.size() + " connected frame block(s), need at least " + + minFrameBlocks + " (gate.min-frame-blocks). The ring may be too small or made of a material " + + "that isn't in gate.frame-materials."); + } // Find an interior seed: a non-frame block adjacent to a frame block, that is not // solid (roughly the middle of the ring). We try several candidates and flood-fill // each; the first one that stays enclosed within maxIrisBlocks wins. Set triedSeeds = new HashSet<>(); + boolean triedAnyInterior = false; for (Block frameBlock : frameBlocks) { for (BlockFace face : NEIGHBORS) { Block candidate = frameBlock.getRelative(face); @@ -103,13 +137,22 @@ public class GateStructureScanner { triedSeeds.add(ck); if (candidate.getType().isSolid()) continue; + triedAnyInterior = true; List iris = floodInterior(candidate, frameKeys); if (iris != null && !iris.isEmpty()) { - return new GateStructure(frameBlocks, chevronBlocks, iris); + return ScanResult.ok(new GateStructure(frameBlocks, chevronBlocks, iris)); } } } - return null; + + if (!triedAnyInterior) { + return ScanResult.fail("The frame has no open interior at all (every block touching the ring is solid). " + + "Leave the middle hollow."); + } + return ScanResult.fail("Found a " + frameBlocks.size() + "-block frame, but its interior isn't fully " + + "enclosed - the empty space either leaks out through a gap in the ring, or is bigger than " + + "gate.max-iris-blocks (" + maxIrisBlocks + "). Check for a 1-block hole anywhere in the ring " + + "(including the floor/ceiling of the arch)."); } /** Flood-fills non-frame blocks starting at seed; fails (returns null) if it escapes the frame boundary. */ diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/SignCreateListener.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/SignCreateListener.java index f09f648..3876feb 100644 --- a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/SignCreateListener.java +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/SignCreateListener.java @@ -60,7 +60,11 @@ public class SignCreateListener implements Listener { RuntimeGate rg = gateManager.createGate(event.getBlock(), network, name, player.getUniqueId(), flags, fixedDestination); if (rg == null) { - player.sendMessage(Component.text("No valid gate structure found. Build the frame first, then place the sign.", NamedTextColor.RED)); + String reason = gateManager.getLastCreateFailureReason(); + player.sendMessage(Component.text("No valid gate structure found.", NamedTextColor.RED)); + if (reason != null) { + player.sendMessage(Component.text(reason, NamedTextColor.GRAY)); + } resetLine(event); return; } diff --git a/stargate-paper/src/main/resources/config.yml b/stargate-paper/src/main/resources/config.yml index 02eaa91..223b6c7 100644 --- a/stargate-paper/src/main/resources/config.yml +++ b/stargate-paper/src/main/resources/config.yml @@ -31,6 +31,7 @@ gate: frame-materials: - OBSIDIAN - GOLD_BLOCK + - GILDED_BLACKSTONE - BIRCH_PLANKS - OAK_PLANKS # Chevron blocks embedded in the frame. Build them as chevron-unlit-material; the