Initial Stargate plugin: Paper gate plugin + Velocity/Bungee cross-server bridges
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
plugins {
|
||||
id("com.gradleup.shadow") version "8.3.5"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":stargate-common"))
|
||||
compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
|
||||
implementation("com.zaxxer:HikariCP:5.1.0")
|
||||
implementation("org.xerial:sqlite-jdbc:3.46.1.3")
|
||||
implementation("com.mysql:mysql-connector-j:8.4.0")
|
||||
}
|
||||
|
||||
tasks.shadowJar {
|
||||
archiveClassifier.set("")
|
||||
relocate("com.zaxxer.hikari", "dev.skywalker3200.stargate.libs.hikari")
|
||||
relocate("org.sqlite", "dev.skywalker3200.stargate.libs.sqlite")
|
||||
relocate("com.mysql", "dev.skywalker3200.stargate.libs.mysql")
|
||||
}
|
||||
|
||||
tasks.build {
|
||||
dependsOn(tasks.shadowJar)
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
filesMatching("plugin.yml") {
|
||||
expand("version" to project.version)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package dev.skywalker3200.stargate.paper;
|
||||
|
||||
import dev.skywalker3200.stargate.common.storage.GateStorage;
|
||||
import dev.skywalker3200.stargate.common.storage.SqlGateStorage;
|
||||
import dev.skywalker3200.stargate.paper.command.StargateCommand;
|
||||
import dev.skywalker3200.stargate.paper.gate.GateManager;
|
||||
import dev.skywalker3200.stargate.paper.listener.SignInteractListener;
|
||||
import dev.skywalker3200.stargate.paper.listener.SignCreateListener;
|
||||
import dev.skywalker3200.stargate.paper.listener.StructureProtectListener;
|
||||
import dev.skywalker3200.stargate.paper.network.CrossServerBridge;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class StargatePlugin extends JavaPlugin {
|
||||
|
||||
private GateStorage storage;
|
||||
private GateManager gateManager;
|
||||
private CrossServerBridge crossServerBridge;
|
||||
private String serverId;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
saveDefaultConfig();
|
||||
this.serverId = getConfig().getString("server-id", "server1");
|
||||
|
||||
try {
|
||||
this.storage = buildStorage();
|
||||
this.storage.init();
|
||||
} catch (Exception e) {
|
||||
getLogger().severe("Failed to initialise storage, disabling Stargate: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
|
||||
this.gateManager = new GateManager(this, storage);
|
||||
this.gateManager.loadAll();
|
||||
|
||||
boolean crossServer = getConfig().getBoolean("cross-server.enabled", false);
|
||||
this.crossServerBridge = new CrossServerBridge(this, gateManager);
|
||||
if (crossServer) {
|
||||
this.crossServerBridge.register();
|
||||
}
|
||||
|
||||
getServer().getPluginManager().registerEvents(new SignCreateListener(this, gateManager), this);
|
||||
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);
|
||||
|
||||
StargateCommand command = new StargateCommand(this, gateManager);
|
||||
getCommand("stargate").setExecutor(command);
|
||||
getCommand("stargate").setTabCompleter(command);
|
||||
|
||||
getLogger().info("Stargate enabled. server-id=" + serverId + " cross-server=" + crossServer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (gateManager != null) {
|
||||
gateManager.closeAllGates();
|
||||
}
|
||||
if (storage != null) {
|
||||
storage.close();
|
||||
}
|
||||
}
|
||||
|
||||
private GateStorage buildStorage() {
|
||||
String type = getConfig().getString("storage.type", "sqlite").toLowerCase();
|
||||
if (type.equals("mysql")) {
|
||||
String host = getConfig().getString("storage.mysql.host", "localhost");
|
||||
int port = getConfig().getInt("storage.mysql.port", 3306);
|
||||
String db = getConfig().getString("storage.mysql.database", "stargate");
|
||||
String user = getConfig().getString("storage.mysql.username", "stargate");
|
||||
String pass = getConfig().getString("storage.mysql.password", "");
|
||||
String prefix = getConfig().getString("storage.mysql.table-prefix", "stargate_");
|
||||
String url = "jdbc:mysql://" + host + ":" + port + "/" + db + "?useSSL=false&autoReconnect=true";
|
||||
return new SqlGateStorage(SqlGateStorage.Driver.MYSQL, url, user, pass, prefix, getLogger());
|
||||
}
|
||||
File dataFile = new File(getDataFolder(), "gates.db");
|
||||
String url = "jdbc:sqlite:" + dataFile.getAbsolutePath();
|
||||
return new SqlGateStorage(SqlGateStorage.Driver.SQLITE, url, null, null, "stargate_", getLogger());
|
||||
}
|
||||
|
||||
public String getServerId() {
|
||||
return serverId;
|
||||
}
|
||||
|
||||
public GateManager getGateManager() {
|
||||
return gateManager;
|
||||
}
|
||||
|
||||
public CrossServerBridge getCrossServerBridge() {
|
||||
return crossServerBridge;
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package dev.skywalker3200.stargate.paper.command;
|
||||
|
||||
import dev.skywalker3200.stargate.paper.StargatePlugin;
|
||||
import dev.skywalker3200.stargate.paper.gate.GateManager;
|
||||
import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class StargateCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
private final StargatePlugin plugin;
|
||||
private final GateManager gateManager;
|
||||
|
||||
public StargateCommand(StargatePlugin plugin, GateManager gateManager) {
|
||||
this.plugin = plugin;
|
||||
this.gateManager = gateManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length == 0) {
|
||||
sender.sendMessage(Component.text("Usage: /sg <list|destroy|reload>", NamedTextColor.YELLOW));
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "reload" -> {
|
||||
if (!sender.hasPermission("stargate.admin")) {
|
||||
sender.sendMessage(Component.text("No permission.", NamedTextColor.RED));
|
||||
return true;
|
||||
}
|
||||
plugin.reloadConfig();
|
||||
gateManager.reloadConfig();
|
||||
gateManager.loadAll();
|
||||
sender.sendMessage(Component.text("Stargate reloaded.", NamedTextColor.GREEN));
|
||||
}
|
||||
case "list" -> {
|
||||
String network = args.length > 1 ? args[1] : gateManager.getConfig().defaultNetwork;
|
||||
List<RuntimeGate> gates = gateManager.getNetwork(network);
|
||||
sender.sendMessage(Component.text("Network '" + network + "' (" + gates.size() + " gate(s)):", NamedTextColor.AQUA));
|
||||
for (RuntimeGate rg : gates) {
|
||||
sender.sendMessage(Component.text(" - " + rg.getGate().getName() + " @ " + rg.getGate().getServerId()
|
||||
+ "/" + rg.getGate().getWorld(), NamedTextColor.GRAY));
|
||||
}
|
||||
}
|
||||
case "destroy" -> {
|
||||
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 destroy it.", NamedTextColor.RED));
|
||||
return true;
|
||||
}
|
||||
boolean allowed = player.hasPermission("stargate.admin")
|
||||
|| (rg.getGate().getOwner() != null && rg.getGate().getOwner().equals(player.getUniqueId()) && player.hasPermission("stargate.destroy"));
|
||||
if (!allowed) {
|
||||
player.sendMessage(Component.text("You can't destroy this stargate.", NamedTextColor.RED));
|
||||
return true;
|
||||
}
|
||||
gateManager.destroyGate(rg);
|
||||
player.sendMessage(Component.text("Destroyed '" + rg.getGate().getName() + "'.", NamedTextColor.YELLOW));
|
||||
}
|
||||
default -> sender.sendMessage(Component.text("Unknown subcommand.", NamedTextColor.RED));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||
if (args.length == 1) {
|
||||
return List.of("list", "destroy", "reload").stream()
|
||||
.filter(s -> s.startsWith(args[0].toLowerCase()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package dev.skywalker3200.stargate.paper.gate;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/** Typed view over the `gate:` / `dialing:` / `network:` sections of config.yml. */
|
||||
public class GateConfig {
|
||||
|
||||
public final Set<Material> frameMaterials;
|
||||
public final Material chevronUnlit;
|
||||
public final Material chevronLit;
|
||||
public final Material irisMaterial;
|
||||
public final int maxFrameBlocks;
|
||||
public final int maxIrisBlocks;
|
||||
public final int minFrameBlocks;
|
||||
public final int openSeconds;
|
||||
public final int chevronTickDelay;
|
||||
public final boolean playSounds;
|
||||
public final String defaultNetwork;
|
||||
|
||||
public GateConfig(FileConfiguration cfg, Logger logger) {
|
||||
Set<Material> materials = new HashSet<>();
|
||||
for (String s : cfg.getStringList("gate.frame-materials")) {
|
||||
Material m = Material.matchMaterial(s);
|
||||
if (m != null) materials.add(m);
|
||||
else logger.warning("[Stargate] Unknown frame material in config: " + s);
|
||||
}
|
||||
if (materials.isEmpty()) materials.add(Material.OBSIDIAN);
|
||||
this.frameMaterials = materials;
|
||||
|
||||
this.chevronUnlit = matOr(cfg.getString("gate.chevron-unlit-material"), Material.BLACK_STAINED_GLASS, logger);
|
||||
this.chevronLit = matOr(cfg.getString("gate.chevron-lit-material"), Material.GLOWSTONE, logger);
|
||||
this.irisMaterial = matOr(cfg.getString("gate.iris-material"), Material.WATER, 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);
|
||||
this.openSeconds = cfg.getInt("dialing.open-seconds", 10);
|
||||
this.chevronTickDelay = cfg.getInt("dialing.chevron-tick-delay", 4);
|
||||
this.playSounds = cfg.getBoolean("dialing.play-sounds", true);
|
||||
this.defaultNetwork = cfg.getString("network.default-network", "main");
|
||||
}
|
||||
|
||||
private Material matOr(String s, Material fallback, Logger logger) {
|
||||
if (s == null) return fallback;
|
||||
Material m = Material.matchMaterial(s);
|
||||
if (m == null) {
|
||||
logger.warning("[Stargate] Unknown material in config: " + s + ", using " + fallback);
|
||||
return fallback;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
package dev.skywalker3200.stargate.paper.gate;
|
||||
|
||||
import dev.skywalker3200.stargate.common.model.Gate;
|
||||
import dev.skywalker3200.stargate.common.storage.GateStorage;
|
||||
import dev.skywalker3200.stargate.paper.StargatePlugin;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.Levelled;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** Owns every gate known to this server: creation, lookup, dialing/animation, and teardown. */
|
||||
public class GateManager {
|
||||
|
||||
private final StargatePlugin plugin;
|
||||
private final GateStorage storage;
|
||||
private GateConfig config;
|
||||
private GateStructureScanner scanner;
|
||||
|
||||
private final Map<UUID, RuntimeGate> gatesById = new HashMap<>();
|
||||
private final Map<String, RuntimeGate> gatesBySignBlock = new HashMap<>(); // "world,x,y,z" -> gate
|
||||
|
||||
public GateManager(StargatePlugin plugin, GateStorage storage) {
|
||||
this.plugin = plugin;
|
||||
this.storage = storage;
|
||||
reloadConfig();
|
||||
}
|
||||
|
||||
public void reloadConfig() {
|
||||
this.config = new GateConfig(plugin.getConfig(), plugin.getLogger());
|
||||
this.scanner = new GateStructureScanner(config.frameMaterials, config.chevronUnlit, config.chevronLit,
|
||||
config.maxFrameBlocks, config.maxIrisBlocks, config.minFrameBlocks);
|
||||
}
|
||||
|
||||
public GateConfig getConfig() { return config; }
|
||||
|
||||
public void loadAll() {
|
||||
gatesById.clear();
|
||||
gatesBySignBlock.clear();
|
||||
for (Gate gate : storage.loadAll()) {
|
||||
GateStructure structure = null;
|
||||
if (gate.getServerId().equals(plugin.getServerId())) {
|
||||
World world = Bukkit.getWorld(gate.getSignWorld());
|
||||
if (world != null) {
|
||||
Block signBlock = world.getBlockAt(gate.getSignX(), gate.getSignY(), gate.getSignZ());
|
||||
Block attached = attachedFrameBlock(signBlock);
|
||||
if (attached != null) {
|
||||
structure = scanner.scan(attached);
|
||||
}
|
||||
if (structure == null) {
|
||||
plugin.getLogger().warning("[Stargate] Could not re-scan structure for gate '" + gate.getName() + "' - it may have been damaged.");
|
||||
}
|
||||
}
|
||||
}
|
||||
RuntimeGate rg = new RuntimeGate(gate, structure);
|
||||
gatesById.put(gate.getId(), rg);
|
||||
if (gate.getServerId().equals(plugin.getServerId())) {
|
||||
gatesBySignBlock.put(signKey(gate.getSignWorld(), gate.getSignX(), gate.getSignY(), gate.getSignZ()), rg);
|
||||
}
|
||||
}
|
||||
plugin.getLogger().info("[Stargate] Loaded " + gatesById.size() + " gate(s), " + gatesBySignBlock.size() + " local.");
|
||||
}
|
||||
|
||||
/** Given the sign block, finds the neighbouring block that belongs to the frame (the wall it's mounted on, or the block below for a sign post). */
|
||||
public Block attachedFrameBlock(Block signBlock) {
|
||||
org.bukkit.block.BlockState state = signBlock.getState();
|
||||
if (state.getBlockData() instanceof org.bukkit.block.data.type.WallSign wallSign) {
|
||||
return signBlock.getRelative(wallSign.getFacing().getOppositeFace());
|
||||
}
|
||||
// sign post or other: just probe all neighbours, scanner will validate
|
||||
return signBlock.getRelative(org.bukkit.block.BlockFace.DOWN);
|
||||
}
|
||||
|
||||
private String signKey(String world, int x, int y, int z) {
|
||||
return world + "," + x + "," + y + "," + z;
|
||||
}
|
||||
|
||||
public RuntimeGate getBySign(Block signBlock) {
|
||||
return gatesBySignBlock.get(signKey(signBlock.getWorld().getName(), signBlock.getX(), signBlock.getY(), signBlock.getZ()));
|
||||
}
|
||||
|
||||
public RuntimeGate getById(UUID id) { return gatesById.get(id); }
|
||||
|
||||
public List<RuntimeGate> getNetwork(String network) {
|
||||
return gatesById.values().stream()
|
||||
.filter(g -> g.getGate().getNetwork().equalsIgnoreCase(network))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<RuntimeGate> all() { return new ArrayList<>(gatesById.values()); }
|
||||
|
||||
/** Other dialable gates in the same network, excluding this one and hidden ones (owner can still see their own hidden gates). */
|
||||
public List<RuntimeGate> destinationsFor(RuntimeGate from) {
|
||||
return getNetwork(from.getGate().getNetwork()).stream()
|
||||
.filter(g -> !g.getGate().getId().equals(from.getGate().getId()))
|
||||
.filter(g -> !g.getGate().isHidden())
|
||||
.sorted((a, b) -> a.getGate().getName().compareToIgnoreCase(b.getGate().getName()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public RuntimeGate createGate(Block signBlock, String network, String name, UUID owner, EnumSet<Gate.Flag> flags) {
|
||||
Block attached = attachedFrameBlock(signBlock);
|
||||
if (attached == null) return null;
|
||||
GateStructure structure = scanner.scan(attached);
|
||||
if (structure == null) return null;
|
||||
|
||||
Location exit = computeExitLocation(structure, signBlock);
|
||||
Gate gate = new Gate(UUID.randomUUID(), name, network, plugin.getServerId(),
|
||||
exit.getWorld().getName(), exit.getBlockX(), exit.getBlockY(), exit.getBlockZ(), exit.getYaw(),
|
||||
signBlock.getX(), signBlock.getY(), signBlock.getZ(), signBlock.getWorld().getName(),
|
||||
signBlockFacing(signBlock), owner, flags, null);
|
||||
|
||||
RuntimeGate rg = new RuntimeGate(gate, structure);
|
||||
gatesById.put(gate.getId(), rg);
|
||||
gatesBySignBlock.put(signKey(gate.getSignWorld(), gate.getSignX(), gate.getSignY(), gate.getSignZ()), rg);
|
||||
storage.saveGate(gate);
|
||||
return rg;
|
||||
}
|
||||
|
||||
public void destroyGate(RuntimeGate rg) {
|
||||
closeGate(rg);
|
||||
gatesById.remove(rg.getGate().getId());
|
||||
gatesBySignBlock.remove(signKey(rg.getGate().getSignWorld(), rg.getGate().getSignX(), rg.getGate().getSignY(), rg.getGate().getSignZ()));
|
||||
storage.deleteGate(rg.getGate().getId());
|
||||
}
|
||||
|
||||
public void saveGate(RuntimeGate rg) {
|
||||
storage.saveGate(rg.getGate());
|
||||
}
|
||||
|
||||
private Location computeExitLocation(GateStructure structure, Block signBlock) {
|
||||
List<Block> iris = structure.getIris();
|
||||
World world = signBlock.getWorld();
|
||||
if (iris.isEmpty()) {
|
||||
return signBlock.getLocation().add(0, 0, 0);
|
||||
}
|
||||
long sumX = 0, sumZ = 0;
|
||||
int minY = Integer.MAX_VALUE;
|
||||
for (Block b : iris) {
|
||||
sumX += b.getX();
|
||||
sumZ += b.getZ();
|
||||
minY = Math.min(minY, b.getY());
|
||||
}
|
||||
double avgX = (double) sumX / iris.size() + 0.5;
|
||||
double avgZ = (double) sumZ / iris.size() + 0.5;
|
||||
float yaw = 0f;
|
||||
org.bukkit.block.BlockState state = signBlock.getState();
|
||||
if (state.getBlockData() instanceof org.bukkit.block.data.type.WallSign wallSign) {
|
||||
// face away from the wall the sign is mounted on, into the room the sign faces
|
||||
yaw = faceToYaw(wallSign.getFacing());
|
||||
}
|
||||
return new Location(world, avgX, minY + 1, avgZ, yaw, 0f);
|
||||
}
|
||||
|
||||
private float faceToYaw(org.bukkit.block.BlockFace face) {
|
||||
return switch (face) {
|
||||
case NORTH -> 180f;
|
||||
case SOUTH -> 0f;
|
||||
case EAST -> -90f;
|
||||
case WEST -> 90f;
|
||||
default -> 0f;
|
||||
};
|
||||
}
|
||||
|
||||
private String signBlockFacing(Block signBlock) {
|
||||
org.bukkit.block.BlockState state = signBlock.getState();
|
||||
if (state.getBlockData() instanceof org.bukkit.block.data.type.WallSign wallSign) {
|
||||
return wallSign.getFacing().name();
|
||||
}
|
||||
return "SELF";
|
||||
}
|
||||
|
||||
// ---- Dialing ----
|
||||
|
||||
/** Starts the chevron-lighting animation, then opens the gate and connects it to the destination. */
|
||||
public void dial(RuntimeGate from, RuntimeGate to, Player initiator) {
|
||||
if (from.isOpen()) closeGate(from);
|
||||
if (to.isOpen()) closeGate(to);
|
||||
|
||||
List<Block> chevrons = from.getStructure() != null ? from.getStructure().getChevrons() : List.of();
|
||||
int delay = Math.max(1, config.chevronTickDelay);
|
||||
|
||||
Runnable finish = () -> {
|
||||
openGate(from, to);
|
||||
openGate(to, from);
|
||||
};
|
||||
|
||||
if (chevrons.isEmpty()) {
|
||||
finish.run();
|
||||
return;
|
||||
}
|
||||
|
||||
final int[] i = {0};
|
||||
final org.bukkit.scheduler.BukkitTask[] taskHolder = new org.bukkit.scheduler.BukkitTask[1];
|
||||
taskHolder[0] = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
|
||||
if (i[0] >= chevrons.size()) {
|
||||
taskHolder[0].cancel();
|
||||
finish.run();
|
||||
return;
|
||||
}
|
||||
Block chevron = chevrons.get(i[0]);
|
||||
chevron.setType(config.chevronLit);
|
||||
if (config.playSounds) {
|
||||
chevron.getWorld().playSound(chevron.getLocation(), Sound.BLOCK_STONE_STEP, 1f, 1.4f);
|
||||
}
|
||||
i[0]++;
|
||||
}, 0L, delay);
|
||||
from.setDialTask(taskHolder[0]);
|
||||
}
|
||||
|
||||
public void openGate(RuntimeGate gate, RuntimeGate connectedTo) {
|
||||
gate.setOpen(true);
|
||||
gate.setConnectedTo(connectedTo);
|
||||
if (gate.getStructure() != null) {
|
||||
for (Block b : gate.getStructure().getIris()) {
|
||||
b.setType(config.irisMaterial);
|
||||
if (b.getBlockData() instanceof Levelled lvl) {
|
||||
lvl.setLevel(0);
|
||||
b.setBlockData(lvl);
|
||||
}
|
||||
}
|
||||
for (Block c : gate.getStructure().getChevrons()) {
|
||||
c.setType(config.chevronLit);
|
||||
}
|
||||
}
|
||||
if (config.playSounds) {
|
||||
World w = Bukkit.getWorld(gate.getGate().getWorld());
|
||||
if (w != null) {
|
||||
w.playSound(new Location(w, gate.getGate().getExitX(), gate.getGate().getExitY(), gate.getGate().getExitZ()),
|
||||
Sound.ENTITY_GENERIC_EXPLODE, 0.5f, 1.8f);
|
||||
}
|
||||
}
|
||||
if (gate.getCloseTask() != null) gate.getCloseTask().cancel();
|
||||
var task = Bukkit.getScheduler().runTaskLater(plugin, () -> closeGate(gate), config.openSeconds * 20L);
|
||||
gate.setCloseTask(task);
|
||||
}
|
||||
|
||||
public void closeGate(RuntimeGate gate) {
|
||||
if (gate.getDialTask() != null) { gate.getDialTask().cancel(); gate.setDialTask(null); }
|
||||
if (gate.getCloseTask() != null) { gate.getCloseTask().cancel(); gate.setCloseTask(null); }
|
||||
gate.setOpen(false);
|
||||
RuntimeGate other = gate.getConnectedTo();
|
||||
gate.setConnectedTo(null);
|
||||
if (gate.getStructure() != null) {
|
||||
for (Block b : gate.getStructure().getIris()) {
|
||||
b.setType(org.bukkit.Material.AIR);
|
||||
}
|
||||
for (Block c : gate.getStructure().getChevrons()) {
|
||||
c.setType(config.chevronUnlit);
|
||||
}
|
||||
}
|
||||
if (other != null && other.isOpen()) {
|
||||
closeGate(other);
|
||||
}
|
||||
}
|
||||
|
||||
public void closeAllGates() {
|
||||
for (RuntimeGate rg : new ArrayList<>(gatesById.values())) {
|
||||
if (rg.isOpen()) closeGate(rg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package dev.skywalker3200.stargate.paper.gate;
|
||||
|
||||
import org.bukkit.block.Block;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** The physical blocks that make up a scanned gate: frame ring, chevrons, and interior iris. */
|
||||
public class GateStructure {
|
||||
private final List<Block> frame;
|
||||
private final List<Block> chevrons;
|
||||
private final List<Block> iris;
|
||||
|
||||
public GateStructure(List<Block> frame, List<Block> chevrons, List<Block> iris) {
|
||||
this.frame = frame;
|
||||
this.chevrons = chevrons;
|
||||
this.iris = iris;
|
||||
}
|
||||
|
||||
public List<Block> getFrame() { return frame; }
|
||||
public List<Block> getChevrons() { return chevrons; }
|
||||
public List<Block> getIris() { return iris; }
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package dev.skywalker3200.stargate.paper.gate;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Flood-fill scanner that discovers a gate's physical structure starting from the block
|
||||
* a control sign is attached to: the frame ring, the chevron blocks embedded in it, and
|
||||
* the enclosed interior ("iris") that gets filled with water while the gate is open.
|
||||
*/
|
||||
public class GateStructureScanner {
|
||||
|
||||
private static final BlockFace[] NEIGHBORS = {
|
||||
BlockFace.UP, BlockFace.DOWN, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST
|
||||
};
|
||||
|
||||
private final Set<Material> frameMaterials;
|
||||
private final Material chevronUnlit;
|
||||
private final Material chevronLit;
|
||||
private final int maxFrameBlocks;
|
||||
private final int maxIrisBlocks;
|
||||
private final int minFrameBlocks;
|
||||
|
||||
public GateStructureScanner(Set<Material> frameMaterials, Material chevronUnlit, Material chevronLit,
|
||||
int maxFrameBlocks, int maxIrisBlocks, int minFrameBlocks) {
|
||||
this.frameMaterials = frameMaterials;
|
||||
this.chevronUnlit = chevronUnlit;
|
||||
this.chevronLit = chevronLit;
|
||||
this.maxFrameBlocks = maxFrameBlocks;
|
||||
this.maxIrisBlocks = maxIrisBlocks;
|
||||
this.minFrameBlocks = minFrameBlocks;
|
||||
}
|
||||
|
||||
private boolean isFrameMaterial(Material m) {
|
||||
return frameMaterials.contains(m) || m == chevronUnlit || m == chevronLit;
|
||||
}
|
||||
|
||||
private boolean isChevronMaterial(Material m) {
|
||||
return m == chevronUnlit || m == chevronLit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans outward from the given seed block (the block the sign is attached to).
|
||||
* Returns null if no valid enclosed structure is found.
|
||||
*/
|
||||
public GateStructure scan(Block seed) {
|
||||
if (!isFrameMaterial(seed.getType())) {
|
||||
// seed itself may be the wall block behind a sign that's part of a bigger build;
|
||||
// try its direct neighbors for the actual frame block.
|
||||
for (BlockFace face : NEIGHBORS) {
|
||||
Block b = seed.getRelative(face);
|
||||
if (isFrameMaterial(b.getType())) {
|
||||
seed = b;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isFrameMaterial(seed.getType())) return null;
|
||||
|
||||
Set<Long> frameKeys = new HashSet<>();
|
||||
List<Block> frameBlocks = new ArrayList<>();
|
||||
List<Block> chevronBlocks = new ArrayList<>();
|
||||
Deque<Block> queue = new ArrayDeque<>();
|
||||
queue.add(seed);
|
||||
frameKeys.add(key(seed));
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
Block cur = queue.poll();
|
||||
frameBlocks.add(cur);
|
||||
if (isChevronMaterial(cur.getType())) chevronBlocks.add(cur);
|
||||
if (frameBlocks.size() > maxFrameBlocks) return null;
|
||||
|
||||
for (BlockFace face : NEIGHBORS) {
|
||||
Block next = cur.getRelative(face);
|
||||
long k = key(next);
|
||||
if (frameKeys.contains(k)) continue;
|
||||
if (isFrameMaterial(next.getType())) {
|
||||
frameKeys.add(k);
|
||||
queue.add(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (frameBlocks.size() < minFrameBlocks) return null;
|
||||
|
||||
// Find an interior seed: a non-frame block adjacent to a frame block, that is not
|
||||
// solid (roughly the middle of the ring). We try several candidates and flood-fill
|
||||
// each; the first one that stays enclosed within maxIrisBlocks wins.
|
||||
Set<Long> triedSeeds = new HashSet<>();
|
||||
for (Block frameBlock : frameBlocks) {
|
||||
for (BlockFace face : NEIGHBORS) {
|
||||
Block candidate = frameBlock.getRelative(face);
|
||||
long ck = key(candidate);
|
||||
if (frameKeys.contains(ck) || triedSeeds.contains(ck)) continue;
|
||||
triedSeeds.add(ck);
|
||||
if (candidate.getType().isSolid()) continue;
|
||||
|
||||
List<Block> iris = floodInterior(candidate, frameKeys);
|
||||
if (iris != null && !iris.isEmpty()) {
|
||||
return new GateStructure(frameBlocks, chevronBlocks, iris);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Flood-fills non-frame blocks starting at seed; fails (returns null) if it escapes the frame boundary. */
|
||||
private List<Block> floodInterior(Block seed, Set<Long> frameKeys) {
|
||||
Set<Long> visited = new HashSet<>();
|
||||
List<Block> interior = new ArrayList<>();
|
||||
Deque<Block> queue = new ArrayDeque<>();
|
||||
queue.add(seed);
|
||||
visited.add(key(seed));
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
Block cur = queue.poll();
|
||||
interior.add(cur);
|
||||
if (interior.size() > maxIrisBlocks) return null;
|
||||
|
||||
for (BlockFace face : NEIGHBORS) {
|
||||
Block next = cur.getRelative(face);
|
||||
long k = key(next);
|
||||
if (visited.contains(k) || frameKeys.contains(k)) continue;
|
||||
if (next.getType().isSolid()) return null; // hit solid, non-frame block: not a clean ring
|
||||
visited.add(k);
|
||||
queue.add(next);
|
||||
}
|
||||
}
|
||||
return interior;
|
||||
}
|
||||
|
||||
private long key(Block b) {
|
||||
return (((long) b.getX() & 0x3FFFFFF) << 38) | (((long) (b.getY() + 512) & 0xFFF) << 26) | ((long) b.getZ() & 0x3FFFFFF);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package dev.skywalker3200.stargate.paper.gate;
|
||||
|
||||
import dev.skywalker3200.stargate.common.model.Gate;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
/** Runtime state for a gate on this server: the persisted model, its scanned blocks, and dial state. */
|
||||
public class RuntimeGate {
|
||||
|
||||
private final Gate gate;
|
||||
private GateStructure structure; // null if structure could not be (re)scanned
|
||||
private boolean open = false;
|
||||
private RuntimeGate connectedTo = null;
|
||||
private BukkitTask closeTask;
|
||||
private BukkitTask dialTask;
|
||||
private int cycleIndex = 0;
|
||||
|
||||
public RuntimeGate(Gate gate, GateStructure structure) {
|
||||
this.gate = gate;
|
||||
this.structure = structure;
|
||||
}
|
||||
|
||||
public Gate getGate() { return gate; }
|
||||
public GateStructure getStructure() { return structure; }
|
||||
public void setStructure(GateStructure structure) { this.structure = structure; }
|
||||
public boolean isOpen() { return open; }
|
||||
public void setOpen(boolean open) { this.open = open; }
|
||||
public RuntimeGate getConnectedTo() { return connectedTo; }
|
||||
public void setConnectedTo(RuntimeGate connectedTo) { this.connectedTo = connectedTo; }
|
||||
public BukkitTask getCloseTask() { return closeTask; }
|
||||
public void setCloseTask(BukkitTask closeTask) { this.closeTask = closeTask; }
|
||||
public BukkitTask getDialTask() { return dialTask; }
|
||||
public void setDialTask(BukkitTask dialTask) { this.dialTask = dialTask; }
|
||||
public int getCycleIndex() { return cycleIndex; }
|
||||
public void setCycleIndex(int cycleIndex) { this.cycleIndex = cycleIndex; }
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
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 dev.skywalker3200.stargate.paper.network.CrossServerBridge;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Walking into an open gate's water plane teleports the player to the connected gate. */
|
||||
public class GateTeleportListener implements Listener {
|
||||
|
||||
private final StargatePlugin plugin;
|
||||
private final GateManager gateManager;
|
||||
private final CrossServerBridge crossServerBridge;
|
||||
|
||||
public GateTeleportListener(StargatePlugin plugin, GateManager gateManager, CrossServerBridge crossServerBridge) {
|
||||
this.plugin = plugin;
|
||||
this.gateManager = gateManager;
|
||||
this.crossServerBridge = crossServerBridge;
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onMove(PlayerMoveEvent event) {
|
||||
Location to = event.getTo();
|
||||
if (to == null) return;
|
||||
if (event.getFrom().getBlockX() == to.getBlockX() && event.getFrom().getBlockY() == to.getBlockY()
|
||||
&& event.getFrom().getBlockZ() == to.getBlockZ()) return;
|
||||
|
||||
Block standing = to.getBlock();
|
||||
List<RuntimeGate> gates = gateManager.all();
|
||||
for (RuntimeGate rg : gates) {
|
||||
if (!rg.isOpen() || rg.getStructure() == null || rg.getConnectedTo() == null) continue;
|
||||
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);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void teleport(org.bukkit.entity.Player player, RuntimeGate entered) {
|
||||
RuntimeGate dest = entered.getConnectedTo();
|
||||
if (dest == null) return;
|
||||
|
||||
boolean remote = !dest.getGate().getServerId().equals(plugin.getServerId());
|
||||
if (remote) {
|
||||
crossServerBridge.sendPlayerThroughGate(player, dest.getGate());
|
||||
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);
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package dev.skywalker3200.stargate.paper.listener;
|
||||
|
||||
import dev.skywalker3200.stargate.common.model.Gate;
|
||||
import dev.skywalker3200.stargate.paper.StargatePlugin;
|
||||
import dev.skywalker3200.stargate.paper.gate.GateManager;
|
||||
import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
|
||||
import dev.skywalker3200.stargate.paper.util.SignRenderer;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.SignChangeEvent;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Handles turning a freshly-placed sign reading "[Stargate]" into a registered gate. */
|
||||
public class SignCreateListener implements Listener {
|
||||
|
||||
private static final String HEADER = "[stargate]";
|
||||
|
||||
private final StargatePlugin plugin;
|
||||
private final GateManager gateManager;
|
||||
|
||||
public SignCreateListener(StargatePlugin plugin, GateManager gateManager) {
|
||||
this.plugin = plugin;
|
||||
this.gateManager = gateManager;
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onSignChange(SignChangeEvent event) {
|
||||
String line0 = stripped(event.line(0));
|
||||
if (line0 == null || !line0.equalsIgnoreCase(HEADER)) return;
|
||||
|
||||
var player = event.getPlayer();
|
||||
if (!player.hasPermission("stargate.create")) {
|
||||
player.sendMessage(Component.text("You don't have permission to create stargates.", NamedTextColor.RED));
|
||||
resetLine(event);
|
||||
return;
|
||||
}
|
||||
|
||||
String network = stripped(event.line(1));
|
||||
if (network == null || network.isBlank()) network = gateManager.getConfig().defaultNetwork;
|
||||
|
||||
String name = stripped(event.line(2));
|
||||
if (name == null || name.isBlank()) name = "Gate-" + Integer.toHexString((int) (Math.random() * 0xFFFF));
|
||||
|
||||
EnumSet<Gate.Flag> flags = EnumSet.of(Gate.Flag.PUBLIC);
|
||||
String flagLine = stripped(event.line(3));
|
||||
if (flagLine != null) {
|
||||
if (flagLine.equalsIgnoreCase("hidden")) { flags.clear(); flags.add(Gate.Flag.HIDDEN); }
|
||||
}
|
||||
|
||||
RuntimeGate rg = gateManager.createGate(event.getBlock(), network, name, player.getUniqueId(), flags);
|
||||
if (rg == null) {
|
||||
player.sendMessage(Component.text("No valid gate structure found. Build the frame first, then place the sign.", NamedTextColor.RED));
|
||||
resetLine(event);
|
||||
return;
|
||||
}
|
||||
|
||||
player.sendMessage(Component.text("Stargate '" + name + "' created on network '" + network + "'.", NamedTextColor.AQUA));
|
||||
SignRenderer.render(event, rg, gateManager);
|
||||
}
|
||||
|
||||
private String stripped(Component c) {
|
||||
if (c == null) return null;
|
||||
return net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText().serialize(c);
|
||||
}
|
||||
|
||||
private void resetLine(SignChangeEvent event) {
|
||||
event.line(0, Component.text(""));
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
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 dev.skywalker3200.stargate.paper.network.CrossServerBridge;
|
||||
import dev.skywalker3200.stargate.paper.util.SignRenderer;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.Sign;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Right-click a gate's sign to cycle its destination, left-click to dial it. */
|
||||
public class SignInteractListener implements Listener {
|
||||
|
||||
private final StargatePlugin plugin;
|
||||
private final GateManager gateManager;
|
||||
private final CrossServerBridge crossServerBridge;
|
||||
|
||||
public SignInteractListener(StargatePlugin plugin, GateManager gateManager, CrossServerBridge crossServerBridge) {
|
||||
this.plugin = plugin;
|
||||
this.gateManager = gateManager;
|
||||
this.crossServerBridge = crossServerBridge;
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onInteract(PlayerInteractEvent event) {
|
||||
Block block = event.getClickedBlock();
|
||||
if (block == null || !(block.getState() instanceof Sign)) return;
|
||||
RuntimeGate rg = gateManager.getBySign(block);
|
||||
if (rg == null) return;
|
||||
|
||||
var player = event.getPlayer();
|
||||
if (!player.hasPermission("stargate.use")) return;
|
||||
event.setCancelled(true);
|
||||
|
||||
if (event.getAction() == Action.RIGHT_CLICK_BLOCK) {
|
||||
List<RuntimeGate> destinations = gateManager.destinationsFor(rg);
|
||||
if (destinations.isEmpty()) {
|
||||
player.sendMessage(Component.text("No other gates on network '" + rg.getGate().getNetwork() + "'.", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
if (rg.getGate().isFixed()) {
|
||||
player.sendMessage(Component.text("This gate's destination is fixed.", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
rg.setCycleIndex(rg.getCycleIndex() + 1);
|
||||
SignRenderer.render(rg, gateManager);
|
||||
player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 1f, 1f);
|
||||
} else if (event.getAction() == Action.LEFT_CLICK_BLOCK) {
|
||||
dial(rg, player);
|
||||
}
|
||||
}
|
||||
|
||||
private void dial(RuntimeGate from, org.bukkit.entity.Player player) {
|
||||
if (from.isOpen()) {
|
||||
player.sendMessage(Component.text("This gate is already active.", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
List<RuntimeGate> destinations = gateManager.destinationsFor(from);
|
||||
RuntimeGate to;
|
||||
if (from.getGate().isFixed()) {
|
||||
to = destinations.stream()
|
||||
.filter(g -> g.getGate().getName().equalsIgnoreCase(from.getGate().getFixedDestination()))
|
||||
.findFirst().orElse(null);
|
||||
} else {
|
||||
if (destinations.isEmpty()) {
|
||||
player.sendMessage(Component.text("No destinations available.", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
int idx = ((from.getCycleIndex() % destinations.size()) + destinations.size()) % destinations.size();
|
||||
to = destinations.get(idx);
|
||||
}
|
||||
if (to == null) {
|
||||
player.sendMessage(Component.text("Destination gate not found.", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
if (to.isOpen()) {
|
||||
player.sendMessage(Component.text("Destination gate is busy.", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
boolean remote = !to.getGate().getServerId().equals(plugin.getServerId());
|
||||
if (remote && !crossServerBridge.isEnabled()) {
|
||||
player.sendMessage(Component.text("Cross-server dialing is disabled on this server.", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
player.sendMessage(Component.text("Dialing " + to.getGate().getName() + "...", NamedTextColor.AQUA));
|
||||
gateManager.dial(from, to, player);
|
||||
SignRenderer.render(from, gateManager);
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package dev.skywalker3200.stargate.paper.listener;
|
||||
|
||||
import dev.skywalker3200.stargate.paper.gate.GateManager;
|
||||
import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Breaking a gate's sign, frame, or chevron blocks destroys its registration (owner/admin only). */
|
||||
public class StructureProtectListener implements Listener {
|
||||
|
||||
private final GateManager gateManager;
|
||||
|
||||
public StructureProtectListener(GateManager gateManager) {
|
||||
this.gateManager = gateManager;
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onBreak(BlockBreakEvent event) {
|
||||
Block block = event.getBlock();
|
||||
RuntimeGate rg = gateManager.getBySign(block);
|
||||
boolean isSign = rg != null;
|
||||
|
||||
if (!isSign) {
|
||||
rg = findByFrameBlock(block);
|
||||
}
|
||||
if (rg == null) return;
|
||||
|
||||
UUID owner = rg.getGate().getOwner();
|
||||
var player = event.getPlayer();
|
||||
boolean allowed = player.hasPermission("stargate.admin")
|
||||
|| (owner != null && owner.equals(player.getUniqueId()) && player.hasPermission("stargate.destroy"));
|
||||
if (!allowed) {
|
||||
event.setCancelled(true);
|
||||
player.sendMessage(Component.text("You can't break this stargate.", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
gateManager.destroyGate(rg);
|
||||
player.sendMessage(Component.text("Stargate '" + rg.getGate().getName() + "' destroyed.", NamedTextColor.YELLOW));
|
||||
}
|
||||
|
||||
private RuntimeGate findByFrameBlock(Block block) {
|
||||
for (RuntimeGate rg : gateManager.all()) {
|
||||
if (rg.getStructure() == null) continue;
|
||||
List<Block> frame = rg.getStructure().getFrame();
|
||||
for (Block b : frame) {
|
||||
if (b.getX() == block.getX() && b.getY() == block.getY() && b.getZ() == block.getZ()
|
||||
&& b.getWorld().equals(block.getWorld())) {
|
||||
return rg;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package dev.skywalker3200.stargate.paper.network;
|
||||
|
||||
import com.google.common.io.ByteArrayDataOutput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
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 org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.messaging.PluginMessageListener;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
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.
|
||||
*/
|
||||
public class CrossServerBridge implements PluginMessageListener {
|
||||
|
||||
private final StargatePlugin plugin;
|
||||
private final GateManager gateManager;
|
||||
private boolean enabled = false;
|
||||
|
||||
public CrossServerBridge(StargatePlugin plugin, GateManager gateManager) {
|
||||
this.plugin = plugin;
|
||||
this.gateManager = gateManager;
|
||||
}
|
||||
|
||||
public void register() {
|
||||
this.enabled = true;
|
||||
Bukkit.getMessenger().registerOutgoingPluginChannel(plugin, StargateChannel.CHANNEL);
|
||||
Bukkit.getMessenger().registerIncomingPluginChannel(plugin, StargateChannel.CHANNEL, this);
|
||||
plugin.getLogger().info("[Stargate] Cross-server bridge registered on channel " + StargateChannel.CHANNEL);
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/** Asks the proxy to move this player to the backend server hosting {@code destGate}. */
|
||||
public void sendPlayerThroughGate(Player player, Gate destGate) {
|
||||
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());
|
||||
player.sendPluginMessage(plugin, StargateChannel.CHANNEL, out.toByteArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPluginMessageReceived(String channel, Player receivingPlayer, byte[] message) {
|
||||
if (!channel.equals(StargateChannel.CHANNEL)) return;
|
||||
try {
|
||||
DataInputStream in = new DataInputStream(new ByteArrayInputStream(message));
|
||||
byte op = in.readByte();
|
||||
if (op != StargateChannel.OP_TELEPORT_DELIVER) return;
|
||||
|
||||
UUID playerId = UUID.fromString(in.readUTF());
|
||||
UUID gateId = UUID.fromString(in.readUTF());
|
||||
|
||||
var 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);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[Stargate] Failed to handle cross-server teleport message: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package dev.skywalker3200.stargate.paper.util;
|
||||
|
||||
import dev.skywalker3200.stargate.paper.gate.GateManager;
|
||||
import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.Sign;
|
||||
import org.bukkit.block.sign.Side;
|
||||
import org.bukkit.event.block.SignChangeEvent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Renders a gate's current state (name, network, selected destination) onto its control sign. */
|
||||
public final class SignRenderer {
|
||||
|
||||
private SignRenderer() {}
|
||||
|
||||
public static void render(SignChangeEvent event, RuntimeGate rg, GateManager gateManager) {
|
||||
event.line(0, Component.text(rg.getGate().getName(), NamedTextColor.DARK_AQUA));
|
||||
event.line(1, Component.text(rg.getGate().getNetwork(), NamedTextColor.GRAY));
|
||||
event.line(2, destinationLine(rg, gateManager));
|
||||
event.line(3, rg.isOpen() ? Component.text("[connected]", NamedTextColor.GREEN) : Component.text(""));
|
||||
}
|
||||
|
||||
public static void render(RuntimeGate rg, GateManager gateManager) {
|
||||
Block b = org.bukkit.Bukkit.getWorld(rg.getGate().getSignWorld())
|
||||
.getBlockAt(rg.getGate().getSignX(), rg.getGate().getSignY(), rg.getGate().getSignZ());
|
||||
if (!(b.getState() instanceof Sign sign)) return;
|
||||
sign.getSide(Side.FRONT).line(0, Component.text(rg.getGate().getName(), NamedTextColor.DARK_AQUA));
|
||||
sign.getSide(Side.FRONT).line(1, Component.text(rg.getGate().getNetwork(), NamedTextColor.GRAY));
|
||||
sign.getSide(Side.FRONT).line(2, destinationLine(rg, gateManager));
|
||||
sign.getSide(Side.FRONT).line(3, rg.isOpen() ? Component.text("[connected]", NamedTextColor.GREEN) : Component.text(""));
|
||||
sign.update(true, false);
|
||||
}
|
||||
|
||||
private static Component destinationLine(RuntimeGate rg, GateManager gateManager) {
|
||||
List<RuntimeGate> destinations = gateManager.destinationsFor(rg);
|
||||
if (destinations.isEmpty()) {
|
||||
return Component.text("no destinations", NamedTextColor.DARK_GRAY);
|
||||
}
|
||||
int idx = ((rg.getCycleIndex() % destinations.size()) + destinations.size()) % destinations.size();
|
||||
RuntimeGate dest = destinations.get(idx);
|
||||
return Component.text("> " + dest.getGate().getName() + " <", NamedTextColor.GOLD);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
# Stargate configuration
|
||||
|
||||
# Unique id for THIS server. Used to tag gates created here and to route cross-server
|
||||
# dials through the proxy. Must be unique across your network and match the server's
|
||||
# name as configured in the Velocity/Bungee proxy config.
|
||||
server-id: "server1"
|
||||
|
||||
storage:
|
||||
# sqlite -> single server, file-based, zero setup
|
||||
# mysql -> required if you want gates to be visible/dialable across multiple
|
||||
# backend servers sharing this same database
|
||||
type: sqlite
|
||||
mysql:
|
||||
host: localhost
|
||||
port: 3306
|
||||
database: stargate
|
||||
username: stargate
|
||||
password: ""
|
||||
table-prefix: "stargate_"
|
||||
|
||||
# Enables sending players to another backend server when they dial a gate whose
|
||||
# server-id differs from this one. Requires the stargate-velocity or stargate-bungee
|
||||
# companion plugin installed on the proxy, and storage.type: mysql so all servers
|
||||
# share gate data.
|
||||
cross-server:
|
||||
enabled: false
|
||||
|
||||
gate:
|
||||
# Any of these materials count as the decorative frame ring. Mix and match to match
|
||||
# your build (the default matches an obsidian/gold-block/birch-planks arch).
|
||||
frame-materials:
|
||||
- OBSIDIAN
|
||||
- GOLD_BLOCK
|
||||
- BIRCH_PLANKS
|
||||
- OAK_PLANKS
|
||||
# Chevron blocks embedded in the frame. Build them as chevron-unlit-material; the
|
||||
# plugin swaps them to chevron-lit-material as the gate dials/opens, and swaps them
|
||||
# back when the gate closes/deactivates.
|
||||
chevron-unlit-material: BLACK_STAINED_GLASS
|
||||
chevron-lit-material: GLOWSTONE
|
||||
# Material poured into the interior (the "event horizon") while the gate is open.
|
||||
iris-material: WATER
|
||||
max-frame-blocks: 300
|
||||
max-iris-blocks: 200
|
||||
min-frame-blocks: 8
|
||||
|
||||
dialing:
|
||||
# Seconds the gate stays open (iris filled) before auto-closing if nobody walks through.
|
||||
open-seconds: 10
|
||||
# Delay in ticks between lighting each chevron during the dial animation.
|
||||
chevron-tick-delay: 4
|
||||
play-sounds: true
|
||||
|
||||
network:
|
||||
default-network: "main"
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Stargate
|
||||
version: '${version}'
|
||||
main: dev.skywalker3200.stargate.paper.StargatePlugin
|
||||
api-version: '1.21'
|
||||
author: skywalker3200
|
||||
description: Network-aware Stargate portals with cross-server dialing.
|
||||
folia-supported: false
|
||||
|
||||
commands:
|
||||
stargate:
|
||||
description: Manage Stargate networks and gates.
|
||||
aliases: [sg]
|
||||
usage: /sg <create|destroy|network|reload|list> ...
|
||||
|
||||
permissions:
|
||||
stargate.use:
|
||||
description: Allows dialing and using stargates.
|
||||
default: true
|
||||
stargate.create:
|
||||
description: Allows creating new stargates.
|
||||
default: op
|
||||
stargate.destroy:
|
||||
description: Allows destroying stargates you do not own.
|
||||
default: op
|
||||
stargate.admin:
|
||||
description: Allows reload, network admin, and bypassing ownership checks.
|
||||
default: op
|
||||
Reference in New Issue
Block a user