diff --git a/README.md b/README.md index ec5a6bd..bba5801 100644 --- a/README.md +++ b/README.md @@ -16,29 +16,52 @@ Build everything with `./gradlew build`. Jars land in each module's `build/libs/ ## Building a gate -1. Build a fully-enclosed ring out of any block(s) in `gate.frame-materials` (default: - obsidian, gold block, gilded blackstone, birch/oak planks). Leave the middle hollow. - No gaps - the ring must be a sealed loop. -2. Nothing else to build for chevrons. On creation, the plugin automatically picks - `gate.chevron-count` (default 6) frame blocks, evenly spaced around the ring, and - remembers what they looked like at rest. While dialing/open those blocks swap to - `gate.chevron-lit-material` (default gilded blackstone); closing the gate restores - them to their original block, so a chevron can rest as plain obsidian and blend - invisibly into the frame until it lights up. -3. Place a sign anywhere - it doesn't need to touch the frame, so it can act like a DHD +A gate's shape is a fixed template in `config.yml` (`gate.template`), matched exactly +against the world - not flood-filled or guessed. The default is an 11x11 ring with 7 +chevrons, laid out top row to bottom row: + +``` +00000000000 +0000*1*0000 +00012221000 +00*22222*00 +0*2222222*0 +01222222210 +0*2222222*0 +00*22222*00 +00012221000 +0000***0000 +00000000000 +``` + +- `*` — frame block, never touched by the plugin. Must be one of `gate.frame-materials` + (default: obsidian, gold block, gilded blackstone, birch/oak planks). +- `1` — a chevron: a frame block (same material rules as `*`) that swaps to + `gate.chevron-lit-material` (default gilded blackstone) while dialing/open, then + reverts to whatever it looked like at rest when the gate closes. So a chevron can + rest as plain obsidian and blend invisibly into the frame until it lights up. +- `2` — interior ("iris"), filled with `gate.iris-material` (default water) while open. +- `0` — ignored, not part of the gate. + +Build it in a single vertical plane, one block thick, aligned to the world's X or Z +axis (facing north/south/east/west - not built at an angle). Edit `gate.template` in +`config.yml` if you want a different size or chevron count/placement. + +1. Build the frame to match the template. +2. Place a sign anywhere - it doesn't need to touch the frame, so it can act like a DHD console standing apart from the gate - with: - Line 1: `[Stargate]` - Line 2: network name (blank = default network) - Line 3: gate name (blank = auto-generated) - Line 4: `hidden` to keep it out of the cycle list, `fixed:GateName` to lock this gate to always dial `GateName` (no right-click cycling), or blank -4. The sign will show "Punch the gate (30s)". Within `gate.link-timeout-seconds`, - left-click (punch) any block of the frame ring, within `gate.max-link-distance` of - the sign. The plugin flood-fills out from that block, confirms it's a fully enclosed - ring, and links the sign to it. +3. The sign will show "Punch the gate (30s)". Within `gate.link-timeout-seconds`, stand + facing the gate square-on and left-click (punch) any `*` or `1` block, within + `gate.max-link-distance` of the sign. The plugin matches the template against the + world starting from that block and links the sign to it. -If punching a block doesn't work, the plugin tells you exactly why in chat - an -unrecognized frame material, a gap in the ring, too far from the sign, etc. +If punching a block doesn't work, the plugin tells you exactly why in chat - wrong +material, no orientation of the template lines up, too far from the sign, etc. ## Using a gate diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateConfig.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateConfig.java index 4ad8116..bda80fd 100644 --- a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateConfig.java +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateConfig.java @@ -4,19 +4,17 @@ import org.bukkit.Material; import org.bukkit.configuration.file.FileConfiguration; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.logging.Logger; /** Typed view over the `gate:` / `dialing:` / `network:` sections of config.yml. */ public class GateConfig { + public final GateTemplate template; public final Set frameMaterials; public final Material chevronLit; - public final int chevronCount; public final Material irisMaterial; - public final int maxFrameBlocks; - public final int maxIrisBlocks; - public final int minFrameBlocks; public final int openSeconds; public final int chevronTickDelay; public final boolean playSounds; @@ -25,6 +23,13 @@ public class GateConfig { public final int maxLinkDistance; public GateConfig(FileConfiguration cfg, Logger logger) { + List templateRows = cfg.getStringList("gate.template"); + if (templateRows.isEmpty()) { + logger.warning("[Stargate] gate.template is empty in config.yml - falling back to a built-in default."); + templateRows = DEFAULT_TEMPLATE; + } + this.template = new GateTemplate(templateRows); + Set materials = new HashSet<>(); for (String s : cfg.getStringList("gate.frame-materials")) { Material m = Material.matchMaterial(s); @@ -35,11 +40,7 @@ public class GateConfig { this.frameMaterials = materials; this.chevronLit = matOr(cfg.getString("gate.chevron-lit-material"), Material.GILDED_BLACKSTONE, logger); - this.chevronCount = cfg.getInt("gate.chevron-count", 6); this.irisMaterial = matOr(cfg.getString("gate.iris-material"), Material.WATER, logger); - this.maxFrameBlocks = cfg.getInt("gate.max-frame-blocks", 300); - this.maxIrisBlocks = cfg.getInt("gate.max-iris-blocks", 200); - this.minFrameBlocks = cfg.getInt("gate.min-frame-blocks", 8); this.openSeconds = cfg.getInt("dialing.open-seconds", 10); this.chevronTickDelay = cfg.getInt("dialing.chevron-tick-delay", 4); this.playSounds = cfg.getBoolean("dialing.play-sounds", true); @@ -57,4 +58,18 @@ public class GateConfig { } return m; } + + private static final List DEFAULT_TEMPLATE = List.of( + "00000000000", + "0000*1*0000", + "00012221000", + "00*22222*00", + "0*2222222*0", + "01222222210", + "0*2222222*0", + "00*22222*00", + "00012221000", + "0000***0000", + "00000000000" + ); } 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 c4b198d..2387009 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 @@ -8,6 +8,7 @@ import org.bukkit.Location; import org.bukkit.Sound; import org.bukkit.World; import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; import org.bukkit.block.data.Levelled; import org.bukkit.entity.Player; @@ -25,7 +26,7 @@ public class GateManager { private final StargatePlugin plugin; private final GateStorage storage; private GateConfig config; - private GateStructureScanner scanner; + private GateTemplateScanner scanner; private final Map gatesById = new HashMap<>(); private final Map gatesBySignBlock = new HashMap<>(); // "world,x,y,z" -> gate @@ -38,8 +39,7 @@ public class GateManager { public void reloadConfig() { this.config = new GateConfig(plugin.getConfig(), plugin.getLogger()); - this.scanner = new GateStructureScanner(config.frameMaterials, config.chevronLit, config.chevronCount, - config.maxFrameBlocks, config.maxIrisBlocks, config.minFrameBlocks); + this.scanner = new GateTemplateScanner(config.template, config.frameMaterials); } public GateConfig getConfig() { return config; } @@ -53,9 +53,13 @@ public class GateManager { World world = Bukkit.getWorld(gate.getWorld()); if (world != null) { Block linked = world.getBlockAt(gate.getLinkX(), gate.getLinkY(), gate.getLinkZ()); - structure = scanner.scan(linked); - if (structure == null) { - plugin.getLogger().warning("[Stargate] Could not re-scan structure for gate '" + gate.getName() + "' - it may have been damaged."); + BlockFace facing = parseFacing(gate.getFacing()); + GateTemplateScanner.ScanResult result = scanner.scan(linked, facing); + if (result.isSuccess()) { + structure = result.structure; + } else { + plugin.getLogger().warning("[Stargate] Could not re-scan structure for gate '" + gate.getName() + + "' - it may have been damaged (" + result.failureReason + ")"); } } } @@ -159,17 +163,18 @@ public class GateManager { + config.maxLinkDistance + "."); } - GateStructureScanner.ScanResult result = scanner.scanWithDiagnostics(clickedBlock); + BlockFace facing = yawToCardinal(player.getLocation().getYaw()); + GateTemplateScanner.ScanResult result = scanner.scan(clickedBlock, facing); if (!result.isSuccess()) { return LinkResult.fail(result.failureReason); } GateStructure structure = result.structure; - Location exit = computeExitLocation(structure, clickedBlock); + Location exit = computeExitLocation(structure, clickedBlock.getWorld(), facing); Gate gate = new Gate(UUID.randomUUID(), pending.name, pending.network, plugin.getServerId(), exit.getWorld().getName(), exit.getBlockX(), exit.getBlockY(), exit.getBlockZ(), exit.getYaw(), signRef.getX(), signRef.getY(), signRef.getZ(), signRef.getWorld().getName(), - signBlockFacing(signRef), pending.owner, pending.flags, pending.fixedDestination, + facing.name(), pending.owner, pending.flags, pending.fixedDestination, clickedBlock.getX(), clickedBlock.getY(), clickedBlock.getZ()); RuntimeGate rg = new RuntimeGate(gate, structure); @@ -192,12 +197,10 @@ public class GateManager { storage.saveGate(rg.getGate()); } - /** frameBlock is whichever block the player punched to link the gate - used only to pick a facing. */ - private Location computeExitLocation(GateStructure structure, Block frameBlock) { + private Location computeExitLocation(GateStructure structure, World world, BlockFace facing) { List iris = structure.getIris(); - World world = frameBlock.getWorld(); if (iris.isEmpty()) { - return frameBlock.getLocation(); + return new Location(world, 0, 0, 0); } long sumX = 0, sumZ = 0; int minY = Integer.MAX_VALUE; @@ -208,23 +211,42 @@ public class GateManager { } double avgX = (double) sumX / iris.size() + 0.5; double avgZ = (double) sumZ / iris.size() + 0.5; - - // Face away from the punched frame block, out through the opposite side of the iris - - // a reasonable guess at "outward" without relying on a sign's wall orientation. - double dx = avgX - (frameBlock.getX() + 0.5); - double dz = avgZ - (frameBlock.getZ() + 0.5); - float yaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); - if (dx == 0 && dz == 0) yaw = 0f; - - return new Location(world, avgX, minY + 1, avgZ, yaw, 0f); + // A player arriving through this gate keeps walking the same direction the linker + // faced when they punched it - i.e. "into" the gate, out the far side. + return new Location(world, avgX, minY + 1, avgZ, faceToYaw(facing), 0f); } - private String signBlockFacing(Block signBlock) { - org.bukkit.block.BlockState state = signBlock.getState(); - if (state.getBlockData() instanceof org.bukkit.block.data.type.WallSign wallSign) { - return wallSign.getFacing().name(); + private float faceToYaw(BlockFace face) { + return switch (face) { + case NORTH -> 180f; + case SOUTH -> 0f; + case EAST -> -90f; + case WEST -> 90f; + default -> 0f; + }; + } + + /** Snaps a look yaw to the nearest of the four cardinal directions. */ + private BlockFace yawToCardinal(float yaw) { + float y = yaw < 0 ? yaw + 360f : yaw; + int i = Math.round(y / 90f) & 3; + return switch (i) { + case 0 -> BlockFace.SOUTH; + case 1 -> BlockFace.WEST; + case 2 -> BlockFace.NORTH; + default -> BlockFace.EAST; + }; + } + + private BlockFace parseFacing(String facing) { + try { + BlockFace face = BlockFace.valueOf(facing); + if (face == BlockFace.NORTH || face == BlockFace.SOUTH || face == BlockFace.EAST || face == BlockFace.WEST) { + return face; + } + } catch (Exception ignored) { } - return "SELF"; + return BlockFace.NORTH; } // ---- Dialing ---- 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 deleted file mode 100644 index 4d4314c..0000000 --- a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateStructureScanner.java +++ /dev/null @@ -1,204 +0,0 @@ -package dev.skywalker3200.stargate.paper.gate; - -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; - -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Deque; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Flood-fill scanner that discovers a gate's physical structure starting from the block - * a control sign is attached to: the frame ring, N evenly-spaced chevron slots picked out - * of that ring, and the enclosed interior ("iris") that gets filled with water while the - * gate is open. - * - * Chevrons are not identified by a special "unlit" material - they're ordinary frame blocks - * that the plugin temporarily swaps to {@code chevronLitMaterial} while dialing/open, then - * restores to whatever they originally looked like. That way a chevron can rest as plain - * obsidian, indistinguishable from the rest of the ring, exactly like the reference build. - */ -public class GateStructureScanner { - - private static final BlockFace[] NEIGHBORS = { - 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 chevronLitMaterial; - private final int chevronCount; - private final int maxFrameBlocks; - private final int maxIrisBlocks; - private final int minFrameBlocks; - - public GateStructureScanner(Set frameMaterials, Material chevronLitMaterial, int chevronCount, - int maxFrameBlocks, int maxIrisBlocks, int minFrameBlocks) { - this.frameMaterials = frameMaterials; - this.chevronLitMaterial = chevronLitMaterial; - this.chevronCount = chevronCount; - this.maxFrameBlocks = maxFrameBlocks; - this.maxIrisBlocks = maxIrisBlocks; - this.minFrameBlocks = minFrameBlocks; - } - - private boolean isFrameMaterial(Material m) { - // a gate re-scanned mid-dial (e.g. server restart) may have chevrons still lit - return frameMaterials.contains(m) || m == chevronLitMaterial; - } - - /** Convenience wrapper for callers that only care about success/failure, not why. */ - public GateStructure scan(Block seed) { - 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 = frameSeed.getRelative(face); - if (isFrameMaterial(b.getType())) { - frameSeed = b; - break; - } - } - } - if (!isFrameMaterial(frameSeed.getType())) { - return ScanResult.fail("The sign isn't touching a block listed in gate.frame-materials. " - + "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<>(); - Deque queue = new ArrayDeque<>(); - queue.add(frameSeed); - frameKeys.add(key(frameSeed)); - - while (!queue.isEmpty()) { - Block cur = queue.poll(); - frameBlocks.add(cur); - 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); - long k = key(next); - if (frameKeys.contains(k)) continue; - if (isFrameMaterial(next.getType())) { - frameKeys.add(k); - queue.add(next); - } - } - } - - 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); - long ck = key(candidate); - if (frameKeys.contains(ck) || triedSeeds.contains(ck)) continue; - triedSeeds.add(ck); - if (candidate.getType().isSolid()) continue; - - triedAnyInterior = true; - List iris = floodInterior(candidate, frameKeys); - if (iris != null && !iris.isEmpty()) { - return ScanResult.ok(new GateStructure(frameBlocks, pickChevrons(frameBlocks), iris)); - } - } - } - - 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)."); - } - - /** Picks N frame blocks spaced evenly across the flood-fill order as chevron slots, remembering their resting look. */ - private List pickChevrons(List frameBlocks) { - int count = Math.max(0, Math.min(chevronCount, frameBlocks.size())); - List slots = new ArrayList<>(count); - if (count == 0) return slots; - double step = frameBlocks.size() / (double) count; - Set chosen = new HashSet<>(); - for (int i = 0; i < count; i++) { - int idx = (int) Math.round(i * step) % frameBlocks.size(); - while (chosen.contains(idx)) idx = (idx + 1) % frameBlocks.size(); - chosen.add(idx); - Block block = frameBlocks.get(idx); - slots.add(new GateStructure.ChevronSlot(block, block.getType())); - } - return slots; - } - - /** Flood-fills non-frame blocks starting at seed; fails (returns null) if it escapes the frame boundary. */ - private List floodInterior(Block seed, Set frameKeys) { - Set visited = new HashSet<>(); - List interior = new ArrayList<>(); - Deque queue = new ArrayDeque<>(); - queue.add(seed); - visited.add(key(seed)); - - while (!queue.isEmpty()) { - Block cur = queue.poll(); - interior.add(cur); - if (interior.size() > maxIrisBlocks) return null; - - for (BlockFace face : NEIGHBORS) { - Block next = cur.getRelative(face); - long k = key(next); - if (visited.contains(k) || frameKeys.contains(k)) continue; - visited.add(k); - if (next.getType().isSolid()) continue; // ground/ceiling/wall acting as a boundary, not part of the interior - queue.add(next); - } - } - return interior; - } - - private long key(Block b) { - return (((long) b.getX() & 0x3FFFFFF) << 38) | (((long) (b.getY() + 512) & 0xFFF) << 26) | ((long) b.getZ() & 0x3FFFFFF); - } -} diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateTemplate.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateTemplate.java new file mode 100644 index 0000000..d41422d --- /dev/null +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateTemplate.java @@ -0,0 +1,64 @@ +package dev.skywalker3200.stargate.paper.gate; + +import java.util.ArrayList; +import java.util.List; + +/** + * A fixed 2D blueprint for a gate, read top-to-bottom / left-to-right, e.g.: + *
+ * 0000*1*0000
+ * 00012221000
+ * 00*22222*00
+ * ...
+ * 
+ * '*' = frame, '1' = chevron (a frame block the plugin lights up while dialing), '2' = interior + * ("iris") filled while open, '0' = ignored - not part of the structure at all. + */ +public class GateTemplate { + + public static final char FRAME = '*'; + public static final char CHEVRON = '1'; + public static final char IRIS = '2'; + public static final char IGNORE = '0'; + + private final char[][] grid; // [row][col] + private final int height; + private final int width; + + public GateTemplate(List rows) { + this.height = rows.size(); + int w = 0; + for (String row : rows) w = Math.max(w, row.length()); + this.width = w; + this.grid = new char[height][width]; + for (int r = 0; r < height; r++) { + String row = rows.get(r); + for (int c = 0; c < width; c++) { + grid[r][c] = c < row.length() ? row.charAt(c) : IGNORE; + } + } + } + + public int getHeight() { return height; } + public int getWidth() { return width; } + + public char symbolAt(int row, int col) { + if (row < 0 || row >= height || col < 0 || col >= width) return IGNORE; + return grid[row][col]; + } + + public boolean isFrameSymbol(char c) { + return c == FRAME || c == CHEVRON; + } + + /** All (row, col) cells matching the given symbol, in row-major order. */ + public List cellsOf(char symbol) { + List cells = new ArrayList<>(); + for (int r = 0; r < height; r++) { + for (int c = 0; c < width; c++) { + if (grid[r][c] == symbol) cells.add(new int[]{r, c}); + } + } + return cells; + } +} diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateTemplateScanner.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateTemplateScanner.java new file mode 100644 index 0000000..d7d657e --- /dev/null +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateTemplateScanner.java @@ -0,0 +1,112 @@ +package dev.skywalker3200.stargate.paper.gate; + +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Matches a {@link GateTemplate} against the world: given a block the player punched and the + * horizontal direction they were facing, tries every plausible way the punched block could be + * one of the template's frame/chevron cells, and checks the rest of the template lines up. + * + * The template is built in a vertical plane facing the player - columns run along the world's + * X axis if the player faced north/south, or along Z if they faced east/west. Row 0 is the top + * of the gate; increasing row means decreasing Y. + */ +public class GateTemplateScanner { + + private final GateTemplate template; + private final Set frameMaterials; + + public GateTemplateScanner(GateTemplate template, Set frameMaterials) { + this.template = template; + this.frameMaterials = frameMaterials; + } + + 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; } + } + + /** True for NORTH/SOUTH, false for EAST/WEST; anything else gets snapped to the nearer of the two axes. */ + private boolean columnsRunAlongX(BlockFace facing) { + return facing == BlockFace.NORTH || facing == BlockFace.SOUTH; + } + + public ScanResult scan(Block punchedBlock, BlockFace facing) { + if (!frameMaterials.contains(punchedBlock.getType())) { + return ScanResult.fail("That block isn't listed in gate.frame-materials."); + } + + boolean alongX = columnsRunAlongX(facing); + + List candidates = new ArrayList<>(); + candidates.addAll(template.cellsOf(GateTemplate.FRAME)); + candidates.addAll(template.cellsOf(GateTemplate.CHEVRON)); + + for (int[] anchor : candidates) { + int anchorRow = anchor[0]; + int anchorCol = anchor[1]; + + List frame = new ArrayList<>(); + List chevrons = new ArrayList<>(); + List iris = new ArrayList<>(); + boolean matched = true; + + outer: + for (int r = 0; r < template.getHeight() && matched; r++) { + for (int c = 0; c < template.getWidth(); c++) { + char symbol = template.symbolAt(r, c); + if (symbol == GateTemplate.IGNORE) continue; + + Block world = worldBlockFor(punchedBlock, alongX, anchorRow, anchorCol, r, c); + + if (template.isFrameSymbol(symbol)) { + if (!frameMaterials.contains(world.getType())) { + matched = false; + break outer; + } + frame.add(world); + if (symbol == GateTemplate.CHEVRON) { + chevrons.add(new GateStructure.ChevronSlot(world, world.getType())); + } + } else if (symbol == GateTemplate.IRIS) { + iris.add(world); + } + } + } + + if (matched) { + return ScanResult.ok(new GateStructure(frame, chevrons, iris)); + } + } + + return ScanResult.fail("Found a frame block, but no orientation of the configured template lines up " + + "starting there. Make sure you're facing the gate square-on (not at an angle) when you punch it, " + + "and that the build matches gate.template exactly."); + } + + private Block worldBlockFor(Block punchedBlock, boolean alongX, int anchorRow, int anchorCol, int r, int c) { + int dy = anchorRow - r; // row increases downward, Y decreases as row increases + int dCol = c - anchorCol; + if (alongX) { + return punchedBlock.getRelative(dCol, dy, 0); + } else { + return punchedBlock.getRelative(0, dy, dCol); + } + } +} diff --git a/stargate-paper/src/main/resources/config.yml b/stargate-paper/src/main/resources/config.yml index 5efbc02..e644e32 100644 --- a/stargate-paper/src/main/resources/config.yml +++ b/stargate-paper/src/main/resources/config.yml @@ -26,26 +26,38 @@ cross-server: enabled: false gate: - # Any of these materials count as the decorative frame ring. Mix and match to match - # your build (the default matches an obsidian/gold-block/birch-planks arch). + # The exact shape of a gate, read top row to bottom row. This is matched precisely + # against the world - no flood-fill guessing. Symbols: + # * = frame block (never touched by the plugin) + # 1 = chevron - a frame block that lights up to chevron-lit-material while + # dialing/open, then reverts to whatever it looked like at rest when closed + # 2 = interior ("iris") - filled with iris-material while open, air while closed + # 0 = ignored - not part of the gate at all + # Must be built in a single vertical plane, one block thick, aligned to the world's + # X or Z axis (i.e. facing north/south/east/west, not built at an angle). + template: + - "00000000000" + - "0000*1*0000" + - "00012221000" + - "00*22222*00" + - "0*2222222*0" + - "01222222210" + - "0*2222222*0" + - "00*22222*00" + - "00012221000" + - "0000***0000" + - "00000000000" + # Any of these materials satisfy a '*' or '1' cell above. Mix and match to match your + # build (the default matches an obsidian/gilded-blackstone ring). frame-materials: - OBSIDIAN - GOLD_BLOCK - GILDED_BLACKSTONE - BIRCH_PLANKS - OAK_PLANKS - # Chevrons are NOT a separate material you have to place - the plugin automatically - # picks this many frame blocks, evenly spaced around the ring, and swaps them to - # chevron-lit-material while dialing/open, then restores whatever they looked like - # at rest when the gate closes. So a chevron can rest as plain obsidian and only - # stand out once it lights up. chevron-lit-material: GILDED_BLACKSTONE - chevron-count: 6 # Material poured into the interior (the "event horizon") while the gate is open. iris-material: WATER - max-frame-blocks: 300 - max-iris-blocks: 200 - min-frame-blocks: 8 # After placing a "[Stargate]" sign, the owner has this long to left-click (punch) a # block of the actual gate frame to link it - the sign itself can sit anywhere, like a # DHD console away from the gate, instead of being mounted directly on the frame.