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
@@ -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() {
@@ -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) {