Revert to flexible flood-fill scanning with geometric chevron placement

The exact-template approach from the last commit over-fit to the
sample ASCII diagram, which was meant to teach the target shape, not
be matched literally cell-for-cell. Back to flood-filling any
connected, fully-enclosed ring the builder makes (any size/shape), but
chevrons are no longer evenly spaced by scan order - the scanner walks
the ring in geometric order and marks local maxima of distance from
the interior's centroid, i.e. the blocks that stick out further than
their ring-neighbours. That's where a real Stargate's chevrons sit
(the 11-wide reference ring naturally produces 7 this way: the top
apex, four shoulder elbows, two side bumps), without hardcoding a
specific size.

Falls back to the old evenly-spaced picker only if the ring isn't a
simple single-thickness loop (each frame block should have exactly
two in-plane neighbours) so odd/thick builds still work.

gate.max-frame-blocks/max-iris-blocks/min-frame-blocks are back;
gate.template is gone.
This commit is contained in:
Michael Burgess
2026-08-09 10:57:00 -04:00
parent ed8415a497
commit fe3b2b0f14
7 changed files with 316 additions and 273 deletions
+17 -35
View File
@@ -16,38 +16,21 @@ Build everything with `./gradlew build`. Jars land in each module's `build/libs/
## Building a gate ## Building a gate
A gate's shape is a fixed template in `config.yml` (`gate.template`), matched exactly There's no fixed size or shape - build any single connected ring out of
against the world - not flood-filled or guessed. The default is an 11x11 ring with 7 `gate.frame-materials` (default: obsidian, gold block, gilded blackstone, birch/oak
chevrons, laid out top row to bottom row: planks) with a fully sealed hollow interior. No gaps; the interior must be enclosed.
``` Chevrons aren't a separate material you place. When the gate is linked, the plugin
00000000000 flood-fills the ring, walks it in geometric order, and picks out the blocks that stick
0000*1*0000 further outward than their immediate ring-neighbours - the "elbow" points of the
00012221000 shape, which is where a real Stargate's chevrons sit (an 11-wide octagonal ring like
00*22222*00 the reference design naturally produces 7 of them: the top apex, the four shoulder
0*2222222*0 elbows, and the two side bumps). Those blocks swap to `gate.chevron-lit-material`
01222222210 (default gilded blackstone) while dialing/open, and revert to whatever they looked
0*2222222*0 like at rest when the gate closes - so a chevron can rest as plain obsidian and blend
00*22222*00 invisibly into the frame until it lights up.
00012221000
0000***0000
00000000000
```
- `*` — frame block, never touched by the plugin. Must be one of `gate.frame-materials` 1. Build the ring.
(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 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: console standing apart from the gate - with:
- Line 1: `[Stargate]` - Line 1: `[Stargate]`
@@ -55,13 +38,12 @@ axis (facing north/south/east/west - not built at an angle). Edit `gate.template
- Line 3: gate name (blank = auto-generated) - Line 3: gate name (blank = auto-generated)
- Line 4: `hidden` to keep it out of the cycle list, `fixed:GateName` to lock this - 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 gate to always dial `GateName` (no right-click cycling), or blank
3. The sign will show "Punch the gate (30s)". Within `gate.link-timeout-seconds`, stand 3. The sign will show "Punch the gate (30s)". Within `gate.link-timeout-seconds`,
facing the gate square-on and left-click (punch) any `*` or `1` block, within left-click (punch) any frame block, within `gate.max-link-distance` of the sign.
`gate.max-link-distance` of the sign. The plugin matches the template against the The plugin scans out from that block and links the sign to it.
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 - wrong 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. material, unsealed interior, too far from the sign, etc.
## Using a gate ## Using a gate
@@ -4,17 +4,18 @@ import org.bukkit.Material;
import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.FileConfiguration;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.logging.Logger; import java.util.logging.Logger;
/** Typed view over the `gate:` / `dialing:` / `network:` sections of config.yml. */ /** Typed view over the `gate:` / `dialing:` / `network:` sections of config.yml. */
public class GateConfig { public class GateConfig {
public final GateTemplate template;
public final Set<Material> frameMaterials; public final Set<Material> frameMaterials;
public final Material chevronLit; public final Material chevronLit;
public final Material irisMaterial; public final Material irisMaterial;
public final int maxFrameBlocks;
public final int maxIrisBlocks;
public final int minFrameBlocks;
public final int openSeconds; public final int openSeconds;
public final int chevronTickDelay; public final int chevronTickDelay;
public final boolean playSounds; public final boolean playSounds;
@@ -23,13 +24,6 @@ public class GateConfig {
public final int maxLinkDistance; public final int maxLinkDistance;
public GateConfig(FileConfiguration cfg, Logger logger) { 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<>(); Set<Material> materials = new HashSet<>();
for (String s : cfg.getStringList("gate.frame-materials")) { for (String s : cfg.getStringList("gate.frame-materials")) {
Material m = Material.matchMaterial(s); Material m = Material.matchMaterial(s);
@@ -41,6 +35,9 @@ public class GateConfig {
this.chevronLit = matOr(cfg.getString("gate.chevron-lit-material"), Material.GILDED_BLACKSTONE, logger); this.chevronLit = matOr(cfg.getString("gate.chevron-lit-material"), Material.GILDED_BLACKSTONE, logger);
this.irisMaterial = matOr(cfg.getString("gate.iris-material"), Material.WATER, logger); 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.openSeconds = cfg.getInt("dialing.open-seconds", 10);
this.chevronTickDelay = cfg.getInt("dialing.chevron-tick-delay", 4); this.chevronTickDelay = cfg.getInt("dialing.chevron-tick-delay", 4);
this.playSounds = cfg.getBoolean("dialing.play-sounds", true); this.playSounds = cfg.getBoolean("dialing.play-sounds", true);
@@ -58,18 +55,4 @@ public class GateConfig {
} }
return m; 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"
);
} }
@@ -26,7 +26,7 @@ public class GateManager {
private final StargatePlugin plugin; private final StargatePlugin plugin;
private final GateStorage storage; private final GateStorage storage;
private GateConfig config; private GateConfig config;
private GateTemplateScanner scanner; private GateStructureScanner scanner;
private final Map<UUID, RuntimeGate> gatesById = new HashMap<>(); private final Map<UUID, RuntimeGate> gatesById = new HashMap<>();
private final Map<String, RuntimeGate> gatesBySignBlock = new HashMap<>(); // "world,x,y,z" -> gate private final Map<String, RuntimeGate> gatesBySignBlock = new HashMap<>(); // "world,x,y,z" -> gate
@@ -39,7 +39,8 @@ public class GateManager {
public void reloadConfig() { public void reloadConfig() {
this.config = new GateConfig(plugin.getConfig(), plugin.getLogger()); this.config = new GateConfig(plugin.getConfig(), plugin.getLogger());
this.scanner = new GateTemplateScanner(config.template, config.frameMaterials); this.scanner = new GateStructureScanner(config.frameMaterials, config.chevronLit,
config.maxFrameBlocks, config.maxIrisBlocks, config.minFrameBlocks);
} }
public GateConfig getConfig() { return config; } public GateConfig getConfig() { return config; }
@@ -53,8 +54,7 @@ public class GateManager {
World world = Bukkit.getWorld(gate.getWorld()); World world = Bukkit.getWorld(gate.getWorld());
if (world != null) { if (world != null) {
Block linked = world.getBlockAt(gate.getLinkX(), gate.getLinkY(), gate.getLinkZ()); Block linked = world.getBlockAt(gate.getLinkX(), gate.getLinkY(), gate.getLinkZ());
BlockFace facing = parseFacing(gate.getFacing()); GateStructureScanner.ScanResult result = scanner.scanWithDiagnostics(linked);
GateTemplateScanner.ScanResult result = scanner.scan(linked, facing);
if (result.isSuccess()) { if (result.isSuccess()) {
structure = result.structure; structure = result.structure;
} else { } else {
@@ -164,7 +164,7 @@ public class GateManager {
} }
BlockFace facing = yawToCardinal(player.getLocation().getYaw()); BlockFace facing = yawToCardinal(player.getLocation().getYaw());
GateTemplateScanner.ScanResult result = scanner.scan(clickedBlock, facing); GateStructureScanner.ScanResult result = scanner.scanWithDiagnostics(clickedBlock);
if (!result.isSuccess()) { if (!result.isSuccess()) {
return LinkResult.fail(result.failureReason); return LinkResult.fail(result.failureReason);
} }
@@ -238,17 +238,6 @@ public class GateManager {
}; };
} }
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 ---- // ---- Dialing ----
/** Starts the chevron-lighting animation, then opens the gate and connects it to the destination. */ /** Starts the chevron-lighting animation, then opens the gate and connects it to the destination. */
@@ -0,0 +1,274 @@
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.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
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 - unlike naive even-spacing - the ring's actual protruding "elbow"
* points, picked out geometrically, which is where a real Stargate's chevrons sit.
*/
public class GateStructureScanner {
private static final BlockFace[] NEIGHBORS = {
BlockFace.UP, BlockFace.DOWN, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST
};
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 maxFrameBlocks;
private final int maxIrisBlocks;
private final int minFrameBlocks;
public GateStructureScanner(Set<Material> frameMaterials, Material chevronLitMaterial,
int maxFrameBlocks, int maxIrisBlocks, int minFrameBlocks) {
this.frameMaterials = frameMaterials;
this.chevronLitMaterial = chevronLitMaterial;
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;
}
public GateStructure scan(Block seed) {
return scanWithDiagnostics(seed).structure;
}
public ScanResult scanWithDiagnostics(Block seed) {
if (!isFrameMaterial(seed.getType())) {
return ScanResult.fail("That block isn't listed in gate.frame-materials.");
}
Set<Long> frameKeys = new HashSet<>();
List<Block> frameBlocks = new ArrayList<>();
Deque<Block> queue = new ArrayDeque<>();
queue.add(seed);
frameKeys.add(key(seed));
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. "
+ "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).");
}
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), 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 + ").");
}
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 boundary, not part of the interior
queue.add(next);
}
}
return interior;
}
/**
* Picks chevrons at the ring's actual protruding "elbow" points, the way a real Stargate's
* chevrons sit - not evenly spaced by scan order. Works by walking the ring in geometric
* order (each frame block should have exactly two ring neighbours) and marking local maxima
* of distance from the interior's centroid, i.e. points that stick out further than their
* immediate neighbours on the ring.
*/
private List<GateStructure.ChevronSlot> pickChevrons(List<Block> frameBlocks, List<Block> iris) {
// The ring is essentially 2D (one block thick); find which world axis is the thin one.
int minX = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE;
int minZ = Integer.MAX_VALUE, maxZ = Integer.MIN_VALUE;
for (Block b : frameBlocks) {
minX = Math.min(minX, b.getX()); maxX = Math.max(maxX, b.getX());
minZ = Math.min(minZ, b.getZ()); maxZ = Math.max(maxZ, b.getZ());
}
boolean horizontalIsX = (maxX - minX) >= (maxZ - minZ);
Map<Long, Block> plane = new HashMap<>();
Map<Block, int[]> coords = new HashMap<>();
for (Block b : frameBlocks) {
int col = horizontalIsX ? b.getX() : b.getZ();
int row = b.getY();
long k = (((long) row) << 32) ^ (col & 0xFFFFFFFFL);
plane.putIfAbsent(k, b);
coords.put(b, new int[]{row, col});
}
List<Block> ordered = walkRing(frameBlocks, plane, horizontalIsX);
if (ordered == null || ordered.size() < 4) {
return fallbackEvenlySpaced(frameBlocks);
}
double centroidRow, centroidCol;
if (!iris.isEmpty()) {
long sumRow = 0, sumCol = 0;
for (Block b : iris) {
sumRow += b.getY();
sumCol += horizontalIsX ? b.getX() : b.getZ();
}
centroidRow = (double) sumRow / iris.size();
centroidCol = (double) sumCol / iris.size();
} else {
long sumRow = 0, sumCol = 0;
for (Block b : frameBlocks) {
sumRow += b.getY();
sumCol += horizontalIsX ? b.getX() : b.getZ();
}
centroidRow = (double) sumRow / frameBlocks.size();
centroidCol = (double) sumCol / frameBlocks.size();
}
int n = ordered.size();
double[] dist = new double[n];
for (int i = 0; i < n; i++) {
int[] rc = coords.get(ordered.get(i));
double dr = rc[0] - centroidRow;
double dc = rc[1] - centroidCol;
dist[i] = dr * dr + dc * dc;
}
List<GateStructure.ChevronSlot> chevrons = new ArrayList<>();
for (int i = 0; i < n; i++) {
double prev = dist[(i - 1 + n) % n];
double next = dist[(i + 1) % n];
if (dist[i] > prev && dist[i] >= next) {
Block b = ordered.get(i);
chevrons.add(new GateStructure.ChevronSlot(b, b.getType()));
}
}
return chevrons.isEmpty() ? fallbackEvenlySpaced(frameBlocks) : chevrons;
}
/** Walks the ring in geometric order assuming each frame block has exactly two in-plane neighbours; null if that assumption fails. */
private List<Block> walkRing(List<Block> frameBlocks, Map<Long, Block> plane, boolean horizontalIsX) {
Map<Block, List<Block>> adjacency = new HashMap<>();
for (Block b : frameBlocks) {
int col = horizontalIsX ? b.getX() : b.getZ();
int row = b.getY();
List<Block> neighbors = new ArrayList<>();
for (int[] d : new int[][]{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}) {
long k = (((long) (row + d[0])) << 32) ^ ((col + d[1]) & 0xFFFFFFFFL);
Block n = plane.get(k);
if (n != null && n != b) neighbors.add(n);
}
if (neighbors.size() != 2) return null; // not a simple single-thickness loop; bail out to fallback
adjacency.put(b, neighbors);
}
Block start = frameBlocks.get(0);
List<Block> ordered = new ArrayList<>();
Block prev = null;
Block current = start;
int guard = frameBlocks.size() + 2;
while (guard-- > 0) {
ordered.add(current);
List<Block> neighbors = adjacency.get(current);
Block next = neighbors.get(0).equals(prev) ? neighbors.get(1) : neighbors.get(0);
prev = current;
current = next;
if (current.equals(start)) break;
}
return ordered.size() == frameBlocks.size() ? ordered : null;
}
private List<GateStructure.ChevronSlot> fallbackEvenlySpaced(List<Block> frameBlocks) {
int count = Math.min(6, frameBlocks.size());
List<GateStructure.ChevronSlot> slots = new ArrayList<>(count);
if (count == 0) return slots;
double step = frameBlocks.size() / (double) count;
for (int i = 0; i < count; i++) {
Block b = frameBlocks.get((int) Math.round(i * step) % frameBlocks.size());
slots.add(new GateStructure.ChevronSlot(b, b.getType()));
}
return slots;
}
private long key(Block b) {
return (((long) b.getX() & 0x3FFFFFF) << 38) | (((long) (b.getY() + 512) & 0xFFF) << 26) | ((long) b.getZ() & 0x3FFFFFF);
}
}
@@ -1,64 +0,0 @@
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;
}
}
@@ -1,112 +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.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);
}
}
}
+14 -23
View File
@@ -26,38 +26,29 @@ cross-server:
enabled: false enabled: false
gate: gate:
# The exact shape of a gate, read top row to bottom row. This is matched precisely # Any of these materials count as the ring's frame (and count as a valid chevron
# against the world - no flood-fill guessing. Symbols: # material too, since chevrons are just frame blocks the plugin temporarily swaps).
# * = frame block (never touched by the plugin) # Mix and match to match your build (the default matches an obsidian/gilded-blackstone
# 1 = chevron - a frame block that lights up to chevron-lit-material while # ring). The ring can be any size or shape, as long as it's a single connected loop
# dialing/open, then reverts to whatever it looked like at rest when closed # made only of these materials with a fully sealed hollow interior - no fixed template
# 2 = interior ("iris") - filled with iris-material while open, air while closed # or size is required.
# 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: frame-materials:
- OBSIDIAN - OBSIDIAN
- GOLD_BLOCK - GOLD_BLOCK
- GILDED_BLACKSTONE - GILDED_BLACKSTONE
- BIRCH_PLANKS - BIRCH_PLANKS
- OAK_PLANKS - OAK_PLANKS
# Chevrons aren't a separate material you place. The plugin flood-fills the ring,
# walks it in geometric order, and picks out the blocks that stick further outward
# than their ring-neighbours - the "elbow" points of the shape, same as a real
# Stargate's chevron placement - then swaps just those to chevron-lit-material while
# dialing/open, restoring whatever they looked like at rest when the gate closes.
chevron-lit-material: GILDED_BLACKSTONE chevron-lit-material: GILDED_BLACKSTONE
# Material poured into the interior (the "event horizon") while the gate is open. # Material poured into the interior (the "event horizon") while the gate is open.
iris-material: WATER 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 # 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 # 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. # DHD console away from the gate, instead of being mounted directly on the frame.