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:
@@ -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) {
|
||||
|
||||
+83
-55
@@ -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) {
|
||||
|
||||
+34
@@ -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;
|
||||
}
|
||||
}
|
||||
+55
@@ -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());
|
||||
}
|
||||
}
|
||||
+5
-13
@@ -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) {
|
||||
|
||||
+17
-6
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user