Replace flood-fill scanner with an exact gate template

Instead of guessing a gate's shape by flood-filling connected frame
material, the plugin now matches a fixed ASCII template against the
world (gate.template in config.yml), the same idea as the classic
Stargate plugin's .gate files. Default is an 11x11 ring with 7
chevrons at their real positions rather than auto-spaced.

- New GateTemplate parses the '*'/'1'/'2'/'0' grid.
- New GateTemplateScanner tries every plausible alignment of a
  punched block against a frame/chevron cell, then verifies the rest
  of the template matches; no more flood-fill leak/enclosure
  diagnostics needed since the shape is exact.
- The player's look direction at punch time (snapped to N/S/E/W)
  decides whether template columns run along world X or Z; this
  facing is now stored in Gate.facing (repurposing the old
  sign-attachment field) so restarts can re-scan deterministically.
- Exit-teleport yaw now comes from that stored facing instead of a
  geometric guess.
- Removed gate.chevron-count/max-frame-blocks/max-iris-blocks/
  min-frame-blocks - the template itself defines size and chevrons.
This commit is contained in:
Michael Burgess
2026-08-09 10:48:47 -04:00
parent 317602c00e
commit ed8415a497
7 changed files with 310 additions and 266 deletions
+39 -16
View File
@@ -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
@@ -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<Material> 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<String> 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<Material> 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<String> 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"
);
}
@@ -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<UUID, RuntimeGate> gatesById = new HashMap<>();
private final Map<String, RuntimeGate> 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<Block> 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;
};
}
return "SELF";
/** 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 BlockFace.NORTH;
}
// ---- Dialing ----
@@ -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<Material> frameMaterials;
private final Material chevronLitMaterial;
private final int chevronCount;
private final int maxFrameBlocks;
private final int maxIrisBlocks;
private final int minFrameBlocks;
public GateStructureScanner(Set<Material> 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<Long> frameKeys = new HashSet<>();
List<Block> frameBlocks = new ArrayList<>();
Deque<Block> 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<Long> 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<Block> 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<GateStructure.ChevronSlot> pickChevrons(List<Block> frameBlocks) {
int count = Math.max(0, Math.min(chevronCount, frameBlocks.size()));
List<GateStructure.ChevronSlot> slots = new ArrayList<>(count);
if (count == 0) return slots;
double step = frameBlocks.size() / (double) count;
Set<Integer> 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<Block> floodInterior(Block seed, Set<Long> frameKeys) {
Set<Long> visited = new HashSet<>();
List<Block> interior = new ArrayList<>();
Deque<Block> 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);
}
}
@@ -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.:
* <pre>
* 0000*1*0000
* 00012221000
* 00*22222*00
* ...
* </pre>
* '*' = 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<String> 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<int[]> cellsOf(char symbol) {
List<int[]> 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;
}
}
@@ -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<Material> frameMaterials;
public GateTemplateScanner(GateTemplate template, Set<Material> 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<int[]> 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<Block> frame = new ArrayList<>();
List<GateStructure.ChevronSlot> chevrons = new ArrayList<>();
List<Block> 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);
}
}
}
+23 -11
View File
@@ -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.