Fix one-way travel, rework iris to SG-1 mechanics, glowstone chevrons, pin-locked shield

One-way travel bug: the exit point landed players directly inside the
destination gate's own iris footprint. Since dialing opens both ends,
arriving there immediately re-triggered the teleport listener and
bounced them straight back - reading exactly like one-way travel.
GateManager.computeSafeLanding now nudges the landing spot along the
player's travel direction until it clears the destination's iris
blocks, and GateTeleportListener adds a short per-player cooldown as a
second guard against re-trigger. Same-server teleports now also
preserve the player's exact yaw/pitch and velocity instead of forcing
a fixed stored orientation, so entering forward always means exiting
forward; the cross-server plugin-messaging protocol was extended to
carry yaw/pitch through the proxy hop for the same reason.

Iris rework to match SG-1 rather than a binary open/closed gate:
- Idle (not connected): iris-idle-material, default AIR - you see
  straight through an inactive ring, not a wall.
- Connected: iris-open-material (WATER, the event horizon) unless the
  gate's separate iris shield is closed, in which case
  iris-shield-material (IRON_BLOCK) blocks travel even though the
  wormhole is active.
- A button placed directly below the control sign toggles the shield
  (GateIrisButtonListener). Closing needs nothing; opening a gate with
  a pin code set (/sg pin <code>) prompts the player to type it in
  chat within gate.pin-timeout-seconds first.

