Auto-select chevron slots instead of requiring a distinct unlit material

Chevrons no longer need to be built out of a special block - the plugin
picks gate.chevron-count frame blocks evenly spaced around the scanned
ring and remembers their resting material, so a chevron can rest as
plain obsidian (matching the rest of the frame) and only reveal itself
by swapping to gate.chevron-lit-material while dialing/open. Restoring
on close now puts back the original block instead of a fixed material,
so alternating decorative frames (e.g. obsidian/gilded blackstone) are
preserved exactly.
This commit is contained in:
Michael Burgess
2026-08-09 10:20:14 -04:00
parent 62277a8d7b
commit c99f4c9f85
6 changed files with 83 additions and 42 deletions
+9 -6
View File
@@ -16,12 +16,15 @@ Build everything with `./gradlew build`. Jars land in each module's `build/libs/
## Building a gate ## Building a gate
1. Build a ring out of any block in `gate.frame-materials` (default: obsidian, gold 1. Build a fully-enclosed ring out of any block(s) in `gate.frame-materials` (default:
block, birch/oak planks — matches an obsidian+gold arch design). Leave the middle obsidian, gold block, gilded blackstone, birch/oak planks). Leave the middle hollow.
hollow. No gaps - the ring must be a sealed loop.
2. Embed a few blocks of `gate.chevron-unlit-material` (default black stained glass) 2. Nothing else to build for chevrons. On creation, the plugin automatically picks
into the ring — these light up to `gate.chevron-lit-material` (default glowstone) `gate.chevron-count` (default 6) frame blocks, evenly spaced around the ring, and
as the gate dials. 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 on the outside of the frame with: 3. Place a sign on the outside of the frame with:
- Line 1: `[Stargate]` - Line 1: `[Stargate]`
- Line 2: network name (blank = default network) - Line 2: network name (blank = default network)
@@ -11,8 +11,8 @@ import java.util.logging.Logger;
public class GateConfig { public class GateConfig {
public final Set<Material> frameMaterials; public final Set<Material> frameMaterials;
public final Material chevronUnlit;
public final Material chevronLit; public final Material chevronLit;
public final int chevronCount;
public final Material irisMaterial; public final Material irisMaterial;
public final int maxFrameBlocks; public final int maxFrameBlocks;
public final int maxIrisBlocks; public final int maxIrisBlocks;
@@ -32,8 +32,8 @@ public class GateConfig {
if (materials.isEmpty()) materials.add(Material.OBSIDIAN); if (materials.isEmpty()) materials.add(Material.OBSIDIAN);
this.frameMaterials = materials; this.frameMaterials = materials;
this.chevronUnlit = matOr(cfg.getString("gate.chevron-unlit-material"), Material.BLACK_STAINED_GLASS, logger); this.chevronLit = matOr(cfg.getString("gate.chevron-lit-material"), Material.GILDED_BLACKSTONE, logger);
this.chevronLit = matOr(cfg.getString("gate.chevron-lit-material"), Material.GLOWSTONE, logger); this.chevronCount = cfg.getInt("gate.chevron-count", 6);
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.maxFrameBlocks = cfg.getInt("gate.max-frame-blocks", 300);
this.maxIrisBlocks = cfg.getInt("gate.max-iris-blocks", 200); this.maxIrisBlocks = cfg.getInt("gate.max-iris-blocks", 200);
@@ -38,7 +38,7 @@ 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 GateStructureScanner(config.frameMaterials, config.chevronUnlit, config.chevronLit, this.scanner = new GateStructureScanner(config.frameMaterials, config.chevronLit, config.chevronCount,
config.maxFrameBlocks, config.maxIrisBlocks, config.minFrameBlocks); config.maxFrameBlocks, config.maxIrisBlocks, config.minFrameBlocks);
} }
@@ -206,7 +206,7 @@ public class GateManager {
if (from.isOpen()) closeGate(from); if (from.isOpen()) closeGate(from);
if (to.isOpen()) closeGate(to); if (to.isOpen()) closeGate(to);
List<Block> chevrons = from.getStructure() != null ? from.getStructure().getChevrons() : List.of(); List<GateStructure.ChevronSlot> chevrons = from.getStructure() != null ? from.getStructure().getChevrons() : List.of();
int delay = Math.max(1, config.chevronTickDelay); int delay = Math.max(1, config.chevronTickDelay);
Runnable finish = () -> { Runnable finish = () -> {
@@ -227,7 +227,7 @@ public class GateManager {
finish.run(); finish.run();
return; return;
} }
Block chevron = chevrons.get(i[0]); Block chevron = chevrons.get(i[0]).getBlock();
chevron.setType(config.chevronLit); chevron.setType(config.chevronLit);
if (config.playSounds) { if (config.playSounds) {
chevron.getWorld().playSound(chevron.getLocation(), Sound.BLOCK_STONE_STEP, 1f, 1.4f); chevron.getWorld().playSound(chevron.getLocation(), Sound.BLOCK_STONE_STEP, 1f, 1.4f);
@@ -248,8 +248,8 @@ public class GateManager {
b.setBlockData(lvl); b.setBlockData(lvl);
} }
} }
for (Block c : gate.getStructure().getChevrons()) { for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) {
c.setType(config.chevronLit); c.getBlock().setType(config.chevronLit);
} }
} }
if (config.playSounds) { if (config.playSounds) {
@@ -274,8 +274,8 @@ public class GateManager {
for (Block b : gate.getStructure().getIris()) { for (Block b : gate.getStructure().getIris()) {
b.setType(org.bukkit.Material.AIR); b.setType(org.bukkit.Material.AIR);
} }
for (Block c : gate.getStructure().getChevrons()) { for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) {
c.setType(config.chevronUnlit); c.getBlock().setType(c.getRestingMaterial());
} }
} }
if (other != null && other.isOpen()) { if (other != null && other.isOpen()) {
@@ -1,22 +1,40 @@
package dev.skywalker3200.stargate.paper.gate; package dev.skywalker3200.stargate.paper.gate;
import org.bukkit.Material;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import java.util.List; import java.util.List;
/** The physical blocks that make up a scanned gate: frame ring, chevrons, and interior iris. */ /** The physical blocks that make up a scanned gate: frame ring, chevrons, and interior iris. */
public class GateStructure { public class GateStructure {
/** A chevron is just a frame block the plugin swaps materials on; we remember what it looked
* like before dialing so closing the gate restores it exactly, even if the frame is decorative
* and mixes several materials (e.g. alternating obsidian / gilded blackstone). */
public static final class ChevronSlot {
private final Block block;
private final Material restingMaterial;
public ChevronSlot(Block block, Material restingMaterial) {
this.block = block;
this.restingMaterial = restingMaterial;
}
public Block getBlock() { return block; }
public Material getRestingMaterial() { return restingMaterial; }
}
private final List<Block> frame; private final List<Block> frame;
private final List<Block> chevrons; private final List<ChevronSlot> chevrons;
private final List<Block> iris; private final List<Block> iris;
public GateStructure(List<Block> frame, List<Block> chevrons, List<Block> iris) { public GateStructure(List<Block> frame, List<ChevronSlot> chevrons, List<Block> iris) {
this.frame = frame; this.frame = frame;
this.chevrons = chevrons; this.chevrons = chevrons;
this.iris = iris; this.iris = iris;
} }
public List<Block> getFrame() { return frame; } public List<Block> getFrame() { return frame; }
public List<Block> getChevrons() { return chevrons; } public List<ChevronSlot> getChevrons() { return chevrons; }
public List<Block> getIris() { return iris; } public List<Block> getIris() { return iris; }
} }
@@ -13,8 +13,14 @@ import java.util.Set;
/** /**
* Flood-fill scanner that discovers a gate's physical structure starting from the block * Flood-fill scanner that discovers a gate's physical structure starting from the block
* a control sign is attached to: the frame ring, the chevron blocks embedded in it, and * a control sign is attached to: the frame ring, N evenly-spaced chevron slots picked out
* the enclosed interior ("iris") that gets filled with water while the gate is open. * 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 { public class GateStructureScanner {
@@ -39,28 +45,25 @@ public class GateStructureScanner {
} }
private final Set<Material> frameMaterials; private final Set<Material> frameMaterials;
private final Material chevronUnlit; private final Material chevronLitMaterial;
private final Material chevronLit; private final int chevronCount;
private final int maxFrameBlocks; private final int maxFrameBlocks;
private final int maxIrisBlocks; private final int maxIrisBlocks;
private final int minFrameBlocks; private final int minFrameBlocks;
public GateStructureScanner(Set<Material> frameMaterials, Material chevronUnlit, Material chevronLit, public GateStructureScanner(Set<Material> frameMaterials, Material chevronLitMaterial, int chevronCount,
int maxFrameBlocks, int maxIrisBlocks, int minFrameBlocks) { int maxFrameBlocks, int maxIrisBlocks, int minFrameBlocks) {
this.frameMaterials = frameMaterials; this.frameMaterials = frameMaterials;
this.chevronUnlit = chevronUnlit; this.chevronLitMaterial = chevronLitMaterial;
this.chevronLit = chevronLit; this.chevronCount = chevronCount;
this.maxFrameBlocks = maxFrameBlocks; this.maxFrameBlocks = maxFrameBlocks;
this.maxIrisBlocks = maxIrisBlocks; this.maxIrisBlocks = maxIrisBlocks;
this.minFrameBlocks = minFrameBlocks; this.minFrameBlocks = minFrameBlocks;
} }
private boolean isFrameMaterial(Material m) { private boolean isFrameMaterial(Material m) {
return frameMaterials.contains(m) || m == chevronUnlit || m == chevronLit; // a gate re-scanned mid-dial (e.g. server restart) may have chevrons still lit
} return frameMaterials.contains(m) || m == chevronLitMaterial;
private boolean isChevronMaterial(Material m) {
return m == chevronUnlit || m == chevronLit;
} }
/** Convenience wrapper for callers that only care about success/failure, not why. */ /** Convenience wrapper for callers that only care about success/failure, not why. */
@@ -84,14 +87,13 @@ public class GateStructureScanner {
} }
} }
if (!isFrameMaterial(frameSeed.getType())) { if (!isFrameMaterial(frameSeed.getType())) {
return ScanResult.fail("The sign isn't touching a block listed in gate.frame-materials " return ScanResult.fail("The sign isn't touching a block listed in gate.frame-materials. "
+ "(nor is it touching the chevron material). Check the sign is mounted directly on the frame, " + "Check the sign is mounted directly on the frame, and that every block type in your ring "
+ "and that every block type in your ring is listed in config.yml."); + "is listed in config.yml.");
} }
Set<Long> frameKeys = new HashSet<>(); Set<Long> frameKeys = new HashSet<>();
List<Block> frameBlocks = new ArrayList<>(); List<Block> frameBlocks = new ArrayList<>();
List<Block> chevronBlocks = new ArrayList<>();
Deque<Block> queue = new ArrayDeque<>(); Deque<Block> queue = new ArrayDeque<>();
queue.add(frameSeed); queue.add(frameSeed);
frameKeys.add(key(frameSeed)); frameKeys.add(key(frameSeed));
@@ -99,7 +101,6 @@ public class GateStructureScanner {
while (!queue.isEmpty()) { while (!queue.isEmpty()) {
Block cur = queue.poll(); Block cur = queue.poll();
frameBlocks.add(cur); frameBlocks.add(cur);
if (isChevronMaterial(cur.getType())) chevronBlocks.add(cur);
if (frameBlocks.size() > maxFrameBlocks) { if (frameBlocks.size() > maxFrameBlocks) {
return ScanResult.fail("The connected frame has more than gate.max-frame-blocks (" + 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 " + ") blocks. Either it's too big, or a frame material is leaking into a much larger structure "
@@ -140,7 +141,7 @@ public class GateStructureScanner {
triedAnyInterior = true; triedAnyInterior = true;
List<Block> iris = floodInterior(candidate, frameKeys); List<Block> iris = floodInterior(candidate, frameKeys);
if (iris != null && !iris.isEmpty()) { if (iris != null && !iris.isEmpty()) {
return ScanResult.ok(new GateStructure(frameBlocks, chevronBlocks, iris)); return ScanResult.ok(new GateStructure(frameBlocks, pickChevrons(frameBlocks), iris));
} }
} }
} }
@@ -155,6 +156,23 @@ public class GateStructureScanner {
+ "(including the floor/ceiling of the arch)."); + "(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. */ /** 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) { private List<Block> floodInterior(Block seed, Set<Long> frameKeys) {
Set<Long> visited = new HashSet<>(); Set<Long> visited = new HashSet<>();
+7 -5
View File
@@ -34,11 +34,13 @@ gate:
- GILDED_BLACKSTONE - GILDED_BLACKSTONE
- BIRCH_PLANKS - BIRCH_PLANKS
- OAK_PLANKS - OAK_PLANKS
# Chevron blocks embedded in the frame. Build them as chevron-unlit-material; the # Chevrons are NOT a separate material you have to place - the plugin automatically
# plugin swaps them to chevron-lit-material as the gate dials/opens, and swaps them # picks this many frame blocks, evenly spaced around the ring, and swaps them to
# back when the gate closes/deactivates. # chevron-lit-material while dialing/open, then restores whatever they looked like
chevron-unlit-material: BLACK_STAINED_GLASS # at rest when the gate closes. So a chevron can rest as plain obsidian and only
chevron-lit-material: GLOWSTONE # 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. # Material poured into the interior (the "event horizon") while the gate is open.
iris-material: WATER iris-material: WATER
max-frame-blocks: 300 max-frame-blocks: 300