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:
@@ -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"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-16
@@ -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. */
|
||||||
|
|||||||
+274
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-112
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user