Also: gate.chevron-lit-material default changed from gilded blackstone
to glowstone per feedback.
This commit is contained in:
Michael Burgess
2026-08-09 11:36:17 -04:00
parent 190f875d23
commit b822389387
13 changed files with 387 additions and 70 deletions
@@ -48,6 +48,7 @@ public class StargatePlugin extends JavaPlugin {
getServer().getPluginManager().registerEvents(new SignInteractListener(this, gateManager, crossServerBridge), this);
getServer().getPluginManager().registerEvents(new StructureProtectListener(gateManager), this);
getServer().getPluginManager().registerEvents(new dev.skywalker3200.stargate.paper.listener.GateTeleportListener(this, gateManager, crossServerBridge), this);
getServer().getPluginManager().registerEvents(new dev.skywalker3200.stargate.paper.listener.GateIrisButtonListener(this, gateManager), this);
StargateCommand command = new StargateCommand(this, gateManager);
getCommand("stargate").setExecutor(command);
@@ -30,7 +30,7 @@ public class StargateCommand implements CommandExecutor, TabCompleter {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0) {
sender.sendMessage(Component.text("Usage: /sg <list|networks|destroy|reload>", NamedTextColor.YELLOW));
sender.sendMessage(Component.text("Usage: /sg <list|networks|destroy|pin|reload>", NamedTextColor.YELLOW));
return true;
}
@@ -81,6 +81,35 @@ public class StargateCommand implements CommandExecutor, TabCompleter {
gateManager.destroyGate(rg);
player.sendMessage(Component.text("Destroyed '" + rg.getGate().getName() + "'.", NamedTextColor.YELLOW));
}
case "pin" -> {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("Players only.", NamedTextColor.RED));
return true;
}
Block target = player.getTargetBlockExact(6);
RuntimeGate rg = target == null ? null : gateManager.getBySign(target);
if (rg == null) {
player.sendMessage(Component.text("Look at a stargate sign to set its pin.", NamedTextColor.RED));
return true;
}
boolean allowed = player.hasPermission("stargate.admin")
|| (rg.getGate().getOwner() != null && rg.getGate().getOwner().equals(player.getUniqueId()) && player.hasPermission("stargate.create"));
if (!allowed) {
player.sendMessage(Component.text("You can't set this stargate's pin.", NamedTextColor.RED));
return true;
}
if (args.length < 2) {
player.sendMessage(Component.text("Usage: /sg pin <code|clear>", NamedTextColor.YELLOW));
return true;
}
if (args[1].equalsIgnoreCase("clear")) {
gateManager.setPinCode(rg, null);
player.sendMessage(Component.text("Pin cleared - iris opens freely once unshielded.", NamedTextColor.GREEN));
} else {
gateManager.setPinCode(rg, args[1]);
player.sendMessage(Component.text("Pin set. Closing the iris will now require it to reopen.", NamedTextColor.GREEN));
}
}
default -> sender.sendMessage(Component.text("Unknown subcommand.", NamedTextColor.RED));
}
return true;
@@ -89,7 +118,7 @@ public class StargateCommand implements CommandExecutor, TabCompleter {
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
if (args.length == 1) {
return List.of("list", "networks", "destroy", "reload").stream()
return List.of("list", "networks", "destroy", "pin", "reload").stream()
.filter(s -> s.startsWith(args[0].toLowerCase()))
.collect(Collectors.toList());
}
@@ -12,8 +12,9 @@ public class GateConfig {
public final Set<Material> frameMaterials;
public final Material chevronLit;
public final Material irisIdleMaterial;
public final Material irisOpenMaterial;
public final Material irisClosedMaterial;
public final Material irisShieldMaterial;
public final int maxFrameBlocks;
public final int maxIrisBlocks;
public final int minFrameBlocks;
@@ -23,6 +24,7 @@ public class GateConfig {
public final String defaultNetwork;
public final int linkTimeoutSeconds;
public final int maxLinkDistance;
public final int pinTimeoutSeconds;
public GateConfig(FileConfiguration cfg, Logger logger) {
Set<Material> materials = new HashSet<>();
@@ -34,9 +36,10 @@ public class GateConfig {
if (materials.isEmpty()) materials.add(Material.OBSIDIAN);
this.frameMaterials = materials;
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.irisIdleMaterial = matOr(cfg.getString("gate.iris-idle-material"), Material.AIR, logger);
this.irisOpenMaterial = matOr(cfg.getString("gate.iris-open-material"), Material.WATER, logger);
this.irisClosedMaterial = matOr(cfg.getString("gate.iris-closed-material"), Material.BEDROCK, logger);
this.irisShieldMaterial = matOr(cfg.getString("gate.iris-shield-material"), Material.IRON_BLOCK, 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);
@@ -46,6 +49,7 @@ public class GateConfig {
this.defaultNetwork = cfg.getString("network.default-network", "main");
this.linkTimeoutSeconds = cfg.getInt("gate.link-timeout-seconds", 30);
this.maxLinkDistance = cfg.getInt("gate.max-link-distance", 64);
this.pinTimeoutSeconds = cfg.getInt("gate.pin-timeout-seconds", 20);
}
private Material matOr(String s, Material fallback, Logger logger) {
@@ -5,18 +5,22 @@ import dev.skywalker3200.stargate.common.storage.GateStorage;
import dev.skywalker3200.stargate.paper.StargatePlugin;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
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;
import org.bukkit.util.Vector;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@@ -57,10 +61,10 @@ public class GateManager {
GateStructureScanner.ScanResult result = scanner.scanWithDiagnostics(linked);
if (result.isSuccess()) {
structure = result.structure;
// a restart can't preserve "open" runtime state, so every gate comes
// back closed - make sure the iris actually reflects that
// a restart can't preserve "connected" runtime state, so every gate
// comes back idle - make sure the interior actually reflects that
for (Block b : structure.getIris()) {
b.setType(config.irisClosedMaterial);
b.setType(config.irisIdleMaterial);
}
} else {
plugin.getLogger().warning("[Stargate] Could not re-scan structure for gate '" + gate.getName()
@@ -175,7 +179,7 @@ public class GateManager {
}
GateStructure structure = result.structure;
for (Block b : structure.getIris()) {
b.setType(config.irisClosedMaterial);
b.setType(config.irisIdleMaterial);
}
Location exit = computeExitLocation(structure, clickedBlock.getWorld(), facing);
@@ -288,13 +292,7 @@ public class GateManager {
gate.setOpen(true);
gate.setConnectedTo(connectedTo);
if (gate.getStructure() != null) {
for (Block b : gate.getStructure().getIris()) {
b.setType(config.irisOpenMaterial);
if (b.getBlockData() instanceof Levelled lvl) {
lvl.setLevel(0);
b.setBlockData(lvl);
}
}
applyIrisMaterial(gate);
for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) {
c.getBlock().setType(config.chevronLit);
}
@@ -319,7 +317,7 @@ public class GateManager {
gate.setConnectedTo(null);
if (gate.getStructure() != null) {
for (Block b : gate.getStructure().getIris()) {
b.setType(config.irisClosedMaterial);
b.setType(config.irisIdleMaterial);
}
for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) {
c.getBlock().setType(c.getRestingMaterial());
@@ -335,4 +333,81 @@ public class GateManager {
if (rg.isOpen()) closeGate(rg);
}
}
/** Sets the iris blocks to whatever the gate's current connected/shield state implies. */
private void applyIrisMaterial(RuntimeGate gate) {
Material mat = gate.getGate().isIrisClosed() ? config.irisShieldMaterial : config.irisOpenMaterial;
for (Block b : gate.getStructure().getIris()) {
b.setType(mat);
if (b.getBlockData() instanceof Levelled lvl) {
lvl.setLevel(0);
b.setBlockData(lvl);
}
}
}
// ---- Iris shield ----
/** Flips the iris shield and, if the gate is currently connected, updates the blocks immediately. Returns the new closed-state. */
public boolean toggleIrisShield(RuntimeGate rg) {
boolean nowClosed = !rg.getGate().isIrisClosed();
rg.getGate().setIrisClosed(nowClosed);
saveGate(rg);
if (rg.isOpen() && rg.getStructure() != null) {
applyIrisMaterial(rg);
}
return nowClosed;
}
/** Opens the shield directly, bypassing the toggle - used once a correct pin has been entered. */
public void openIrisShield(RuntimeGate rg) {
rg.getGate().setIrisClosed(false);
saveGate(rg);
if (rg.isOpen() && rg.getStructure() != null) {
applyIrisMaterial(rg);
}
}
public void setPinCode(RuntimeGate rg, String pin) {
rg.getGate().setPinCode(pin);
saveGate(rg);
}
// ---- Landing spot: never inside the destination's own iris footprint ----
/**
* Picks a safe arrival spot at {@code dest}: the gate's exit point, nudged along
* {@code travelDirection} (horizontal component only) until it's clear of the destination's
* iris blocks. Without this, arriving players land back inside the iris and immediately
* trigger another teleport, bouncing them straight back where they came from.
*/
public Location computeSafeLanding(RuntimeGate dest, Vector travelDirection, float yaw, float pitch) {
World world = Bukkit.getWorld(dest.getGate().getWorld());
if (world == null) return null;
double x = dest.getGate().getExitX() + 0.5;
double y = dest.getGate().getExitY();
double z = dest.getGate().getExitZ() + 0.5;
Set<Long> irisXZ = new HashSet<>();
if (dest.getStructure() != null) {
for (Block b : dest.getStructure().getIris()) {
irisXZ.add((((long) b.getX()) << 32) ^ (b.getZ() & 0xFFFFFFFFL));
}
}
Vector dir = travelDirection.clone();
dir.setY(0);
if (dir.lengthSquared() < 0.0001) dir = new Vector(0, 0, 1);
dir.normalize();
for (int i = 0; i < 8 && !irisXZ.isEmpty(); i++) {
long key = ((long) Math.floor(x) << 32) ^ ((long) Math.floor(z) & 0xFFFFFFFFL);
if (!irisXZ.contains(key)) break;
x += dir.getX() * 0.5;
z += dir.getZ() * 0.5;
}
return new Location(world, x, y, z, yaw, pitch);
}
}
@@ -0,0 +1,122 @@
package dev.skywalker3200.stargate.paper.listener;
import dev.skywalker3200.stargate.paper.StargatePlugin;
import dev.skywalker3200.stargate.paper.gate.GateManager;
import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
import io.papermc.paper.event.player.AsyncChatEvent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.scheduler.BukkitTask;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* A button placed directly below a gate's control sign toggles its iris shield. Closing it
* requires nothing; opening a shield that has a pin code set prompts the player to type it in
* chat first, matching an iris/DHD access-code mechanic.
*/
public class GateIrisButtonListener implements Listener {
private final StargatePlugin plugin;
private final GateManager gateManager;
private final Map<UUID, PendingPin> pendingPins = new HashMap<>();
private static final class PendingPin {
final RuntimeGate gate;
BukkitTask timeoutTask;
PendingPin(RuntimeGate gate) { this.gate = gate; }
}
public GateIrisButtonListener(StargatePlugin plugin, GateManager gateManager) {
this.plugin = plugin;
this.gateManager = gateManager;
}
@EventHandler(ignoreCancelled = true)
public void onInteract(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
Block block = event.getClickedBlock();
if (block == null || !block.getType().name().endsWith("_BUTTON")) return;
Block signSpot = block.getRelative(BlockFace.UP);
RuntimeGate rg = gateManager.getBySign(signSpot);
if (rg == null) return;
Player player = event.getPlayer();
if (!player.hasPermission("stargate.use")) return;
event.setCancelled(true);
String pin = rg.getGate().getPinCode();
if (rg.getGate().isIrisClosed() && pin != null && !pin.isBlank()) {
beginPinEntry(player, rg);
return;
}
boolean nowClosed = gateManager.toggleIrisShield(rg);
player.sendMessage(Component.text("Iris " + (nowClosed ? "closed." : "opened."),
nowClosed ? NamedTextColor.RED : NamedTextColor.GREEN));
player.playSound(player.getLocation(), Sound.BLOCK_IRON_DOOR_OPEN, 1f, nowClosed ? 0.7f : 1.3f);
}
private void beginPinEntry(Player player, RuntimeGate rg) {
cancelPending(player.getUniqueId());
PendingPin pending = new PendingPin(rg);
int timeout = gateManager.getConfig().pinTimeoutSeconds;
pending.timeoutTask = Bukkit.getScheduler().runTaskLater(plugin, () -> {
pendingPins.remove(player.getUniqueId());
if (player.isOnline()) {
player.sendMessage(Component.text("Pin entry timed out.", NamedTextColor.RED));
}
}, timeout * 20L);
pendingPins.put(player.getUniqueId(), pending);
player.sendMessage(Component.text("This iris is locked. Type the pin code in chat within "
+ timeout + "s.", NamedTextColor.YELLOW));
}
private void cancelPending(UUID playerId) {
PendingPin pending = pendingPins.remove(playerId);
if (pending != null && pending.timeoutTask != null) {
pending.timeoutTask.cancel();
}
}
@EventHandler(ignoreCancelled = true)
public void onChat(AsyncChatEvent event) {
Player player = event.getPlayer();
PendingPin pending = pendingPins.get(player.getUniqueId());
if (pending == null) return;
event.setCancelled(true);
String typed = PlainTextComponentSerializer.plainText().serialize(event.message()).trim();
Bukkit.getScheduler().runTask(plugin, () -> {
String expected = pending.gate.getGate().getPinCode();
if (expected != null && expected.equals(typed)) {
cancelPending(player.getUniqueId());
gateManager.openIrisShield(pending.gate);
player.sendMessage(Component.text("Correct. Iris opened.", NamedTextColor.GREEN));
player.playSound(player.getLocation(), Sound.BLOCK_IRON_DOOR_OPEN, 1f, 1.3f);
} else {
player.sendMessage(Component.text("Incorrect pin.", NamedTextColor.RED));
}
});
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
cancelPending(event.getPlayer().getUniqueId());
}
}
@@ -6,18 +6,30 @@ import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
import dev.skywalker3200.stargate.paper.network.CrossServerBridge;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.util.Vector;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/** Walking into an open gate's water plane teleports the player to the connected gate. */
/**
* Walking into an open gate's event horizon teleports the player to the connected gate,
* arriving just past the destination's iris (never inside it - that would immediately
* re-trigger this same listener and bounce them straight back) and facing the same absolute
* direction they were already walking, so "enter forward" always means "exit forward".
*/
public class GateTeleportListener implements Listener {
private final StargatePlugin plugin;
private final GateManager gateManager;
private final CrossServerBridge crossServerBridge;
private final Map<UUID, Long> cooldownUntil = new HashMap<>();
private static final long COOLDOWN_MS = 1500;
public GateTeleportListener(StargatePlugin plugin, GateManager gateManager, CrossServerBridge crossServerBridge) {
this.plugin = plugin;
@@ -32,6 +44,10 @@ public class GateTeleportListener implements Listener {
if (event.getFrom().getBlockX() == to.getBlockX() && event.getFrom().getBlockY() == to.getBlockY()
&& event.getFrom().getBlockZ() == to.getBlockZ()) return;
Player player = event.getPlayer();
Long until = cooldownUntil.get(player.getUniqueId());
if (until != null && System.currentTimeMillis() < until) return;
Block standing = to.getBlock();
List<RuntimeGate> gates = gateManager.all();
for (RuntimeGate rg : gates) {
@@ -39,28 +55,33 @@ public class GateTeleportListener implements Listener {
for (Block iris : rg.getStructure().getIris()) {
if (iris.getX() == standing.getX() && iris.getY() == standing.getY() && iris.getZ() == standing.getZ()
&& iris.getWorld().equals(standing.getWorld())) {
teleport(event.getPlayer(), rg);
teleport(player, rg);
return;
}
}
}
}
private void teleport(org.bukkit.entity.Player player, RuntimeGate entered) {
private void teleport(Player player, RuntimeGate entered) {
RuntimeGate dest = entered.getConnectedTo();
if (dest == null) return;
Location loc = player.getLocation();
Vector direction = loc.getDirection();
Vector velocity = player.getVelocity();
boolean remote = !dest.getGate().getServerId().equals(plugin.getServerId());
if (remote) {
crossServerBridge.sendPlayerThroughGate(player, dest.getGate());
crossServerBridge.sendPlayerThroughGate(player, dest.getGate(), loc.getYaw(), loc.getPitch());
return;
}
var world = org.bukkit.Bukkit.getWorld(dest.getGate().getWorld());
if (world == null) return;
Location exit = new Location(world, dest.getGate().getExitX() + 0.5, dest.getGate().getExitY(),
dest.getGate().getExitZ() + 0.5, dest.getGate().getExitYaw(), 0f);
player.teleport(exit);
player.playSound(exit, org.bukkit.Sound.ENTITY_ENDERMAN_TELEPORT, 1f, 1f);
Location landing = gateManager.computeSafeLanding(dest, direction, loc.getYaw(), loc.getPitch());
if (landing == null) return;
cooldownUntil.put(player.getUniqueId(), System.currentTimeMillis() + COOLDOWN_MS);
player.teleport(landing);
player.setVelocity(velocity);
player.playSound(landing, org.bukkit.Sound.ENTITY_ENDERMAN_TELEPORT, 1f, 1f);
}
}
@@ -6,11 +6,12 @@ import dev.skywalker3200.stargate.common.model.Gate;
import dev.skywalker3200.stargate.common.network.StargateChannel;
import dev.skywalker3200.stargate.paper.StargatePlugin;
import dev.skywalker3200.stargate.paper.gate.GateManager;
import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.PluginMessageListener;
import org.bukkit.util.Vector;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
@@ -19,7 +20,7 @@ import java.util.UUID;
/**
* Talks to the stargate-velocity / stargate-bungee companion plugin over plugin messaging so a
* player dialing a gate hosted on another backend server actually gets moved there and then
* warped to the right spot once they land.
* warped to the right spot once they land, facing the same direction they entered with.
*/
public class CrossServerBridge implements PluginMessageListener {
@@ -43,14 +44,16 @@ public class CrossServerBridge implements PluginMessageListener {
return enabled;
}
/** Asks the proxy to move this player to the backend server hosting {@code destGate}. */
public void sendPlayerThroughGate(Player player, Gate destGate) {
/** Asks the proxy to move this player to the backend server hosting {@code destGate}, carrying their look direction along. */
public void sendPlayerThroughGate(Player player, Gate destGate, float yaw, float pitch) {
if (!enabled) return;
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeByte(StargateChannel.OP_TELEPORT_REQUEST);
out.writeUTF(player.getUniqueId().toString());
out.writeUTF(destGate.getServerId());
out.writeUTF(destGate.getId().toString());
out.writeFloat(yaw);
out.writeFloat(pitch);
player.sendPluginMessage(plugin, StargateChannel.CHANNEL, out.toByteArray());
}
@@ -64,21 +67,23 @@ public class CrossServerBridge implements PluginMessageListener {
UUID playerId = UUID.fromString(in.readUTF());
UUID gateId = UUID.fromString(in.readUTF());
float yaw = in.readFloat();
float pitch = in.readFloat();
var rg = gateManager.getById(gateId);
RuntimeGate rg = gateManager.getById(gateId);
if (rg == null) {
plugin.getLogger().warning("[Stargate] Received teleport-deliver for unknown gate " + gateId);
return;
}
Gate gate = rg.getGate();
World world = Bukkit.getWorld(gate.getWorld());
if (world == null) return;
Bukkit.getScheduler().runTask(plugin, () -> {
Player p = Bukkit.getPlayer(playerId);
if (p == null) return;
Location exit = new Location(world, gate.getExitX() + 0.5, gate.getExitY(), gate.getExitZ() + 0.5, gate.getExitYaw(), 0f);
p.teleport(exit);
double yawRad = Math.toRadians(yaw);
Vector direction = new Vector(-Math.sin(yawRad), 0, Math.cos(yawRad));
Location landing = gateManager.computeSafeLanding(rg, direction, yaw, pitch);
if (landing == null) return;
p.teleport(landing);
});
} catch (Exception e) {
plugin.getLogger().warning("[Stargate] Failed to handle cross-server teleport message: " + e.getMessage());
+11 -5
View File
@@ -43,12 +43,15 @@ gate:
# 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
# The interior ("iris") reflects whether the gate is usable. Closed/idle, it's solid
# bedrock - nothing can walk or fall through it. Only while open does it become
# iris-open-material (the "event horizon") and let players/items pass.
chevron-lit-material: GLOWSTONE
# The interior has three looks, matching SG-1: idle (not connected) is just empty
# air - you can see straight through the ring. Connected, it's the event horizon
# (iris-open-material) UNLESS the iris shield is closed, in which case it shows
# iris-shield-material instead and blocks all travel even though the gate is active.
# The shield is toggled with a button placed directly below the control sign.
iris-idle-material: AIR
iris-open-material: WATER
iris-closed-material: BEDROCK
iris-shield-material: IRON_BLOCK
max-frame-blocks: 300
max-iris-blocks: 200
min-frame-blocks: 8
@@ -58,6 +61,9 @@ gate:
link-timeout-seconds: 30
# How far (in blocks) the punched frame block may be from the sign.
max-link-distance: 64
# If a gate has a pin code set (/sg pin <code>), this is how long a player has to
# type it in chat after pressing the iris button while the shield is closed.
pin-timeout-seconds: 20
dialing:
# Seconds the gate stays open (iris filled) before auto-closing if nobody walks through.