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
1. Build a ring out of any block in `gate.frame-materials` (default: obsidian, gold
block, birch/oak planks — matches an obsidian+gold arch design). Leave the middle
hollow.
2. Embed a few blocks of `gate.chevron-unlit-material` (default black stained glass)
into the ring — these light up to `gate.chevron-lit-material` (default glowstone)
as the gate dials.
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 on the outside of the frame with:
- Line 1: `[Stargate]`
- Line 2: network name (blank = default network)
@@ -11,8 +11,8 @@ import java.util.logging.Logger;
public class GateConfig {
public final Set<Material> frameMaterials;
public final Material chevronUnlit;
public final Material chevronLit;
public final int chevronCount;
public final Material irisMaterial;
public final int maxFrameBlocks;
public final int maxIrisBlocks;
@@ -32,8 +32,8 @@ public class GateConfig {
if (materials.isEmpty()) materials.add(Material.OBSIDIAN);
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.GLOWSTONE, logger);
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);
@@ -38,7 +38,7 @@ public class GateManager {
public void reloadConfig() {
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);
}
@@ -206,7 +206,7 @@ public class GateManager {
if (from.isOpen()) closeGate(from);
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);
Runnable finish = () -> {
@@ -227,7 +227,7 @@ public class GateManager {
finish.run();
return;
}
Block chevron = chevrons.get(i[0]);
Block chevron = chevrons.get(i[0]).getBlock();
chevron.setType(config.chevronLit);
if (config.playSounds) {
chevron.getWorld().playSound(chevron.getLocation(), Sound.BLOCK_STONE_STEP, 1f, 1.4f);
@@ -248,8 +248,8 @@ public class GateManager {
b.setBlockData(lvl);
}
}
for (Block c : gate.getStructure().getChevrons()) {
c.setType(config.chevronLit);
for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) {
c.getBlock().setType(config.chevronLit);
}
}
if (config.playSounds) {
@@ -274,8 +274,8 @@ public class GateManager {
for (Block b : gate.getStructure().getIris()) {
b.setType(org.bukkit.Material.AIR);
}
for (Block c : gate.getStructure().getChevrons()) {
c.setType(config.chevronUnlit);
for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) {
c.getBlock().setType(c.getRestingMaterial());
}
}
if (other != null && other.isOpen()) {
@@ -1,22 +1,40 @@
package dev.skywalker3200.stargate.paper.gate;
import org.bukkit.Material;
import org.bukkit.block.Block;
import java.util.List;
/** The physical blocks that make up a scanned gate: frame ring, chevrons, and interior iris. */
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> chevrons;
private final List<ChevronSlot> chevrons;
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.chevrons = chevrons;
this.iris = iris;
}
public List<Block> getFrame() { return frame; }
public List<Block> getChevrons() { return chevrons; }
public List<ChevronSlot> getChevrons() { return chevrons; }
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
* a control sign is attached to: the frame ring, the chevron blocks embedded in it, and
* the enclosed interior ("iris") that gets filled with water while the gate is open.
* 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 {
@@ -39,28 +45,25 @@ public class GateStructureScanner {
}
private final Set<Material> frameMaterials;
private final Material chevronUnlit;
private final Material chevronLit;
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 chevronUnlit, Material chevronLit,
public GateStructureScanner(Set<Material> frameMaterials, Material chevronLitMaterial, int chevronCount,
int maxFrameBlocks, int maxIrisBlocks, int minFrameBlocks) {
this.frameMaterials = frameMaterials;
this.chevronUnlit = chevronUnlit;
this.chevronLit = chevronLit;
this.chevronLitMaterial = chevronLitMaterial;
this.chevronCount = chevronCount;
this.maxFrameBlocks = maxFrameBlocks;
this.maxIrisBlocks = maxIrisBlocks;
this.minFrameBlocks = minFrameBlocks;
}
private boolean isFrameMaterial(Material m) {
return frameMaterials.contains(m) || m == chevronUnlit || m == chevronLit;
}
private boolean isChevronMaterial(Material m) {
return 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;
}
/** Convenience wrapper for callers that only care about success/failure, not why. */
@@ -84,14 +87,13 @@ public class GateStructureScanner {
}
}
if (!isFrameMaterial(frameSeed.getType())) {
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, "
+ "and that every block type in your ring is listed in config.yml.");
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<>();
List<Block> chevronBlocks = new ArrayList<>();
Deque<Block> queue = new ArrayDeque<>();
queue.add(frameSeed);
frameKeys.add(key(frameSeed));
@@ -99,7 +101,6 @@ public class GateStructureScanner {
while (!queue.isEmpty()) {
Block cur = queue.poll();
frameBlocks.add(cur);
if (isChevronMaterial(cur.getType())) chevronBlocks.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 "
@@ -140,7 +141,7 @@ public class GateStructureScanner {
triedAnyInterior = true;
List<Block> iris = floodInterior(candidate, frameKeys);
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).");
}
/** 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<>();
+7 -5
View File
@@ -34,11 +34,13 @@ gate:
- GILDED_BLACKSTONE
- BIRCH_PLANKS
- OAK_PLANKS
# Chevron blocks embedded in the frame. Build them as chevron-unlit-material; the
# plugin swaps them to chevron-lit-material as the gate dials/opens, and swaps them
# back when the gate closes/deactivates.
chevron-unlit-material: BLACK_STAINED_GLASS
chevron-lit-material: GLOWSTONE
# 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