Initial Stargate plugin: Paper gate plugin + Velocity/Bungee cross-server bridges

This commit is contained in:
Michael Burgess
2026-08-09 09:53:55 -04:00
commit 83a0392ee9
34 changed files with 2317 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
dependencies {
compileOnly("com.zaxxer:HikariCP:5.1.0")
implementation("com.zaxxer:HikariCP:5.1.0")
compileOnly("org.xerial:sqlite-jdbc:3.46.1.3")
implementation("org.xerial:sqlite-jdbc:3.46.1.3")
compileOnly("com.mysql:mysql-connector-j:8.4.0")
implementation("com.mysql:mysql-connector-j:8.4.0")
}
@@ -0,0 +1,92 @@
package dev.skywalker3200.stargate.common.model;
import java.util.EnumSet;
import java.util.Set;
import java.util.UUID;
/**
* A single stargate: its physical location, the sign that controls it, and the network
* it belongs to. Not tied to Bukkit types so it can be shared with the storage layer only.
*/
public class Gate {
public enum Flag {
PUBLIC, // visible to everyone when cycling destinations
HIDDEN, // only reachable by exact name, not shown while cycling
FIXED // destination cannot be changed by right-click; always dials the configured target
}
private final UUID id;
private String name;
private String network;
private String serverId;
private String world;
private int exitX;
private int exitY;
private int exitZ;
private float exitYaw;
private int signX;
private int signY;
private int signZ;
private String signWorld;
private String facing;
private UUID owner;
private final Set<Flag> flags;
private String fixedDestination;
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) {
this.id = id;
this.name = name;
this.network = network;
this.serverId = serverId;
this.world = world;
this.exitX = exitX;
this.exitY = exitY;
this.exitZ = exitZ;
this.exitYaw = exitYaw;
this.signX = signX;
this.signY = signY;
this.signZ = signZ;
this.signWorld = signWorld;
this.facing = facing;
this.owner = owner;
this.flags = flags == null ? EnumSet.noneOf(Flag.class) : flags;
this.fixedDestination = fixedDestination;
}
public UUID getId() { return id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getNetwork() { return network; }
public void setNetwork(String network) { this.network = network; }
public String getServerId() { return serverId; }
public void setServerId(String serverId) { this.serverId = serverId; }
public String getWorld() { return world; }
public void setWorld(String world) { this.world = world; }
public int getExitX() { return exitX; }
public int getExitY() { return exitY; }
public int getExitZ() { return exitZ; }
public float getExitYaw() { return exitYaw; }
public void setExit(int x, int y, int z, float yaw) { this.exitX = x; this.exitY = y; this.exitZ = z; this.exitYaw = yaw; }
public int getSignX() { return signX; }
public int getSignY() { return signY; }
public int getSignZ() { return signZ; }
public String getSignWorld() { return signWorld; }
public String getFacing() { return facing; }
public UUID getOwner() { return owner; }
public void setOwner(UUID owner) { this.owner = owner; }
public Set<Flag> getFlags() { return flags; }
public boolean isPublic() { return flags.contains(Flag.PUBLIC); }
public boolean isHidden() { return flags.contains(Flag.HIDDEN); }
public boolean isFixed() { return flags.contains(Flag.FIXED); }
public String getFixedDestination() { return fixedDestination; }
public void setFixedDestination(String fixedDestination) { this.fixedDestination = fixedDestination; }
/** Fully-qualified identity used for cross-server lookups: server/network/name */
public String qualifiedName() {
return serverId + "/" + network + "/" + name;
}
}
@@ -0,0 +1,18 @@
package dev.skywalker3200.stargate.common.network;
/**
* Shared plugin-messaging channel identifiers used between the Paper plugin and the
* Bungee/Velocity proxy companions to move a player to the backend server that hosts
* their destination gate.
*/
public final class StargateChannel {
private StargateChannel() {}
/** Modern namespaced channel (Paper/Velocity require this format). */
public static final String CHANNEL = "stargate:teleport";
/** Sub-channel byte sent first in the payload. */
public static final byte OP_TELEPORT_REQUEST = 1; // backend -> proxy: move this player to <server>, remember pending warp
public static final byte OP_TELEPORT_DELIVER = 2; // proxy -> new backend: this player just arrived, warp them to <gate id>
}
@@ -0,0 +1,26 @@
package dev.skywalker3200.stargate.common.storage;
import dev.skywalker3200.stargate.common.model.Gate;
import java.util.List;
import java.util.UUID;
/**
* Persistence for gates. Implementations back onto SQLite (single server) or MySQL
* (shared across a Bungee/Velocity network so every backend server sees the same gates).
*/
public interface GateStorage {
void init() throws Exception;
void close();
void saveGate(Gate gate);
void deleteGate(UUID id);
List<Gate> loadAll();
/** Reload gates belonging to other servers (call periodically when networked). */
List<Gate> loadAllForNetwork(String network);
}
@@ -0,0 +1,209 @@
package dev.skywalker3200.stargate.common.storage;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import dev.skywalker3200.stargate.common.model.Gate;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Logger;
/**
* JDBC-backed implementation of {@link GateStorage}. Works for both SQLite (file-based,
* one server) and MySQL (shared, used when networking multiple backend servers together
* behind Bungee/Velocity so they all see the same gate table).
*/
public class SqlGateStorage implements GateStorage {
public enum Driver { SQLITE, MYSQL }
private final Driver driver;
private final String jdbcUrl;
private final String user;
private final String pass;
private final String tablePrefix;
private final Logger logger;
private HikariDataSource dataSource;
public SqlGateStorage(Driver driver, String jdbcUrl, String user, String pass, String tablePrefix, Logger logger) {
this.driver = driver;
this.jdbcUrl = jdbcUrl;
this.user = user;
this.pass = pass;
this.tablePrefix = tablePrefix == null ? "stargate_" : tablePrefix;
this.logger = logger;
}
@Override
public void init() throws Exception {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(jdbcUrl);
if (driver == Driver.MYSQL) {
config.setUsername(user);
config.setPassword(pass);
config.setMaximumPoolSize(8);
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
} else {
config.setMaximumPoolSize(1); // SQLite is single-writer
}
this.dataSource = new HikariDataSource(config);
try (Connection c = dataSource.getConnection(); Statement s = c.createStatement()) {
s.executeUpdate("CREATE TABLE IF NOT EXISTS " + tablePrefix + "gates (" +
"id VARCHAR(36) PRIMARY KEY," +
"name VARCHAR(64) NOT NULL," +
"network VARCHAR(64) NOT NULL," +
"server_id VARCHAR(64) NOT NULL," +
"world VARCHAR(64) NOT NULL," +
"exit_x INTEGER NOT NULL," +
"exit_y INTEGER NOT NULL," +
"exit_z INTEGER NOT NULL," +
"exit_yaw REAL NOT NULL," +
"sign_x INTEGER NOT NULL," +
"sign_y INTEGER NOT NULL," +
"sign_z INTEGER NOT NULL," +
"sign_world VARCHAR(64) NOT NULL," +
"facing VARCHAR(16)," +
"owner VARCHAR(36)," +
"flags VARCHAR(128)," +
"fixed_destination VARCHAR(64)" +
")");
s.executeUpdate("CREATE INDEX IF NOT EXISTS idx_" + tablePrefix + "network ON " + tablePrefix + "gates(network)");
}
logger.info("[Stargate] Storage initialised (" + driver + ")");
}
@Override
public void close() {
if (dataSource != null) dataSource.close();
}
@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 (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
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 " +
"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)";
}
try (Connection c = dataSource.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, gate.getId().toString());
ps.setString(2, gate.getName());
ps.setString(3, gate.getNetwork());
ps.setString(4, gate.getServerId());
ps.setString(5, gate.getWorld());
ps.setInt(6, gate.getExitX());
ps.setInt(7, gate.getExitY());
ps.setInt(8, gate.getExitZ());
ps.setFloat(9, gate.getExitYaw());
ps.setInt(10, gate.getSignX());
ps.setInt(11, gate.getSignY());
ps.setInt(12, gate.getSignZ());
ps.setString(13, gate.getSignWorld());
ps.setString(14, gate.getFacing());
ps.setString(15, gate.getOwner() == null ? null : gate.getOwner().toString());
ps.setString(16, serializeFlags(gate.getFlags()));
ps.setString(17, gate.getFixedDestination());
ps.executeUpdate();
} catch (Exception e) {
logger.severe("[Stargate] Failed to save gate " + gate.getName() + ": " + e.getMessage());
}
}
@Override
public void deleteGate(UUID id) {
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("DELETE FROM " + tablePrefix + "gates WHERE id = ?")) {
ps.setString(1, id.toString());
ps.executeUpdate();
} catch (Exception e) {
logger.severe("[Stargate] Failed to delete gate " + id + ": " + e.getMessage());
}
}
@Override
public List<Gate> loadAll() {
List<Gate> gates = new ArrayList<>();
try (Connection c = dataSource.getConnection();
Statement s = c.createStatement();
ResultSet rs = s.executeQuery("SELECT * FROM " + tablePrefix + "gates")) {
while (rs.next()) {
gates.add(fromRow(rs));
}
} catch (Exception e) {
logger.severe("[Stargate] Failed to load gates: " + e.getMessage());
}
return gates;
}
@Override
public List<Gate> loadAllForNetwork(String network) {
List<Gate> gates = new ArrayList<>();
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("SELECT * FROM " + tablePrefix + "gates WHERE network = ?")) {
ps.setString(1, network);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
gates.add(fromRow(rs));
}
}
} catch (Exception e) {
logger.severe("[Stargate] Failed to load network " + network + ": " + e.getMessage());
}
return gates;
}
private Gate fromRow(ResultSet rs) throws Exception {
String ownerStr = rs.getString("owner");
String fixedDest = rs.getString("fixed_destination");
return new Gate(
UUID.fromString(rs.getString("id")),
rs.getString("name"),
rs.getString("network"),
rs.getString("server_id"),
rs.getString("world"),
rs.getInt("exit_x"), rs.getInt("exit_y"), rs.getInt("exit_z"), rs.getFloat("exit_yaw"),
rs.getInt("sign_x"), rs.getInt("sign_y"), rs.getInt("sign_z"), rs.getString("sign_world"),
rs.getString("facing"),
ownerStr == null ? null : UUID.fromString(ownerStr),
deserializeFlags(rs.getString("flags")),
fixedDest
);
}
private String serializeFlags(Set<Gate.Flag> flags) {
StringBuilder sb = new StringBuilder();
for (Gate.Flag f : flags) {
if (sb.length() > 0) sb.append(',');
sb.append(f.name());
}
return sb.toString();
}
private Set<Gate.Flag> deserializeFlags(String s) {
Set<Gate.Flag> flags = EnumSet.noneOf(Gate.Flag.class);
if (s == null || s.isEmpty()) return flags;
for (String part : s.split(",")) {
try {
flags.add(Gate.Flag.valueOf(part.trim()));
} catch (IllegalArgumentException ignored) {
}
}
return flags;
}
}