From 56c089f656ae2a21880cd9fab0bf474734b7d081 Mon Sep 17 00:00:00 2001 From: Michael Burgess Date: Sun, 9 Aug 2026 10:08:21 -0400 Subject: [PATCH] Add gilded blackstone to default frame materials, report scan failure reasons Sign creation now tells the player exactly why the structure scan failed (missing frame material, ring not sealed, interior too large, etc.) instead of one generic error. --- .../stargate/paper/gate/GateManager.java | 21 +++++- .../paper/gate/GateStructureScanner.java | 75 +++++++++++++++---- .../paper/listener/SignCreateListener.java | 6 +- stargate-paper/src/main/resources/config.yml | 1 + 4 files changed, 83 insertions(+), 20 deletions(-) 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