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:
@@ -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()) {
|
||||||
|
|||||||
+21
-3
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-18
@@ -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<>();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user