Decouple the control sign from the gate frame: place then punch to link

Sign creation no longer scans the block it's mounted on. Instead,
placing a "[Stargate]" sign starts a pending link (shown as "Punch the
gate" on the sign) and the next block the owner left-clicks within
gate.link-timeout-seconds and gate.max-link-distance becomes the scan
seed. This lets the sign act like a DHD console standing apart from
the gate instead of being physically attached to the frame.

- Gate now stores the punched link block's coordinates (link_x/y/z)
  so restarts re-scan from there instead of deriving a seed from the
  sign's attachment, which no longer applies.
- New GateLinkListener handles the punch and reports scan failures
  via the existing diagnostic messages; also clears pending state on
  disconnect.
- Exit-location facing is now derived from the punched frame block's
  position relative to the iris, since a WallSign facing is no longer
  guaranteed to exist or be relevant.
This commit is contained in:
Michael Burgess
2026-08-09 10:32:46 -04:00
parent 89d4fa0b63
commit 317602c00e
11 changed files with 254 additions and 86 deletions
+8 -4
View File
@@ -25,16 +25,20 @@ Build everything with `./gradlew build`. Jars land in each module's `build/libs/
`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 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.
The plugin flood-fills the frame from the block behind the sign, confirms it's a
fully enclosed ring, and registers the gate. If nothing happens, the ring isn't
sealed or none of its blocks match `frame-materials`.
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.
## Using a gate
@@ -33,11 +33,18 @@ public class Gate {
private UUID owner;
private final Set<Flag> flags;
private String fixedDestination;
// The frame block the owner punched to link this gate's sign to its structure - the sign
// itself can sit anywhere (a DHD-style console away from the gate); this is what we re-scan
// from on server restart.
private int linkX;
private int linkY;
private int linkZ;
public Gate(UUID id, String name, String network, String serverId, String world,
int exitX, int exitY, int exitZ, float exitYaw,
int signX, int signY, int signZ, String signWorld, String facing,
UUID owner, Set<Flag> flags, String fixedDestination) {
UUID owner, Set<Flag> flags, String fixedDestination,
int linkX, int linkY, int linkZ) {
this.id = id;
this.name = name;
this.network = network;
@@ -55,6 +62,9 @@ public class Gate {
this.owner = owner;
this.flags = flags == null ? EnumSet.noneOf(Flag.class) : flags;
this.fixedDestination = fixedDestination;
this.linkX = linkX;
this.linkY = linkY;
this.linkZ = linkZ;
}
public UUID getId() { return id; }
@@ -84,6 +94,9 @@ public class Gate {
public boolean isFixed() { return flags.contains(Flag.FIXED); }
public String getFixedDestination() { return fixedDestination; }
public void setFixedDestination(String fixedDestination) { this.fixedDestination = fixedDestination; }
public int getLinkX() { return linkX; }
public int getLinkY() { return linkY; }
public int getLinkZ() { return linkZ; }
/** Fully-qualified identity used for cross-server lookups: server/network/name */
public String qualifiedName() {
@@ -75,13 +75,28 @@ public class SqlGateStorage implements GateStorage {
"facing VARCHAR(16)," +
"owner VARCHAR(36)," +
"flags VARCHAR(128)," +
"fixed_destination VARCHAR(64)" +
"fixed_destination VARCHAR(64)," +
"link_x INTEGER NOT NULL DEFAULT 0," +
"link_y INTEGER NOT NULL DEFAULT 0," +
"link_z INTEGER NOT NULL DEFAULT 0" +
")");
s.executeUpdate("CREATE INDEX IF NOT EXISTS idx_" + tablePrefix + "network ON " + tablePrefix + "gates(network)");
migrateColumn(s, "link_x");
migrateColumn(s, "link_y");
migrateColumn(s, "link_z");
}
logger.info("[Stargate] Storage initialised (" + driver + ")");
}
/** Best-effort ALTER TABLE for databases created before link_x/y/z existed; ignored if the column is already there. */
private void migrateColumn(Statement s, String column) {
try {
s.executeUpdate("ALTER TABLE " + tablePrefix + "gates ADD COLUMN " + column + " INTEGER NOT NULL DEFAULT 0");
} catch (Exception ignored) {
// column already exists
}
}
@Override
public void close() {
if (dataSource != null) dataSource.close();
@@ -90,16 +105,17 @@ public class SqlGateStorage implements GateStorage {
@Override
public void saveGate(Gate gate) {
String sql = "REPLACE INTO " + tablePrefix + "gates " +
"(id,name,network,server_id,world,exit_x,exit_y,exit_z,exit_yaw,sign_x,sign_y,sign_z,sign_world,facing,owner,flags,fixed_destination) " +
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
"(id,name,network,server_id,world,exit_x,exit_y,exit_z,exit_yaw,sign_x,sign_y,sign_z,sign_world,facing,owner,flags,fixed_destination,link_x,link_y,link_z) " +
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
if (driver == Driver.MYSQL) {
sql = "INSERT INTO " + tablePrefix + "gates " +
"(id,name,network,server_id,world,exit_x,exit_y,exit_z,exit_yaw,sign_x,sign_y,sign_z,sign_world,facing,owner,flags,fixed_destination) " +
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE " +
"(id,name,network,server_id,world,exit_x,exit_y,exit_z,exit_yaw,sign_x,sign_y,sign_z,sign_world,facing,owner,flags,fixed_destination,link_x,link_y,link_z) " +
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE " +
"name=VALUES(name),network=VALUES(network),server_id=VALUES(server_id),world=VALUES(world)," +
"exit_x=VALUES(exit_x),exit_y=VALUES(exit_y),exit_z=VALUES(exit_z),exit_yaw=VALUES(exit_yaw)," +
"sign_x=VALUES(sign_x),sign_y=VALUES(sign_y),sign_z=VALUES(sign_z),sign_world=VALUES(sign_world)," +
"facing=VALUES(facing),owner=VALUES(owner),flags=VALUES(flags),fixed_destination=VALUES(fixed_destination)";
"facing=VALUES(facing),owner=VALUES(owner),flags=VALUES(flags),fixed_destination=VALUES(fixed_destination)," +
"link_x=VALUES(link_x),link_y=VALUES(link_y),link_z=VALUES(link_z)";
}
try (Connection c = dataSource.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, gate.getId().toString());
@@ -119,6 +135,9 @@ public class SqlGateStorage implements GateStorage {
ps.setString(15, gate.getOwner() == null ? null : gate.getOwner().toString());
ps.setString(16, serializeFlags(gate.getFlags()));
ps.setString(17, gate.getFixedDestination());
ps.setInt(18, gate.getLinkX());
ps.setInt(19, gate.getLinkY());
ps.setInt(20, gate.getLinkZ());
ps.executeUpdate();
} catch (Exception e) {
logger.severe("[Stargate] Failed to save gate " + gate.getName() + ": " + e.getMessage());
@@ -182,7 +201,8 @@ public class SqlGateStorage implements GateStorage {
rs.getString("facing"),
ownerStr == null ? null : UUID.fromString(ownerStr),
deserializeFlags(rs.getString("flags")),
fixedDest
fixedDest,
rs.getInt("link_x"), rs.getInt("link_y"), rs.getInt("link_z")
);
}
@@ -44,6 +44,7 @@ public class StargatePlugin extends JavaPlugin {
}
getServer().getPluginManager().registerEvents(new SignCreateListener(this, gateManager), this);
getServer().getPluginManager().registerEvents(new dev.skywalker3200.stargate.paper.listener.GateLinkListener(gateManager), this);
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);
@@ -21,6 +21,8 @@ public class GateConfig {
public final int chevronTickDelay;
public final boolean playSounds;
public final String defaultNetwork;
public final int linkTimeoutSeconds;
public final int maxLinkDistance;
public GateConfig(FileConfiguration cfg, Logger logger) {
Set<Material> materials = new HashSet<>();
@@ -42,6 +44,8 @@ public class GateConfig {
this.chevronTickDelay = cfg.getInt("dialing.chevron-tick-delay", 4);
this.playSounds = cfg.getBoolean("dialing.play-sounds", true);
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);
}
private Material matOr(String s, Material fallback, Logger logger) {
@@ -50,13 +50,10 @@ public class GateManager {
for (Gate gate : storage.loadAll()) {
GateStructure structure = null;
if (gate.getServerId().equals(plugin.getServerId())) {
World world = Bukkit.getWorld(gate.getSignWorld());
World world = Bukkit.getWorld(gate.getWorld());
if (world != null) {
Block signBlock = world.getBlockAt(gate.getSignX(), gate.getSignY(), gate.getSignZ());
Block attached = attachedFrameBlock(signBlock);
if (attached != null) {
structure = scanner.scan(attached);
}
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.");
}
@@ -71,16 +68,6 @@ public class GateManager {
plugin.getLogger().info("[Stargate] Loaded " + gatesById.size() + " gate(s), " + gatesBySignBlock.size() + " local.");
}
/** Given the sign block, finds the neighbouring block that belongs to the frame (the wall it's mounted on, or the block below for a sign post). */
public Block attachedFrameBlock(Block signBlock) {
org.bukkit.block.BlockState state = signBlock.getState();
if (state.getBlockData() instanceof org.bukkit.block.data.type.WallSign wallSign) {
return signBlock.getRelative(wallSign.getFacing().getOppositeFace());
}
// sign post or other: just probe all neighbours, scanner will validate
return signBlock.getRelative(org.bukkit.block.BlockFace.DOWN);
}
private String signKey(String world, int x, int y, int z) {
return world + "," + x + "," + y + "," + z;
}
@@ -108,42 +95,90 @@ public class GateManager {
.collect(Collectors.toList());
}
private String lastCreateFailureReason;
// ---- Sign placement -> punch-to-link creation flow ----
/** Set only when {@link #createGate} just returned null - explains why the scan failed. */
public String getLastCreateFailureReason() {
return lastCreateFailureReason;
private final Map<UUID, PendingGateCreation> pendingCreations = new HashMap<>();
public boolean hasPendingCreation(UUID playerId) {
return pendingCreations.containsKey(playerId);
}
public RuntimeGate createGate(Block signBlock, String network, String name, UUID owner, EnumSet<Gate.Flag> flags) {
return createGate(signBlock, network, name, owner, flags, null);
/** Called when a player places a "[Stargate]" sign; starts the countdown to punch a frame block. */
public void beginPendingCreation(Player player, Block signBlock, String network, String name, UUID owner,
EnumSet<Gate.Flag> flags, String fixedDestination) {
cancelPending(player.getUniqueId());
PendingGateCreation pending = new PendingGateCreation(signBlock.getLocation(), network, name, owner, flags, fixedDestination);
pending.timeoutTask = Bukkit.getScheduler().runTaskLater(plugin, () -> {
pendingCreations.remove(player.getUniqueId());
dev.skywalker3200.stargate.paper.util.SignRenderer.renderExpired(signBlock);
if (player.isOnline()) {
player.sendMessage(net.kyori.adventure.text.Component.text(
"Stargate sign link timed out. Break and replace the sign to try again.",
net.kyori.adventure.text.format.NamedTextColor.RED));
}
}, config.linkTimeoutSeconds * 20L);
pendingCreations.put(player.getUniqueId(), pending);
}
public RuntimeGate createGate(Block signBlock, String network, String name, UUID owner, EnumSet<Gate.Flag> flags, String fixedDestination) {
lastCreateFailureReason = null;
Block attached = attachedFrameBlock(signBlock);
if (attached == null) {
lastCreateFailureReason = "Couldn't determine which block the sign is attached to.";
return null;
public void cancelPending(UUID playerId) {
PendingGateCreation pending = pendingCreations.remove(playerId);
if (pending != null && pending.timeoutTask != null) {
pending.timeoutTask.cancel();
}
GateStructureScanner.ScanResult result = scanner.scanWithDiagnostics(attached);
}
/** Result of punching a block while a gate-creation link is pending. */
public static final class LinkResult {
public final RuntimeGate gate; // non-null on success
public final String failureReason; // non-null on failure
private LinkResult(RuntimeGate gate, String failureReason) {
this.gate = gate;
this.failureReason = failureReason;
}
static LinkResult ok(RuntimeGate g) { return new LinkResult(g, null); }
static LinkResult fail(String reason) { return new LinkResult(null, reason); }
}
/**
* Tries to link a pending sign to the structure reachable from the punched block.
* Returns null if the player has no pending creation (i.e. this click isn't ours to handle).
*/
public LinkResult attemptLink(Player player, Block clickedBlock) {
PendingGateCreation pending = pendingCreations.get(player.getUniqueId());
if (pending == null) return null;
Block signRef = pending.signLocation.getBlock();
if (!clickedBlock.getWorld().equals(pending.signLocation.getWorld())) {
return LinkResult.fail("That's a different world than the sign.");
}
double distance = clickedBlock.getLocation().distance(pending.signLocation);
if (distance > config.maxLinkDistance) {
return LinkResult.fail("That block is " + (int) distance + " blocks from the sign - gate.max-link-distance is "
+ config.maxLinkDistance + ".");
}
GateStructureScanner.ScanResult result = scanner.scanWithDiagnostics(clickedBlock);
if (!result.isSuccess()) {
lastCreateFailureReason = result.failureReason;
return null;
return LinkResult.fail(result.failureReason);
}
GateStructure structure = result.structure;
Location exit = computeExitLocation(structure, signBlock);
Gate gate = new Gate(UUID.randomUUID(), name, network, plugin.getServerId(),
Location exit = computeExitLocation(structure, clickedBlock);
Gate gate = new Gate(UUID.randomUUID(), pending.name, pending.network, plugin.getServerId(),
exit.getWorld().getName(), exit.getBlockX(), exit.getBlockY(), exit.getBlockZ(), exit.getYaw(),
signBlock.getX(), signBlock.getY(), signBlock.getZ(), signBlock.getWorld().getName(),
signBlockFacing(signBlock), owner, flags, fixedDestination);
signRef.getX(), signRef.getY(), signRef.getZ(), signRef.getWorld().getName(),
signBlockFacing(signRef), pending.owner, pending.flags, pending.fixedDestination,
clickedBlock.getX(), clickedBlock.getY(), clickedBlock.getZ());
RuntimeGate rg = new RuntimeGate(gate, structure);
gatesById.put(gate.getId(), rg);
gatesBySignBlock.put(signKey(gate.getSignWorld(), gate.getSignX(), gate.getSignY(), gate.getSignZ()), rg);
storage.saveGate(gate);
return rg;
cancelPending(player.getUniqueId());
return LinkResult.ok(rg);
}
public void destroyGate(RuntimeGate rg) {
@@ -157,11 +192,12 @@ public class GateManager {
storage.saveGate(rg.getGate());
}
private Location computeExitLocation(GateStructure structure, Block signBlock) {
/** frameBlock is whichever block the player punched to link the gate - used only to pick a facing. */
private Location computeExitLocation(GateStructure structure, Block frameBlock) {
List<Block> iris = structure.getIris();
World world = signBlock.getWorld();
World world = frameBlock.getWorld();
if (iris.isEmpty()) {
return signBlock.getLocation().add(0, 0, 0);
return frameBlock.getLocation();
}
long sumX = 0, sumZ = 0;
int minY = Integer.MAX_VALUE;
@@ -172,23 +208,15 @@ public class GateManager {
}
double avgX = (double) sumX / iris.size() + 0.5;
double avgZ = (double) sumZ / iris.size() + 0.5;
float yaw = 0f;
org.bukkit.block.BlockState state = signBlock.getState();
if (state.getBlockData() instanceof org.bukkit.block.data.type.WallSign wallSign) {
// face away from the wall the sign is mounted on, into the room the sign faces
yaw = faceToYaw(wallSign.getFacing());
}
return new Location(world, avgX, minY + 1, avgZ, yaw, 0f);
}
private float faceToYaw(org.bukkit.block.BlockFace face) {
return switch (face) {
case NORTH -> 180f;
case SOUTH -> 0f;
case EAST -> -90f;
case WEST -> 90f;
default -> 0f;
};
// 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);
}
private String signBlockFacing(Block signBlock) {
@@ -0,0 +1,34 @@
package dev.skywalker3200.stargate.paper.gate;
import dev.skywalker3200.stargate.common.model.Gate;
import org.bukkit.Location;
import org.bukkit.scheduler.BukkitTask;
import java.util.EnumSet;
import java.util.UUID;
/**
* A "[Stargate]" sign that's been placed but not yet linked to a physical gate structure -
* the player has until the timeout to left-click ("punch") a frame block, acting like a DHD
* that doesn't have to be built touching the gate itself.
*/
public class PendingGateCreation {
public final Location signLocation;
public final String network;
public final String name;
public final UUID owner;
public final EnumSet<Gate.Flag> flags;
public final String fixedDestination;
public BukkitTask timeoutTask;
public PendingGateCreation(Location signLocation, String network, String name, UUID owner,
EnumSet<Gate.Flag> flags, String fixedDestination) {
this.signLocation = signLocation;
this.network = network;
this.name = name;
this.owner = owner;
this.flags = flags;
this.fixedDestination = fixedDestination;
}
}
@@ -0,0 +1,55 @@
package dev.skywalker3200.stargate.paper.listener;
import dev.skywalker3200.stargate.paper.gate.GateManager;
import dev.skywalker3200.stargate.paper.util.SignRenderer;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.block.Block;
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;
/**
* While a "[Stargate]" sign is waiting to be linked, the next block the owner punches
* (left-clicks) is scanned as the gate's frame - lets the control sign act like a DHD
* sitting away from the gate itself instead of being mounted directly on it.
*/
public class GateLinkListener implements Listener {
private final GateManager gateManager;
public GateLinkListener(GateManager gateManager) {
this.gateManager = gateManager;
}
@EventHandler(ignoreCancelled = true)
public void onInteract(PlayerInteractEvent event) {
if (event.getAction() != Action.LEFT_CLICK_BLOCK) return;
var player = event.getPlayer();
if (!gateManager.hasPendingCreation(player.getUniqueId())) return;
Block clicked = event.getClickedBlock();
if (clicked == null) return;
event.setCancelled(true);
GateManager.LinkResult result = gateManager.attemptLink(player, clicked);
if (result == null) return; // pending vanished between the check and now (e.g. timed out this tick)
if (result.gate == null) {
player.sendMessage(Component.text("Couldn't link there.", NamedTextColor.RED));
player.sendMessage(Component.text(result.failureReason, NamedTextColor.GRAY));
return;
}
SignRenderer.render(result.gate, gateManager);
player.sendMessage(Component.text("Stargate '" + result.gate.getGate().getName() + "' linked on network '"
+ result.gate.getGate().getNetwork() + "'.", NamedTextColor.AQUA));
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
gateManager.cancelPending(event.getPlayer().getUniqueId());
}
}
@@ -3,7 +3,6 @@ package dev.skywalker3200.stargate.paper.listener;
import dev.skywalker3200.stargate.common.model.Gate;
import dev.skywalker3200.stargate.paper.StargatePlugin;
import dev.skywalker3200.stargate.paper.gate.GateManager;
import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
import dev.skywalker3200.stargate.paper.util.SignRenderer;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
@@ -58,19 +57,12 @@ public class SignCreateListener implements Listener {
}
}
RuntimeGate rg = gateManager.createGate(event.getBlock(), network, name, player.getUniqueId(), flags, fixedDestination);
if (rg == null) {
String reason = gateManager.getLastCreateFailureReason();
player.sendMessage(Component.text("No valid gate structure found.", NamedTextColor.RED));
if (reason != null) {
player.sendMessage(Component.text(reason, NamedTextColor.GRAY));
}
resetLine(event);
return;
}
int timeout = gateManager.getConfig().linkTimeoutSeconds;
gateManager.beginPendingCreation(player, event.getBlock(), network, name, player.getUniqueId(), flags, fixedDestination);
SignRenderer.renderPending(event, name, network, timeout);
player.sendMessage(Component.text("Stargate '" + name + "' created on network '" + network + "'.", NamedTextColor.AQUA));
SignRenderer.render(event, rg, gateManager);
player.sendMessage(Component.text("Sign placed. Left-click (punch) any block of the gate's frame within "
+ timeout + "s to link it.", NamedTextColor.AQUA));
}
private String stripped(Component c) {
@@ -11,16 +11,27 @@ import org.bukkit.event.block.SignChangeEvent;
import java.util.List;
/** Renders a gate's current state (name, network, selected destination) onto its control sign. */
/** Renders a gate sign's state: pending link, expired, or a live gate's name/network/destination. */
public final class SignRenderer {
private SignRenderer() {}
public static void render(SignChangeEvent event, RuntimeGate rg, GateManager gateManager) {
event.line(0, Component.text(rg.getGate().getName(), NamedTextColor.DARK_AQUA));
event.line(1, Component.text(rg.getGate().getNetwork(), NamedTextColor.GRAY));
event.line(2, destinationLine(rg, gateManager));
event.line(3, rg.isOpen() ? Component.text("[connected]", NamedTextColor.GREEN) : Component.text(""));
/** Shown immediately when the "[Stargate]" sign is placed, before it's linked to a structure. */
public static void renderPending(SignChangeEvent event, String name, String network, int timeoutSeconds) {
event.line(0, Component.text(name, NamedTextColor.DARK_AQUA));
event.line(1, Component.text(network, NamedTextColor.GRAY));
event.line(2, Component.text("Punch the gate", NamedTextColor.YELLOW));
event.line(3, Component.text("(" + timeoutSeconds + "s)", NamedTextColor.YELLOW));
}
/** Shown if the player never punched a frame block in time. */
public static void renderExpired(Block signBlock) {
if (!(signBlock.getState() instanceof Sign sign)) return;
sign.getSide(Side.FRONT).line(0, Component.text("[Stargate]", NamedTextColor.DARK_GRAY));
sign.getSide(Side.FRONT).line(1, Component.text(""));
sign.getSide(Side.FRONT).line(2, Component.text("link expired", NamedTextColor.RED));
sign.getSide(Side.FRONT).line(3, Component.text(""));
sign.update(true, false);
}
public static void render(RuntimeGate rg, GateManager gateManager) {
@@ -46,6 +46,12 @@ gate:
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.
link-timeout-seconds: 30
# How far (in blocks) the punched frame block may be from the sign.
max-link-distance: 64
dialing:
# Seconds the gate stays open (iris filled) before auto-closing if nobody walks through.