Decouple the control sign from the gate frame: place then punch to link

Sign creation no longer scans the block it's mounted on. Instead,
placing a "[Stargate]" sign starts a pending link (shown as "Punch the
gate" on the sign) and the next block the owner left-clicks within
gate.link-timeout-seconds and gate.max-link-distance becomes the scan
seed. This lets the sign act like a DHD console standing apart from
the gate instead of being physically attached to the frame.

- Gate now stores the punched link block's coordinates (link_x/y/z)
  so restarts re-scan from there instead of deriving a seed from the
  sign's attachment, which no longer applies.
- New GateLinkListener handles the punch and reports scan failures
  via the existing diagnostic messages; also clears pending state on
  disconnect.
- Exit-location facing is now derived from the punched frame block's
  position relative to the iris, since a WallSign facing is no longer
  guaranteed to exist or be relevant.
This commit is contained in:
Michael Burgess
2026-08-09 10:32:46 -04:00
parent 89d4fa0b63
commit 317602c00e
11 changed files with 254 additions and 86 deletions
@@ -33,11 +33,18 @@ public class Gate {
private UUID owner;
private final Set<Flag> flags;
private String fixedDestination;
// The frame block the owner punched to link this gate's sign to its structure - the sign
// itself can sit anywhere (a DHD-style console away from the gate); this is what we re-scan
// from on server restart.
private int linkX;
private int linkY;
private int linkZ;
public Gate(UUID id, String name, String network, String serverId, String world,
int exitX, int exitY, int exitZ, float exitYaw,
int signX, int signY, int signZ, String signWorld, String facing,
UUID owner, Set<Flag> flags, String fixedDestination) {
UUID owner, Set<Flag> flags, String fixedDestination,
int linkX, int linkY, int linkZ) {
this.id = id;
this.name = name;
this.network = network;
@@ -55,6 +62,9 @@ public class Gate {
this.owner = owner;
this.flags = flags == null ? EnumSet.noneOf(Flag.class) : flags;
this.fixedDestination = fixedDestination;
this.linkX = linkX;
this.linkY = linkY;
this.linkZ = linkZ;
}
public UUID getId() { return id; }
@@ -84,6 +94,9 @@ public class Gate {
public boolean isFixed() { return flags.contains(Flag.FIXED); }
public String getFixedDestination() { return fixedDestination; }
public void setFixedDestination(String fixedDestination) { this.fixedDestination = fixedDestination; }
public int getLinkX() { return linkX; }
public int getLinkY() { return linkY; }
public int getLinkZ() { return linkZ; }
/** Fully-qualified identity used for cross-server lookups: server/network/name */
public String qualifiedName() {
@@ -75,13 +75,28 @@ public class SqlGateStorage implements GateStorage {
"facing VARCHAR(16)," +
"owner VARCHAR(36)," +
"flags VARCHAR(128)," +
"fixed_destination VARCHAR(64)" +
"fixed_destination VARCHAR(64)," +
"link_x INTEGER NOT NULL DEFAULT 0," +
"link_y INTEGER NOT NULL DEFAULT 0," +
"link_z INTEGER NOT NULL DEFAULT 0" +
")");
s.executeUpdate("CREATE INDEX IF NOT EXISTS idx_" + tablePrefix + "network ON " + tablePrefix + "gates(network)");
migrateColumn(s, "link_x");
migrateColumn(s, "link_y");
migrateColumn(s, "link_z");
}
logger.info("[Stargate] Storage initialised (" + driver + ")");
}
/** Best-effort ALTER TABLE for databases created before link_x/y/z existed; ignored if the column is already there. */
private void migrateColumn(Statement s, String column) {
try {
s.executeUpdate("ALTER TABLE " + tablePrefix + "gates ADD COLUMN " + column + " INTEGER NOT NULL DEFAULT 0");
} catch (Exception ignored) {
// column already exists
}
}
@Override
public void close() {
if (dataSource != null) dataSource.close();
@@ -90,16 +105,17 @@ 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) " +
"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) " +
"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) " +
"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) " +
"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)";
"facing=VALUES(facing),owner=VALUES(owner),flags=VALUES(flags),fixed_destination=VALUES(fixed_destination)," +
"link_x=VALUES(link_x),link_y=VALUES(link_y),link_z=VALUES(link_z)";
}
try (Connection c = dataSource.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, gate.getId().toString());
@@ -119,6 +135,9 @@ public class SqlGateStorage implements GateStorage {
ps.setString(15, gate.getOwner() == null ? null : gate.getOwner().toString());
ps.setString(16, serializeFlags(gate.getFlags()));
ps.setString(17, gate.getFixedDestination());
ps.setInt(18, gate.getLinkX());
ps.setInt(19, gate.getLinkY());
ps.setInt(20, gate.getLinkZ());
ps.executeUpdate();
} catch (Exception e) {
logger.severe("[Stargate] Failed to save gate " + gate.getName() + ": " + e.getMessage());
@@ -182,7 +201,8 @@ public class SqlGateStorage implements GateStorage {
rs.getString("facing"),
ownerStr == null ? null : UUID.fromString(ownerStr),
deserializeFlags(rs.getString("flags")),
fixedDest
fixedDest,
rs.getInt("link_x"), rs.getInt("link_y"), rs.getInt("link_z")
);
}