diff --git a/README.md b/README.md index 95fea30..ec5a6bd 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/stargate-common/src/main/java/dev/skywalker3200/stargate/common/model/Gate.java b/stargate-common/src/main/java/dev/skywalker3200/stargate/common/model/Gate.java index 67cc1c5..fa3ce14 100644 --- a/stargate-common/src/main/java/dev/skywalker3200/stargate/common/model/Gate.java +++ b/stargate-common/src/main/java/dev/skywalker3200/stargate/common/model/Gate.java @@ -33,11 +33,18 @@ public class Gate { private UUID owner; private final Set 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 flags, String fixedDestination) { + UUID owner, Set 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() { diff --git a/stargate-common/src/main/java/dev/skywalker3200/stargate/common/storage/SqlGateStorage.java b/stargate-common/src/main/java/dev/skywalker3200/stargate/common/storage/SqlGateStorage.java index 110372d..e908e59 100644 --- a/stargate-common/src/main/java/dev/skywalker3200/stargate/common/storage/SqlGateStorage.java +++ b/stargate-common/src/main/java/dev/skywalker3200/stargate/common/storage/SqlGateStorage.java @@ -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") ); } diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/StargatePlugin.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/StargatePlugin.java index a90bada..c5d5145 100644 --- a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/StargatePlugin.java +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/StargatePlugin.java @@ -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); diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateConfig.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateConfig.java index 950f2f2..4ad8116 100644 --- a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateConfig.java +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateConfig.java @@ -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 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) { diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateManager.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateManager.java index 25c7cc7..c4b198d 100644 --- a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateManager.java +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/GateManager.java @@ -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 pendingCreations = new HashMap<>(); + + public boolean hasPendingCreation(UUID playerId) { + return pendingCreations.containsKey(playerId); } - public RuntimeGate createGate(Block signBlock, String network, String name, UUID owner, EnumSet 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 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 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 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) { diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/PendingGateCreation.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/PendingGateCreation.java new file mode 100644 index 0000000..a7b2b92 --- /dev/null +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/gate/PendingGateCreation.java @@ -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 flags; + public final String fixedDestination; + public BukkitTask timeoutTask; + + public PendingGateCreation(Location signLocation, String network, String name, UUID owner, + EnumSet flags, String fixedDestination) { + this.signLocation = signLocation; + this.network = network; + this.name = name; + this.owner = owner; + this.flags = flags; + this.fixedDestination = fixedDestination; + } +} diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/GateLinkListener.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/GateLinkListener.java new file mode 100644 index 0000000..590c5c7 --- /dev/null +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/GateLinkListener.java @@ -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()); + } +} diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/SignCreateListener.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/SignCreateListener.java index 3876feb..28548d0 100644 --- a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/SignCreateListener.java +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/SignCreateListener.java @@ -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) { diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/util/SignRenderer.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/util/SignRenderer.java index a2ed360..8b84b2a 100644 --- a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/util/SignRenderer.java +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/util/SignRenderer.java @@ -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) { diff --git a/stargate-paper/src/main/resources/config.yml b/stargate-paper/src/main/resources/config.yml index 5b89169..5efbc02 100644 --- a/stargate-paper/src/main/resources/config.yml +++ b/stargate-paper/src/main/resources/config.yml @@ -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.