├── .gitignore ├── src └── main │ ├── resources │ ├── velocity-plugin.json │ ├── multi-proxy-config.yml │ ├── perms.conf │ ├── config.yml │ └── messages.yml │ └── java │ └── dev │ └── aquestry │ └── nebula │ ├── event │ ├── events │ │ ├── ProxyShutdown.java │ │ ├── ServerPostConnect.java │ │ ├── PlayerAvailableCommands.java │ │ ├── PlayerChat.java │ │ ├── PlayerChooseInitialServer.java │ │ ├── Disconnect.java │ │ ├── ServerPreConnect.java │ │ └── PluginMessage.java │ └── EventManager.java │ ├── commands │ ├── LobbyCommand.java │ ├── QueueCommand.java │ ├── ProxyCommand.java │ ├── PartyCommand.java │ ├── ContainerCommand.java │ └── GroupCommand.java │ ├── model │ ├── Proxy.java │ ├── Queue.java │ ├── Group.java │ ├── Node.java │ ├── Container.java │ └── Party.java │ ├── data │ ├── Config.java │ └── Messages.java │ ├── container │ ├── AutoDeleter.java │ ├── ContainerStateChecker.java │ ├── DefaultsManager.java │ └── ContainerManager.java │ ├── file │ ├── PermissionManager.java │ ├── PermissionFile.java │ ├── MessageLoader.java │ └── FileLoader.java │ ├── network │ ├── MultiProxySender.java │ ├── MultiProxyServer.java │ └── SshUtil.java │ ├── Nebula.java │ └── feature │ ├── PartyManager.java │ ├── QueueProcessor.java │ └── Util.java └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | gradle 4 | build 5 | move.ps1 6 | nebula.iml 7 | gradlew 8 | gradlew.bat -------------------------------------------------------------------------------- /src/main/resources/velocity-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "nebula", 3 | "name": "Nebula", 4 | "version": "1.0", 5 | "authors": ["Aquestry"], 6 | "description": "Dynamic server manager.", 7 | "main": "dev.aquestry.nebula.Nebula", 8 | "dependencies": [ 9 | { 10 | "id": "velocity", 11 | "optional": true 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /src/main/resources/multi-proxy-config.yml: -------------------------------------------------------------------------------- 1 | file-version: 1 2 | # Use multi proxy mode 3 | enabled: false 4 | # Your HMAC secret, pls change it. 5 | hmac-secret: 1234 6 | # Port for the multi proxy api server 7 | port: 5000 8 | # Define the level of this proxy will be used to determine the master proxy. 9 | # DON'T GIVE TWO PROXIES THE SAME LEVEL! 10 | level: 2 11 | # Define the other proxies 12 | proxies: 13 | example: 14 | ip: 0.0.0.0 # IP of the other proxy 15 | port: 5000 # Port from the other api -------------------------------------------------------------------------------- /src/main/resources/perms.conf: -------------------------------------------------------------------------------- 1 | default-group=default 2 | groups { 3 | admin { 4 | level=100 5 | members=[ 6 | "74401e14-f69c-48f3-b393-64be488dff8f" 7 | ] 8 | permissions=[ 9 | "velocity.admin", 10 | "velocity.command.server", 11 | "group.admin" 12 | ] 13 | prefix="[Admin] " 14 | } 15 | default { 16 | level=1 17 | members=[ 18 | "05271a2e-00c1-47bb-bf63-8afcfc155c7f" 19 | ] 20 | permissions=[ 21 | "group.default", 22 | "test.test" 23 | ] 24 | prefix="[Player] " 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/event/events/ProxyShutdown.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.event.events; 2 | 3 | import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; 4 | import dev.aquestry.nebula.data.Config; 5 | import dev.aquestry.nebula.model.Container; 6 | import dev.aquestry.nebula.Nebula; 7 | import java.util.ArrayList; 8 | 9 | public class ProxyShutdown { 10 | public ProxyShutdown(ProxyShutdownEvent event) { 11 | Config.quitting = true; 12 | for(Container container : new ArrayList<>(Config.containerMap)) { 13 | if(container != null) { 14 | Nebula.containerManager.delete(container, null); 15 | } 16 | } 17 | Nebula.ssh.closeAll(); 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/event/events/ServerPostConnect.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.event.events; 2 | 3 | import com.velocitypowered.api.event.player.ServerPostConnectEvent; 4 | import com.velocitypowered.api.proxy.Player; 5 | import com.velocitypowered.api.proxy.server.RegisteredServer; 6 | import dev.aquestry.nebula.Nebula; 7 | 8 | public class ServerPostConnect { 9 | public ServerPostConnect(ServerPostConnectEvent event) { 10 | Player player = event.getPlayer(); 11 | RegisteredServer server = player.getCurrentServer().get().getServer(); 12 | Nebula.util.getBackendServer(server.getServerInfo().getName()).removePendingPlayerConnection(player); 13 | Nebula.util.sendInfotoBackend(player); 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/event/events/PlayerAvailableCommands.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.event.events; 2 | 3 | import com.mojang.brigadier.tree.RootCommandNode; 4 | import com.velocitypowered.api.event.command.PlayerAvailableCommandsEvent; 5 | import dev.aquestry.nebula.Nebula; 6 | 7 | public class PlayerAvailableCommands { 8 | public PlayerAvailableCommands(PlayerAvailableCommandsEvent event) { 9 | RootCommandNode node = event.getRootNode(); 10 | if(!Nebula.util.getBackendServer(event.getPlayer().getCurrentServer().get().getServerInfo().getName()).getFlags().contains("lobby")) { 11 | node.removeChildByName("queue"); 12 | } else { 13 | node.removeChildByName("lobby"); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/commands/LobbyCommand.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.commands; 2 | 3 | import com.velocitypowered.api.command.SimpleCommand; 4 | import com.velocitypowered.api.proxy.Player; 5 | import dev.aquestry.nebula.Nebula; 6 | 7 | public class LobbyCommand implements SimpleCommand { 8 | public void execute(SimpleCommand.Invocation invocation) { 9 | String[] args = invocation.arguments(); 10 | if (invocation.source() instanceof Player player) { 11 | if(!Nebula.util.getBackendServer(player.getCurrentServer().get().getServerInfo().getName()).getFlags().contains("lobby")) { 12 | Nebula.util.connectPlayer(player, Nebula.defaultsManager.getTarget(), false); 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/event/events/PlayerChat.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.event.events; 2 | 3 | import com.velocitypowered.api.event.player.PlayerChatEvent; 4 | import com.velocitypowered.api.proxy.Player; 5 | import dev.aquestry.nebula.Nebula; 6 | import net.kyori.adventure.text.Component; 7 | 8 | public class PlayerChat { 9 | public PlayerChat(PlayerChatEvent event) { 10 | Player player = event.getPlayer(); 11 | Component message = Nebula.mm.deserialize( Nebula.permissionManager.getGroup(player.getUniqueId().toString()).getPrefix() + player.getUsername() + ": " + event.getMessage()); 12 | for(Player p : player.getCurrentServer().get().getServer().getPlayersConnected()) { 13 | p.sendMessage(message); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/event/events/PlayerChooseInitialServer.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.event.events; 2 | 3 | import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent; 4 | import dev.aquestry.nebula.model.Container; 5 | import dev.aquestry.nebula.Nebula; 6 | import net.kyori.adventure.text.Component; 7 | 8 | public class PlayerChooseInitialServer { 9 | public PlayerChooseInitialServer(PlayerChooseInitialServerEvent event) { 10 | Container target = Nebula.defaultsManager.getTarget(); 11 | if(target != null) { 12 | event.setInitialServer(Nebula.server.getServer(target.getServerName()).get()); 13 | } else { 14 | event.getPlayer().disconnect(Component.text("Please rejoin!")); 15 | Nebula.defaultsManager.createDefault(); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/event/events/Disconnect.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.event.events; 2 | 3 | import com.velocitypowered.api.event.connection.DisconnectEvent; 4 | import com.velocitypowered.api.proxy.Player; 5 | import dev.aquestry.nebula.data.Config; 6 | import dev.aquestry.nebula.model.Container; 7 | import dev.aquestry.nebula.Nebula; 8 | 9 | public class Disconnect { 10 | public Disconnect(DisconnectEvent event) { 11 | Player player = event.getPlayer(); 12 | if(Nebula.partyManager.getParty(player).isPresent()) { 13 | Nebula.partyManager.quit(player); 14 | } else { 15 | Nebula.queueProcessor.leaveQueue(player, false); 16 | } 17 | for(Container container : Config.containerMap) { 18 | container.removePendingPlayerConnection(player); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/event/events/ServerPreConnect.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.event.events; 2 | 3 | import com.velocitypowered.api.event.player.ServerPreConnectEvent; 4 | import com.velocitypowered.api.proxy.Player; 5 | import com.velocitypowered.api.proxy.server.RegisteredServer; 6 | import dev.aquestry.nebula.data.Config; 7 | import dev.aquestry.nebula.Nebula; 8 | 9 | public class ServerPreConnect { 10 | public ServerPreConnect(ServerPreConnectEvent event) { 11 | Player player = event.getPlayer(); 12 | RegisteredServer target = event.getOriginalServer(); 13 | if (Nebula.util.getPlayerCount(target) >= Config.defaultmax || !Nebula.util.getBackendServer(target.getServerInfo().getName()).isOnline()) { 14 | event.setResult(ServerPreConnectEvent.ServerResult.denied()); 15 | } 16 | if(target.getPlayersConnected().contains(player)) { 17 | event.setResult(ServerPreConnectEvent.ServerResult.denied()); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/model/Proxy.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.model; 2 | 3 | import dev.aquestry.nebula.Nebula; 4 | 5 | public class Proxy { 6 | 7 | private final String name; 8 | private final String ip; 9 | private final int port; 10 | private boolean online; 11 | private int level; 12 | 13 | public Proxy(String name, String ip, int port, boolean online) { 14 | this.name = name; 15 | this.ip = ip; 16 | this.port = port; 17 | this.online = online; 18 | } 19 | 20 | public String getName() { return name; } 21 | public String getIP() { return ip; } 22 | public int getPort() { return port; } 23 | public boolean isOnline() { return online; } 24 | public void setLevel(int level) { this.level = level; Nebula.util.log("Proxy: {} has a level of {}.", getName(), level); } 25 | public void setOnline(boolean online) { this.online = online; Nebula.util.log("Proxy: " + getName() + " is " + (online ? "online" : "offline")); } 26 | public int getLevel() { return level; } 27 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/model/Queue.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.model; 2 | 3 | import com.velocitypowered.api.proxy.Player; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | public class Queue { 8 | 9 | private final String name; 10 | private final String template; 11 | private final int neededPlayers; 12 | private final int preload; 13 | private String localEnvVars; 14 | private List inQueue = new ArrayList<>(); 15 | 16 | public Queue(String name, String template, int neededPlayers, int preload, String localEnvVars) { 17 | this.name = name; 18 | this.template = template; 19 | this.neededPlayers = neededPlayers; 20 | this.preload = preload; 21 | this.localEnvVars = localEnvVars; 22 | } 23 | 24 | public String getLocalEnvVars() { return localEnvVars; } 25 | public String getName() { return name; } 26 | public String getTemplate() { return template; } 27 | public int getNeededPlayers() { return neededPlayers; } 28 | public List getInQueue() { return inQueue; } 29 | public int getPreload() { return preload; } 30 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/model/Group.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.model; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class Group { 7 | 8 | private final String name; 9 | private final String prefix; 10 | private final int level; 11 | private final List permissions = new ArrayList<>(); 12 | private final List members = new ArrayList<>(); 13 | 14 | public Group(String name, String prefix, int level) { 15 | this.name = name; 16 | this.prefix = prefix; 17 | this.level = level; 18 | } 19 | 20 | public String getPrefix() { return prefix; } 21 | public String getName() { return name; } 22 | public int getLevel() { return level; } 23 | public List getPermissions() { return permissions; } 24 | public List getMembers() { return members; } 25 | public void addPermission(String permission) { if (!permissions.contains(permission)) permissions.add(permission); } 26 | public void removePermission(String permission) { permissions.remove(permission); } 27 | public boolean hasPermission(String permission) { return permissions.contains(permission); } 28 | public void addMember(String uuid) { if (!members.contains(uuid)) members.add(uuid); } 29 | public void removeMember(String uuid) { members.remove(uuid); } 30 | public boolean hasMember(String uuid) { return members.contains(uuid); } 31 | public void clearMembers() { members.clear(); } 32 | public void clearPermissions() { permissions.clear(); } 33 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/model/Node.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.model; 2 | 3 | import dev.aquestry.nebula.data.Config; 4 | import java.util.List; 5 | 6 | public class Node { 7 | 8 | private final String serverName; 9 | private final String password; 10 | private final String privateKeyFile; 11 | private final String username; 12 | private final String ip; 13 | private final String tag; 14 | private final int port; 15 | private int freePort; 16 | 17 | public Node(String serverName, String ip, String username, String password, String privateKeyFile, int port, int freePort, String tag) { 18 | this.serverName = serverName; 19 | this.password = (password != null) ? password : "none"; 20 | this.privateKeyFile = (privateKeyFile != null) ? privateKeyFile : "none"; 21 | this.username = username; 22 | this.ip = ip; 23 | this.port = port; 24 | this.freePort = freePort; 25 | this.tag = tag; 26 | } 27 | 28 | public void setFreePort(int freePort) { this.freePort = freePort; } 29 | public String getServerName() { return serverName; } 30 | public String getPassword() { return password; } 31 | public String getPrivateKeyFile() { return privateKeyFile; } 32 | public String getUsername() { return username; } 33 | public String getIp() { return ip; } 34 | public String getTag() { return tag; } 35 | public int getPort() { return port; } 36 | public int getFreePort() { return freePort; } 37 | 38 | public List getBackendServers() { 39 | return Config.containerMap.stream() 40 | .filter(backendServer -> backendServer.getHoldServer().equals(this)) 41 | .toList(); 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | file-version: 1 2 | # Additional environment variables for backend Servers (use "none" for none) 3 | env-vars: PAPER_VELOCITY_SECRET=1234,FOO2=bar1 4 | # Should the templates be pulled at start? (will take its time) 5 | pull-start: true 6 | # Docherhub Template for the Default-Servers. 7 | lobby-template: anton691/simple-lobby:latest 8 | # Player limit for default server. 9 | lobby-max: 5 10 | # Threshold for creating a new lobby server. 11 | # When a server reaches this player count, a new server will be prepared. 12 | # The current server will continue accepting players until it reaches the player limit (5), 13 | # at which point new players will start connecting to the newly created lobby server. 14 | lobby-min: 3 15 | # If you enable this the server where the proxy itself is run will be used as a node, so you don't need other nodes. 16 | # This works without an ssh server too, but will still need docker but not ruby to be installed. 17 | local-node: false 18 | nodes: 19 | example: 20 | ip: localhost 21 | username: notrootpls 22 | password: 1234 # type 'none', if you are not using a password 23 | privateKeyFile: /path/to/private.key # type 'none', if you are not using a key 24 | port: 22 # default port 22 25 | # Gamemodes which players can join. 26 | gamemodes: 27 | Duels: 28 | templateName: anton691/simple-duels:latest 29 | neededPlayers: 2 30 | preload: 1 31 | env-vars: FOO=bar,FOO2=bar2 # Additional environment variables for this gamemode only (use "none" for none) 32 | Parkour: 33 | templateName: anton691/simple-parkour:latest 34 | neededPlayers: 1 35 | preload: 1 36 | env-vars: FOO=bar,FOO2=bar2 # Additional environment variables for this gamemode only (use "none" for none) -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/data/Config.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.data; 2 | 3 | import dev.aquestry.nebula.model.Container; 4 | import dev.aquestry.nebula.model.Proxy; 5 | import dev.aquestry.nebula.model.Queue; 6 | import dev.aquestry.nebula.model.Node; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | 12 | public class Config { 13 | public static String envVars; 14 | public static String HMACSecret; 15 | public static String defaultGroupName; 16 | public static String defaultServerTemplate; 17 | public static boolean quitting; 18 | public static boolean multiProxyMode; 19 | public static boolean pullStart; 20 | public static boolean localNode; 21 | public static int multiProxyPort; 22 | public static int multiProxyLevel; 23 | public static int defaultmax; 24 | public static int defaultmin; 25 | public static List nodeMap = new ArrayList<>(); 26 | public static List proxyMap = new ArrayList<>(); 27 | public static List containerMap = new ArrayList<>(); 28 | public static List queueMap = new ArrayList<>(); 29 | public static List alltemplates = new ArrayList<>(); 30 | public static final Map cooldownsPluginMessage = new ConcurrentHashMap<>(); 31 | public static String Icon = """ 32 | \n 33 | ███╗░░██╗███████╗██████╗░██╗░░░██╗██╗░░░░░░█████╗░ 34 | ████╗░██║██╔════╝██╔══██╗██║░░░██║██║░░░░░██╔══██╗ 35 | ██╔██╗██║█████╗░░██████╦╝██║░░░██║██║░░░░░███████║ 36 | ██║╚████║██╔══╝░░██╔══██╗██║░░░██║██║░░░░░██╔══██║ 37 | ██║░╚███║███████╗██████╦╝╚██████╔╝███████╗██║░░██║ 38 | ╚═╝░░╚══╝╚══════╝╚═════╝░░╚═════╝░╚══════╝╚═╝░░╚═╝ 39 | \s"""; 40 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/event/EventManager.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.event; 2 | 3 | import com.velocitypowered.api.event.Subscribe; 4 | import com.velocitypowered.api.event.command.PlayerAvailableCommandsEvent; 5 | import com.velocitypowered.api.event.connection.DisconnectEvent; 6 | import com.velocitypowered.api.event.connection.PluginMessageEvent; 7 | import com.velocitypowered.api.event.permission.PermissionsSetupEvent; 8 | import com.velocitypowered.api.event.player.*; 9 | import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; 10 | import dev.aquestry.nebula.Nebula; 11 | import dev.aquestry.nebula.event.events.*; 12 | 13 | public class EventManager { 14 | @Subscribe 15 | public void PlayerChooseInitialServer(PlayerChooseInitialServerEvent event) { 16 | new PlayerChooseInitialServer(event); 17 | } 18 | @Subscribe 19 | public void ServerPreConnect(ServerPreConnectEvent event) { 20 | new ServerPreConnect(event); 21 | } 22 | @Subscribe 23 | public void ProxyShutdown(ProxyShutdownEvent event) { 24 | new ProxyShutdown(event); 25 | } 26 | @Subscribe 27 | public void PlayerAvailableCommands(PlayerAvailableCommandsEvent event) { 28 | new PlayerAvailableCommands(event); 29 | } 30 | @Subscribe 31 | public void ServerConnected(ServerPostConnectEvent event) { 32 | new ServerPostConnect(event); 33 | } 34 | @Subscribe 35 | public void Disconnect(DisconnectEvent event) { 36 | new Disconnect(event); 37 | } 38 | @Subscribe 39 | public void PluginMessage(PluginMessageEvent event) { 40 | new PluginMessage(event); 41 | } 42 | @Subscribe 43 | public void PlayerChat(PlayerChatEvent event) { 44 | new PlayerChat(event); 45 | } 46 | @Subscribe 47 | public void PermissionsSetup(PermissionsSetupEvent event) { 48 | event.setProvider(Nebula.permissionManager); 49 | } 50 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/model/Container.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.model; 2 | 3 | import com.velocitypowered.api.command.CommandSource; 4 | import com.velocitypowered.api.proxy.Player; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class Container { 9 | 10 | private final Node node; 11 | private final int port; 12 | private final String serverName; 13 | private final CommandSource creator; 14 | private final String template; 15 | private boolean online; 16 | private final List pendingPlayerConnections = new ArrayList<>(); 17 | public List flags = new ArrayList<>(); 18 | 19 | public Container(String serverName, Node node, int port, boolean online, CommandSource creator, String template, String... starterFlags) { 20 | this.serverName = serverName; 21 | this.node = node; 22 | this.port = port; 23 | this.online = online; 24 | this.creator = creator; 25 | this.template = template; 26 | this.flags.addAll(List.of(starterFlags)); 27 | } 28 | 29 | public String getServerName() { return serverName; } 30 | public int getPort() { return port; } 31 | public Node getHoldServer() { return node; } 32 | public boolean isOnline() { return online; } 33 | public CommandSource getCreator() { return creator; } 34 | public void addPendingPlayerConnection(Player player) { if (!pendingPlayerConnections.contains(player)) { pendingPlayerConnections.add(player);} } 35 | public List getPendingPlayerConnections() { return new ArrayList<>(pendingPlayerConnections); } 36 | public void removePendingPlayerConnection(Player player) { pendingPlayerConnections.remove(player); } 37 | public void setOnline(boolean online) { this.online = online; } 38 | public void removeFlag(String flag) { this.flags.remove(flag); } 39 | public List getFlags() { return new ArrayList<>(flags); } 40 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/model/Party.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.model; 2 | 3 | import com.velocitypowered.api.proxy.Player; 4 | import dev.aquestry.nebula.Nebula; 5 | import dev.aquestry.nebula.data.Messages; 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | public class Party { 12 | 13 | private Player leader; 14 | private final List members = new ArrayList<>(); 15 | private final Map invites = new HashMap<>(); 16 | 17 | public Party(Player leader) { 18 | this.leader = leader; 19 | this.members.add(leader); 20 | } 21 | 22 | public Player getLeader() { return leader; } 23 | public List getMembers() { return new ArrayList<>(members); } 24 | public boolean isMember(Player player) { return members.contains(player); } 25 | public boolean isInvited(Player player) { return invites.containsKey(player); } 26 | public void addMember(Player player) { if (!members.contains(player)) members.add(player); } 27 | public void removeMember(Player player) { members.remove(player); } 28 | public void removeInvite(Player player) { invites.remove(player); } 29 | public void addInvite(Player player) { if (!invites.containsKey(player)) invites.put(player, System.currentTimeMillis()); } 30 | public void newLeader(Player player) { leader = player; } 31 | public List getInvites() { return invites.keySet().stream().map(Player::getUsername).toList(); } 32 | 33 | public void refreshInvites() { 34 | long currentTime = System.currentTimeMillis(); 35 | invites.entrySet().removeIf(entry -> { 36 | boolean isExpired = currentTime - entry.getValue() > 30000; 37 | if (isExpired) { 38 | Nebula.util.sendMessage(entry.getKey(), Messages.INVITE_NOT_ACCEPT.replace("", leader.getUsername())); 39 | Nebula.util.sendMessage(leader, Messages.INVITE_TO_PLAYER_NOT_ACCEPT.replace("", entry.getKey().getUsername())); 40 | } 41 | return isExpired; 42 | }); 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/event/events/PluginMessage.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.event.events; 2 | 3 | import com.velocitypowered.api.event.connection.PluginMessageEvent; 4 | import com.velocitypowered.api.proxy.ServerConnection; 5 | import dev.aquestry.nebula.Nebula; 6 | import dev.aquestry.nebula.data.Config; 7 | import java.nio.charset.StandardCharsets; 8 | 9 | public class PluginMessage { 10 | public PluginMessage(PluginMessageEvent event) { 11 | if (!(event.getSource() instanceof ServerConnection)) return; 12 | String message = new String(event.getData(), StandardCharsets.UTF_8); 13 | if (!event.getIdentifier().equals(Nebula.channelMain)) return; 14 | event.setResult(PluginMessageEvent.ForwardResult.handled()); 15 | String[] parts = message.split(":"); 16 | if (parts.length < 2) return; 17 | String action = parts[0]; 18 | String playerName = parts[1]; 19 | long now = System.currentTimeMillis(); 20 | if (Config.cooldownsPluginMessage.getOrDefault(playerName, 0L) + 1000 > now) return; 21 | Config.cooldownsPluginMessage.put(playerName, now); 22 | switch (action) { 23 | case "lobby" -> 24 | Nebula.server.getPlayer(playerName).ifPresent(player -> 25 | Nebula.util.connectPlayer(player, Nebula.defaultsManager.getTarget(), true)); 26 | case "queue" -> { 27 | if (parts.length == 3) { 28 | Nebula.server.getPlayer(playerName).ifPresentOrElse( 29 | player -> Nebula.queueProcessor.joinQueue(player, parts[2]), 30 | () -> Nebula.util.log("Player {} not found", playerName)); 31 | } else { 32 | Nebula.util.log("Incorrect queue plugin message format: {}", message); 33 | } 34 | } 35 | case "leave_queue" -> { 36 | if (parts.length == 2) { 37 | Nebula.server.getPlayer(playerName).ifPresent(player -> 38 | Nebula.queueProcessor.leaveQueue(player, true)); 39 | } else { 40 | Nebula.util.log("Incorrect leave queue plugin message format: {}", message); 41 | } 42 | } 43 | default -> Nebula.util.log("Unknown plugin message action: {}", action); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/container/AutoDeleter.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.container; 2 | 3 | import dev.aquestry.nebula.data.Config; 4 | import dev.aquestry.nebula.model.Container; 5 | import dev.aquestry.nebula.Nebula; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | public class AutoDeleter { 12 | 13 | private final Map deletionTimers = new HashMap<>(); 14 | private static final long DELETION_DELAY = 2000; 15 | 16 | public void process() { 17 | if(Config.quitting) return; 18 | long currentTime = System.currentTimeMillis(); 19 | List serversToDelete = new ArrayList<>(); 20 | boolean lobbyServerDeleted = false; 21 | for (Container container : Config.containerMap) { 22 | if (container.getFlags().contains("custom") || container.getFlags().contains("retry")) { 23 | continue; 24 | } 25 | boolean conditionsMet = Nebula.util.getPlayerCount(container) == 0 && 26 | container.getPendingPlayerConnections().isEmpty() && !container.getFlags().contains("preload"); 27 | if (container.getFlags().contains("lobby")) { 28 | conditionsMet = conditionsMet && !lobbyServerDeleted && canDeleteLobbyServer(container); 29 | } 30 | if (conditionsMet) { 31 | if (!deletionTimers.containsKey(container)) { 32 | deletionTimers.put(container, currentTime); 33 | } else { 34 | long timerStarted = deletionTimers.get(container); 35 | if (currentTime - timerStarted >= DELETION_DELAY) { 36 | serversToDelete.add(container); 37 | deletionTimers.remove(container); 38 | if (container.getFlags().contains("lobby")) { 39 | lobbyServerDeleted = true; 40 | } 41 | } 42 | } 43 | } else { 44 | deletionTimers.remove(container); 45 | } 46 | } 47 | serversToDelete.forEach(container -> Nebula.containerManager.delete(container, null)); 48 | } 49 | 50 | private boolean canDeleteLobbyServer(Container serverToExclude) { 51 | return Config.containerMap.stream().anyMatch(container -> !container.equals(serverToExclude) 52 | && container.getFlags().contains("lobby") 53 | && container.isOnline() 54 | && Nebula.util.getPlayerCount(container) < Config.defaultmin); 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/commands/QueueCommand.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.commands; 2 | 3 | import com.velocitypowered.api.command.SimpleCommand; 4 | import com.velocitypowered.api.proxy.Player; 5 | import dev.aquestry.nebula.data.Config; 6 | import dev.aquestry.nebula.data.Messages; 7 | import dev.aquestry.nebula.model.Queue; 8 | import dev.aquestry.nebula.Nebula; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | public class QueueCommand implements SimpleCommand { 14 | @Override 15 | public void execute(Invocation invocation) { 16 | String[] args = invocation.arguments(); 17 | if (invocation.source() instanceof Player player) { 18 | if (args.length == 0) { 19 | Nebula.util.sendMessage(player, Messages.USAGE_QUEUE); 20 | return; 21 | } 22 | switch (args[0]) { 23 | case "leave" -> Nebula.queueProcessor.leaveQueue(player, true); 24 | case "join" -> { 25 | if (args.length == 2) { 26 | Nebula.queueProcessor.joinQueue(player, args[1]); 27 | } else { 28 | Nebula.util.sendMessage(player, Messages.USAGE_QUEUE); 29 | } 30 | } 31 | default -> Nebula.util.sendMessage(player, Messages.USAGE_QUEUE); 32 | } 33 | } 34 | } 35 | 36 | @Override 37 | public List suggest(Invocation invocation) { 38 | if (invocation.source() instanceof Player player) { 39 | String[] args = invocation.arguments(); 40 | List suggestions = new ArrayList<>(); 41 | boolean inQueue = Nebula.queueProcessor.isInAnyQueue(player); 42 | if(inQueue) { 43 | suggestions.add("leave"); 44 | } else { 45 | suggestions.add("join"); 46 | } 47 | if (args.length == 0) { return suggestions; } 48 | if (args.length == 1) { 49 | return suggestions.stream() 50 | .filter(command -> command.startsWith(args[0].toLowerCase())) 51 | .collect(Collectors.toList()); 52 | } 53 | if (args.length == 2 && "join".equalsIgnoreCase(args[0]) && !inQueue) { 54 | return Config.queueMap.stream() 55 | .map(Queue::getName) 56 | .filter(name -> name.toLowerCase().startsWith(args[1].toLowerCase())) 57 | .collect(Collectors.toList()); 58 | } 59 | } 60 | return List.of(); 61 | } 62 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/container/ContainerStateChecker.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.container; 2 | 3 | import com.velocitypowered.api.command.CommandSource; 4 | import com.velocitypowered.api.proxy.server.RegisteredServer; 5 | import dev.aquestry.nebula.Nebula; 6 | import dev.aquestry.nebula.data.Config; 7 | import dev.aquestry.nebula.data.Messages; 8 | import dev.aquestry.nebula.model.Container; 9 | import java.util.ArrayList; 10 | import java.util.Optional; 11 | 12 | public class ContainerStateChecker { 13 | public void pingServers() { 14 | if(Config.quitting) return; 15 | Nebula.util.checkLobbys(false); 16 | for (Container container : new ArrayList<>(Config.containerMap)) { 17 | Optional registeredServer = Nebula.server.getServer(container.getServerName()); 18 | registeredServer.ifPresent(regServer -> regServer.ping().whenComplete((result, exception) -> { 19 | if (exception == null) { 20 | try { 21 | if (regServer.getServerInfo().getName().equals(container.getServerName())) { 22 | synchronized (container) { 23 | if (!container.isOnline()) { 24 | container.setOnline(true); 25 | Nebula.queueProcessor.process(); 26 | Nebula.util.callPending(container); 27 | CommandSource creator = container.getCreator(); 28 | Nebula.util.sendMessage(creator, Messages.ONLINE.replace("", container.getServerName())); 29 | } 30 | } 31 | } 32 | } catch (Exception e) { 33 | Nebula.util.log("Error while executing success response for server: {}", regServer.getServerInfo().getName()); 34 | } 35 | } else { 36 | try { 37 | if (regServer.getServerInfo().getName().equals(container.getServerName())) { 38 | synchronized (container) { 39 | if (container.isOnline()) { 40 | Nebula.util.checkLobbys(true); 41 | container.setOnline(false); 42 | CommandSource creator = container.getCreator(); 43 | Nebula.util.sendMessage(creator, Messages.OFFLINE.replace("", container.getServerName())); 44 | } 45 | } 46 | } 47 | } catch (Exception e) { 48 | Nebula.util.log("Error while executing failure response for server: {}", regServer.getServerInfo().getName()); 49 | } 50 | } 51 | })); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/commands/ProxyCommand.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.commands; 2 | 3 | import com.velocitypowered.api.command.CommandSource; 4 | import com.velocitypowered.api.command.SimpleCommand; 5 | import com.velocitypowered.api.proxy.ConsoleCommandSource; 6 | import dev.aquestry.nebula.Nebula; 7 | import dev.aquestry.nebula.data.Config; 8 | import dev.aquestry.nebula.model.Proxy; 9 | import java.util.List; 10 | import java.util.concurrent.CompletableFuture; 11 | import java.util.stream.Collectors; 12 | 13 | public class ProxyCommand implements SimpleCommand { 14 | @Override 15 | public void execute(Invocation invocation) { 16 | CommandSource source = invocation.source(); 17 | String[] args = invocation.arguments(); 18 | if (args.length < 2) { 19 | return; 20 | } 21 | String action = args[0].toLowerCase(); 22 | String proxyName = args[1]; 23 | Proxy proxy = Config.proxyMap.stream() 24 | .filter(Proxy::isOnline) 25 | .filter(p -> p.getName().equalsIgnoreCase(proxyName)) 26 | .findFirst() 27 | .orElse(null); 28 | if (proxy == null) { 29 | return; 30 | } 31 | switch (action) { 32 | case "nodes": 33 | Nebula.util.sendMessage(source, Nebula.multiProxySender.getNodes(proxy)); 34 | break; 35 | case "containers": 36 | Nebula.util.sendMessage(source, Nebula.multiProxySender.getContainers(proxy)); 37 | break; 38 | } 39 | } 40 | 41 | @Override 42 | public CompletableFuture> suggestAsync(Invocation invocation) { 43 | String[] args = invocation.arguments(); 44 | if (args.length == 0) { 45 | return CompletableFuture.completedFuture(List.of("nodes", "containers")); 46 | } 47 | if (args.length == 1) { 48 | return CompletableFuture.completedFuture( 49 | List.of("nodes", "containers").stream() 50 | .filter(option -> option.startsWith(args[0].toLowerCase())) 51 | .collect(Collectors.toList()) 52 | ); 53 | } 54 | if (args.length == 2 && (args[0].equalsIgnoreCase("nodes") || args[0].equalsIgnoreCase("containers"))) { 55 | return CompletableFuture.completedFuture( 56 | Config.proxyMap.stream() 57 | .filter(Proxy::isOnline) 58 | .map(Proxy::getName) 59 | .filter(name -> name.startsWith(args[1])) 60 | .collect(Collectors.toList()) 61 | ); 62 | } 63 | return CompletableFuture.completedFuture(List.of()); 64 | } 65 | 66 | @Override 67 | public boolean hasPermission(SimpleCommand.Invocation invocation) { 68 | CommandSource sender = invocation.source(); 69 | return sender.hasPermission("velocity.admin") || sender instanceof ConsoleCommandSource; 70 | } 71 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/data/Messages.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.data; 2 | 3 | public class Messages { 4 | public static String PREFIX; 5 | 6 | public static String USAGE_CONTAINER; 7 | public static String KILL_CONTAINER; 8 | public static String DELETE_CONTAINER; 9 | public static String PULL_TEMPLATE; 10 | public static String DONE_PULL; 11 | public static String CREATE_CONTAINER; 12 | public static String START_CONTAINER; 13 | public static String SERVER_RUNNING; 14 | public static String SERVER_STOPPED; 15 | public static String SERVER_NOT_FOUND; 16 | public static String SERVER_CONNECT; 17 | public static String ALREADY_EXISTS; 18 | public static String ERROR_CREATE; 19 | public static String ERROR_DELETE; 20 | public static String ERROR_KILL; 21 | public static String ERROR_PULL; 22 | public static String ERROR_START; 23 | public static String DONE; 24 | public static String ONLINE; 25 | public static String OFFLINE; 26 | 27 | public static String USAGE_QUEUE; 28 | public static String ADDED_TO_QUEUE; 29 | public static String REMOVED_FROM_QUEUE; 30 | public static String ALREADY_IN_QUEUE; 31 | public static String NOT_IN_QUEUE; 32 | public static String LOBBY_ONLY; 33 | public static String QUEUE_NOT_FOUND; 34 | 35 | public static String TARGET_INVITE_NOT_FOUND; 36 | public static String TARGET_INVITE_ALREADY; 37 | public static String SENT_INVITE; 38 | public static String NO_INVITE_FROM_LEADER; 39 | public static String ALREADY_IN_PARTY; 40 | public static String JOINED_PARTY; 41 | public static String LEFT_PARTY; 42 | public static String NO_PARTY_TO_LEAVE; 43 | public static String INVITED_MESSAGE; 44 | public static String INVITE_NOT_ACCEPT; 45 | public static String INVITE_TO_PLAYER_NOT_ACCEPT; 46 | public static String QUEUE_PLAYER_COUNT_MISMATCH ; 47 | public static String PARTY_NOT_ALLOWED; 48 | public static String USAGE_PARTY; 49 | 50 | public static String GROUP_USAGE; 51 | public static String GROUP_ASSIGN_SUCCESS; 52 | public static String GROUP_ASSIGN_PLAYER_NOT_FOUND; 53 | public static String GROUP_ASSIGN_GROUP_NOT_FOUND; 54 | public static String GROUP_ASSIGN_ALREADY; 55 | public static String GROUP_CREATE_SUCCESS; 56 | public static String GROUP_CREATE_ALREADY_EXISTS; 57 | public static String GROUP_CREATE_INVALID_LEVEL; 58 | public static String GROUP_DELETE_SUCCESS; 59 | public static String GROUP_DELETE_DEFAULT; 60 | public static String GROUP_DELETE_NOT_FOUND; 61 | public static String GROUP_LIST_HEADER; 62 | public static String GROUP_LIST_ITEM; 63 | public static String GROUP_LIST_EMPTY; 64 | public static String GROUP_INFO_NOT_FOUND; 65 | public static String GROUP_PERMISSION_ADD_SUCCESS; 66 | public static String GROUP_PERMISSION_REMOVE_SUCCESS; 67 | public static String GROUP_PERMISSION_LIST_HEADER; 68 | public static String GROUP_PERMISSION_REMOVE_NOT_FOUND; 69 | public static String GROUP_PERMISSION_ALREADY_EXISTS; 70 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/file/PermissionManager.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.file; 2 | 3 | import com.velocitypowered.api.permission.PermissionFunction; 4 | import com.velocitypowered.api.permission.PermissionProvider; 5 | import com.velocitypowered.api.permission.PermissionSubject; 6 | import com.velocitypowered.api.permission.Tristate; 7 | import com.velocitypowered.api.proxy.Player; 8 | import dev.aquestry.nebula.Nebula; 9 | import dev.aquestry.nebula.data.Config; 10 | import dev.aquestry.nebula.model.Group; 11 | 12 | public class PermissionManager implements PermissionProvider { 13 | public boolean hasPermission(Player player, String permission) { 14 | return Nebula.permissionFile.runtimeGroups.stream() 15 | .anyMatch(group -> group.hasMember(player.getUniqueId().toString()) && group.hasPermission(permission)); 16 | } 17 | 18 | @Override 19 | public PermissionFunction createFunction(PermissionSubject subject) { 20 | return permission -> Tristate.fromBoolean( 21 | subject instanceof Player && hasPermission((Player) subject, permission) 22 | ); 23 | } 24 | 25 | public Group getGroup(String uuid) { 26 | Group group = Nebula.permissionFile.runtimeGroups.stream() 27 | .filter(g -> g.hasMember(uuid)) 28 | .findFirst() 29 | .orElse(null); 30 | if(group == null) { 31 | Nebula.util.log("Giving {} the default group.", uuid); 32 | group = Nebula.permissionFile.runtimeGroups.stream().filter(g -> g.getName().equals(Config.defaultGroupName)).toList().getFirst(); 33 | assignGroup(uuid, group); 34 | Nebula.server.getPlayer(uuid).ifPresent(p -> Nebula.util.sendInfotoBackend(p)); 35 | } 36 | return group; 37 | } 38 | 39 | public String getGroupData(Group group) { 40 | return group.getName() 41 | + "?" + group.getPrefix().replace(" ", "") 42 | + "?" + group.getLevel() 43 | + "?" + String.join(",:", Nebula.permissionFile.getGroupMembers(group.getName()) 44 | + "°" + String.join(",", group.getPermissions())); 45 | } 46 | 47 | public String getAllGroups() { 48 | return String.join("~", Nebula.permissionFile.runtimeGroups.stream().map(g -> g.getName() + "?" 49 | + g.getPrefix().replace(" ", "") 50 | + "?" + g.getLevel() 51 | + "?" + String.join(",", Nebula.permissionFile.getGroupMembers(g.getName()) 52 | + "°" + String.join(",", g.getPermissions()))).toList()); 53 | } 54 | 55 | public void assignGroup(String uuid, Group group) { 56 | Nebula.permissionFile.addMemberToGroup(group, uuid); 57 | for(Group g : Nebula.permissionFile.runtimeGroups) { 58 | if(g.hasMember(uuid) && !g.equals(group)) { 59 | Nebula.permissionFile.removeMemberFromGroup(g, uuid); 60 | } 61 | } 62 | Nebula.util.log("'{}' is now in group {} sending info to backend.", uuid, group.getName()); 63 | Nebula.server.getAllPlayers().stream().forEach(p -> { 64 | if(p.getUniqueId().toString().equals(uuid)) { 65 | Nebula.util.sendInfotoBackend(p); 66 | } 67 | }); 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/container/DefaultsManager.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.container; 2 | 3 | import dev.aquestry.nebula.data.Config; 4 | import dev.aquestry.nebula.model.Container; 5 | import dev.aquestry.nebula.Nebula; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class DefaultsManager { 10 | public Container getTarget() { 11 | Container target = getServerWithLowestPlayerCount(); 12 | if(target != null) { 13 | int count = Nebula.server.getServer(target.getServerName()).get().getPlayersConnected().size(); 14 | if(count + 1 == Config.defaultmin && !isOtherUnderMin(target)) { 15 | createDefault(); 16 | } 17 | } 18 | target = getServerBetweenMinAndMaxPlayers(); 19 | if(target != null) { 20 | return target; 21 | } 22 | return getServerWithLowestPlayerCount(); 23 | } 24 | 25 | private List getAvailableServers() { 26 | List servers = new ArrayList<>(); 27 | for (Container server : Config.containerMap) { 28 | if (server.getFlags().contains("lobby") && server.isOnline()) { 29 | servers.add(server); 30 | } 31 | } 32 | return servers; 33 | } 34 | 35 | private Container getServerWithLowestPlayerCount() { 36 | if (getAvailableServers().isEmpty()) { 37 | return null; 38 | } 39 | Container serverWithLowestCount = getAvailableServers().getFirst(); 40 | int lowestPlayerCount = Nebula.server.getServer(serverWithLowestCount.getServerName()) 41 | .get() 42 | .getPlayersConnected() 43 | .size(); 44 | for (Container container : getAvailableServers()) { 45 | int playerCount = Nebula.server.getServer(container.getServerName()) 46 | .get() 47 | .getPlayersConnected() 48 | .size(); 49 | if (playerCount < lowestPlayerCount) { 50 | serverWithLowestCount = container; 51 | lowestPlayerCount = playerCount; 52 | } 53 | } 54 | return serverWithLowestCount; 55 | } 56 | 57 | private boolean isOtherUnderMin(Container other) { 58 | for (Container container : getAvailableServers()) { 59 | int playerCount = Nebula.server.getServer(container.getServerName()) 60 | .get() 61 | .getPlayersConnected() 62 | .size(); 63 | if (playerCount < Config.defaultmin && !container.equals(other)) { 64 | return true; 65 | } 66 | } 67 | return false; 68 | } 69 | 70 | private Container getServerBetweenMinAndMaxPlayers() { 71 | for (Container container : getAvailableServers()) { 72 | int playerCount = Nebula.server.getServer(container.getServerName()) 73 | .get() 74 | .getPlayersConnected() 75 | .size(); 76 | if (playerCount >= Config.defaultmin && playerCount < Config.defaultmax) { 77 | return container; 78 | } 79 | } 80 | return null; 81 | } 82 | 83 | public void createDefault() { 84 | String name = "Lobby-" + Nebula.util.generateUniqueString(); 85 | Container temp = Nebula.containerManager.createFromTemplate( 86 | Config.defaultServerTemplate, 87 | name, 88 | Nebula.server.getConsoleCommandSource(), 89 | "lobby", "retry" 90 | ); 91 | } 92 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/network/MultiProxySender.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.network; 2 | 3 | import dev.aquestry.nebula.Nebula; 4 | import dev.aquestry.nebula.data.Config; 5 | import dev.aquestry.nebula.model.Group; 6 | import dev.aquestry.nebula.model.Proxy; 7 | import java.io.BufferedReader; 8 | import java.io.InputStreamReader; 9 | import java.io.PrintWriter; 10 | import java.net.InetSocketAddress; 11 | import java.net.Socket; 12 | import java.util.function.Consumer; 13 | 14 | public class MultiProxySender { 15 | public void pingProxies() { 16 | for (Proxy proxy : Config.proxyMap) { 17 | sendMessage(proxy, "GET&LEVEL", response -> { 18 | if (!response.equals("INVALID")) { 19 | int level = Integer.parseInt(response); 20 | if (level == Config.multiProxyLevel) { 21 | Nebula.util.log("{} has the same level, shutting down!", proxy.getName()); 22 | Nebula.server.shutdown(); 23 | System.exit(0); 24 | } 25 | if (proxy.getLevel() != level) { 26 | proxy.setLevel(level); 27 | } 28 | if (!proxy.isOnline()) { 29 | proxy.setOnline(true); 30 | if(hasHighestLevel()) { 31 | sendGroups(proxy); 32 | } 33 | } 34 | }}, e -> { 35 | if (proxy.isOnline()) { 36 | proxy.setOnline(false); 37 | } 38 | }); 39 | } 40 | } 41 | 42 | private void sendGroups(Proxy proxy) { 43 | sendMessage(proxy 44 | , "POST&PERM&UPDATE&" + Nebula.permissionManager.getAllGroups(), response -> {} 45 | , e -> Nebula.util.log("Failed to connect to proxy {} for group post." 46 | , proxy.getName())); 47 | } 48 | 49 | public String getNodes(Proxy proxy) { 50 | final String[] result = {"FAILED"}; 51 | sendMessage(proxy, "GET&NODES", 52 | response -> result[0] = response, e -> {}); 53 | return result[0]; 54 | } 55 | 56 | public String getContainers(Proxy proxy) { 57 | final String[] result = {"FAILED"}; 58 | sendMessage(proxy, "GET&SERVERS", 59 | response -> result[0] = response, e -> {}); 60 | return result[0]; 61 | } 62 | 63 | public void updateGroup(Group group) { 64 | for(Proxy proxy : Config.proxyMap.stream().filter(Proxy::isOnline).toList()) { 65 | sendMessage(proxy 66 | , "POST&PERM&UPDATE&" + Nebula.permissionManager.getGroupData(group), response -> {} 67 | , e -> Nebula.util.log("Failed to connect to proxy {} for group post." 68 | , proxy.getName())); 69 | } 70 | } 71 | 72 | public void sendDelete(String groupName) { 73 | for(Proxy proxy : Config.proxyMap.stream().filter(Proxy::isOnline).toList()) { 74 | sendMessage(proxy 75 | , "POST&PERM&DELETE&" + groupName, response -> {} 76 | , e -> Nebula.util.log("Failed to connect to proxy {} for group deletion." 77 | , proxy.getName())); 78 | } 79 | } 80 | 81 | private void sendMessage(Proxy proxy, String message, Consumer onSuccess, Consumer onFailure) { 82 | if(Config.quitting) return; 83 | try (Socket socket = new Socket()) { 84 | socket.connect(new InetSocketAddress(proxy.getIP(), proxy.getPort()), 2000); 85 | socket.setSoTimeout(2000); 86 | PrintWriter out = new PrintWriter(socket.getOutputStream(), true); 87 | BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); 88 | message = message + "|" + Nebula.util.calculateHMAC(message); 89 | out.println(message); 90 | onSuccess.accept(in.readLine()); 91 | } catch (Exception e) { 92 | onFailure.accept(e.getMessage()); 93 | } 94 | } 95 | 96 | public boolean hasHighestLevel() { 97 | return Config.proxyMap.stream() 98 | .filter(Proxy::isOnline) 99 | .allMatch(proxy -> proxy.getLevel() < Config.multiProxyLevel); 100 | } 101 | } -------------------------------------------------------------------------------- /src/main/java/dev/aquestry/nebula/commands/PartyCommand.java: -------------------------------------------------------------------------------- 1 | package dev.aquestry.nebula.commands; 2 | 3 | import com.velocitypowered.api.command.CommandSource; 4 | import com.velocitypowered.api.command.SimpleCommand; 5 | import com.velocitypowered.api.proxy.Player; 6 | import dev.aquestry.nebula.Nebula; 7 | import dev.aquestry.nebula.data.Messages; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | public class PartyCommand implements SimpleCommand { 12 | @Override 13 | public void execute(Invocation invocation) { 14 | String[] args = invocation.arguments(); 15 | if (invocation.source() instanceof Player player) { 16 | if (args.length >= 1) { 17 | switch (args[0].toLowerCase()) { 18 | case "accept": 19 | if (args.length >= 2) { 20 | Nebula.partyManager.tryJoin(player, args[1]); 21 | } else { 22 | if(!Nebula.partyManager.getAllInvites(player).isEmpty()) { 23 | Nebula.partyManager.tryJoin(player, Nebula.partyManager.getAllInvites(player).getFirst()); 24 | } else { 25 | sendUsageMessage(player); 26 | } 27 | } 28 | break; 29 | case "decline": 30 | if (args.length >= 2) { 31 | Nebula.partyManager.tryDecline(player, args[1]); 32 | } else { 33 | if(!Nebula.partyManager.getAllInvites(player).isEmpty()) { 34 | Nebula.partyManager.tryDecline(player, Nebula.partyManager.getAllInvites(player).getFirst()); 35 | } else { 36 | sendUsageMessage(player); 37 | } 38 | } 39 | break; 40 | case "invite": 41 | if (args.length >= 2) { 42 | Nebula.partyManager.inviteCommand(player, args[1]); 43 | } else { 44 | sendUsageMessage(player); 45 | } 46 | break; 47 | case "leave": 48 | Nebula.partyManager.quit(player); 49 | break; 50 | default: 51 | sendUsageMessage(player); 52 | break; 53 | } 54 | } else { 55 | sendUsageMessage(player); 56 | } 57 | } 58 | } 59 | 60 | @Override 61 | public List suggest(Invocation invocation) { 62 | if (invocation.source() instanceof Player player) { 63 | String[] args = invocation.arguments(); 64 | List suggestions = new ArrayList<>(); 65 | boolean isLeader = Nebula.partyManager.getParty(player).map(p -> p.getLeader().equals(player)).orElse(false); 66 | boolean inParty = Nebula.partyManager.getParty(player).isPresent(); 67 | if(inParty) { 68 | suggestions.add("leave"); 69 | } 70 | if(isLeader|| !inParty) { 71 | suggestions.add("invite"); 72 | } 73 | if(!Nebula.partyManager.getAllInvites(player).isEmpty()) { 74 | suggestions.addAll(List.of("accept", "decline")); 75 | } 76 | if (args.length == 0) { return suggestions; } 77 | if (args.length == 1) { 78 | return suggestions.stream() 79 | .filter(subcommand -> subcommand.toLowerCase().startsWith(args[0].toLowerCase())) 80 | .toList(); 81 | } 82 | if (args.length == 2 && "invite".equalsIgnoreCase(args[0]) && (isLeader || !inParty)) { 83 | return Nebula.server.getAllPlayers().stream() 84 | .filter(p -> Nebula.partyManager.getParty(p).isEmpty()) 85 | .filter(p -> p.getUsername().toLowerCase().startsWith(args[1].toLowerCase())) 86 | .filter(p -> !Nebula.partyManager.isInvitedInMyParty(player, p)) 87 | .filter(p -> !p.equals(player)) 88 | .map(Player::getUsername) 89 | .toList(); 90 | } 91 | if (args.length == 2 && ("accept".equalsIgnoreCase(args[0]) || "decline".equalsIgnoreCase(args[0])) && !Nebula.partyManager.getAllInvites(player).isEmpty()) { 92 | return Nebula.partyManager.getAllInvites(player); 93 | } 94 | } 95 | return List.of(); 96 | } 97 | 98 | private void sendUsageMessage(CommandSource source) { 99 | Nebula.util.sendMessage(source, Messages.USAGE_PARTY); 100 | } 101 | } -------------------------------------------------------------------------------- /src/main/resources/messages.yml: -------------------------------------------------------------------------------- 1 | ## File Version 2 | file-version: 1 3 | # MiniMessage support 4 | # Prefix before each message. 5 | prefix: "[<#AA00AA>Server] " 6 | #
 is a placeholder for the prefix.
 7 | container:
 8 |   usage: "
Usage: /container  "
 9 |   kill-start: "
Killing server instance ."
10 |   delete-start: "
Deleting server instance ."
11 |   server-pull: "
Pulling template