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:
@@ -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
|
`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
|
them to their original block, so a chevron can rest as plain obsidian and blend
|
||||||
invisibly into the frame until it lights up.
|
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 1: `[Stargate]`
|
||||||
- Line 2: network name (blank = default network)
|
- Line 2: network name (blank = default network)
|
||||||
- Line 3: gate name (blank = auto-generated)
|
- Line 3: gate name (blank = auto-generated)
|
||||||
- Line 4: `hidden` to keep it out of the cycle list, `fixed:GateName` to lock this
|
- 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
|
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
|
If punching a block doesn't work, the plugin tells you exactly why in chat - an
|
||||||
fully enclosed ring, and registers the gate. If nothing happens, the ring isn't
|
unrecognized frame material, a gap in the ring, too far from the sign, etc.
|
||||||
sealed or none of its blocks match `frame-materials`.
|
|
||||||
|
|
||||||
## Using a gate
|
## Using a gate
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,18 @@ public class Gate {
|
|||||||
private UUID owner;
|
private UUID owner;
|
||||||
private final Set<Flag> flags;
|
private final Set<Flag> flags;
|
||||||
private String fixedDestination;
|
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,
|
public Gate(UUID id, String name, String network, String serverId, String world,
|
||||||
int exitX, int exitY, int exitZ, float exitYaw,
|
int exitX, int exitY, int exitZ, float exitYaw,
|
||||||
int signX, int signY, int signZ, String signWorld, String facing,
|
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.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.network = network;
|
this.network = network;
|
||||||
@@ -55,6 +62,9 @@ public class Gate {
|
|||||||
this.owner = owner;
|
this.owner = owner;
|
||||||
this.flags = flags == null ? EnumSet.noneOf(Flag.class) : flags;
|
this.flags = flags == null ? EnumSet.noneOf(Flag.class) : flags;
|
||||||
this.fixedDestination = fixedDestination;
|
this.fixedDestination = fixedDestination;
|
||||||
|
this.linkX = linkX;
|
||||||
|
this.linkY = linkY;
|
||||||
|
this.linkZ = linkZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UUID getId() { return id; }
|
public UUID getId() { return id; }
|
||||||
@@ -84,6 +94,9 @@ public class Gate {
|
|||||||
public boolean isFixed() { return flags.contains(Flag.FIXED); }
|
public boolean isFixed() { return flags.contains(Flag.FIXED); }
|
||||||
public String getFixedDestination() { return fixedDestination; }
|
public String getFixedDestination() { return fixedDestination; }
|
||||||
public void setFixedDestination(String fixedDestination) { this.fixedDestination = 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 */
|
/** Fully-qualified identity used for cross-server lookups: server/network/name */
|
||||||
public String qualifiedName() {
|
public String qualifiedName() {
|
||||||
|
|||||||
+27
-7
@@ -75,13 +75,28 @@ public class SqlGateStorage implements GateStorage {
|
|||||||
"facing VARCHAR(16)," +
|
"facing VARCHAR(16)," +
|
||||||
"owner VARCHAR(36)," +
|
"owner VARCHAR(36)," +
|
||||||
"flags VARCHAR(128)," +
|
"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)");
|
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 + ")");
|
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
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
if (dataSource != null) dataSource.close();
|
if (dataSource != null) dataSource.close();
|
||||||
@@ -90,16 +105,17 @@ public class SqlGateStorage implements GateStorage {
|
|||||||
@Override
|
@Override
|
||||||
public void saveGate(Gate gate) {
|
public void saveGate(Gate gate) {
|
||||||
String sql = "REPLACE INTO " + tablePrefix + "gates " +
|
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) " +
|
"(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 (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
|
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
|
||||||
if (driver == Driver.MYSQL) {
|
if (driver == Driver.MYSQL) {
|
||||||
sql = "INSERT INTO " + tablePrefix + "gates " +
|
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) " +
|
"(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 " +
|
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE " +
|
||||||
"name=VALUES(name),network=VALUES(network),server_id=VALUES(server_id),world=VALUES(world)," +
|
"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)," +
|
"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)," +
|
"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)) {
|
try (Connection c = dataSource.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, gate.getId().toString());
|
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(15, gate.getOwner() == null ? null : gate.getOwner().toString());
|
||||||
ps.setString(16, serializeFlags(gate.getFlags()));
|
ps.setString(16, serializeFlags(gate.getFlags()));
|
||||||
ps.setString(17, gate.getFixedDestination());
|
ps.setString(17, gate.getFixedDestination());
|
||||||
|
ps.setInt(18, gate.getLinkX());
|
||||||
|
ps.setInt(19, gate.getLinkY());
|
||||||
|
ps.setInt(20, gate.getLinkZ());
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.severe("[Stargate] Failed to save gate " + gate.getName() + ": " + e.getMessage());
|
logger.severe("[Stargate] Failed to save gate " + gate.getName() + ": " + e.getMessage());
|
||||||
@@ -182,7 +201,8 @@ public class SqlGateStorage implements GateStorage {
|
|||||||
rs.getString("facing"),
|
rs.getString("facing"),
|
||||||
ownerStr == null ? null : UUID.fromString(ownerStr),
|
ownerStr == null ? null : UUID.fromString(ownerStr),
|
||||||
deserializeFlags(rs.getString("flags")),
|
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 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 SignInteractListener(this, gateManager, crossServerBridge), this);
|
||||||
getServer().getPluginManager().registerEvents(new StructureProtectListener(gateManager), 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.GateTeleportListener(this, gateManager, crossServerBridge), this);
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ public class GateConfig {
|
|||||||
public final int chevronTickDelay;
|
public final int chevronTickDelay;
|
||||||
public final boolean playSounds;
|
public final boolean playSounds;
|
||||||
public final String defaultNetwork;
|
public final String defaultNetwork;
|
||||||
|
public final int linkTimeoutSeconds;
|
||||||
|
public final int maxLinkDistance;
|
||||||
|
|
||||||
public GateConfig(FileConfiguration cfg, Logger logger) {
|
public GateConfig(FileConfiguration cfg, Logger logger) {
|
||||||
Set<Material> materials = new HashSet<>();
|
Set<Material> materials = new HashSet<>();
|
||||||
@@ -42,6 +44,8 @@ public class GateConfig {
|
|||||||
this.chevronTickDelay = cfg.getInt("dialing.chevron-tick-delay", 4);
|
this.chevronTickDelay = cfg.getInt("dialing.chevron-tick-delay", 4);
|
||||||
this.playSounds = cfg.getBoolean("dialing.play-sounds", true);
|
this.playSounds = cfg.getBoolean("dialing.play-sounds", true);
|
||||||
this.defaultNetwork = cfg.getString("network.default-network", "main");
|
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) {
|
private Material matOr(String s, Material fallback, Logger logger) {
|
||||||
|
|||||||
+83
-55
@@ -50,13 +50,10 @@ public class GateManager {
|
|||||||
for (Gate gate : storage.loadAll()) {
|
for (Gate gate : storage.loadAll()) {
|
||||||
GateStructure structure = null;
|
GateStructure structure = null;
|
||||||
if (gate.getServerId().equals(plugin.getServerId())) {
|
if (gate.getServerId().equals(plugin.getServerId())) {
|
||||||
World world = Bukkit.getWorld(gate.getSignWorld());
|
World world = Bukkit.getWorld(gate.getWorld());
|
||||||
if (world != null) {
|
if (world != null) {
|
||||||
Block signBlock = world.getBlockAt(gate.getSignX(), gate.getSignY(), gate.getSignZ());
|
Block linked = world.getBlockAt(gate.getLinkX(), gate.getLinkY(), gate.getLinkZ());
|
||||||
Block attached = attachedFrameBlock(signBlock);
|
structure = scanner.scan(linked);
|
||||||
if (attached != null) {
|
|
||||||
structure = scanner.scan(attached);
|
|
||||||
}
|
|
||||||
if (structure == null) {
|
if (structure == null) {
|
||||||
plugin.getLogger().warning("[Stargate] Could not re-scan structure for gate '" + gate.getName() + "' - it may have been damaged.");
|
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.");
|
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) {
|
private String signKey(String world, int x, int y, int z) {
|
||||||
return world + "," + x + "," + y + "," + z;
|
return world + "," + x + "," + y + "," + z;
|
||||||
}
|
}
|
||||||
@@ -108,42 +95,90 @@ public class GateManager {
|
|||||||
.collect(Collectors.toList());
|
.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. */
|
private final Map<UUID, PendingGateCreation> pendingCreations = new HashMap<>();
|
||||||
public String getLastCreateFailureReason() {
|
|
||||||
return lastCreateFailureReason;
|
public boolean hasPendingCreation(UUID playerId) {
|
||||||
|
return pendingCreations.containsKey(playerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RuntimeGate createGate(Block signBlock, String network, String name, UUID owner, EnumSet<Gate.Flag> flags) {
|
/** Called when a player places a "[Stargate]" sign; starts the countdown to punch a frame block. */
|
||||||
return createGate(signBlock, network, name, owner, flags, null);
|
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) {
|
public void cancelPending(UUID playerId) {
|
||||||
lastCreateFailureReason = null;
|
PendingGateCreation pending = pendingCreations.remove(playerId);
|
||||||
Block attached = attachedFrameBlock(signBlock);
|
if (pending != null && pending.timeoutTask != null) {
|
||||||
if (attached == null) {
|
pending.timeoutTask.cancel();
|
||||||
lastCreateFailureReason = "Couldn't determine which block the sign is attached to.";
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
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()) {
|
if (!result.isSuccess()) {
|
||||||
lastCreateFailureReason = result.failureReason;
|
return LinkResult.fail(result.failureReason);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
GateStructure structure = result.structure;
|
GateStructure structure = result.structure;
|
||||||
|
|
||||||
Location exit = computeExitLocation(structure, signBlock);
|
Location exit = computeExitLocation(structure, clickedBlock);
|
||||||
Gate gate = new Gate(UUID.randomUUID(), name, network, plugin.getServerId(),
|
Gate gate = new Gate(UUID.randomUUID(), pending.name, pending.network, plugin.getServerId(),
|
||||||
exit.getWorld().getName(), exit.getBlockX(), exit.getBlockY(), exit.getBlockZ(), exit.getYaw(),
|
exit.getWorld().getName(), exit.getBlockX(), exit.getBlockY(), exit.getBlockZ(), exit.getYaw(),
|
||||||
signBlock.getX(), signBlock.getY(), signBlock.getZ(), signBlock.getWorld().getName(),
|
signRef.getX(), signRef.getY(), signRef.getZ(), signRef.getWorld().getName(),
|
||||||
signBlockFacing(signBlock), owner, flags, fixedDestination);
|
signBlockFacing(signRef), pending.owner, pending.flags, pending.fixedDestination,
|
||||||
|
clickedBlock.getX(), clickedBlock.getY(), clickedBlock.getZ());
|
||||||
|
|
||||||
RuntimeGate rg = new RuntimeGate(gate, structure);
|
RuntimeGate rg = new RuntimeGate(gate, structure);
|
||||||
gatesById.put(gate.getId(), rg);
|
gatesById.put(gate.getId(), rg);
|
||||||
gatesBySignBlock.put(signKey(gate.getSignWorld(), gate.getSignX(), gate.getSignY(), gate.getSignZ()), rg);
|
gatesBySignBlock.put(signKey(gate.getSignWorld(), gate.getSignX(), gate.getSignY(), gate.getSignZ()), rg);
|
||||||
storage.saveGate(gate);
|
storage.saveGate(gate);
|
||||||
return rg;
|
|
||||||
|
cancelPending(player.getUniqueId());
|
||||||
|
return LinkResult.ok(rg);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void destroyGate(RuntimeGate rg) {
|
public void destroyGate(RuntimeGate rg) {
|
||||||
@@ -157,11 +192,12 @@ public class GateManager {
|
|||||||
storage.saveGate(rg.getGate());
|
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();
|
List<Block> iris = structure.getIris();
|
||||||
World world = signBlock.getWorld();
|
World world = frameBlock.getWorld();
|
||||||
if (iris.isEmpty()) {
|
if (iris.isEmpty()) {
|
||||||
return signBlock.getLocation().add(0, 0, 0);
|
return frameBlock.getLocation();
|
||||||
}
|
}
|
||||||
long sumX = 0, sumZ = 0;
|
long sumX = 0, sumZ = 0;
|
||||||
int minY = Integer.MAX_VALUE;
|
int minY = Integer.MAX_VALUE;
|
||||||
@@ -172,23 +208,15 @@ public class GateManager {
|
|||||||
}
|
}
|
||||||
double avgX = (double) sumX / iris.size() + 0.5;
|
double avgX = (double) sumX / iris.size() + 0.5;
|
||||||
double avgZ = (double) sumZ / 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) {
|
// Face away from the punched frame block, out through the opposite side of the iris -
|
||||||
return switch (face) {
|
// a reasonable guess at "outward" without relying on a sign's wall orientation.
|
||||||
case NORTH -> 180f;
|
double dx = avgX - (frameBlock.getX() + 0.5);
|
||||||
case SOUTH -> 0f;
|
double dz = avgZ - (frameBlock.getZ() + 0.5);
|
||||||
case EAST -> -90f;
|
float yaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
|
||||||
case WEST -> 90f;
|
if (dx == 0 && dz == 0) yaw = 0f;
|
||||||
default -> 0f;
|
|
||||||
};
|
return new Location(world, avgX, minY + 1, avgZ, yaw, 0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String signBlockFacing(Block signBlock) {
|
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.common.model.Gate;
|
||||||
import dev.skywalker3200.stargate.paper.StargatePlugin;
|
import dev.skywalker3200.stargate.paper.StargatePlugin;
|
||||||
import dev.skywalker3200.stargate.paper.gate.GateManager;
|
import dev.skywalker3200.stargate.paper.gate.GateManager;
|
||||||
import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
|
|
||||||
import dev.skywalker3200.stargate.paper.util.SignRenderer;
|
import dev.skywalker3200.stargate.paper.util.SignRenderer;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
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);
|
int timeout = gateManager.getConfig().linkTimeoutSeconds;
|
||||||
if (rg == null) {
|
gateManager.beginPendingCreation(player, event.getBlock(), network, name, player.getUniqueId(), flags, fixedDestination);
|
||||||
String reason = gateManager.getLastCreateFailureReason();
|
SignRenderer.renderPending(event, name, network, timeout);
|
||||||
player.sendMessage(Component.text("No valid gate structure found.", NamedTextColor.RED));
|
|
||||||
if (reason != null) {
|
|
||||||
player.sendMessage(Component.text(reason, NamedTextColor.GRAY));
|
|
||||||
}
|
|
||||||
resetLine(event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
player.sendMessage(Component.text("Stargate '" + name + "' created on network '" + network + "'.", NamedTextColor.AQUA));
|
player.sendMessage(Component.text("Sign placed. Left-click (punch) any block of the gate's frame within "
|
||||||
SignRenderer.render(event, rg, gateManager);
|
+ timeout + "s to link it.", NamedTextColor.AQUA));
|
||||||
}
|
}
|
||||||
|
|
||||||
private String stripped(Component c) {
|
private String stripped(Component c) {
|
||||||
|
|||||||
+17
-6
@@ -11,16 +11,27 @@ import org.bukkit.event.block.SignChangeEvent;
|
|||||||
|
|
||||||
import java.util.List;
|
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 {
|
public final class SignRenderer {
|
||||||
|
|
||||||
private SignRenderer() {}
|
private SignRenderer() {}
|
||||||
|
|
||||||
public static void render(SignChangeEvent event, RuntimeGate rg, GateManager gateManager) {
|
/** Shown immediately when the "[Stargate]" sign is placed, before it's linked to a structure. */
|
||||||
event.line(0, Component.text(rg.getGate().getName(), NamedTextColor.DARK_AQUA));
|
public static void renderPending(SignChangeEvent event, String name, String network, int timeoutSeconds) {
|
||||||
event.line(1, Component.text(rg.getGate().getNetwork(), NamedTextColor.GRAY));
|
event.line(0, Component.text(name, NamedTextColor.DARK_AQUA));
|
||||||
event.line(2, destinationLine(rg, gateManager));
|
event.line(1, Component.text(network, NamedTextColor.GRAY));
|
||||||
event.line(3, rg.isOpen() ? Component.text("[connected]", NamedTextColor.GREEN) : Component.text(""));
|
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) {
|
public static void render(RuntimeGate rg, GateManager gateManager) {
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ gate:
|
|||||||
max-frame-blocks: 300
|
max-frame-blocks: 300
|
||||||
max-iris-blocks: 200
|
max-iris-blocks: 200
|
||||||
min-frame-blocks: 8
|
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:
|
dialing:
|
||||||
# Seconds the gate stays open (iris filled) before auto-closing if nobody walks through.
|
# Seconds the gate stays open (iris filled) before auto-closing if nobody walks through.
|
||||||
|
|||||||
Reference in New Issue
Block a user