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:
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
+49
-27
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
/** Snaps a look yaw to the nearest of the four cardinal directions. */
|
||||
private BlockFace yawToCardinal(float yaw) {
|
||||
float y = yaw < 0 ? yaw + 360f : yaw;
|
||||
int i = Math.round(y / 90f) & 3;
|
||||
return switch (i) {
|
||||
case 0 -> BlockFace.SOUTH;
|
||||
case 1 -> BlockFace.WEST;
|
||||
case 2 -> BlockFace.NORTH;
|
||||
default -> BlockFace.EAST;
|
||||
};
|
||||
}
|
||||
|
||||
private BlockFace parseFacing(String facing) {
|
||||
try {
|
||||
BlockFace face = BlockFace.valueOf(facing);
|
||||
if (face == BlockFace.NORTH || face == BlockFace.SOUTH || face == BlockFace.EAST || face == BlockFace.WEST) {
|
||||
return face;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return "SELF";
|
||||
return BlockFace.NORTH;
|
||||
}
|
||||
|
||||
// ---- Dialing ----
|
||||
|
||||
-204
@@ -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;
|
||||
}
|
||||
}
|
||||
+112
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user