Initial Stargate plugin: Paper gate plugin + Velocity/Bungee cross-server bridges
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
+18
@@ -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>
|
||||
}
|
||||
+26
@@ -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);
|
||||
}
|
||||
+209
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user