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:
@@ -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
|
||||
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`
|
||||
(default gilded blackstone) while dialing/open, and revert to whatever they looked
|
||||
like at rest when the gate closes - so a chevron can rest as plain obsidian and blend
|
||||
(default glowstone) while dialing/open, and revert to whatever they looked like at
|
||||
rest when the gate closes - so a chevron can rest as plain obsidian and blend
|
||||
invisibly into the frame until it lights up.
|
||||
|
||||
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:
|
||||
- Line 1: `[Stargate]`
|
||||
- Line 2: network name (blank = default network)
|
||||
- Line 3: gate name (blank = auto-generated)
|
||||
- Line 4: `hidden` to keep it out of the cycle list, `fixed:GateName` to lock this
|
||||
gate to always dial `GateName` (no right-click cycling), or blank
|
||||
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.
|
||||
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
|
||||
gates on the same network.
|
||||
- **Left-click** the sign: dials the shown destination — chevrons light in sequence,
|
||||
then the interior switches from `gate.iris-closed-material` (default bedrock - solid,
|
||||
nothing can pass through a closed/idle gate) to `gate.iris-open-material` (default
|
||||
water, the "event horizon"). Walk into it to teleport. It auto-closes after
|
||||
`dialing.open-seconds`, switching the interior back to bedrock.
|
||||
- **Left-click** the sign: dials the shown destination - chevrons light in sequence,
|
||||
then the interior switches from `gate.iris-idle-material` (default air - an idle gate
|
||||
is just an empty ring you can see straight through) to either the event horizon
|
||||
(`gate.iris-open-material`, default water) or, if the iris shield is closed, to
|
||||
`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
|
||||
|
||||
@@ -93,5 +106,8 @@ server's Stargate instance teleports them to the gate's exit point.
|
||||
## Commands
|
||||
|
||||
- `/sg list [network]`
|
||||
- `/sg networks`
|
||||
- `/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`
|
||||
|
||||
+11
-5
@@ -22,7 +22,9 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
/** BungeeCord counterpart of the Velocity bridge: same wire protocol, same job. */
|
||||
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
|
||||
public void onEnable() {
|
||||
@@ -44,6 +46,8 @@ public class StargateBungeePlugin extends Plugin implements Listener {
|
||||
UUID playerId = UUID.fromString(in.readUTF());
|
||||
String targetServer = in.readUTF();
|
||||
String gateId = in.readUTF();
|
||||
float yaw = in.readFloat();
|
||||
float pitch = in.readFloat();
|
||||
|
||||
ProxiedPlayer player = ProxyServer.getInstance().getPlayer(playerId);
|
||||
ServerInfo target = ProxyServer.getInstance().getServerInfo(targetServer);
|
||||
@@ -52,7 +56,7 @@ public class StargateBungeePlugin extends Plugin implements Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingGateByPlayer.put(playerId, gateId);
|
||||
pendingGateByPlayer.put(playerId, new PendingWarp(gateId, yaw, pitch));
|
||||
player.connect(target);
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("Failed to process stargate plugin message: " + e.getMessage());
|
||||
@@ -62,8 +66,8 @@ public class StargateBungeePlugin extends Plugin implements Listener {
|
||||
@EventHandler
|
||||
public void onServerConnected(ServerConnectedEvent event) {
|
||||
ProxiedPlayer player = event.getPlayer();
|
||||
String gateId = pendingGateByPlayer.remove(player.getUniqueId());
|
||||
if (gateId == null) return;
|
||||
PendingWarp warp = pendingGateByPlayer.remove(player.getUniqueId());
|
||||
if (warp == null) return;
|
||||
|
||||
Server server = event.getServer();
|
||||
try {
|
||||
@@ -71,7 +75,9 @@ public class StargateBungeePlugin extends Plugin implements Listener {
|
||||
DataOutputStream out = new DataOutputStream(bytes);
|
||||
out.writeByte(StargateChannel.OP_TELEPORT_DELIVER);
|
||||
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());
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("Failed to deliver stargate teleport: " + e.getMessage());
|
||||
|
||||
@@ -39,6 +39,12 @@ public class Gate {
|
||||
private int linkX;
|
||||
private int linkY;
|
||||
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,
|
||||
int exitX, int exitY, int exitZ, float exitYaw,
|
||||
@@ -97,6 +103,10 @@ public class Gate {
|
||||
public int getLinkX() { return linkX; }
|
||||
public int getLinkY() { return linkY; }
|
||||
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 */
|
||||
public String qualifiedName() {
|
||||
|
||||
+30
-12
@@ -78,18 +78,22 @@ public class SqlGateStorage implements GateStorage {
|
||||
"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" +
|
||||
"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)");
|
||||
migrateColumn(s, "link_x");
|
||||
migrateColumn(s, "link_y");
|
||||
migrateColumn(s, "link_z");
|
||||
migrateIntColumn(s, "link_x");
|
||||
migrateIntColumn(s, "link_y");
|
||||
migrateIntColumn(s, "link_z");
|
||||
migrateIntColumn(s, "iris_closed");
|
||||
migrateStringColumn(s, "pin_code", "VARCHAR(32)");
|
||||
}
|
||||
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) {
|
||||
/** Best-effort ALTER TABLE for databases created before this column existed; ignored if it's already there. */
|
||||
private void migrateIntColumn(Statement s, String column) {
|
||||
try {
|
||||
s.executeUpdate("ALTER TABLE " + tablePrefix + "gates ADD COLUMN " + column + " INTEGER NOT NULL DEFAULT 0");
|
||||
} 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
|
||||
public void close() {
|
||||
if (dataSource != null) dataSource.close();
|
||||
@@ -105,17 +117,18 @@ public class SqlGateStorage implements GateStorage {
|
||||
@Override
|
||||
public void saveGate(Gate gate) {
|
||||
String sql = "REPLACE INTO " + tablePrefix + "gates " +
|
||||
"(id,name,network,server_id,world,exit_x,exit_y,exit_z,exit_yaw,sign_x,sign_y,sign_z,sign_world,facing,owner,flags,fixed_destination,link_x,link_y,link_z) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
|
||||
"(id,name,network,server_id,world,exit_x,exit_y,exit_z,exit_yaw,sign_x,sign_y,sign_z,sign_world,facing,owner,flags,fixed_destination,link_x,link_y,link_z,iris_closed,pin_code) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
|
||||
if (driver == Driver.MYSQL) {
|
||||
sql = "INSERT INTO " + tablePrefix + "gates " +
|
||||
"(id,name,network,server_id,world,exit_x,exit_y,exit_z,exit_yaw,sign_x,sign_y,sign_z,sign_world,facing,owner,flags,fixed_destination,link_x,link_y,link_z) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE " +
|
||||
"(id,name,network,server_id,world,exit_x,exit_y,exit_z,exit_yaw,sign_x,sign_y,sign_z,sign_world,facing,owner,flags,fixed_destination,link_x,link_y,link_z,iris_closed,pin_code) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE " +
|
||||
"name=VALUES(name),network=VALUES(network),server_id=VALUES(server_id),world=VALUES(world)," +
|
||||
"exit_x=VALUES(exit_x),exit_y=VALUES(exit_y),exit_z=VALUES(exit_z),exit_yaw=VALUES(exit_yaw)," +
|
||||
"sign_x=VALUES(sign_x),sign_y=VALUES(sign_y),sign_z=VALUES(sign_z),sign_world=VALUES(sign_world)," +
|
||||
"facing=VALUES(facing),owner=VALUES(owner),flags=VALUES(flags),fixed_destination=VALUES(fixed_destination)," +
|
||||
"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)) {
|
||||
ps.setString(1, gate.getId().toString());
|
||||
@@ -138,6 +151,8 @@ public class SqlGateStorage implements GateStorage {
|
||||
ps.setInt(18, gate.getLinkX());
|
||||
ps.setInt(19, gate.getLinkY());
|
||||
ps.setInt(20, gate.getLinkZ());
|
||||
ps.setInt(21, gate.isIrisClosed() ? 1 : 0);
|
||||
ps.setString(22, gate.getPinCode());
|
||||
ps.executeUpdate();
|
||||
} catch (Exception e) {
|
||||
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 {
|
||||
String ownerStr = rs.getString("owner");
|
||||
String fixedDest = rs.getString("fixed_destination");
|
||||
return new Gate(
|
||||
Gate gate = new Gate(
|
||||
UUID.fromString(rs.getString("id")),
|
||||
rs.getString("name"),
|
||||
rs.getString("network"),
|
||||
@@ -204,6 +219,9 @@ public class SqlGateStorage implements GateStorage {
|
||||
fixedDest,
|
||||
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) {
|
||||
|
||||
@@ -48,6 +48,7 @@ public class StargatePlugin extends JavaPlugin {
|
||||
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);
|
||||
getServer().getPluginManager().registerEvents(new dev.skywalker3200.stargate.paper.listener.GateIrisButtonListener(this, gateManager), this);
|
||||
|
||||
StargateCommand command = new StargateCommand(this, gateManager);
|
||||
getCommand("stargate").setExecutor(command);
|
||||
|
||||
+31
-2
@@ -30,7 +30,7 @@ public class StargateCommand implements CommandExecutor, TabCompleter {
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -81,6 +81,35 @@ public class StargateCommand implements CommandExecutor, TabCompleter {
|
||||
gateManager.destroyGate(rg);
|
||||
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));
|
||||
}
|
||||
return true;
|
||||
@@ -89,7 +118,7 @@ public class StargateCommand implements CommandExecutor, TabCompleter {
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||
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()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ public class GateConfig {
|
||||
|
||||
public final Set<Material> frameMaterials;
|
||||
public final Material chevronLit;
|
||||
public final Material irisIdleMaterial;
|
||||
public final Material irisOpenMaterial;
|
||||
public final Material irisClosedMaterial;
|
||||
public final Material irisShieldMaterial;
|
||||
public final int maxFrameBlocks;
|
||||
public final int maxIrisBlocks;
|
||||
public final int minFrameBlocks;
|
||||
@@ -23,6 +24,7 @@ public class GateConfig {
|
||||
public final String defaultNetwork;
|
||||
public final int linkTimeoutSeconds;
|
||||
public final int maxLinkDistance;
|
||||
public final int pinTimeoutSeconds;
|
||||
|
||||
public GateConfig(FileConfiguration cfg, Logger logger) {
|
||||
Set<Material> materials = new HashSet<>();
|
||||
@@ -34,9 +36,10 @@ public class GateConfig {
|
||||
if (materials.isEmpty()) materials.add(Material.OBSIDIAN);
|
||||
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.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.maxIrisBlocks = cfg.getInt("gate.max-iris-blocks", 200);
|
||||
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.linkTimeoutSeconds = cfg.getInt("gate.link-timeout-seconds", 30);
|
||||
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) {
|
||||
|
||||
+87
-12
@@ -5,18 +5,22 @@ import dev.skywalker3200.stargate.common.storage.GateStorage;
|
||||
import dev.skywalker3200.stargate.paper.StargatePlugin;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.data.Levelled;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -57,10 +61,10 @@ public class GateManager {
|
||||
GateStructureScanner.ScanResult result = scanner.scanWithDiagnostics(linked);
|
||||
if (result.isSuccess()) {
|
||||
structure = result.structure;
|
||||
// a restart can't preserve "open" runtime state, so every gate comes
|
||||
// back closed - make sure the iris actually reflects that
|
||||
// a restart can't preserve "connected" runtime state, so every gate
|
||||
// comes back idle - make sure the interior actually reflects that
|
||||
for (Block b : structure.getIris()) {
|
||||
b.setType(config.irisClosedMaterial);
|
||||
b.setType(config.irisIdleMaterial);
|
||||
}
|
||||
} else {
|
||||
plugin.getLogger().warning("[Stargate] Could not re-scan structure for gate '" + gate.getName()
|
||||
@@ -175,7 +179,7 @@ public class GateManager {
|
||||
}
|
||||
GateStructure structure = result.structure;
|
||||
for (Block b : structure.getIris()) {
|
||||
b.setType(config.irisClosedMaterial);
|
||||
b.setType(config.irisIdleMaterial);
|
||||
}
|
||||
|
||||
Location exit = computeExitLocation(structure, clickedBlock.getWorld(), facing);
|
||||
@@ -288,13 +292,7 @@ public class GateManager {
|
||||
gate.setOpen(true);
|
||||
gate.setConnectedTo(connectedTo);
|
||||
if (gate.getStructure() != null) {
|
||||
for (Block b : gate.getStructure().getIris()) {
|
||||
b.setType(config.irisOpenMaterial);
|
||||
if (b.getBlockData() instanceof Levelled lvl) {
|
||||
lvl.setLevel(0);
|
||||
b.setBlockData(lvl);
|
||||
}
|
||||
}
|
||||
applyIrisMaterial(gate);
|
||||
for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) {
|
||||
c.getBlock().setType(config.chevronLit);
|
||||
}
|
||||
@@ -319,7 +317,7 @@ public class GateManager {
|
||||
gate.setConnectedTo(null);
|
||||
if (gate.getStructure() != null) {
|
||||
for (Block b : gate.getStructure().getIris()) {
|
||||
b.setType(config.irisClosedMaterial);
|
||||
b.setType(config.irisIdleMaterial);
|
||||
}
|
||||
for (GateStructure.ChevronSlot c : gate.getStructure().getChevrons()) {
|
||||
c.getBlock().setType(c.getRestingMaterial());
|
||||
@@ -335,4 +333,81 @@ public class GateManager {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+122
@@ -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());
|
||||
}
|
||||
}
|
||||
+31
-10
@@ -6,18 +6,30 @@ import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
|
||||
import dev.skywalker3200.stargate.paper.network.CrossServerBridge;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.HashMap;
|
||||
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 {
|
||||
|
||||
private final StargatePlugin plugin;
|
||||
private final GateManager gateManager;
|
||||
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) {
|
||||
this.plugin = plugin;
|
||||
@@ -32,6 +44,10 @@ public class GateTeleportListener implements Listener {
|
||||
if (event.getFrom().getBlockX() == to.getBlockX() && event.getFrom().getBlockY() == to.getBlockY()
|
||||
&& 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();
|
||||
List<RuntimeGate> gates = gateManager.all();
|
||||
for (RuntimeGate rg : gates) {
|
||||
@@ -39,28 +55,33 @@ public class GateTeleportListener implements Listener {
|
||||
for (Block iris : rg.getStructure().getIris()) {
|
||||
if (iris.getX() == standing.getX() && iris.getY() == standing.getY() && iris.getZ() == standing.getZ()
|
||||
&& iris.getWorld().equals(standing.getWorld())) {
|
||||
teleport(event.getPlayer(), rg);
|
||||
teleport(player, rg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void teleport(org.bukkit.entity.Player player, RuntimeGate entered) {
|
||||
private void teleport(Player player, RuntimeGate entered) {
|
||||
RuntimeGate dest = entered.getConnectedTo();
|
||||
if (dest == null) return;
|
||||
|
||||
Location loc = player.getLocation();
|
||||
Vector direction = loc.getDirection();
|
||||
Vector velocity = player.getVelocity();
|
||||
|
||||
boolean remote = !dest.getGate().getServerId().equals(plugin.getServerId());
|
||||
if (remote) {
|
||||
crossServerBridge.sendPlayerThroughGate(player, dest.getGate());
|
||||
crossServerBridge.sendPlayerThroughGate(player, dest.getGate(), loc.getYaw(), loc.getPitch());
|
||||
return;
|
||||
}
|
||||
|
||||
var world = org.bukkit.Bukkit.getWorld(dest.getGate().getWorld());
|
||||
if (world == null) return;
|
||||
Location exit = new Location(world, dest.getGate().getExitX() + 0.5, dest.getGate().getExitY(),
|
||||
dest.getGate().getExitZ() + 0.5, dest.getGate().getExitYaw(), 0f);
|
||||
player.teleport(exit);
|
||||
player.playSound(exit, org.bukkit.Sound.ENTITY_ENDERMAN_TELEPORT, 1f, 1f);
|
||||
Location landing = gateManager.computeSafeLanding(dest, direction, loc.getYaw(), loc.getPitch());
|
||||
if (landing == null) return;
|
||||
|
||||
cooldownUntil.put(player.getUniqueId(), System.currentTimeMillis() + COOLDOWN_MS);
|
||||
player.teleport(landing);
|
||||
player.setVelocity(velocity);
|
||||
player.playSound(landing, org.bukkit.Sound.ENTITY_ENDERMAN_TELEPORT, 1f, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-10
@@ -6,11 +6,12 @@ import dev.skywalker3200.stargate.common.model.Gate;
|
||||
import dev.skywalker3200.stargate.common.network.StargateChannel;
|
||||
import dev.skywalker3200.stargate.paper.StargatePlugin;
|
||||
import dev.skywalker3200.stargate.paper.gate.GateManager;
|
||||
import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.messaging.PluginMessageListener;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
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
|
||||
* 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 {
|
||||
|
||||
@@ -43,14 +44,16 @@ public class CrossServerBridge implements PluginMessageListener {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/** Asks the proxy to move this player to the backend server hosting {@code destGate}. */
|
||||
public void sendPlayerThroughGate(Player player, Gate 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, float yaw, float pitch) {
|
||||
if (!enabled) return;
|
||||
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||
out.writeByte(StargateChannel.OP_TELEPORT_REQUEST);
|
||||
out.writeUTF(player.getUniqueId().toString());
|
||||
out.writeUTF(destGate.getServerId());
|
||||
out.writeUTF(destGate.getId().toString());
|
||||
out.writeFloat(yaw);
|
||||
out.writeFloat(pitch);
|
||||
player.sendPluginMessage(plugin, StargateChannel.CHANNEL, out.toByteArray());
|
||||
}
|
||||
|
||||
@@ -64,21 +67,23 @@ public class CrossServerBridge implements PluginMessageListener {
|
||||
|
||||
UUID playerId = 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) {
|
||||
plugin.getLogger().warning("[Stargate] Received teleport-deliver for unknown gate " + gateId);
|
||||
return;
|
||||
}
|
||||
Gate gate = rg.getGate();
|
||||
World world = Bukkit.getWorld(gate.getWorld());
|
||||
if (world == null) return;
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
Player p = Bukkit.getPlayer(playerId);
|
||||
if (p == null) return;
|
||||
Location exit = new Location(world, gate.getExitX() + 0.5, gate.getExitY(), gate.getExitZ() + 0.5, gate.getExitYaw(), 0f);
|
||||
p.teleport(exit);
|
||||
double yawRad = Math.toRadians(yaw);
|
||||
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) {
|
||||
plugin.getLogger().warning("[Stargate] Failed to handle cross-server teleport message: " + e.getMessage());
|
||||
|
||||
@@ -43,12 +43,15 @@ gate:
|
||||
# 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
|
||||
# dialing/open, restoring whatever they looked like at rest when the gate closes.
|
||||
chevron-lit-material: GILDED_BLACKSTONE
|
||||
# The interior ("iris") reflects whether the gate is usable. Closed/idle, it's solid
|
||||
# bedrock - nothing can walk or fall through it. Only while open does it become
|
||||
# iris-open-material (the "event horizon") and let players/items pass.
|
||||
chevron-lit-material: GLOWSTONE
|
||||
# The interior has three looks, matching SG-1: idle (not connected) is just empty
|
||||
# air - you can see straight through the ring. Connected, it's the event horizon
|
||||
# (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-closed-material: BEDROCK
|
||||
iris-shield-material: IRON_BLOCK
|
||||
max-frame-blocks: 300
|
||||
max-iris-blocks: 200
|
||||
min-frame-blocks: 8
|
||||
@@ -58,6 +61,9 @@ gate:
|
||||
link-timeout-seconds: 30
|
||||
# How far (in blocks) the punched frame block may be from the sign.
|
||||
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:
|
||||
# Seconds the gate stays open (iris filled) before auto-closing if nobody walks through.
|
||||
|
||||
+6
-2
@@ -38,7 +38,7 @@ public class StargateVelocityPlugin {
|
||||
// playerId -> (targetServerName, gateId) pending until they finish connecting
|
||||
private final Map<UUID, PendingWarp> pending = new ConcurrentHashMap<>();
|
||||
|
||||
private record PendingWarp(String gateId) {}
|
||||
private record PendingWarp(String gateId, float yaw, float pitch) {}
|
||||
|
||||
@Inject
|
||||
public StargateVelocityPlugin(ProxyServer server, Logger logger) {
|
||||
@@ -66,6 +66,8 @@ public class StargateVelocityPlugin {
|
||||
UUID playerId = UUID.fromString(in.readUTF());
|
||||
String targetServer = in.readUTF();
|
||||
String gateId = in.readUTF();
|
||||
float yaw = in.readFloat();
|
||||
float pitch = in.readFloat();
|
||||
|
||||
Optional<Player> playerOpt = server.getPlayer(playerId);
|
||||
Optional<RegisteredServer> targetOpt = server.getServer(targetServer);
|
||||
@@ -74,7 +76,7 @@ public class StargateVelocityPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
pending.put(playerId, new PendingWarp(gateId));
|
||||
pending.put(playerId, new PendingWarp(gateId, yaw, pitch));
|
||||
playerOpt.get().createConnectionRequest(targetOpt.get()).connectWithIndication().thenAccept(success -> {
|
||||
if (Boolean.TRUE.equals(success)) {
|
||||
deliver(playerId, targetOpt.get());
|
||||
@@ -102,6 +104,8 @@ public class StargateVelocityPlugin {
|
||||
out.writeByte(StargateChannel.OP_TELEPORT_DELIVER);
|
||||
out.writeUTF(playerId.toString());
|
||||
out.writeUTF(warp.gateId());
|
||||
out.writeFloat(warp.yaw());
|
||||
out.writeFloat(warp.pitch());
|
||||
connection.get().sendPluginMessage(channel, bytes.toByteArray());
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to deliver stargate teleport", e);
|
||||
|
||||
Reference in New Issue
Block a user