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

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

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

Also: gate.chevron-lit-material default changed from gilded blackstone
to glowstone per feedback.
This commit is contained in:
Michael Burgess
2026-08-09 11:36:17 -04:00
parent 190f875d23
commit b822389387
13 changed files with 387 additions and 70 deletions
+25 -9
View File
@@ -34,19 +34,22 @@ further outward than their immediate ring-neighbours - the "elbow" points of the
shape, which is where a real Stargate's chevrons sit (an 11-wide octagonal ring like shape, which is where a real Stargate's chevrons sit (an 11-wide octagonal ring like
the reference design naturally produces 7 of them: the top apex, the four shoulder the reference design naturally produces 7 of them: the top apex, the four shoulder
elbows, and the two side bumps). Those blocks swap to `gate.chevron-lit-material` elbows, and the two side bumps). Those blocks swap to `gate.chevron-lit-material`
(default gilded blackstone) while dialing/open, and revert to whatever they looked (default glowstone) while dialing/open, and revert to whatever they looked like at
like at rest when the gate closes - so a chevron can rest as plain obsidian and blend rest when the gate closes - 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.
1. Build the ring. 1. Build the ring.
2. Place a sign anywhere - it doesn't need to touch the frame, so it can act like a DHD 2. Optionally place a button directly below where you'll put the sign - this becomes
the iris shield toggle (see below). Not required; a gate without one just has no
shield control.
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: 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
3. The sign will show "Punch the gate (30s)". Within `gate.link-timeout-seconds`, 4. The sign will show "Punch the gate (30s)". Within `gate.link-timeout-seconds`,
left-click (punch) any frame block, within `gate.max-link-distance` of the sign. left-click (punch) any frame block, within `gate.max-link-distance` of the sign.
The plugin scans out from that block and links the sign to it. The plugin scans out from that block and links the sign to it.
@@ -57,11 +60,21 @@ material, unsealed interior, too far from the sign, etc.
- **Right-click** the sign: cycles the destination shown on line 3 among the other - **Right-click** the sign: cycles the destination shown on line 3 among the other
gates on the same network. gates on the same network.
- **Left-click** the sign: dials the shown destination chevrons light in sequence, - **Left-click** the sign: dials the shown destination - chevrons light in sequence,
then the interior switches from `gate.iris-closed-material` (default bedrock - solid, then the interior switches from `gate.iris-idle-material` (default air - an idle gate
nothing can pass through a closed/idle gate) to `gate.iris-open-material` (default is just an empty ring you can see straight through) to either the event horizon
water, the "event horizon"). Walk into it to teleport. It auto-closes after (`gate.iris-open-material`, default water) or, if the iris shield is closed, to
`dialing.open-seconds`, switching the interior back to bedrock. `gate.iris-shield-material` (default iron block) instead, blocking travel even
though the gate is actively connected. It auto-closes after `dialing.open-seconds`,
switching back to the idle material either way.
- Walking into the event horizon teleports you to the connected gate, arriving just
clear of its iris (never inside it - that would immediately teleport you right back)
and still facing whatever direction you were already walking, so entering forward
always means exiting forward.
- **Right-click a button placed directly below the sign** toggles the iris shield.
Closing it never needs anything extra. If the gate has a pin set (`/sg pin <code>`),
opening a closed shield prompts you to type the code in chat within
`gate.pin-timeout-seconds` before it'll open.
## Multi-world ## Multi-world
@@ -93,5 +106,8 @@ server's Stargate instance teleports them to the gate's exit point.
## Commands ## Commands
- `/sg list [network]` - `/sg list [network]`
- `/sg networks`
- `/sg destroy` (look at a gate's sign) - `/sg destroy` (look at a gate's sign)
- `/sg pin <code|clear>` (look at a gate's sign) — requires that code in chat before a
closed iris shield will open on that gate
- `/sg reload` - `/sg reload`
@@ -22,7 +22,9 @@ import java.util.concurrent.ConcurrentHashMap;
/** BungeeCord counterpart of the Velocity bridge: same wire protocol, same job. */ /** BungeeCord counterpart of the Velocity bridge: same wire protocol, same job. */
public class StargateBungeePlugin extends Plugin implements Listener { public class StargateBungeePlugin extends Plugin implements Listener {
private final Map<UUID, String> pendingGateByPlayer = new ConcurrentHashMap<>(); private record PendingWarp(String gateId, float yaw, float pitch) {}
private final Map<UUID, PendingWarp> pendingGateByPlayer = new ConcurrentHashMap<>();
@Override @Override
public void onEnable() { public void onEnable() {
@@ -44,6 +46,8 @@ public class StargateBungeePlugin extends Plugin implements Listener {
UUID playerId = UUID.fromString(in.readUTF()); UUID playerId = UUID.fromString(in.readUTF());
String targetServer = in.readUTF(); String targetServer = in.readUTF();
String gateId = in.readUTF(); String gateId = in.readUTF();
float yaw = in.readFloat();
float pitch = in.readFloat();
ProxiedPlayer player = ProxyServer.getInstance().getPlayer(playerId); ProxiedPlayer player = ProxyServer.getInstance().getPlayer(playerId);
ServerInfo target = ProxyServer.getInstance().getServerInfo(targetServer); ServerInfo target = ProxyServer.getInstance().getServerInfo(targetServer);
@@ -52,7 +56,7 @@ public class StargateBungeePlugin extends Plugin implements Listener {
return; return;
} }
pendingGateByPlayer.put(playerId, gateId); pendingGateByPlayer.put(playerId, new PendingWarp(gateId, yaw, pitch));
player.connect(target); player.connect(target);
} catch (Exception e) { } catch (Exception e) {
getLogger().warning("Failed to process stargate plugin message: " + e.getMessage()); getLogger().warning("Failed to process stargate plugin message: " + e.getMessage());
@@ -62,8 +66,8 @@ public class StargateBungeePlugin extends Plugin implements Listener {
@EventHandler @EventHandler
public void onServerConnected(ServerConnectedEvent event) { public void onServerConnected(ServerConnectedEvent event) {
ProxiedPlayer player = event.getPlayer(); ProxiedPlayer player = event.getPlayer();
String gateId = pendingGateByPlayer.remove(player.getUniqueId()); PendingWarp warp = pendingGateByPlayer.remove(player.getUniqueId());
if (gateId == null) return; if (warp == null) return;
Server server = event.getServer(); Server server = event.getServer();
try { try {
@@ -71,7 +75,9 @@ public class StargateBungeePlugin extends Plugin implements Listener {
DataOutputStream out = new DataOutputStream(bytes); DataOutputStream out = new DataOutputStream(bytes);
out.writeByte(StargateChannel.OP_TELEPORT_DELIVER); out.writeByte(StargateChannel.OP_TELEPORT_DELIVER);
out.writeUTF(player.getUniqueId().toString()); out.writeUTF(player.getUniqueId().toString());
out.writeUTF(gateId); out.writeUTF(warp.gateId());
out.writeFloat(warp.yaw());
out.writeFloat(warp.pitch());
server.sendData(StargateChannel.CHANNEL, bytes.toByteArray()); server.sendData(StargateChannel.CHANNEL, bytes.toByteArray());
} catch (Exception e) { } catch (Exception e) {
getLogger().warning("Failed to deliver stargate teleport: " + e.getMessage()); getLogger().warning("Failed to deliver stargate teleport: " + e.getMessage());
@@ -39,6 +39,12 @@ public class Gate {
private int linkX; private int linkX;
private int linkY; private int linkY;
private int linkZ; private int linkZ;
// Iris shield: a separate safety barrier from the wormhole connection itself. While the
// gate is connected, the iris being closed blocks travel and shows a solid barrier instead
// of the event horizon. Defaults open so a freshly linked gate works immediately.
private boolean irisClosed = false;
// If set, opening a closed iris via the button requires typing this in chat first.
private String pinCode;
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,
@@ -97,6 +103,10 @@ public class Gate {
public int getLinkX() { return linkX; } public int getLinkX() { return linkX; }
public int getLinkY() { return linkY; } public int getLinkY() { return linkY; }
public int getLinkZ() { return linkZ; } public int getLinkZ() { return linkZ; }
public boolean isIrisClosed() { return irisClosed; }
public void setIrisClosed(boolean irisClosed) { this.irisClosed = irisClosed; }
public String getPinCode() { return pinCode; }
public void setPinCode(String pinCode) { this.pinCode = pinCode; }
/** 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() {
@@ -78,18 +78,22 @@ public class SqlGateStorage implements GateStorage {
"fixed_destination VARCHAR(64)," + "fixed_destination VARCHAR(64)," +
"link_x INTEGER NOT NULL DEFAULT 0," + "link_x INTEGER NOT NULL DEFAULT 0," +
"link_y INTEGER NOT NULL DEFAULT 0," + "link_y INTEGER NOT NULL DEFAULT 0," +
"link_z INTEGER NOT NULL DEFAULT 0" + "link_z INTEGER NOT NULL DEFAULT 0," +
"iris_closed INTEGER NOT NULL DEFAULT 0," +
"pin_code VARCHAR(32)" +
")"); ")");
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"); migrateIntColumn(s, "link_x");
migrateColumn(s, "link_y"); migrateIntColumn(s, "link_y");
migrateColumn(s, "link_z"); migrateIntColumn(s, "link_z");
migrateIntColumn(s, "iris_closed");
migrateStringColumn(s, "pin_code", "VARCHAR(32)");
} }
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. */ /** Best-effort ALTER TABLE for databases created before this column existed; ignored if it's already there. */
private void migrateColumn(Statement s, String column) { private void migrateIntColumn(Statement s, String column) {
try { try {
s.executeUpdate("ALTER TABLE " + tablePrefix + "gates ADD COLUMN " + column + " INTEGER NOT NULL DEFAULT 0"); s.executeUpdate("ALTER TABLE " + tablePrefix + "gates ADD COLUMN " + column + " INTEGER NOT NULL DEFAULT 0");
} catch (Exception ignored) { } catch (Exception ignored) {
@@ -97,6 +101,14 @@ public class SqlGateStorage implements GateStorage {
} }
} }
private void migrateStringColumn(Statement s, String column, String type) {
try {
s.executeUpdate("ALTER TABLE " + tablePrefix + "gates ADD COLUMN " + column + " " + type);
} catch (Exception ignored) {
// column already exists
}
}
@Override @Override
public void close() { public void close() {
if (dataSource != null) dataSource.close(); if (dataSource != null) dataSource.close();
@@ -105,17 +117,18 @@ 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,link_x,link_y,link_z) " + "(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,iris_closed,pin_code) " +
"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,link_x,link_y,link_z) " + "(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,iris_closed,pin_code) " +
"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)"; "link_x=VALUES(link_x),link_y=VALUES(link_y),link_z=VALUES(link_z)," +
"iris_closed=VALUES(iris_closed),pin_code=VALUES(pin_code)";
} }
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());
@@ -138,6 +151,8 @@ public class SqlGateStorage implements GateStorage {
ps.setInt(18, gate.getLinkX()); ps.setInt(18, gate.getLinkX());
ps.setInt(19, gate.getLinkY()); ps.setInt(19, gate.getLinkY());
ps.setInt(20, gate.getLinkZ()); ps.setInt(20, gate.getLinkZ());
ps.setInt(21, gate.isIrisClosed() ? 1 : 0);
ps.setString(22, gate.getPinCode());
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());
@@ -190,7 +205,7 @@ public class SqlGateStorage implements GateStorage {
private Gate fromRow(ResultSet rs) throws Exception { private Gate fromRow(ResultSet rs) throws Exception {
String ownerStr = rs.getString("owner"); String ownerStr = rs.getString("owner");
String fixedDest = rs.getString("fixed_destination"); String fixedDest = rs.getString("fixed_destination");
return new Gate( Gate gate = new Gate(
UUID.fromString(rs.getString("id")), UUID.fromString(rs.getString("id")),
rs.getString("name"), rs.getString("name"),
rs.getString("network"), rs.getString("network"),
@@ -204,6 +219,9 @@ public class SqlGateStorage implements GateStorage {
fixedDest, fixedDest,
rs.getInt("link_x"), rs.getInt("link_y"), rs.getInt("link_z") rs.getInt("link_x"), rs.getInt("link_y"), rs.getInt("link_z")
); );
gate.setIrisClosed(rs.getInt("iris_closed") != 0);
gate.setPinCode(rs.getString("pin_code"));
return gate;
} }
private String serializeFlags(Set<Gate.Flag> flags) { private String serializeFlags(Set<Gate.Flag> flags) {
@@ -48,6 +48,7 @@ public class StargatePlugin extends JavaPlugin {
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);
getServer().getPluginManager().registerEvents(new dev.skywalker3200.stargate.paper.listener.GateIrisButtonListener(this, gateManager), this);
StargateCommand command = new StargateCommand(this, gateManager); StargateCommand command = new StargateCommand(this, gateManager);
getCommand("stargate").setExecutor(command); getCommand("stargate").setExecutor(command);
@@ -30,7 +30,7 @@ public class StargateCommand implements CommandExecutor, TabCompleter {
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0) { if (args.length == 0) {
sender.sendMessage(Component.text("Usage: /sg <list|networks|destroy|reload>", NamedTextColor.YELLOW)); sender.sendMessage(Component.text("Usage: /sg <list|networks|destroy|pin|reload>", NamedTextColor.YELLOW));
return true; return true;
} }
@@ -81,6 +81,35 @@ public class StargateCommand implements CommandExecutor, TabCompleter {
gateManager.destroyGate(rg); gateManager.destroyGate(rg);
player.sendMessage(Component.text("Destroyed '" + rg.getGate().getName() + "'.", NamedTextColor.YELLOW)); player.sendMessage(Component.text("Destroyed '" + rg.getGate().getName() + "'.", NamedTextColor.YELLOW));
} }
case "pin" -> {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("Players only.", NamedTextColor.RED));
return true;
}
Block target = player.getTargetBlockExact(6);
RuntimeGate rg = target == null ? null : gateManager.getBySign(target);
if (rg == null) {
player.sendMessage(Component.text("Look at a stargate sign to set its pin.", NamedTextColor.RED));
return true;
}
boolean allowed = player.hasPermission("stargate.admin")
|| (rg.getGate().getOwner() != null && rg.getGate().getOwner().equals(player.getUniqueId()) && player.hasPermission("stargate.create"));
if (!allowed) {
player.sendMessage(Component.text("You can't set this stargate's pin.", NamedTextColor.RED));
return true;
}
if (args.length < 2) {
player.sendMessage(Component.text("Usage: /sg pin <code|clear>", NamedTextColor.YELLOW));
return true;
}
if (args[1].equalsIgnoreCase("clear")) {
gateManager.setPinCode(rg, null);
player.sendMessage(Component.text("Pin cleared - iris opens freely once unshielded.", NamedTextColor.GREEN));
} else {
gateManager.setPinCode(rg, args[1]);
player.sendMessage(Component.text("Pin set. Closing the iris will now require it to reopen.", NamedTextColor.GREEN));
}
}
default -> sender.sendMessage(Component.text("Unknown subcommand.", NamedTextColor.RED)); default -> sender.sendMessage(Component.text("Unknown subcommand.", NamedTextColor.RED));
} }
return true; return true;
@@ -89,7 +118,7 @@ public class StargateCommand implements CommandExecutor, TabCompleter {
@Override @Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) { public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
if (args.length == 1) { if (args.length == 1) {
return List.of("list", "networks", "destroy", "reload").stream() return List.of("list", "networks", "destroy", "pin", "reload").stream()
.filter(s -> s.startsWith(args[0].toLowerCase())) .filter(s -> s.startsWith(args[0].toLowerCase()))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@@ -12,8 +12,9 @@ public class GateConfig {
public final Set<Material> frameMaterials; public final Set<Material> frameMaterials;
public final Material chevronLit; public final Material chevronLit;
public final Material irisIdleMaterial;
public final Material irisOpenMaterial; public final Material irisOpenMaterial;
public final Material irisClosedMaterial; public final Material irisShieldMaterial;
public final int maxFrameBlocks; public final int maxFrameBlocks;
public final int maxIrisBlocks; public final int maxIrisBlocks;
public final int minFrameBlocks; public final int minFrameBlocks;
@@ -23,6 +24,7 @@ public class GateConfig {
public final String defaultNetwork; public final String defaultNetwork;
public final int linkTimeoutSeconds; public final int linkTimeoutSeconds;
public final int maxLinkDistance; public final int maxLinkDistance;
public final int pinTimeoutSeconds;
public GateConfig(FileConfiguration cfg, Logger logger) { public GateConfig(FileConfiguration cfg, Logger logger) {
Set<Material> materials = new HashSet<>(); Set<Material> materials = new HashSet<>();
@@ -34,9 +36,10 @@ public class GateConfig {
if (materials.isEmpty()) materials.add(Material.OBSIDIAN); if (materials.isEmpty()) materials.add(Material.OBSIDIAN);
this.frameMaterials = materials; this.frameMaterials = materials;
this.chevronLit = matOr(cfg.getString("gate.chevron-lit-material"), Material.GILDED_BLACKSTONE, logger); this.chevronLit = matOr(cfg.getString("gate.chevron-lit-material"), Material.GLOWSTONE, logger);
this.irisIdleMaterial = matOr(cfg.getString("gate.iris-idle-material"), Material.AIR, logger);
this.irisOpenMaterial = matOr(cfg.getString("gate.iris-open-material"), Material.WATER, logger); this.irisOpenMaterial = matOr(cfg.getString("gate.iris-open-material"), Material.WATER, logger);
this.irisClosedMaterial = matOr(cfg.getString("gate.iris-closed-material"), Material.BEDROCK, logger); this.irisShieldMaterial = matOr(cfg.getString("gate.iris-shield-material"), Material.IRON_BLOCK, logger);
this.maxFrameBlocks = cfg.getInt("gate.max-frame-blocks", 300); this.maxFrameBlocks = cfg.getInt("gate.max-frame-blocks", 300);
this.maxIrisBlocks = cfg.getInt("gate.max-iris-blocks", 200); this.maxIrisBlocks = cfg.getInt("gate.max-iris-blocks", 200);
this.minFrameBlocks = cfg.getInt("gate.min-frame-blocks", 8); this.minFrameBlocks = cfg.getInt("gate.min-frame-blocks", 8);
@@ -46,6 +49,7 @@ public class GateConfig {
this.defaultNetwork = cfg.getString("network.default-network", "main"); this.defaultNetwork = cfg.getString("network.default-network", "main");
this.linkTimeoutSeconds = cfg.getInt("gate.link-timeout-seconds", 30); this.linkTimeoutSeconds = cfg.getInt("gate.link-timeout-seconds", 30);
this.maxLinkDistance = cfg.getInt("gate.max-link-distance", 64); this.maxLinkDistance = cfg.getInt("gate.max-link-distance", 64);
this.pinTimeoutSeconds = cfg.getInt("gate.pin-timeout-seconds", 20);
} }
private Material matOr(String s, Material fallback, Logger logger) { private Material matOr(String s, Material fallback, Logger logger) {
@@ -5,18 +5,22 @@ import dev.skywalker3200.stargate.common.storage.GateStorage;
import dev.skywalker3200.stargate.paper.StargatePlugin; import dev.skywalker3200.stargate.paper.StargatePlugin;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound; import org.bukkit.Sound;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.block.BlockFace; import org.bukkit.block.BlockFace;
import org.bukkit.block.data.Levelled; import org.bukkit.block.data.Levelled;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -57,10 +61,10 @@ public class GateManager {
GateStructureScanner.ScanResult result = scanner.scanWithDiagnostics(linked); GateStructureScanner.ScanResult result = scanner.scanWithDiagnostics(linked);
if (result.isSuccess()) { if (result.isSuccess()) {
structure = result.structure; structure = result.structure;
// a restart can't preserve "open" runtime state, so every gate comes // a restart can't preserve "connected" runtime state, so every gate
// back closed - make sure the iris actually reflects that // comes back idle - make sure the interior actually reflects that
for (Block b : structure.getIris()) { for (Block b : structure.getIris()) {
b.setType(config.irisClosedMaterial); b.setType(config.irisIdleMaterial);
} }
} else { } else {
plugin.getLogger().warning("[Stargate] Could not re-scan structure for gate '" + gate.getName() plugin.getLogger().warning("[Stargate] Could not re-scan structure for gate '" + gate.getName()
@@ -175,7 +179,7 @@ public class GateManager {
} }
GateStructure structure = result.structure; GateStructure structure = result.structure;
for (Block b : structure.getIris()) { for (Block b : structure.getIris()) {
b.setType(config.irisClosedMaterial); b.setType(config.irisIdleMaterial);
} }
Location exit = computeExitLocation(structure, clickedBlock.getWorld(), facing); Location exit = computeExitLocation(structure, clickedBlock.getWorld(), facing);
@@ -288,13 +292,7 @@ public class GateManager {
gate.setOpen(true); gate.setOpen(true);
gate.setConnectedTo(connectedTo); gate.setConnectedTo(connectedTo);
if (gate.getStructure() != null) { if (gate.getStructure() != null) {
for (Block b : gate.getStructure().getIris()) { applyIrisMaterial(gate);
b.setType(config.irisOpenMaterial);
if (b.getBlockData() instanceof Levelled lvl) {
lvl.setLevel(0);
b.setBlockData(lvl);
}
}
for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) { for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) {
c.getBlock().setType(config.chevronLit); c.getBlock().setType(config.chevronLit);
} }
@@ -319,7 +317,7 @@ public class GateManager {
gate.setConnectedTo(null); gate.setConnectedTo(null);
if (gate.getStructure() != null) { if (gate.getStructure() != null) {
for (Block b : gate.getStructure().getIris()) { for (Block b : gate.getStructure().getIris()) {
b.setType(config.irisClosedMaterial); b.setType(config.irisIdleMaterial);
} }
for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) { for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) {
c.getBlock().setType(c.getRestingMaterial()); c.getBlock().setType(c.getRestingMaterial());
@@ -335,4 +333,81 @@ public class GateManager {
if (rg.isOpen()) closeGate(rg); if (rg.isOpen()) closeGate(rg);
} }
} }
/** Sets the iris blocks to whatever the gate's current connected/shield state implies. */
private void applyIrisMaterial(RuntimeGate gate) {
Material mat = gate.getGate().isIrisClosed() ? config.irisShieldMaterial : config.irisOpenMaterial;
for (Block b : gate.getStructure().getIris()) {
b.setType(mat);
if (b.getBlockData() instanceof Levelled lvl) {
lvl.setLevel(0);
b.setBlockData(lvl);
}
}
}
// ---- Iris shield ----
/** Flips the iris shield and, if the gate is currently connected, updates the blocks immediately. Returns the new closed-state. */
public boolean toggleIrisShield(RuntimeGate rg) {
boolean nowClosed = !rg.getGate().isIrisClosed();
rg.getGate().setIrisClosed(nowClosed);
saveGate(rg);
if (rg.isOpen() && rg.getStructure() != null) {
applyIrisMaterial(rg);
}
return nowClosed;
}
/** Opens the shield directly, bypassing the toggle - used once a correct pin has been entered. */
public void openIrisShield(RuntimeGate rg) {
rg.getGate().setIrisClosed(false);
saveGate(rg);
if (rg.isOpen() && rg.getStructure() != null) {
applyIrisMaterial(rg);
}
}
public void setPinCode(RuntimeGate rg, String pin) {
rg.getGate().setPinCode(pin);
saveGate(rg);
}
// ---- Landing spot: never inside the destination's own iris footprint ----
/**
* Picks a safe arrival spot at {@code dest}: the gate's exit point, nudged along
* {@code travelDirection} (horizontal component only) until it's clear of the destination's
* iris blocks. Without this, arriving players land back inside the iris and immediately
* trigger another teleport, bouncing them straight back where they came from.
*/
public Location computeSafeLanding(RuntimeGate dest, Vector travelDirection, float yaw, float pitch) {
World world = Bukkit.getWorld(dest.getGate().getWorld());
if (world == null) return null;
double x = dest.getGate().getExitX() + 0.5;
double y = dest.getGate().getExitY();
double z = dest.getGate().getExitZ() + 0.5;
Set<Long> irisXZ = new HashSet<>();
if (dest.getStructure() != null) {
for (Block b : dest.getStructure().getIris()) {
irisXZ.add((((long) b.getX()) << 32) ^ (b.getZ() & 0xFFFFFFFFL));
}
}
Vector dir = travelDirection.clone();
dir.setY(0);
if (dir.lengthSquared() < 0.0001) dir = new Vector(0, 0, 1);
dir.normalize();
for (int i = 0; i < 8 && !irisXZ.isEmpty(); i++) {
long key = ((long) Math.floor(x) << 32) ^ ((long) Math.floor(z) & 0xFFFFFFFFL);
if (!irisXZ.contains(key)) break;
x += dir.getX() * 0.5;
z += dir.getZ() * 0.5;
}
return new Location(world, x, y, z, yaw, pitch);
}
} }
@@ -0,0 +1,122 @@
package dev.skywalker3200.stargate.paper.listener;
import dev.skywalker3200.stargate.paper.StargatePlugin;
import dev.skywalker3200.stargate.paper.gate.GateManager;
import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
import io.papermc.paper.event.player.AsyncChatEvent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.scheduler.BukkitTask;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* A button placed directly below a gate's control sign toggles its iris shield. Closing it
* requires nothing; opening a shield that has a pin code set prompts the player to type it in
* chat first, matching an iris/DHD access-code mechanic.
*/
public class GateIrisButtonListener implements Listener {
private final StargatePlugin plugin;
private final GateManager gateManager;
private final Map<UUID, PendingPin> pendingPins = new HashMap<>();
private static final class PendingPin {
final RuntimeGate gate;
BukkitTask timeoutTask;
PendingPin(RuntimeGate gate) { this.gate = gate; }
}
public GateIrisButtonListener(StargatePlugin plugin, GateManager gateManager) {
this.plugin = plugin;
this.gateManager = gateManager;
}
@EventHandler(ignoreCancelled = true)
public void onInteract(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
Block block = event.getClickedBlock();
if (block == null || !block.getType().name().endsWith("_BUTTON")) return;
Block signSpot = block.getRelative(BlockFace.UP);
RuntimeGate rg = gateManager.getBySign(signSpot);
if (rg == null) return;
Player player = event.getPlayer();
if (!player.hasPermission("stargate.use")) return;
event.setCancelled(true);
String pin = rg.getGate().getPinCode();
if (rg.getGate().isIrisClosed() && pin != null && !pin.isBlank()) {
beginPinEntry(player, rg);
return;
}
boolean nowClosed = gateManager.toggleIrisShield(rg);
player.sendMessage(Component.text("Iris " + (nowClosed ? "closed." : "opened."),
nowClosed ? NamedTextColor.RED : NamedTextColor.GREEN));
player.playSound(player.getLocation(), Sound.BLOCK_IRON_DOOR_OPEN, 1f, nowClosed ? 0.7f : 1.3f);
}
private void beginPinEntry(Player player, RuntimeGate rg) {
cancelPending(player.getUniqueId());
PendingPin pending = new PendingPin(rg);
int timeout = gateManager.getConfig().pinTimeoutSeconds;
pending.timeoutTask = Bukkit.getScheduler().runTaskLater(plugin, () -> {
pendingPins.remove(player.getUniqueId());
if (player.isOnline()) {
player.sendMessage(Component.text("Pin entry timed out.", NamedTextColor.RED));
}
}, timeout * 20L);
pendingPins.put(player.getUniqueId(), pending);
player.sendMessage(Component.text("This iris is locked. Type the pin code in chat within "
+ timeout + "s.", NamedTextColor.YELLOW));
}
private void cancelPending(UUID playerId) {
PendingPin pending = pendingPins.remove(playerId);
if (pending != null && pending.timeoutTask != null) {
pending.timeoutTask.cancel();
}
}
@EventHandler(ignoreCancelled = true)
public void onChat(AsyncChatEvent event) {
Player player = event.getPlayer();
PendingPin pending = pendingPins.get(player.getUniqueId());
if (pending == null) return;
event.setCancelled(true);
String typed = PlainTextComponentSerializer.plainText().serialize(event.message()).trim();
Bukkit.getScheduler().runTask(plugin, () -> {
String expected = pending.gate.getGate().getPinCode();
if (expected != null && expected.equals(typed)) {
cancelPending(player.getUniqueId());
gateManager.openIrisShield(pending.gate);
player.sendMessage(Component.text("Correct. Iris opened.", NamedTextColor.GREEN));
player.playSound(player.getLocation(), Sound.BLOCK_IRON_DOOR_OPEN, 1f, 1.3f);
} else {
player.sendMessage(Component.text("Incorrect pin.", NamedTextColor.RED));
}
});
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
cancelPending(event.getPlayer().getUniqueId());
}
}
@@ -6,18 +6,30 @@ import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
import dev.skywalker3200.stargate.paper.network.CrossServerBridge; import dev.skywalker3200.stargate.paper.network.CrossServerBridge;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.util.Vector;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID;
/** Walking into an open gate's water plane teleports the player to the connected gate. */ /**
* Walking into an open gate's event horizon teleports the player to the connected gate,
* arriving just past the destination's iris (never inside it - that would immediately
* re-trigger this same listener and bounce them straight back) and facing the same absolute
* direction they were already walking, so "enter forward" always means "exit forward".
*/
public class GateTeleportListener implements Listener { public class GateTeleportListener implements Listener {
private final StargatePlugin plugin; private final StargatePlugin plugin;
private final GateManager gateManager; private final GateManager gateManager;
private final CrossServerBridge crossServerBridge; private final CrossServerBridge crossServerBridge;
private final Map<UUID, Long> cooldownUntil = new HashMap<>();
private static final long COOLDOWN_MS = 1500;
public GateTeleportListener(StargatePlugin plugin, GateManager gateManager, CrossServerBridge crossServerBridge) { public GateTeleportListener(StargatePlugin plugin, GateManager gateManager, CrossServerBridge crossServerBridge) {
this.plugin = plugin; this.plugin = plugin;
@@ -32,6 +44,10 @@ public class GateTeleportListener implements Listener {
if (event.getFrom().getBlockX() == to.getBlockX() && event.getFrom().getBlockY() == to.getBlockY() if (event.getFrom().getBlockX() == to.getBlockX() && event.getFrom().getBlockY() == to.getBlockY()
&& event.getFrom().getBlockZ() == to.getBlockZ()) return; && event.getFrom().getBlockZ() == to.getBlockZ()) return;
Player player = event.getPlayer();
Long until = cooldownUntil.get(player.getUniqueId());
if (until != null && System.currentTimeMillis() < until) return;
Block standing = to.getBlock(); Block standing = to.getBlock();
List<RuntimeGate> gates = gateManager.all(); List<RuntimeGate> gates = gateManager.all();
for (RuntimeGate rg : gates) { for (RuntimeGate rg : gates) {
@@ -39,28 +55,33 @@ public class GateTeleportListener implements Listener {
for (Block iris : rg.getStructure().getIris()) { for (Block iris : rg.getStructure().getIris()) {
if (iris.getX() == standing.getX() && iris.getY() == standing.getY() && iris.getZ() == standing.getZ() if (iris.getX() == standing.getX() && iris.getY() == standing.getY() && iris.getZ() == standing.getZ()
&& iris.getWorld().equals(standing.getWorld())) { && iris.getWorld().equals(standing.getWorld())) {
teleport(event.getPlayer(), rg); teleport(player, rg);
return; return;
} }
} }
} }
} }
private void teleport(org.bukkit.entity.Player player, RuntimeGate entered) { private void teleport(Player player, RuntimeGate entered) {
RuntimeGate dest = entered.getConnectedTo(); RuntimeGate dest = entered.getConnectedTo();
if (dest == null) return; if (dest == null) return;
Location loc = player.getLocation();
Vector direction = loc.getDirection();
Vector velocity = player.getVelocity();
boolean remote = !dest.getGate().getServerId().equals(plugin.getServerId()); boolean remote = !dest.getGate().getServerId().equals(plugin.getServerId());
if (remote) { if (remote) {
crossServerBridge.sendPlayerThroughGate(player, dest.getGate()); crossServerBridge.sendPlayerThroughGate(player, dest.getGate(), loc.getYaw(), loc.getPitch());
return; return;
} }
var world = org.bukkit.Bukkit.getWorld(dest.getGate().getWorld()); Location landing = gateManager.computeSafeLanding(dest, direction, loc.getYaw(), loc.getPitch());
if (world == null) return; if (landing == null) return;
Location exit = new Location(world, dest.getGate().getExitX() + 0.5, dest.getGate().getExitY(),
dest.getGate().getExitZ() + 0.5, dest.getGate().getExitYaw(), 0f); cooldownUntil.put(player.getUniqueId(), System.currentTimeMillis() + COOLDOWN_MS);
player.teleport(exit); player.teleport(landing);
player.playSound(exit, org.bukkit.Sound.ENTITY_ENDERMAN_TELEPORT, 1f, 1f); player.setVelocity(velocity);
player.playSound(landing, org.bukkit.Sound.ENTITY_ENDERMAN_TELEPORT, 1f, 1f);
} }
} }
@@ -6,11 +6,12 @@ import dev.skywalker3200.stargate.common.model.Gate;
import dev.skywalker3200.stargate.common.network.StargateChannel; import dev.skywalker3200.stargate.common.network.StargateChannel;
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 org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.PluginMessageListener; import org.bukkit.plugin.messaging.PluginMessageListener;
import org.bukkit.util.Vector;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.DataInputStream; import java.io.DataInputStream;
@@ -19,7 +20,7 @@ import java.util.UUID;
/** /**
* Talks to the stargate-velocity / stargate-bungee companion plugin over plugin messaging so a * Talks to the stargate-velocity / stargate-bungee companion plugin over plugin messaging so a
* player dialing a gate hosted on another backend server actually gets moved there and then * player dialing a gate hosted on another backend server actually gets moved there and then
* warped to the right spot once they land. * warped to the right spot once they land, facing the same direction they entered with.
*/ */
public class CrossServerBridge implements PluginMessageListener { public class CrossServerBridge implements PluginMessageListener {
@@ -43,14 +44,16 @@ public class CrossServerBridge implements PluginMessageListener {
return enabled; return enabled;
} }
/** Asks the proxy to move this player to the backend server hosting {@code destGate}. */ /** Asks the proxy to move this player to the backend server hosting {@code destGate}, carrying their look direction along. */
public void sendPlayerThroughGate(Player player, Gate destGate) { public void sendPlayerThroughGate(Player player, Gate destGate, float yaw, float pitch) {
if (!enabled) return; if (!enabled) return;
ByteArrayDataOutput out = ByteStreams.newDataOutput(); ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeByte(StargateChannel.OP_TELEPORT_REQUEST); out.writeByte(StargateChannel.OP_TELEPORT_REQUEST);
out.writeUTF(player.getUniqueId().toString()); out.writeUTF(player.getUniqueId().toString());
out.writeUTF(destGate.getServerId()); out.writeUTF(destGate.getServerId());
out.writeUTF(destGate.getId().toString()); out.writeUTF(destGate.getId().toString());
out.writeFloat(yaw);
out.writeFloat(pitch);
player.sendPluginMessage(plugin, StargateChannel.CHANNEL, out.toByteArray()); player.sendPluginMessage(plugin, StargateChannel.CHANNEL, out.toByteArray());
} }
@@ -64,21 +67,23 @@ public class CrossServerBridge implements PluginMessageListener {
UUID playerId = UUID.fromString(in.readUTF()); UUID playerId = UUID.fromString(in.readUTF());
UUID gateId = UUID.fromString(in.readUTF()); UUID gateId = UUID.fromString(in.readUTF());
float yaw = in.readFloat();
float pitch = in.readFloat();
var rg = gateManager.getById(gateId); RuntimeGate rg = gateManager.getById(gateId);
if (rg == null) { if (rg == null) {
plugin.getLogger().warning("[Stargate] Received teleport-deliver for unknown gate " + gateId); plugin.getLogger().warning("[Stargate] Received teleport-deliver for unknown gate " + gateId);
return; return;
} }
Gate gate = rg.getGate();
World world = Bukkit.getWorld(gate.getWorld());
if (world == null) return;
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
Player p = Bukkit.getPlayer(playerId); Player p = Bukkit.getPlayer(playerId);
if (p == null) return; if (p == null) return;
Location exit = new Location(world, gate.getExitX() + 0.5, gate.getExitY(), gate.getExitZ() + 0.5, gate.getExitYaw(), 0f); double yawRad = Math.toRadians(yaw);
p.teleport(exit); Vector direction = new Vector(-Math.sin(yawRad), 0, Math.cos(yawRad));
Location landing = gateManager.computeSafeLanding(rg, direction, yaw, pitch);
if (landing == null) return;
p.teleport(landing);
}); });
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("[Stargate] Failed to handle cross-server teleport message: " + e.getMessage()); plugin.getLogger().warning("[Stargate] Failed to handle cross-server teleport message: " + e.getMessage());
+11 -5
View File
@@ -43,12 +43,15 @@ gate:
# than their ring-neighbours - the "elbow" points of the shape, same as a real # than their ring-neighbours - the "elbow" points of the shape, same as a real
# Stargate's chevron placement - then swaps just those to chevron-lit-material while # Stargate's chevron placement - then swaps just those to chevron-lit-material while
# dialing/open, restoring whatever they looked like at rest when the gate closes. # dialing/open, restoring whatever they looked like at rest when the gate closes.
chevron-lit-material: GILDED_BLACKSTONE chevron-lit-material: GLOWSTONE
# The interior ("iris") reflects whether the gate is usable. Closed/idle, it's solid # The interior has three looks, matching SG-1: idle (not connected) is just empty
# bedrock - nothing can walk or fall through it. Only while open does it become # air - you can see straight through the ring. Connected, it's the event horizon
# iris-open-material (the "event horizon") and let players/items pass. # (iris-open-material) UNLESS the iris shield is closed, in which case it shows
# iris-shield-material instead and blocks all travel even though the gate is active.
# The shield is toggled with a button placed directly below the control sign.
iris-idle-material: AIR
iris-open-material: WATER iris-open-material: WATER
iris-closed-material: BEDROCK iris-shield-material: IRON_BLOCK
max-frame-blocks: 300 max-frame-blocks: 300
max-iris-blocks: 200 max-iris-blocks: 200
min-frame-blocks: 8 min-frame-blocks: 8
@@ -58,6 +61,9 @@ gate:
link-timeout-seconds: 30 link-timeout-seconds: 30
# How far (in blocks) the punched frame block may be from the sign. # How far (in blocks) the punched frame block may be from the sign.
max-link-distance: 64 max-link-distance: 64
# If a gate has a pin code set (/sg pin <code>), this is how long a player has to
# type it in chat after pressing the iris button while the shield is closed.
pin-timeout-seconds: 20
dialing: 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.
@@ -38,7 +38,7 @@ public class StargateVelocityPlugin {
// playerId -> (targetServerName, gateId) pending until they finish connecting // playerId -> (targetServerName, gateId) pending until they finish connecting
private final Map<UUID, PendingWarp> pending = new ConcurrentHashMap<>(); private final Map<UUID, PendingWarp> pending = new ConcurrentHashMap<>();
private record PendingWarp(String gateId) {} private record PendingWarp(String gateId, float yaw, float pitch) {}
@Inject @Inject
public StargateVelocityPlugin(ProxyServer server, Logger logger) { public StargateVelocityPlugin(ProxyServer server, Logger logger) {
@@ -66,6 +66,8 @@ public class StargateVelocityPlugin {
UUID playerId = UUID.fromString(in.readUTF()); UUID playerId = UUID.fromString(in.readUTF());
String targetServer = in.readUTF(); String targetServer = in.readUTF();
String gateId = in.readUTF(); String gateId = in.readUTF();
float yaw = in.readFloat();
float pitch = in.readFloat();
Optional<Player> playerOpt = server.getPlayer(playerId); Optional<Player> playerOpt = server.getPlayer(playerId);
Optional<RegisteredServer> targetOpt = server.getServer(targetServer); Optional<RegisteredServer> targetOpt = server.getServer(targetServer);
@@ -74,7 +76,7 @@ public class StargateVelocityPlugin {
return; return;
} }
pending.put(playerId, new PendingWarp(gateId)); pending.put(playerId, new PendingWarp(gateId, yaw, pitch));
playerOpt.get().createConnectionRequest(targetOpt.get()).connectWithIndication().thenAccept(success -> { playerOpt.get().createConnectionRequest(targetOpt.get()).connectWithIndication().thenAccept(success -> {
if (Boolean.TRUE.equals(success)) { if (Boolean.TRUE.equals(success)) {
deliver(playerId, targetOpt.get()); deliver(playerId, targetOpt.get());
@@ -102,6 +104,8 @@ public class StargateVelocityPlugin {
out.writeByte(StargateChannel.OP_TELEPORT_DELIVER); out.writeByte(StargateChannel.OP_TELEPORT_DELIVER);
out.writeUTF(playerId.toString()); out.writeUTF(playerId.toString());
out.writeUTF(warp.gateId()); out.writeUTF(warp.gateId());
out.writeFloat(warp.yaw());
out.writeFloat(warp.pitch());
connection.get().sendPluginMessage(channel, bytes.toByteArray()); connection.get().sendPluginMessage(channel, bytes.toByteArray());
} catch (Exception e) { } catch (Exception e) {
logger.warn("Failed to deliver stargate teleport", e); logger.warn("Failed to deliver stargate teleport", e);