diff --git a/balancer-velocity/pom.xml b/balancer-velocity/pom.xml
new file mode 100644
index 0000000..a3913e0
--- /dev/null
+++ b/balancer-velocity/pom.xml
@@ -0,0 +1,107 @@
+
+
+ 4.0.0
+
+
+ com.jaimemartz
+ 2.3.3
+ playerbalancer-parent
+
+
+ playerbalancer-velocity
+
+ PlayerBalancer Velocity
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+
+ PlayerBalancer-Velocity-${project.version}
+
+
+ org.bstats
+ com.jaimemartz.playerbalancer.metrics
+
+
+ ninja.leaping.configurate
+ com.jaimemartz.playerbalancer.libs.ninja.leaping.configurate
+
+
+ com.typesafe.config
+ com.jaimemartz.playerbalancer.libs.com.typesafe.config
+
+
+
+
+
+ package
+
+ shade
+
+
+
+
+
+
+
+
+
+ papermc
+ https://repo.papermc.io/repository/maven-public/
+
+
+ CodeMC
+ https://repo.codemc.org/repository/maven-public
+
+
+
+
+
+ com.velocitypowered
+ velocity-api
+ 3.2.0-SNAPSHOT
+ provided
+
+
+ com.google.guava
+ guava
+ 25.1-jre
+ provided
+
+
+ com.google.inject
+ guice
+ 6.0.0
+ provided
+
+
+ com.imaginarycode.minecraft
+ RedisBungee
+ 0.6.5-SNAPSHOT
+ provided
+
+
+ org.spongepowered
+ configurate-hocon
+ 3.7.3
+ compile
+
+
+
+ com.google.guava
+ guava
+
+
+
+
+ org.bstats
+ bstats-velocity
+ 3.0.2
+ compile
+
+
+
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/PlayerBalancer.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/PlayerBalancer.java
new file mode 100644
index 0000000..e92a014
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/PlayerBalancer.java
@@ -0,0 +1,355 @@
+package com.jaimemartz.playerbalancer.velocity;
+
+import com.google.common.reflect.TypeToken;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.inject.Inject;
+import com.jaimemartz.playerbalancer.velocity.commands.FallbackCommand;
+import com.jaimemartz.playerbalancer.velocity.commands.MainCommand;
+import com.jaimemartz.playerbalancer.velocity.commands.ManageCommand;
+import com.jaimemartz.playerbalancer.velocity.connection.ServerAssignRegistry;
+import com.jaimemartz.playerbalancer.velocity.helper.NetworkManager;
+import com.jaimemartz.playerbalancer.velocity.helper.PasteHelper;
+import com.jaimemartz.playerbalancer.velocity.helper.PlayerLocker;
+import com.jaimemartz.playerbalancer.velocity.listeners.PlayerDisconnectListener;
+import com.jaimemartz.playerbalancer.velocity.listeners.PluginMessageListener;
+import com.jaimemartz.playerbalancer.velocity.listeners.ProxyReloadListener;
+import com.jaimemartz.playerbalancer.velocity.listeners.ServerConnectListener;
+import com.jaimemartz.playerbalancer.velocity.listeners.ServerKickListener;
+import com.jaimemartz.playerbalancer.velocity.ping.StatusManager;
+import com.jaimemartz.playerbalancer.velocity.section.SectionManager;
+import com.jaimemartz.playerbalancer.velocity.settings.SettingsHolder;
+import com.velocitypowered.api.command.CommandManager;
+import com.velocitypowered.api.command.CommandMeta;
+import com.velocitypowered.api.command.SimpleCommand;
+import com.velocitypowered.api.event.Subscribe;
+import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
+import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
+import com.velocitypowered.api.plugin.Dependency;
+import com.velocitypowered.api.plugin.Plugin;
+import com.velocitypowered.api.plugin.PluginContainer;
+import com.velocitypowered.api.plugin.annotation.DataDirectory;
+import com.velocitypowered.api.proxy.ProxyServer;
+import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
+import lombok.Getter;
+import ninja.leaping.configurate.commented.CommentedConfigurationNode;
+import ninja.leaping.configurate.hocon.HoconConfigurationLoader;
+import ninja.leaping.configurate.loader.ConfigurationLoader;
+import org.bstats.charts.SingleLineChart;
+import org.bstats.velocity.Metrics;
+import org.slf4j.Logger;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Optional;
+
+@Plugin(
+ id = "playerbalancer",
+ name = "PlayerBalancer Velocity",
+ version = "2.3.3",
+ description = "PlayerBalancer is a plugin for setting up a network with multiple lobbies of different types.",
+ authors = {"jaime29010", "BGHDDevelopment", "HappyAreaBean"},
+ dependencies = {
+ @Dependency(id = "redisbungee", optional = true)
+ }
+)
+@Getter
+public class PlayerBalancer {
+ private boolean failed = false;
+ private StatusManager statusManager;
+ private SettingsHolder settings;
+ private SectionManager sectionManager;
+ private NetworkManager networkManager;
+ private ConfigurationLoader loader;
+
+ private FallbackCommand fallbackCommand;
+ private SimpleCommand mainCommand, manageCommand;
+ private CommandMeta mainCommandMeta, manageCommandMeta, fallbackCommandMeta;
+ private Object connectListener, kickListener, reloadListener, pluginMessageListener;
+
+ public static final LegacyChannelIdentifier PB_CHANNEL = new LegacyChannelIdentifier("playerbalancer:main");
+
+ private final ProxyServer proxyServer;
+ private final Logger logger;
+ private final Metrics.Factory metricsFactory;
+ private final PluginContainer container;
+ private final Path dataDirectory;
+
+ @Inject
+ public PlayerBalancer(ProxyServer proxyServer, Logger logger, Metrics.Factory metricsFactory, PluginContainer container, @DataDirectory Path dataDirectory) {
+ this.proxyServer = proxyServer;
+ this.logger = logger;
+ this.metricsFactory = metricsFactory;
+ this.container = container;
+ this.dataDirectory = dataDirectory;
+ }
+
+ @Subscribe
+ public void onProxyInitialization(ProxyInitializeEvent event) {
+ Metrics metrics = metricsFactory.make(this, 1636);
+ metrics.addCustomChart(new SingleLineChart("configured_sections", () -> {
+ if (sectionManager != null) {
+ return sectionManager.getSections().size();
+ } else {
+ return 0;
+ }
+ }));
+
+ updateCheck();
+
+ this.execStart();
+ }
+
+ public void updateCheck() {
+ try {
+ Optional pluginVersion = container.getDescription().getVersion();
+ if (!pluginVersion.isPresent()) return;
+ String urlString = "https://updatecheck.bghddevelopment.com";
+ URL url = new URL(urlString);
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("GET");
+ connection.setRequestProperty("User-Agent", "Mozilla/5.0");
+ BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+ String input;
+ StringBuilder response = new StringBuilder();
+ while ((input = reader.readLine()) != null) {
+ response.append(input);
+ }
+ reader.close();
+ JsonObject object = new JsonParser().parse(response.toString()).getAsJsonObject();
+
+ if (object.has("plugins")) {
+ JsonObject plugins = object.get("plugins").getAsJsonObject();
+ JsonObject info = plugins.get("PlayerBalancer").getAsJsonObject();
+ String version = info.get("version").getAsString();
+ if (version.equals(pluginVersion.get())) {
+ getLogger().info(("PlayerBalancer is on the latest version."));
+ } else {
+ getLogger().warn("");
+ getLogger().warn("");
+ getLogger().warn("Your PlayerBalancer version is out of date!");
+ getLogger().warn("We recommend updating ASAP!");
+ getLogger().warn("");
+ getLogger().warn("Your Version: " + pluginVersion.get());
+ getLogger().warn("Newest Version: " + version);
+ getLogger().warn("");
+ getLogger().warn("");
+ }
+ } else {
+ logger.error("Wrong response from update API, contact plugin developer!");
+ }
+ } catch (Exception ex) {
+ logger.error("Failed to get updater check. (" + ex.getMessage() + ")");
+ }
+ }
+
+ @Subscribe
+ public void onProxyShutdown(ProxyShutdownEvent event) {
+ this.execStop();
+ }
+
+ private void execStart() {
+ if (!dataDirectory.toFile().exists())
+ dataDirectory.toFile().mkdir();
+
+ File file = new File(dataDirectory.toFile(), "plugin.conf");
+
+ if (!file.exists()) {
+ try (InputStream in = getClass().getResourceAsStream("velocity.conf")) {
+ Files.copy(in, file.toPath());
+ } catch (IOException e) {
+ logger.error("Unable to copy velocity.conf", e);
+ }
+ }
+
+ if (loader == null) {
+ loader = HoconConfigurationLoader.builder().setFile(file).build();
+ }
+
+ try {
+ CommandManager commandManager = proxyServer.getCommandManager();
+ mainCommand = new MainCommand(this);
+ mainCommandMeta = commandManager.metaBuilder("playerbalancer")
+ .aliases("balancer")
+ .plugin(this)
+ .build();
+ commandManager.register(mainCommandMeta, mainCommand);
+
+ CommentedConfigurationNode node = loader.load();
+ settings = node.getValue(TypeToken.of(SettingsHolder.class));
+
+ if (settings.getGeneralProps().isEnabled()) {
+ if (settings.getGeneralProps().isAutoReload()) {
+ reloadListener = new ProxyReloadListener(this);
+ proxyServer.getEventManager().register(this, reloadListener);
+ }
+
+ networkManager = new NetworkManager(this);
+
+ sectionManager = new SectionManager(this);
+ sectionManager.load();
+
+ statusManager = new StatusManager(this);
+
+ if (settings.getFeaturesProps().getServerCheckerProps().isEnabled()) {
+ statusManager.start();
+ }
+
+ if (settings.getFeaturesProps().getFallbackCommandProps().isEnabled()) {
+ fallbackCommand = new FallbackCommand(this);
+ fallbackCommandMeta = commandManager
+ .metaBuilder(settings.getFeaturesProps().getFallbackCommandProps().getCommand().getName())
+ .aliases(settings.getFeaturesProps().getFallbackCommandProps().getCommand().getAliasesArray())
+ .plugin(this)
+ .build();
+ commandManager.register(fallbackCommandMeta, fallbackCommand);
+ }
+
+ connectListener = new ServerConnectListener(this);
+ proxyServer.getEventManager().register(this, connectListener);
+
+ if (settings.getGeneralProps().isPluginMessaging()) {
+ proxyServer.getChannelRegistrar().register(PB_CHANNEL);
+
+ proxyServer.getEventManager().register(this, statusManager);
+
+ pluginMessageListener = new PluginMessageListener(this);
+ proxyServer.getEventManager().register(this, pluginMessageListener);
+ }
+
+ manageCommand = new ManageCommand(this);
+ manageCommandMeta = commandManager
+ .metaBuilder("section")
+ .plugin(this)
+ .build();
+ commandManager.register(manageCommandMeta, manageCommand);
+
+ proxyServer.getEventManager().register(this, new PlayerDisconnectListener(this));
+
+ if (settings.getFeaturesProps().getKickHandlerProps().isEnabled()) {
+ kickListener = new ServerKickListener(this);
+ proxyServer.getEventManager().register(this, kickListener);
+ }
+
+ PasteHelper.reset();
+ getLogger().info("The plugin has finished loading without any problems");
+ } else {
+ getLogger().warn("-----------------------------------------------------");
+ getLogger().warn("WARNING: This plugin is disabled, do not forget to set enabled on the config to true");
+ getLogger().warn("Nothing is going to work until you do that, you can reload me by using the /balancer command");
+ getLogger().warn("-----------------------------------------------------");
+ }
+ } catch (Exception e) {
+ this.failed = true;
+ getLogger().error("The plugin could not continue loading due to an unexpected exception", e);
+ }
+ }
+
+ private void execStop() {
+ if (mainCommand != null) {
+ proxyServer.getCommandManager().unregister(mainCommandMeta);
+ mainCommand = null;
+ }
+
+ if (settings.getGeneralProps().isEnabled()) {
+ // Do not try to do anything if the plugin has not loaded correctly
+ if (failed) return;
+
+ if (settings.getGeneralProps().isAutoReload()) {
+ if (reloadListener != null) {
+ proxyServer.getEventManager().unregisterListener(this, reloadListener);
+ reloadListener = null;
+ }
+ }
+
+ if (settings.getFeaturesProps().getServerCheckerProps().isEnabled()) {
+ if (statusManager != null) {
+ statusManager.stop();
+ }
+ }
+
+ if (settings.getFeaturesProps().getFallbackCommandProps().isEnabled()) {
+ if (fallbackCommand != null) {
+ proxyServer.getCommandManager().unregister(fallbackCommandMeta);
+ fallbackCommand = null;
+ }
+ }
+
+ if (settings.getFeaturesProps().getKickHandlerProps().isEnabled()) {
+ if (kickListener != null) {
+ proxyServer.getEventManager().unregisterListener(this, kickListener);
+ kickListener = null;
+ }
+ }
+
+ if (connectListener != null) {
+ proxyServer.getEventManager().unregisterListener(this, connectListener);
+ connectListener = null;
+ }
+
+ if (settings.getGeneralProps().isPluginMessaging()) {
+ if (pluginMessageListener != null) {
+ proxyServer.getChannelRegistrar().unregister(PB_CHANNEL);
+ proxyServer.getEventManager().unregisterListener(this, pluginMessageListener);
+ pluginMessageListener = null;
+ }
+ }
+
+ if (manageCommand != null) {
+ proxyServer.getCommandManager().unregister(manageCommandMeta);
+ manageCommand = null;
+ }
+
+ if (sectionManager != null) {
+ sectionManager.flush();
+ }
+
+ ServerAssignRegistry.getTable().clear();
+ }
+
+ PlayerLocker.flush();
+ failed = false;
+ }
+
+ public boolean reloadPlugin() {
+ getLogger().info("Reloading the plugin...");
+ long starting = System.currentTimeMillis();
+
+ this.execStop();
+ this.execStart();
+
+ if (!failed) {
+ long ending = System.currentTimeMillis() - starting;
+ getLogger().info(String.format("The plugin has been reloaded, took %sms", ending));
+ }
+
+ return !failed;
+ }
+
+ public SettingsHolder getSettings() {
+ return settings;
+ }
+
+ public SectionManager getSectionManager() {
+ return sectionManager;
+ }
+
+ public StatusManager getStatusManager() {
+ return statusManager;
+ }
+
+ public FallbackCommand getFallbackCommand() {
+ return fallbackCommand;
+ }
+
+ public NetworkManager getNetworkManager() {
+ return networkManager;
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/commands/FallbackCommand.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/commands/FallbackCommand.java
new file mode 100644
index 0000000..2abd8af
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/commands/FallbackCommand.java
@@ -0,0 +1,125 @@
+package com.jaimemartz.playerbalancer.velocity.commands;
+
+import com.google.common.collect.Iterables;
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.connection.ConnectionIntent;
+import com.jaimemartz.playerbalancer.velocity.section.ServerSection;
+import com.jaimemartz.playerbalancer.velocity.settings.props.MessagesProps;
+import com.jaimemartz.playerbalancer.velocity.settings.props.features.FallbackCommandProps;
+import com.jaimemartz.playerbalancer.velocity.utils.MessageUtils;
+import com.velocitypowered.api.command.CommandSource;
+import com.velocitypowered.api.command.SimpleCommand;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.ServerConnection;
+import com.velocitypowered.api.proxy.server.ServerInfo;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+
+import java.util.Optional;
+
+import static com.jaimemartz.playerbalancer.velocity.utils.MessageUtils.safeNull;
+
+public class FallbackCommand implements SimpleCommand {
+ private final PlayerBalancer plugin;
+ private final MessagesProps messages;
+ protected final FallbackCommandProps props;
+
+ /**
+ * Constructor for `fallback-command`
+ */
+ public FallbackCommand(PlayerBalancer plugin) {
+ this.messages = plugin.getSettings().getMessagesProps();
+ this.props = plugin.getSettings().getFeaturesProps().getFallbackCommandProps();
+ this.plugin = plugin;
+ }
+
+ @Override
+ public void execute(Invocation invocation) {
+ CommandSource source = invocation.source();
+ String[] args = invocation.arguments();
+
+ if (source instanceof Player) {
+ Player player = (Player) source;
+ ServerSection target = getSection(player);
+
+ if (target != null) {
+ if (args.length == 1) {
+ try {
+ int number = Integer.parseInt(args[0]);
+ if (number <= 0) {
+ MessageUtils.send(player, messages.getInvalidInputMessage());
+ } else if (number > target.getServers().size()) {
+ MessageUtils.send(player, messages.getInvalidInputMessage());
+ } else {
+ ServerInfo server = Iterables.get(target.getServers(), number - 1).getServerInfo();
+ ConnectionIntent.direct(plugin, player, server, null);
+ }
+ } catch (NumberFormatException e) {
+ MessageUtils.send(player, messages.getInvalidInputMessage());
+ }
+ } else {
+ if (props.isPreventSameSection()) {
+ Optional current = player.getCurrentServer();
+ if (current.isPresent()) {
+ if (target.getServers().contains(current.get().getServer())) {
+ MessageUtils.send(player, plugin.getSettings().getMessagesProps().getSameSectionMessage(),
+ (str) -> str.replace("{server}", current.get().getServerInfo().getName())
+ .replace("{section}", target.getName())
+ .replace("{alias}", safeNull(target.getProps().getAlias()))
+ );
+ return;
+ }
+ }
+ }
+
+ ConnectionIntent.simple(plugin, player, target);
+ }
+ }
+ } else {
+ source.sendMessage(Component.text("This command can only be executed by a player", NamedTextColor.RED));
+ }
+ }
+
+ public ServerSection getSection(Player player) {
+ ServerSection current = plugin.getSectionManager().getByPlayer(player);
+
+ if (current != null) {
+ if (props.getExcludedSections().contains(current.getName())) {
+ MessageUtils.send(player, messages.getUnavailableServerMessage());
+ return null;
+ }
+
+ ServerSection target = current.getParent();
+
+ String bindName = props.getRules().get(current.getName());
+ if (bindName != null) {
+ ServerSection bind = plugin.getSectionManager().getByName(bindName);
+ if (bind != null) {
+ target = bind;
+ }
+ }
+
+ if (target == null) {
+ MessageUtils.send(player, messages.getUnavailableServerMessage());
+ return null;
+ }
+
+ if (props.isRestrictive()) {
+ if (current.getPosition() >= 0 && target.getPosition() < 0) {
+ MessageUtils.send(player, messages.getUnavailableServerMessage());
+ return null;
+ }
+ }
+
+ return target;
+ } else {
+ if (plugin.getSettings().getFeaturesProps().getBalancerProps().isDefaultPrincipal()) {
+ return plugin.getSectionManager().getPrincipal();
+ } else {
+ MessageUtils.send(player, messages.getUnavailableServerMessage());
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/commands/MainCommand.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/commands/MainCommand.java
new file mode 100644
index 0000000..aaf98c4
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/commands/MainCommand.java
@@ -0,0 +1,142 @@
+package com.jaimemartz.playerbalancer.velocity.commands;
+
+import com.google.common.base.Strings;
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.helper.PasteHelper;
+import com.velocitypowered.api.command.CommandSource;
+import com.velocitypowered.api.command.SimpleCommand;
+import com.velocitypowered.api.proxy.Player;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.event.ClickEvent;
+import net.kyori.adventure.text.event.HoverEvent;
+import net.kyori.adventure.text.format.TextDecoration;
+
+import static net.kyori.adventure.text.Component.text;
+import static net.kyori.adventure.text.format.NamedTextColor.AQUA;
+import static net.kyori.adventure.text.format.NamedTextColor.GRAY;
+import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
+import static net.kyori.adventure.text.format.NamedTextColor.RED;
+
+public class MainCommand implements SimpleCommand {
+ private final PlayerBalancer plugin;
+
+ public MainCommand(PlayerBalancer plugin) {
+ this.plugin = plugin;
+ }
+
+ @Override
+ public void execute(Invocation invocation) {
+ CommandSource sender = invocation.source();
+ String[] args = invocation.arguments();
+
+ if (args.length != 0) {
+ switch (args[0].toLowerCase()) {
+ case "paste": {
+ if (sender.hasPermission("playerbalancer.admin")) {
+ if (args.length == 2) {
+ switch (args[1].toLowerCase()) {
+ case "all": {
+ PasteHelper.PLUGIN.send(plugin, sender);
+ PasteHelper.VELOCITY.send(plugin, sender);
+ PasteHelper.LOGS.send(plugin, sender);
+ break;
+ }
+
+ case "plugin": {
+ PasteHelper.PLUGIN.send(plugin, sender);
+ break;
+ }
+
+ case "velocity": {
+ PasteHelper.VELOCITY.send(plugin, sender);
+ break;
+ }
+
+ case "logs": {
+ PasteHelper.LOGS.send(plugin, sender);
+ break;
+ }
+
+ default: {
+ sender.sendMessage(text("This is not a valid argument for this command! Execute /balancer paste for help", RED));
+ }
+ }
+ } else {
+ if (sender instanceof Player) {
+ sender.sendMessage(text("Available paste types:", AQUA));
+
+ sender.sendMessage(text("Click one:", AQUA)
+ .append(text(" [")
+ .color(GRAY)
+ .append(text("All")
+ .color(RED)
+ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/balancer paste all"))
+ .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, text("Click to paste all", RED)))
+ )
+ .append(Component.text("]", GRAY)))
+ .append(text(" [")
+ .color(GRAY)
+ .append(text("Plugin")
+ .color(RED)
+ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/balancer paste plugin"))
+ .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, text("Click to paste plugin config", RED)))
+ )
+ .append(Component.text("]", GRAY))
+ )
+ .append(text(" [")
+ .color(GRAY)
+ .append(text("Velocity")
+ .color(RED)
+ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/balancer paste velocity"))
+ .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, text("Click to paste Velocity config", RED)))
+ )
+ .append(Component.text("]", GRAY)))
+ .append(text(" [")
+ .color(GRAY)
+ .append(text("Logs")
+ .color(RED)
+ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/balancer paste logs"))
+ .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, text("Click to paste logs", RED)))
+ )
+ .append(Component.text("]", GRAY)))
+ );
+ } else {
+ sender.sendMessage(text("Usage: /balancer paste [all|plugin|velocity|logs]", RED));
+ }
+
+ }
+ } else {
+ sender.sendMessage(text("You do not have permission to execute this command!"));
+ }
+ break;
+ }
+
+ case "reload": {
+ if (sender.hasPermission("playerbalancer.admin")) {
+ sender.sendMessage(text("Reloading the configuration, this may take a while...", GREEN));
+ if (plugin.reloadPlugin()) {
+ sender.sendMessage(text("The plugin has been successfully reloaded", GREEN));
+ } else {
+ sender.sendMessage(text("Something went badly while reloading the plugin", RED));
+ }
+ } else {
+ sender.sendMessage(text("You do not have permission to execute this command!", RED));
+ }
+ break;
+ }
+
+ default: {
+ sender.sendMessage(text("This is not a valid argument for this command! Execute /balancer for help", RED));
+ }
+ }
+ } else {
+ sender.sendMessage(text(Strings.repeat("-", 53), GRAY, TextDecoration.STRIKETHROUGH));
+ sender.sendMessage(text("PlayerBalancer " + plugin.getContainer().getDescription().getVersion().orElse("-.-.-"), GRAY));
+ sender.sendMessage(text("Available commands:", GRAY));
+ sender.sendMessage(text("/balancer", AQUA).append(text(" - ", GRAY)).append(text("Shows you this message", RED)));
+ sender.sendMessage(text("/balancer paste [all|plugin|velocity|logs]", AQUA).append(text(" - ", GRAY)).append(text("Creates a paste with the important files", RED)));
+ sender.sendMessage(text("/balancer reload", AQUA).append(text(" - ", GRAY)).append(text("Reloads the plugin completely", RED)));
+ sender.sendMessage(text(Strings.repeat("-", 53), GRAY, TextDecoration.STRIKETHROUGH));
+ }
+ }
+}
\ No newline at end of file
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/commands/ManageCommand.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/commands/ManageCommand.java
new file mode 100644
index 0000000..c392f4f
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/commands/ManageCommand.java
@@ -0,0 +1,273 @@
+package com.jaimemartz.playerbalancer.velocity.commands;
+
+import com.google.common.base.Strings;
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.connection.ConnectionIntent;
+import com.jaimemartz.playerbalancer.velocity.ping.ServerStatus;
+import com.jaimemartz.playerbalancer.velocity.section.SectionManager;
+import com.jaimemartz.playerbalancer.velocity.section.ServerSection;
+import com.jaimemartz.playerbalancer.velocity.utils.MessageUtils;
+import com.velocitypowered.api.command.CommandSource;
+import com.velocitypowered.api.command.SimpleCommand;
+import com.velocitypowered.api.proxy.Player;
+import net.kyori.adventure.text.event.ClickEvent;
+import net.kyori.adventure.text.event.HoverEvent;
+import net.kyori.adventure.text.format.TextDecoration;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+import static net.kyori.adventure.text.Component.newline;
+import static net.kyori.adventure.text.Component.text;
+import static net.kyori.adventure.text.Component.textOfChildren;
+import static net.kyori.adventure.text.format.NamedTextColor.AQUA;
+import static net.kyori.adventure.text.format.NamedTextColor.GRAY;
+import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
+import static net.kyori.adventure.text.format.NamedTextColor.RED;
+import static net.kyori.adventure.text.format.NamedTextColor.WHITE;
+
+public class ManageCommand implements SimpleCommand {
+ private final PlayerBalancer plugin;
+
+ public ManageCommand(PlayerBalancer plugin) {
+ this.plugin = plugin;
+ }
+
+ @Override
+ public boolean hasPermission(Invocation invocation) {
+ return invocation.source().hasPermission("playerbalancer.admin");
+ }
+
+ @Override
+ public void execute(Invocation invocation) {
+ CommandSource sender = invocation.source();
+ String[] args = invocation.arguments();
+
+ if (args.length != 0) {
+ switch (args[0].toLowerCase()) {
+ case "connect": {
+ if (args.length >= 2) {
+ String input = args[1];
+ ServerSection section = plugin.getSectionManager().getByName(input);
+ if (section != null) {
+ if (args.length == 3) {
+ Optional player = plugin.getProxyServer().getPlayer(args[2]);
+ if (player.isPresent()) {
+ ConnectionIntent.simple(plugin, player.get(), section);
+ } else {
+ sender.sendMessage(text("There is no player with that name connected to this proxy", RED));
+ }
+ } else {
+ if (sender instanceof Player) {
+ ConnectionIntent.simple(plugin, (Player) sender, section);
+ } else {
+ sender.sendMessage(text("This command variant can only be executed by a player", RED));
+ }
+ }
+ } else {
+ MessageUtils.send(sender, plugin.getSettings().getMessagesProps().getUnknownSectionMessage());
+ }
+ } else {
+ sender.sendMessage(text("Usage: /section connect [player]", RED));
+ }
+ break;
+ }
+
+ case "info": {
+ if (args.length == 2) {
+ String input = args[1];
+ SectionManager manager = plugin.getSectionManager();
+ ServerSection section = manager.getByName(input);
+
+ if (section != null) {
+ sender.sendMessage(text(Strings.repeat("-", 53), GRAY, TextDecoration.STRIKETHROUGH));
+
+ sender.sendMessage(text("Information of section: ", GRAY)
+ .append(text(section.getName(), RED))
+ );
+
+ sender.sendMessage(text("Principal: ", GRAY)
+ .append(
+ text(manager.isPrincipal(section) ? "yes" : "no")
+ .color(manager.isPrincipal(section) ? GREEN : RED)
+ )
+ );
+
+ sender.sendMessage(text("Dummy: ", GRAY)
+ .append(
+ text(manager.isDummy(section) ? "yes" : "no")
+ .color(manager.isDummy(section) ? GREEN : RED)
+ )
+ );
+
+ sender.sendMessage(text("Reiterative: ", GRAY)
+ .append(
+ text(manager.isReiterative(section) ? "yes" : "no")
+ .color(manager.isReiterative(section) ? GREEN : RED))
+ );
+
+ if (section.getParent() != null) {
+ sender.sendMessage(text("Parent: ", GRAY)
+ .append(text(section.getParent().getName(), AQUA))
+ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, String.format("/section info %s", section.getParent().getName())))
+ .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, text("Click me for info of " + section.getParent().getName(), RED)))
+ );
+ } else {
+ sender.sendMessage(text("Parent: ", GRAY)
+ .append(text("None", AQUA))
+ );
+ }
+
+ if (section.getProps().getAlias() != null) {
+ sender.sendMessage(text("Alias: ", GRAY)
+ .append(text("\"", AQUA))
+ .append(text(section.getProps().getAlias(), RED))
+ .append(text("\"", AQUA))
+
+ );
+ } else {
+ sender.sendMessage(text("Alias: ", GRAY)
+ .append(text("None", AQUA))
+
+ );
+ }
+
+ sender.sendMessage(text("Position: ", GRAY)
+ .append(text(String.valueOf(section.getPosition()), AQUA))
+
+ );
+
+ sender.sendMessage(text("Provider: ", GRAY)
+ .append(text(section.getImplicitProvider().name(), AQUA))
+ .append(text(String.format(" (%s)", section.isInherited() ? "Implicit" : "Explicit"), GRAY))
+
+ );
+
+ if (section.getServer() != null) {
+ sender.sendMessage(text("Section Server: ", GRAY)
+ .append(text(section.getServer().getServerInfo().getName(), AQUA))
+ );
+ } else {
+ sender.sendMessage(text("Section Server: ", GRAY)
+ .append(text("None", AQUA))
+ );
+ }
+
+ if (section.getCommand() != null) {
+ sender.sendMessage(text("Section Command: ", GRAY)
+ .append(text(section.getCommand().getCommandProps().getName(), AQUA))
+ .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT,
+ textOfChildren(
+ text("Name: ", GRAY),
+ text(section.getCommand().getCommandProps().getName()).color(AQUA),
+
+ newline(),
+
+ text("Permission: ", GRAY),
+ text("\"", AQUA),
+ text(section.getCommand().getCommandProps().getPermission(), RED),
+ text("\"", AQUA),
+
+ newline(),
+
+ text("Aliases: ", GRAY),
+ text(Arrays.toString(section.getCommand().getCommandProps().getAliasesArray()), AQUA)
+ )
+ )
+ )
+ );
+ } else {
+ sender.sendMessage(text("Section Command: ", GRAY)
+ .append(text("None", AQUA))
+ );
+ }
+
+ if (!section.getServers().isEmpty()) {
+ sender.sendMessage(text("Section Servers: ", GRAY));
+
+ section.getServers().forEach(server -> {
+ ServerStatus status = plugin.getStatusManager().getStatus(server.getServerInfo());
+ boolean accessible = plugin.getStatusManager().isAccessible(server.getServerInfo());
+ sender.sendMessage(
+ text("• Server: ")
+ .append(text(server.getServerInfo().getName()))
+ .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT,
+ textOfChildren(
+ text("Online: ", GRAY),
+ text(status.isOnline() ? "yes" : "no", status.isOnline() ? GREEN : RED),
+
+ newline(),
+
+ text("Accessible: ", GRAY),
+ text(accessible ? "yes" : "no", accessible ? GREEN : RED),
+
+ newline(),
+
+ text("Description: ", GRAY),
+ text("\"", AQUA),
+ status.getDescription().color(WHITE),
+ text("\"", AQUA),
+
+ newline(),
+
+ text("Address: ", GRAY),
+ text(server.getServerInfo().getAddress().toString(), AQUA)
+ )
+ ))
+ .append(text((String.format(" (%d/%d) ",
+ status.getPlayers(),
+ status.getMaximum()))))
+ .color(status.isOnline() ? GREEN : RED)
+
+ );
+ });
+ } else {
+ sender.sendMessage(text("Section Servers: ", GRAY)
+ .append(text("None"))
+ .color(AQUA)
+
+ );
+ }
+
+ sender.sendMessage(text(Strings.repeat("-", 53), GRAY, TextDecoration.STRIKETHROUGH));
+ } else {
+ MessageUtils.send(sender, plugin.getSettings().getMessagesProps().getUnknownSectionMessage());
+ }
+ } else {
+ sender.sendMessage(text("Usage: /section info ", RED));
+ }
+ break;
+ }
+
+ case "list": {
+ if (!plugin.getSectionManager().getSections().isEmpty()) {
+ sender.sendMessage(text("These are the registered sections: ", GRAY));
+
+ plugin.getSectionManager().getSections().forEach((name, section) -> {
+ sender.sendMessage(text("• Section: ", GRAY)
+ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, String.format("/section info %s", name)))
+ .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, text("Click me for info of section " + name, RED)))
+ .append(text(name, AQUA))
+ );
+ });
+ } else {
+ sender.sendMessage(text("There are no sections to list", GRAY));
+ }
+ break;
+ }
+
+ default: {
+ sender.sendMessage(text("This is not a valid argument for this command! Execute /section for help", RED));
+ }
+ }
+ } else {
+ sender.sendMessage(text(Strings.repeat("-", 53), GRAY, TextDecoration.STRIKETHROUGH));
+ sender.sendMessage(text("Available commands:", GRAY));
+ sender.sendMessage(text("/section", AQUA).append(text(" - ", GRAY)).append(text("Shows you this message", RED)));
+ sender.sendMessage(text("/section list", AQUA).append(text(" - ", GRAY)).append(text("Tells you which sections are configured in the plugin", RED)));
+ sender.sendMessage(text("/section info ", AQUA).append(text(" - ", GRAY)).append(text("Tells you info about the specified section", RED)));
+ sender.sendMessage(text("/section connect [player]", AQUA).append(text(" - ", GRAY)).append(text("Connects you or the specified player to that section", RED)));
+ sender.sendMessage(text(Strings.repeat("-", 53), GRAY, TextDecoration.STRIKETHROUGH));
+ }
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/ConnectionIntent.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/ConnectionIntent.java
new file mode 100644
index 0000000..614c56a
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/ConnectionIntent.java
@@ -0,0 +1,145 @@
+package com.jaimemartz.playerbalancer.velocity.connection;
+
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.helper.PlayerLocker;
+import com.jaimemartz.playerbalancer.velocity.section.ServerSection;
+import com.jaimemartz.playerbalancer.velocity.utils.MessageUtils;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+import com.velocitypowered.api.proxy.server.ServerInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+import static com.jaimemartz.playerbalancer.velocity.utils.MessageUtils.safeNull;
+
+public abstract class ConnectionIntent {
+ private final PlayerBalancer plugin;
+ private final Player player;
+ private final ServerSection section;
+ private final List exclusions;
+
+ public ConnectionIntent(PlayerBalancer plugin, Player player, ServerSection section) {
+ this.plugin = plugin;
+ this.player = player;
+ this.section = tryRoute(player, section);
+ this.exclusions = new ArrayList<>();
+ }
+
+ public List getExclusions() {
+ return exclusions;
+ }
+
+ public void execute() {
+ MessageUtils.send(player, plugin.getSettings().getMessagesProps().getConnectingMessage(),
+ (str) -> str.replace("{section}", section.getName())
+ .replace("{alias}", safeNull(section.getProps().getAlias()))
+ );
+
+ // Ensure a new copy of the section servers
+ List servers = new ArrayList<>(section.getServers());
+
+ // Prevents connections to the same server
+ player.getCurrentServer().ifPresent(current -> servers.remove(current.getServer()));
+
+ if (section.getImplicitProvider() != ProviderType.NONE) {
+ ServerInfo target = this.fetchServer(player, section, section.getImplicitProvider(), servers);
+ if (target != null) {
+ this.connect(target, (response) -> {
+ if (response) { // only if the connect has been executed correctly
+ MessageUtils.send(player, plugin.getSettings().getMessagesProps().getConnectedMessage(),
+ (str) -> str.replace("{server}", target.getName())
+ .replace("{section}", section.getName())
+ .replace("{alias}", safeNull(section.getProps().getAlias()))
+ );
+ }
+ });
+ } else {
+ MessageUtils.send(player, plugin.getSettings().getMessagesProps().getFailureMessage());
+ }
+ }
+ }
+
+ private ServerInfo fetchServer(Player player, ServerSection section, ProviderType provider, List servers) {
+ if (plugin.getSectionManager().isReiterative(section)) {
+ if (ServerAssignRegistry.hasAssignedServer(player, section)) {
+ ServerInfo target = ServerAssignRegistry.getAssignedServer(player, section);
+ if (plugin.getStatusManager().isAccessible(target)) {
+ return target;
+ } else {
+ ServerAssignRegistry.revokeTarget(player, section);
+ }
+ }
+ }
+
+ int intents = plugin.getSettings().getFeaturesProps().getServerCheckerProps().getAttempts();
+ for (int intent = 1; intent <= intents; intent++) {
+ if (servers.size() == 0) return null;
+
+ RegisteredServer target = provider.requestTarget(plugin, section, servers, player);
+ if (target == null) continue;
+
+ if (plugin.getStatusManager().isAccessible(target.getServerInfo())) {
+ return target.getServerInfo();
+ } else {
+ servers.remove(target);
+ }
+ }
+
+ return null;
+ }
+
+ private ServerSection tryRoute(Player player, ServerSection section) {
+ if (plugin.getSettings().getFeaturesProps().getPermissionRouterProps().isEnabled()) {
+ Map routes = plugin.getSettings().getFeaturesProps().getPermissionRouterProps().getRules().get(section.getName());
+ ServerSection current = plugin.getSectionManager().getByPlayer(player);
+
+ if (routes != null) {
+ for (Map.Entry route : routes.entrySet()) {
+ if (player.hasPermission(route.getKey())) {
+ ServerSection bind = plugin.getSectionManager().getByName(route.getValue());
+
+ if (bind != null) {
+ if (current == bind) {
+ break;
+ }
+
+ return bind;
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ return section;
+ }
+
+ public abstract void connect(ServerInfo server, Consumer callback);
+
+ public static void simple(PlayerBalancer plugin, Player player, ServerSection section) {
+ new ConnectionIntent(plugin, player, section) {
+ @Override
+ public void connect(ServerInfo server, Consumer callback) {
+ ConnectionIntent.direct(plugin, player, server, callback);
+ }
+ }.execute();
+ }
+
+ public static void direct(PlayerBalancer plugin, Player player, ServerInfo server, Consumer callback) {
+ PlayerLocker.lock(player);
+ plugin.getProxyServer().getServer(server.getName()).ifPresent((rServer) -> {
+ player.createConnectionRequest(rServer).connect()
+ .whenComplete((result, throwable) -> {
+ plugin.getProxyServer().getScheduler().buildTask(plugin, () -> PlayerLocker.unlock(player))
+ .delay(5, TimeUnit.SECONDS).schedule();
+
+ callback.accept(result.isSuccessful());
+ });
+ });
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/ProviderType.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/ProviderType.java
new file mode 100644
index 0000000..fbb288b
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/ProviderType.java
@@ -0,0 +1,100 @@
+package com.jaimemartz.playerbalancer.velocity.connection;
+
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.connection.provider.AbstractProvider;
+import com.jaimemartz.playerbalancer.velocity.connection.provider.types.NullProvider;
+import com.jaimemartz.playerbalancer.velocity.connection.provider.types.progressive.ProgressiveFillerProvider;
+import com.jaimemartz.playerbalancer.velocity.connection.provider.types.progressive.ProgressiveLowestProvider;
+import com.jaimemartz.playerbalancer.velocity.connection.provider.types.progressive.ProgressiveProvider;
+import com.jaimemartz.playerbalancer.velocity.connection.provider.types.random.RandomFillerProvider;
+import com.jaimemartz.playerbalancer.velocity.connection.provider.types.random.RandomLowestProvider;
+import com.jaimemartz.playerbalancer.velocity.connection.provider.types.random.RandomProvider;
+import com.jaimemartz.playerbalancer.velocity.section.ServerSection;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+
+import java.util.List;
+
+public enum ProviderType {
+ NONE {
+ NullProvider provider = new NullProvider();
+
+ @Override
+ public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List servers, Player player) {
+ return provider.requestTarget(plugin, section, servers, player);
+ }
+ },
+
+ RANDOM {
+ RandomProvider provider = new RandomProvider();
+
+ @Override
+ public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List servers, Player player) {
+ return provider.requestTarget(plugin, section, servers, player);
+ }
+ },
+
+ RANDOM_LOWEST {
+ RandomLowestProvider provider = new RandomLowestProvider();
+
+ @Override
+ public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List servers, Player player) {
+ return provider.requestTarget(plugin, section, servers, player);
+ }
+ },
+
+ RANDOM_FILLER {
+ RandomFillerProvider provider = new RandomFillerProvider();
+
+ @Override
+ public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List servers, Player player) {
+ return provider.requestTarget(plugin, section, servers, player);
+ }
+ },
+
+ PROGRESSIVE {
+ ProgressiveProvider provider = new ProgressiveProvider();
+
+ @Override
+ public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List servers, Player player) {
+ return provider.requestTarget(plugin, section, servers, player);
+ }
+ },
+
+ PROGRESSIVE_LOWEST {
+ ProgressiveLowestProvider provider = new ProgressiveLowestProvider();
+
+ @Override
+ public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List servers, Player player) {
+ return provider.requestTarget(plugin, section, servers, player);
+ }
+ },
+
+ PROGRESSIVE_FILLER {
+ ProgressiveFillerProvider provider = new ProgressiveFillerProvider();
+
+ @Override
+ public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List servers, Player player) {
+ return provider.requestTarget(plugin, section, servers, player);
+ }
+ },
+
+ EXTERNAL {
+ @Override
+ public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List servers, Player player) {
+ AbstractProvider provider = section.getExternalProvider();
+ if (provider == null) {
+ plugin.getLogger().warn("Target requested to the EXTERNAL provider with the section not having a provider instance, falling back to RANDOM...");
+ return RANDOM.requestTarget(plugin, section, servers, player);
+ }
+ return provider.requestTarget(plugin, section, servers, player);
+ }
+ };
+
+ public abstract RegisteredServer requestTarget(
+ PlayerBalancer plugin,
+ ServerSection section,
+ List servers,
+ Player player
+ );
+}
\ No newline at end of file
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/ServerAssignRegistry.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/ServerAssignRegistry.java
new file mode 100644
index 0000000..88f164a
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/ServerAssignRegistry.java
@@ -0,0 +1,55 @@
+package com.jaimemartz.playerbalancer.velocity.connection;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+import com.jaimemartz.playerbalancer.velocity.section.ServerSection;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.server.ServerInfo;
+
+import java.util.Map;
+
+public class ServerAssignRegistry {
+ private static final Table table = HashBasedTable.create();
+
+ public static void assignTarget(Player player, ServerSection section, ServerInfo server) {
+ synchronized (table) {
+ table.put(player, section, server);
+ }
+ }
+
+ public static void revokeTarget(Player player, ServerSection section) {
+ synchronized (table) {
+ table.remove(player, section);
+ }
+ }
+
+ public static ServerInfo getAssignedServer(Player player, ServerSection section) {
+ synchronized (table) {
+ return table.get(player, section);
+ }
+ }
+
+ public static Map getAssignments(Player player) {
+ synchronized (table) {
+ return table.row(player);
+ }
+ }
+
+ public static void clearAsssignedServers(Player player) {
+ synchronized (table) {
+ table.row(player).clear();
+ }
+ }
+
+ public static boolean hasAssignedServer(Player player, ServerSection section) {
+ synchronized (table) {
+ return table.contains(player, section);
+ }
+ }
+
+ public static Table getTable() {
+ synchronized (table) {
+ return table;
+ }
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/AbstractProvider.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/AbstractProvider.java
new file mode 100644
index 0000000..d9e2bbb
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/AbstractProvider.java
@@ -0,0 +1,17 @@
+package com.jaimemartz.playerbalancer.velocity.connection.provider;
+
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.section.ServerSection;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+
+import java.util.List;
+
+public abstract class AbstractProvider {
+ public abstract RegisteredServer requestTarget(
+ PlayerBalancer plugin,
+ ServerSection section,
+ List servers,
+ Player player
+ );
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/NullProvider.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/NullProvider.java
new file mode 100644
index 0000000..93934dd
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/NullProvider.java
@@ -0,0 +1,16 @@
+package com.jaimemartz.playerbalancer.velocity.connection.provider.types;
+
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.connection.provider.AbstractProvider;
+import com.jaimemartz.playerbalancer.velocity.section.ServerSection;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+
+import java.util.List;
+
+public class NullProvider extends AbstractProvider {
+ @Override
+ public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List servers, Player player) {
+ return null;
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/progressive/ProgressiveFillerProvider.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/progressive/ProgressiveFillerProvider.java
new file mode 100644
index 0000000..b6897a4
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/progressive/ProgressiveFillerProvider.java
@@ -0,0 +1,30 @@
+package com.jaimemartz.playerbalancer.velocity.connection.provider.types.progressive;
+
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.connection.provider.AbstractProvider;
+import com.jaimemartz.playerbalancer.velocity.ping.ServerStatus;
+import com.jaimemartz.playerbalancer.velocity.section.ServerSection;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+
+import java.util.List;
+
+public class ProgressiveFillerProvider extends AbstractProvider {
+ @Override
+ public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List servers, Player player) {
+ int max = Integer.MIN_VALUE;
+ RegisteredServer target = null;
+
+ for (RegisteredServer server : servers) {
+ ServerStatus status = plugin.getStatusManager().getStatus(server.getServerInfo());
+ int count = plugin.getNetworkManager().getPlayers(server.getServerInfo());
+
+ if (count > max && count <= status.getMaximum()) {
+ max = count;
+ target = server;
+ }
+ }
+
+ return target;
+ }
+}
\ No newline at end of file
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/progressive/ProgressiveLowestProvider.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/progressive/ProgressiveLowestProvider.java
new file mode 100644
index 0000000..0b85665
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/progressive/ProgressiveLowestProvider.java
@@ -0,0 +1,28 @@
+package com.jaimemartz.playerbalancer.velocity.connection.provider.types.progressive;
+
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.connection.provider.AbstractProvider;
+import com.jaimemartz.playerbalancer.velocity.section.ServerSection;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+
+import java.util.List;
+
+public class ProgressiveLowestProvider extends AbstractProvider {
+ @Override
+ public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List servers, Player player) {
+ int min = Integer.MAX_VALUE;
+ RegisteredServer target = null;
+
+ for (RegisteredServer server : servers) {
+ int count = plugin.getNetworkManager().getPlayers(server.getServerInfo());
+
+ if (count < min) {
+ min = count;
+ target = server;
+ }
+ }
+
+ return target;
+ }
+}
\ No newline at end of file
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/progressive/ProgressiveProvider.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/progressive/ProgressiveProvider.java
new file mode 100644
index 0000000..5e0147a
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/progressive/ProgressiveProvider.java
@@ -0,0 +1,24 @@
+package com.jaimemartz.playerbalancer.velocity.connection.provider.types.progressive;
+
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.connection.provider.AbstractProvider;
+import com.jaimemartz.playerbalancer.velocity.ping.ServerStatus;
+import com.jaimemartz.playerbalancer.velocity.section.ServerSection;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+
+import java.util.List;
+
+public class ProgressiveProvider extends AbstractProvider {
+ @Override
+ public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List servers, Player player) {
+ for (RegisteredServer server : servers) {
+ ServerStatus status = plugin.getStatusManager().getStatus(server.getServerInfo());
+ if (plugin.getNetworkManager().getPlayers(server.getServerInfo()) < status.getMaximum()) {
+ return server;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/random/RandomFillerProvider.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/random/RandomFillerProvider.java
new file mode 100644
index 0000000..b1c86f9
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/random/RandomFillerProvider.java
@@ -0,0 +1,35 @@
+package com.jaimemartz.playerbalancer.velocity.connection.provider.types.random;
+
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.connection.provider.AbstractProvider;
+import com.jaimemartz.playerbalancer.velocity.section.ServerSection;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.jaimemartz.playerbalancer.velocity.utils.RandomUtils.random;
+
+public class RandomFillerProvider extends AbstractProvider {
+ @Override
+ public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List servers, Player player) {
+ List results = new ArrayList<>();
+ int max = Integer.MIN_VALUE;
+
+ for (RegisteredServer server : servers) {
+ int count = plugin.getNetworkManager().getPlayers(server.getServerInfo());
+
+ if (count >= max) {
+ if (count > max) {
+ max = count;
+ results.clear();
+ }
+
+ results.add(server);
+ }
+ }
+
+ return random(results);
+ }
+}
\ No newline at end of file
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/random/RandomLowestProvider.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/random/RandomLowestProvider.java
new file mode 100644
index 0000000..1549e63
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/random/RandomLowestProvider.java
@@ -0,0 +1,34 @@
+package com.jaimemartz.playerbalancer.velocity.connection.provider.types.random;
+
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.connection.provider.AbstractProvider;
+import com.jaimemartz.playerbalancer.velocity.section.ServerSection;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.jaimemartz.playerbalancer.velocity.utils.RandomUtils.random;
+
+public class RandomLowestProvider extends AbstractProvider {
+ @Override
+ public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List servers, Player player) {
+ List results = new ArrayList<>();
+ int min = Integer.MAX_VALUE;
+
+ for (RegisteredServer server : servers) {
+ int count = plugin.getNetworkManager().getPlayers(server.getServerInfo());
+
+ if (count <= min) {
+ if (count < min) {
+ min = count;
+ results.clear();
+ }
+ results.add(server);
+ }
+ }
+
+ return random(results);
+ }
+}
\ No newline at end of file
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/random/RandomProvider.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/random/RandomProvider.java
new file mode 100644
index 0000000..50efc3b
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/connection/provider/types/random/RandomProvider.java
@@ -0,0 +1,18 @@
+package com.jaimemartz.playerbalancer.velocity.connection.provider.types.random;
+
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.connection.provider.AbstractProvider;
+import com.jaimemartz.playerbalancer.velocity.section.ServerSection;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+
+import java.util.List;
+
+import static com.jaimemartz.playerbalancer.velocity.utils.RandomUtils.random;
+
+public class RandomProvider extends AbstractProvider {
+ @Override
+ public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List servers, Player player) {
+ return random(servers);
+ }
+}
\ No newline at end of file
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/helper/NetworkManager.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/helper/NetworkManager.java
new file mode 100644
index 0000000..83ec4ed
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/helper/NetworkManager.java
@@ -0,0 +1,31 @@
+package com.jaimemartz.playerbalancer.velocity.helper;
+
+import com.imaginarycode.minecraft.redisbungee.RedisBungeeAPI;
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+import com.velocitypowered.api.proxy.server.ServerInfo;
+
+import java.util.Optional;
+
+public class NetworkManager {
+
+ private final PlayerBalancer plugin;
+
+ public NetworkManager(PlayerBalancer plugin) {
+ this.plugin = plugin;
+ }
+
+ public int getPlayers(ServerInfo server) {
+ if (plugin.getSettings().getGeneralProps().isRedisBungee()) {
+ try {
+ return RedisBungeeAPI.getRedisBungeeApi().getPlayersOnServer(server.getName()).size();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ Optional serverConnection = plugin.getProxyServer().getServer(server.getName());
+
+ return serverConnection.map(registeredServer -> registeredServer.getPlayersConnected().size()).orElse(0);
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/helper/PasteHelper.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/helper/PasteHelper.java
new file mode 100644
index 0000000..b965897
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/helper/PasteHelper.java
@@ -0,0 +1,135 @@
+package com.jaimemartz.playerbalancer.velocity.helper;
+
+import com.google.common.io.CharStreams;
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.utils.GuestPaste.PasteException;
+import com.jaimemartz.playerbalancer.velocity.utils.HastebinPaste;
+import com.velocitypowered.api.command.CommandSource;
+import com.velocitypowered.api.proxy.Player;
+import net.kyori.adventure.text.event.ClickEvent;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.function.BiConsumer;
+
+import static net.kyori.adventure.text.Component.text;
+import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
+import static net.kyori.adventure.text.format.NamedTextColor.RED;
+
+public enum PasteHelper {
+ PLUGIN((sender, address) -> {
+ if (sender instanceof Player) {
+ sender.sendMessage(text("Click me for the PlayerBalancer configuration", GREEN)
+ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, address.toString()))
+ );
+ } else {
+ sender.sendMessage(text("PlayerBalancer configuration link: " + address.toString()));
+ }
+ }, true) {
+ @Override
+ public URL paste(PlayerBalancer plugin) throws Exception {
+ File file = new File(plugin.getDataDirectory().toFile(), "plugin.conf");
+ try (FileInputStream stream = new FileInputStream(file)) {
+ try (InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) {
+ String content = CharStreams.toString(reader);
+ HastebinPaste paste = new HastebinPaste(HASTEBIN_HOST, content);
+ return paste.paste();
+ }
+ }
+ }
+ },
+
+ VELOCITY((sender, address) -> {
+ if (sender instanceof Player) {
+ sender.sendMessage(text("Click me for the Velocity configuration", GREEN)
+ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, address.toString()))
+ );
+ } else {
+ sender.sendMessage(text("Velocity configuration link: " + address.toString()));
+ }
+ }, true) {
+ @Override
+ public URL paste(PlayerBalancer plugin) throws Exception {
+ File file = new File("velocity.toml");
+ try (FileInputStream stream = new FileInputStream(file)) {
+ try (InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) {
+ String content = CharStreams.toString(reader);
+ content = content.replaceAll("[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}", "?.?.?.?");
+ HastebinPaste paste = new HastebinPaste(HASTEBIN_HOST, content);
+ return paste.paste();
+ }
+ }
+ }
+ },
+
+ LOGS((sender, address) -> {
+ if (sender instanceof Player) {
+ sender.sendMessage(text("Click me for the server logs", GREEN)
+ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, address.toString()))
+ );
+ } else {
+ sender.sendMessage(text("Server logs link: " + address.toString()));
+ }
+ }, false) {
+ @Override
+ public URL paste(PlayerBalancer plugin) throws Exception {
+ File file = new File("logs/latest.log");
+ try (FileInputStream stream = new FileInputStream(file)) {
+ try (InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) {
+ String content = CharStreams.toString(reader);
+ HastebinPaste paste = new HastebinPaste(HASTEBIN_HOST, content);
+ return paste.paste();
+ }
+ }
+ }
+ };
+
+ private static final String HASTEBIN_HOST = "https://haste.zneix.eu/";
+
+ private URL lastPasteUrl;
+
+ private final BiConsumer consumer;
+ private final boolean cache;
+
+ PasteHelper(BiConsumer consumer, boolean cache) {
+ this.consumer = consumer;
+ this.cache = cache;
+ }
+
+ public void send(PlayerBalancer plugin, CommandSource sender) {
+ if (lastPasteUrl == null || !cache) {
+ try {
+ lastPasteUrl = paste(plugin);
+ } catch (PasteException e) {
+ sender.sendMessage(text("An exception occurred while trying to send the paste: " + e.getMessage(), RED));
+
+ } catch (Exception e) {
+ sender.sendMessage(text("An internal error occurred while attempting to perform this command", RED));
+ e.printStackTrace();
+ }
+ } else {
+ sender.sendMessage(text("This is a cached link, reload the plugin for it to refresh!", RED));
+ }
+
+ if (lastPasteUrl != null) {
+ consumer.accept(sender, lastPasteUrl);
+ } else {
+ sender.sendMessage(text("Could not create the paste, try again...", RED));
+ }
+ }
+
+ public URL getLastPasteURL() {
+ return lastPasteUrl;
+ }
+
+ public abstract URL paste(PlayerBalancer plugin) throws Exception;
+
+ public static void reset() {
+ for (PasteHelper helper : values()) {
+ helper.lastPasteUrl = null;
+ }
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/helper/PlayerLocker.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/helper/PlayerLocker.java
new file mode 100644
index 0000000..fac9550
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/helper/PlayerLocker.java
@@ -0,0 +1,38 @@
+package com.jaimemartz.playerbalancer.velocity.helper;
+
+import com.velocitypowered.api.proxy.Player;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+public class PlayerLocker {
+ private static final Set storage = Collections.synchronizedSet(new HashSet());
+
+ public static boolean lock(Player player) {
+ if (storage.contains(player.getUniqueId())) {
+ return false;
+ } else {
+ storage.add(player.getUniqueId());
+ return true;
+ }
+ }
+
+ public static boolean unlock(Player player) {
+ if (storage.contains(player.getUniqueId())) {
+ storage.remove(player.getUniqueId());
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public static boolean isLocked(Player player) {
+ return storage.contains(player.getUniqueId());
+ }
+
+ public static void flush() {
+ storage.clear();
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/listeners/PlayerDisconnectListener.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/listeners/PlayerDisconnectListener.java
new file mode 100644
index 0000000..d4c7b8c
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/listeners/PlayerDisconnectListener.java
@@ -0,0 +1,24 @@
+package com.jaimemartz.playerbalancer.velocity.listeners;
+
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.connection.ServerAssignRegistry;
+import com.jaimemartz.playerbalancer.velocity.helper.PlayerLocker;
+import com.velocitypowered.api.event.Subscribe;
+import com.velocitypowered.api.event.connection.DisconnectEvent;
+import com.velocitypowered.api.proxy.Player;
+
+public class PlayerDisconnectListener {
+ private final PlayerBalancer plugin;
+
+ public PlayerDisconnectListener(PlayerBalancer plugin) {
+ this.plugin = plugin;
+ }
+
+ @Subscribe
+ public void onDisconnect(DisconnectEvent event) {
+ Player player = event.getPlayer();
+ PlayerLocker.unlock(player);
+
+ ServerAssignRegistry.clearAsssignedServers(player);
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/listeners/PluginMessageListener.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/listeners/PluginMessageListener.java
new file mode 100644
index 0000000..e30491c
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/listeners/PluginMessageListener.java
@@ -0,0 +1,269 @@
+package com.jaimemartz.playerbalancer.velocity.listeners;
+
+import com.google.common.io.ByteArrayDataInput;
+import com.google.common.io.ByteStreams;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSerializer;
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.connection.ConnectionIntent;
+import com.jaimemartz.playerbalancer.velocity.helper.PlayerLocker;
+import com.jaimemartz.playerbalancer.velocity.ping.ServerStatus;
+import com.jaimemartz.playerbalancer.velocity.section.ServerSection;
+import com.velocitypowered.api.event.Subscribe;
+import com.velocitypowered.api.event.connection.PluginMessageEvent;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.ServerConnection;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+import com.velocitypowered.api.proxy.server.ServerInfo;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Optional;
+
+public class PluginMessageListener {
+ private final PlayerBalancer plugin;
+ private final Gson gson;
+
+ public PluginMessageListener(PlayerBalancer plugin) {
+ this.plugin = plugin;
+ GsonBuilder builder = new GsonBuilder();
+
+ // Only serialize the name of ServerInfo
+ builder.registerTypeAdapter(ServerInfo.class, (JsonSerializer) (server, type, context) ->
+ context.serialize(server.getName())
+ );
+
+ builder.serializeNulls();
+ gson = builder.create();
+ }
+
+ @Subscribe
+ public void onPluginMessage(PluginMessageEvent event) {
+ if (event.getIdentifier().equals(PlayerBalancer.PB_CHANNEL) && event.getSource() instanceof ServerConnection) {
+ ByteArrayDataInput in = ByteStreams.newDataInput(event.getData());
+ String request = in.readUTF();
+ ServerConnection serverConnection = ((ServerConnection) event.getSource());
+ ServerInfo sender = serverConnection.getServerInfo();
+
+ switch (request) {
+ case "Connect": {
+ if (event.getTarget() instanceof Player) {
+ Player player = (Player) event.getTarget();
+ ServerSection section = plugin.getSectionManager().getByName(in.readUTF());
+
+ if (section == null)
+ break;
+
+ ConnectionIntent.simple(plugin, player, section);
+ }
+ break;
+ }
+
+ case "ConnectOther": {
+ Optional player = plugin.getProxyServer().getPlayer(in.readUTF());
+
+ if (!player.isPresent())
+ break;
+
+ ServerSection section = plugin.getSectionManager().getByName(in.readUTF());
+
+ if (section == null)
+ break;
+
+ ConnectionIntent.simple(plugin, player.get(), section);
+ break;
+ }
+
+ case "GetSectionByName": {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ DataOutputStream out = new DataOutputStream(stream);
+
+ ServerSection section = plugin.getSectionManager().getByName(in.readUTF());
+
+ if (section == null)
+ break;
+
+ try {
+ String output = gson.toJson(section);
+ out.writeUTF("GetSectionByName");
+ out.writeUTF(output);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ serverConnection.sendPluginMessage(PlayerBalancer.PB_CHANNEL, stream.toByteArray());
+ break;
+ }
+
+ case "GetSectionByServer": {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ DataOutputStream out = new DataOutputStream(stream);
+
+ Optional server = plugin.getProxyServer().getServer(in.readUTF());
+
+ if (!server.isPresent())
+ break;
+
+ ServerSection section = plugin.getSectionManager().getByServer(server.get());
+
+ if (section == null)
+ break;
+
+ try {
+ String output = gson.toJson(section);
+ out.writeUTF("GetSectionByServer");
+ out.writeUTF(output);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ serverConnection.sendPluginMessage(PlayerBalancer.PB_CHANNEL, stream.toByteArray());
+ break;
+ }
+
+ case "GetSectionOfPlayer": {
+ if (event.getTarget() instanceof Player) {
+ Player player = (Player) event.getTarget();
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ DataOutputStream out = new DataOutputStream(stream);
+
+ ServerSection section = plugin.getSectionManager().getByPlayer(player);
+
+ if (section == null)
+ break;
+
+ try {
+ String output = gson.toJson(section);
+ out.writeUTF("GetSectionOfPlayer");
+ out.writeUTF(output);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ serverConnection.sendPluginMessage(PlayerBalancer.PB_CHANNEL, stream.toByteArray());
+ }
+ break;
+ }
+
+ case "GetSectionPlayerCount": {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ DataOutputStream out = new DataOutputStream(stream);
+
+ ServerSection section = plugin.getSectionManager().getByName(in.readUTF());
+
+ if (section == null)
+ break;
+
+ try {
+ out.writeUTF("GetSectionPlayerCount");
+ out.writeInt(section.getServers().stream().reduce(
+ 0,
+ (integer, serverInfo) -> integer + serverInfo.getPlayersConnected().size(),
+ Integer::sum));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ serverConnection.sendPluginMessage(PlayerBalancer.PB_CHANNEL, stream.toByteArray());
+ break;
+ }
+
+ case "GetServerStatus": {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ DataOutputStream out = new DataOutputStream(stream);
+
+ Optional server = plugin.getProxyServer().getServer(in.readUTF());
+ if (!server.isPresent())
+ break;
+
+ ServerStatus status = plugin.getStatusManager().getStatus(server.get().getServerInfo());
+
+ try {
+ String output = gson.toJson(status);
+ out.writeUTF("GetServerStatus");
+ out.writeUTF(output);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ serverConnection.sendPluginMessage(PlayerBalancer.PB_CHANNEL, stream.toByteArray());
+ }
+
+ case "ClearPlayerBypass": {
+ if (event.getTarget() instanceof Player) {
+ Player player = (Player) event.getTarget();
+ PlayerLocker.unlock(player);
+ }
+ break;
+ }
+
+ case "SetPlayerBypass": {
+ if (event.getTarget() instanceof Player) {
+ Player player = (Player) event.getTarget();
+ PlayerLocker.lock(player);
+ }
+ break;
+ }
+
+ case "BypassConnect": {
+ if (event.getTarget() instanceof Player) {
+ Player player = (Player) event.getTarget();
+
+ Optional server = plugin.getProxyServer().getServer(in.readUTF());
+ if (!server.isPresent())
+ break;
+
+ ConnectionIntent.direct(
+ plugin,
+ player,
+ server.get().getServerInfo(),
+ null
+ );
+ }
+ break;
+ }
+
+ case "FallbackPlayer": {
+ if (event.getTarget() instanceof Player) {
+ Player player = (Player) event.getTarget();
+ ServerSection target = plugin.getFallbackCommand().getSection(player);
+
+ if (target == null)
+ break;
+
+ ConnectionIntent.simple(
+ plugin,
+ player,
+ target
+ );
+ }
+
+ break;
+ }
+
+ case "FallbackOtherPlayer": {
+ Optional player = plugin.getProxyServer().getPlayer(in.readUTF());
+
+ if (!player.isPresent())
+ break;
+
+ ServerSection target = plugin.getFallbackCommand().getSection(player.get());
+
+ if (target == null)
+ break;
+
+ ConnectionIntent.simple(
+ plugin,
+ player.get(),
+ target
+ );
+
+ break;
+ }
+ }
+ }
+ }
+}
+
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/listeners/ProxyReloadListener.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/listeners/ProxyReloadListener.java
new file mode 100644
index 0000000..ed37f8b
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/listeners/ProxyReloadListener.java
@@ -0,0 +1,19 @@
+package com.jaimemartz.playerbalancer.velocity.listeners;
+
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.velocitypowered.api.event.Subscribe;
+import com.velocitypowered.api.event.proxy.ProxyReloadEvent;
+
+public class ProxyReloadListener {
+ private final PlayerBalancer plugin;
+
+ public ProxyReloadListener(PlayerBalancer plugin) {
+ this.plugin = plugin;
+ }
+
+ @Subscribe
+ public void onReload(ProxyReloadEvent event) {
+ plugin.getLogger().info("Velocity has been reloaded, reloading the plugin...");
+ plugin.reloadPlugin();
+ }
+}
\ No newline at end of file
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/listeners/ServerConnectListener.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/listeners/ServerConnectListener.java
new file mode 100644
index 0000000..38a2a21
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/listeners/ServerConnectListener.java
@@ -0,0 +1,80 @@
+package com.jaimemartz.playerbalancer.velocity.listeners;
+
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.connection.ConnectionIntent;
+import com.jaimemartz.playerbalancer.velocity.connection.ServerAssignRegistry;
+import com.jaimemartz.playerbalancer.velocity.helper.PlayerLocker;
+import com.jaimemartz.playerbalancer.velocity.section.ServerSection;
+import com.jaimemartz.playerbalancer.velocity.utils.MessageUtils;
+import com.velocitypowered.api.event.Subscribe;
+import com.velocitypowered.api.event.player.ServerPreConnectEvent;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.ServerConnection;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+import com.velocitypowered.api.proxy.server.ServerInfo;
+
+import java.util.function.Consumer;
+
+public class ServerConnectListener {
+ private final PlayerBalancer plugin;
+
+ public ServerConnectListener(PlayerBalancer plugin) {
+ this.plugin = plugin;
+ }
+
+ @Subscribe
+ public void onConnect(ServerPreConnectEvent event) {
+ Player player = event.getPlayer();
+ RegisteredServer target = event.getOriginalServer();
+
+ if (PlayerLocker.isLocked(player))
+ return;
+
+ ServerSection section = getSection(player, target);
+
+ if (section == null)
+ return;
+
+ new ConnectionIntent(plugin, player, section) {
+ @Override
+ public void connect(ServerInfo server, Consumer callback) {
+ if (plugin.getSectionManager().isReiterative(section)) {
+ ServerAssignRegistry.assignTarget(player, section, server);
+ }
+
+ plugin.getProxyServer().getServer(server.getName()).ifPresent(registeredServer -> {
+ event.setResult(ServerPreConnectEvent.ServerResult.allowed(registeredServer));
+ callback.accept(true);
+ });
+ }
+ }.execute();
+ }
+
+ private ServerSection getSection(Player player, RegisteredServer target) {
+ ServerSection section = plugin.getSectionManager().getByServer(target);
+
+ if (section != null) {
+ // Checks only for servers (not the section server)
+ if (!target.equals(section.getServer())) {
+ if (plugin.getSectionManager().isDummy(section)) {
+ return null;
+ }
+
+ if (player.hasPermission("playerbalancer.bypass")) {
+ MessageUtils.send(player, plugin.getSettings().getMessagesProps().getBypassMessage());
+ return null;
+ }
+
+ ServerConnection serverConnection = player.getCurrentServer().orElse(null);
+ if (serverConnection != null && section.getServers().contains(serverConnection.getServer())) {
+ if (plugin.getSectionManager().isReiterative(section)) {
+ ServerAssignRegistry.assignTarget(player, section, target.getServerInfo());
+ }
+ return null;
+ }
+ }
+ }
+
+ return section;
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/listeners/ServerKickListener.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/listeners/ServerKickListener.java
new file mode 100644
index 0000000..d0869ae
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/listeners/ServerKickListener.java
@@ -0,0 +1,148 @@
+package com.jaimemartz.playerbalancer.velocity.listeners;
+
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.connection.ConnectionIntent;
+import com.jaimemartz.playerbalancer.velocity.helper.PlayerLocker;
+import com.jaimemartz.playerbalancer.velocity.section.ServerSection;
+import com.jaimemartz.playerbalancer.velocity.settings.props.MessagesProps;
+import com.jaimemartz.playerbalancer.velocity.settings.props.features.KickHandlerProps;
+import com.jaimemartz.playerbalancer.velocity.utils.MessageUtils;
+import com.velocitypowered.api.event.Subscribe;
+import com.velocitypowered.api.event.player.KickedFromServerEvent;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.ServerConnection;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+import com.velocitypowered.api.proxy.server.ServerInfo;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+public class ServerKickListener {
+ private final KickHandlerProps props;
+ private final MessagesProps messages;
+ private final PlayerBalancer plugin;
+
+ public ServerKickListener(PlayerBalancer plugin) {
+ this.props = plugin.getSettings().getFeaturesProps().getKickHandlerProps();
+ this.messages = plugin.getSettings().getMessagesProps();
+ this.plugin = plugin;
+ }
+
+ @Subscribe
+ public void onKick(KickedFromServerEvent event) {
+ Player player = event.getPlayer();
+ RegisteredServer from = event.getServer();
+
+ boolean matches = false;
+ String reason = PlainTextComponentSerializer.plainText().serialize(event.getServerKickReason().orElse(Component.empty()));
+
+ for (String string : props.getReasons()) {
+ if (reason.matches(string)) {
+ matches = true;
+ break;
+ }
+ }
+
+ if (props.isInverted()) {
+ matches = !matches;
+ }
+
+ if (props.isDebug()) {
+ plugin.getLogger().info(String.format("The player %s got kicked from %s, reason: \"%s\". Matched reasons: %s",
+ player.getUsername(),
+ from.getServerInfo().getName(),
+ reason,
+ matches
+ ));
+ }
+
+ if (!matches)
+ return;
+
+ ServerSection section = getSection(player, from);
+
+ if (section == null)
+ return;
+
+ ConnectionIntent intent = new ConnectionIntent(plugin, player, section) {
+ @Override
+ public void connect(ServerInfo server, Consumer callback) {
+ PlayerLocker.lock(player);
+ Optional registeredServer = plugin.getProxyServer().getServer(server.getName());
+ if (registeredServer.isPresent()) {
+ event.setResult(KickedFromServerEvent.RedirectPlayer.create(registeredServer.get()));
+ MessageUtils.send(player, messages.getKickMessage(), (str) -> str
+ .replace("{reason}", reason)
+ .replace("{from}", from.getServerInfo().getName())
+ .replace("{to}", server.getName()));
+ callback.accept(true);
+ }
+ plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
+ PlayerLocker.unlock(player);
+ }).delay(5, TimeUnit.SECONDS).schedule();
+ }
+ };
+
+ intent.getExclusions().add(from.getServerInfo());
+ intent.execute();
+ }
+
+ private ServerSection getSection(Player player, RegisteredServer from) {
+ Optional serverConnection = player.getCurrentServer();
+ if (!serverConnection.isPresent()) {
+ if (props.isForcePrincipal()) {
+ return plugin.getSectionManager().getPrincipal();
+ } else {
+ return null;
+ }
+ }
+
+ if (!serverConnection.get().getServer().equals(from)) {
+ return null;
+ }
+
+ ServerSection current = plugin.getSectionManager().getByServer(from);
+
+ if (current != null) {
+ if (props.getExcludedSections().contains(current.getName())) {
+ return null;
+ }
+ }
+
+ if (current != null) {
+ ServerSection target = current.getParent();
+
+ String bindName = props.getRules().get(current.getName());
+ if (bindName != null) {
+ ServerSection bind = plugin.getSectionManager().getByName(bindName);
+ if (bind != null) {
+ target = bind;
+ }
+ }
+
+ if (target == null) {
+ MessageUtils.send(player, messages.getUnavailableServerMessage());
+ return null;
+ }
+
+ if (props.isRestrictive()) {
+ if (current.getPosition() >= 0 && target.getPosition() < 0) {
+ return null;
+ }
+ }
+
+ return target;
+ } else {
+ if (plugin.getSettings().getFeaturesProps().getBalancerProps().isDefaultPrincipal()) {
+ return plugin.getSectionManager().getPrincipal();
+ } else {
+ MessageUtils.send(player, messages.getUnavailableServerMessage());
+ }
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/ping/PingTactic.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/ping/PingTactic.java
new file mode 100644
index 0000000..31af9ed
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/ping/PingTactic.java
@@ -0,0 +1,55 @@
+package com.jaimemartz.playerbalancer.velocity.ping;
+
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.utils.ServerListPing;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+import com.velocitypowered.api.proxy.server.ServerPing;
+
+import java.io.IOException;
+import java.util.function.Consumer;
+
+public enum PingTactic {
+ CUSTOM {
+ @Override
+ public void ping(RegisteredServer server, Consumer callback, PlayerBalancer plugin) {
+ plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
+ try {
+ ServerListPing utility = new ServerListPing();
+ ServerListPing.StatusResponse response = utility.ping(
+ server.getServerInfo().getAddress(),
+ plugin.getSettings().getFeaturesProps().getServerCheckerProps().getTimeout());
+ callback.accept(new ServerStatus(
+ response.getDescription(),
+ response.getPlayers().getOnline(),
+ response.getPlayers().getMax()));
+ } catch (IOException e) {
+ callback.accept(null);
+ }
+ }).schedule();
+ }
+ },
+
+ GENERIC {
+ @Override
+ public void ping(RegisteredServer server, Consumer callback, PlayerBalancer plugin) {
+ try {
+ server.ping().whenComplete((ping, throwable) -> {
+ if (ping != null) {
+ // Using deprecated method for bungee 1.8 compatibility
+ callback.accept(new ServerStatus(
+ ping.getDescriptionComponent(),
+ ping.getPlayers().map(ServerPing.Players::getOnline).orElse(0),
+ ping.getPlayers().map(ServerPing.Players::getMax).orElse(0)
+ ));
+ } else {
+ callback.accept(null);
+ }
+ });
+ } catch (Exception e) {
+ callback.accept(null);
+ }
+ }
+ };
+
+ public abstract void ping(RegisteredServer server, Consumer callback, PlayerBalancer plugin);
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/ping/ServerStatus.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/ping/ServerStatus.java
new file mode 100644
index 0000000..1b34aea
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/ping/ServerStatus.java
@@ -0,0 +1,78 @@
+package com.jaimemartz.playerbalancer.velocity.ping;
+
+import com.velocitypowered.api.proxy.server.ServerInfo;
+import net.kyori.adventure.text.Component;
+
+public class ServerStatus {
+ private final Component description;
+ private final int players, maximum;
+ private final boolean online;
+ private final boolean fabricated;
+ private boolean outdated = true;
+
+ /**
+ * Constructor when cannot ping the server
+ */
+ public ServerStatus() {
+ this.description = Component.text("Server Unreachable");
+ this.players = 0;
+ this.maximum = 0;
+ this.online = false;
+ this.fabricated = true;
+ }
+
+ /**
+ * Constructor when we have to return defaults
+ * Defaulting to be accessible as this is used when the server checker is disabled
+ * @param server the server for providing basic info about itself
+ */
+ public ServerStatus(ServerInfo server) {
+ this.description = Component.text(server.getName());
+ this.players = 0;
+ this.maximum = Integer.MAX_VALUE;
+ this.online = true;
+ this.fabricated = true;
+ }
+
+ /**
+ * Constructor when we have to store ping results
+ * @param description the description (aka MOTD) from the ping result
+ * @param players the count of players online from the ping result
+ * @param maximum the maximum amount of players possible from the ping result
+ */
+ public ServerStatus(Component description, int players, int maximum) {
+ this.description = description;
+ this.players = players;
+ this.maximum = maximum;
+ this.online = maximum != 0 && players < maximum;
+ this.fabricated = false;
+ }
+
+ public Component getDescription() {
+ return description;
+ }
+
+ public int getPlayers() {
+ return players;
+ }
+
+ public int getMaximum() {
+ return maximum;
+ }
+
+ public boolean isOnline() {
+ return online;
+ }
+
+ public boolean isFabricated() {
+ return fabricated;
+ }
+
+ public boolean isOutdated() {
+ return outdated;
+ }
+
+ public void setOutdated(boolean outdated) {
+ this.outdated = outdated;
+ }
+}
\ No newline at end of file
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/ping/StatusManager.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/ping/StatusManager.java
new file mode 100644
index 0000000..95024b7
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/ping/StatusManager.java
@@ -0,0 +1,153 @@
+package com.jaimemartz.playerbalancer.velocity.ping;
+
+import com.google.common.io.ByteArrayDataInput;
+import com.google.common.io.ByteStreams;
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.section.ServerSection;
+import com.jaimemartz.playerbalancer.velocity.settings.props.features.ServerCheckerProps;
+import com.jaimemartz.playerbalancer.velocity.utils.RegExUtils;
+import com.velocitypowered.api.event.Subscribe;
+import com.velocitypowered.api.event.connection.PluginMessageEvent;
+import com.velocitypowered.api.proxy.ServerConnection;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+import com.velocitypowered.api.proxy.server.ServerInfo;
+import com.velocitypowered.api.scheduler.ScheduledTask;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+public class StatusManager {
+ private final PlayerBalancer plugin;
+ private final ServerCheckerProps props;
+
+ private boolean stopped = true;
+ private PingTactic tactic;
+ private ScheduledTask task;
+
+ private final Map storage = new HashMap<>();
+ private final Map overriders = new HashMap<>();
+
+ public StatusManager(PlayerBalancer plugin) {
+ this.props = plugin.getSettings().getFeaturesProps().getServerCheckerProps();
+ this.plugin = plugin;
+ }
+
+ public void start() {
+ if (task != null) {
+ stop();
+ }
+
+ stopped = false;
+ tactic = props.getTactic();
+ plugin.getLogger().info(String.format("Starting the ping task, the interval is %s",
+ props.getInterval()
+ ));
+
+ task = plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
+ storage.forEach((k, v) -> v.setOutdated(true));
+
+ for (ServerSection section : plugin.getSectionManager().getSections().values()) {
+ for (RegisteredServer server : section.getServers()) {
+ if (stopped) {
+ break;
+ }
+
+ if (getStatus(server.getServerInfo()).isOutdated()) {
+ update(server);
+ }
+ }
+ }
+ }).repeat(props.getInterval(), TimeUnit.MILLISECONDS).schedule();
+ }
+
+ public void stop() {
+ if (task != null) {
+ task.cancel();
+ task = null;
+ stopped = true;
+ }
+ }
+
+ private void update(RegisteredServer server) {
+ tactic.ping(server, (status) -> {
+ if (status == null) {
+ status = new ServerStatus();
+ }
+
+ if (props.isDebug()) {
+ plugin.getLogger().info(String.format(
+ "Updated server %s, status: [Description: \"%s\", Players: %s, Maximum Players: %s, Online: %s]",
+ server.getServerInfo().getName(), status.getDescription(), status.getPlayers(), status.getMaximum(), status.isOnline()
+ ));
+ }
+
+ status.setOutdated(false);
+ storage.put(server.getServerInfo(), status);
+ }, plugin);
+ }
+
+ public ServerStatus getStatus(ServerInfo server) {
+ ServerStatus status = storage.get(server);
+
+ if (status == null) {
+ return new ServerStatus(server);
+ } else {
+ return status;
+ }
+ }
+
+ public boolean isAccessible(ServerInfo server) {
+ Boolean override = overriders.get(server);
+
+ if (override != null) {
+ return override;
+ }
+
+ ServerStatus status = getStatus(server);
+
+ if (!status.isOnline()) {
+ return false;
+ }
+
+ for (String pattern : props.getMarkerDescs()) {
+ if (RegExUtils.matches(LegacyComponentSerializer.legacyAmpersand().serialize(status.getDescription()), pattern)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Subscribe
+ public void onPluginMessage(PluginMessageEvent event) {
+ if (event.getIdentifier().equals(PlayerBalancer.PB_CHANNEL) && event.getSource() instanceof ServerConnection) {
+ ByteArrayDataInput in = ByteStreams.newDataInput(event.getData());
+ String request = in.readUTF();
+ ServerInfo sender = ((ServerConnection) event.getSource()).getServerInfo();
+
+ switch (request) {
+ case "ClearStatusOverride": {
+ Optional server = plugin.getProxyServer().getServer(in.readUTF());
+
+ if (!server.isPresent())
+ break;
+
+ overriders.remove(server.get().getServerInfo());
+ break;
+ }
+
+ case "SetStatusOverride": {
+ Optional server = plugin.getProxyServer().getServer(in.readUTF());
+
+ if (!server.isPresent())
+ break;
+
+ overriders.put(server.get().getServerInfo(), in.readBoolean());
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/section/SectionCommand.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/section/SectionCommand.java
new file mode 100644
index 0000000..d576998
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/section/SectionCommand.java
@@ -0,0 +1,31 @@
+package com.jaimemartz.playerbalancer.velocity.section;
+
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.commands.FallbackCommand;
+import com.jaimemartz.playerbalancer.velocity.settings.props.shared.CommandProps;
+import com.velocitypowered.api.proxy.Player;
+
+public class SectionCommand extends FallbackCommand {
+ private final ServerSection section;
+ private final String permission;
+
+ public SectionCommand(PlayerBalancer plugin, ServerSection section) {
+ super(plugin);
+ this.section = section;
+ this.permission = getCommandProps().getPermission();
+ }
+
+ @Override
+ public boolean hasPermission(Invocation invocation) {
+ return invocation.source().hasPermission(permission);
+ }
+
+ @Override
+ public ServerSection getSection(Player player) {
+ return section;
+ }
+
+ public CommandProps getCommandProps() {
+ return section.getProps().getCommandProps();
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/section/SectionManager.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/section/SectionManager.java
new file mode 100644
index 0000000..5998f26
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/section/SectionManager.java
@@ -0,0 +1,396 @@
+package com.jaimemartz.playerbalancer.velocity.section;
+
+import com.jaimemartz.playerbalancer.velocity.PlayerBalancer;
+import com.jaimemartz.playerbalancer.velocity.settings.props.features.BalancerProps;
+import com.jaimemartz.playerbalancer.velocity.settings.props.shared.SectionProps;
+import com.velocitypowered.api.command.CommandMeta;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.ServerConnection;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+import com.velocitypowered.api.proxy.server.ServerInfo;
+import com.velocitypowered.api.scheduler.ScheduledTask;
+import lombok.Getter;
+
+import java.net.InetSocketAddress;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class SectionManager {
+ private final PlayerBalancer plugin;
+ private final BalancerProps props;
+ @Getter
+ private ServerSection principal;
+ private ScheduledTask refreshTask;
+
+ @Getter
+ private final Map sections = Collections.synchronizedMap(new HashMap<>());
+ @Getter
+ private final Map servers = Collections.synchronizedMap(new HashMap<>());
+
+ private static final Map stages = Collections.synchronizedMap(new LinkedHashMap<>());
+
+ public SectionManager(PlayerBalancer plugin) {
+ this.props = plugin.getSettings().getFeaturesProps().getBalancerProps();
+ this.plugin = plugin;
+ }
+
+ public void load() throws RuntimeException {
+ plugin.getLogger().info("Executing section initialization stages, this may take a while...");
+ long starting = System.currentTimeMillis();
+
+ stages.put("constructing-sections", new SectionStage("Constructing sections") {
+ @Override
+ public void execute(String sectionName, SectionProps sectionProps, ServerSection section) throws RuntimeException {
+ ServerSection object = new ServerSection(sectionName, sectionProps);
+ sections.put(sectionName, object);
+ }
+ });
+
+ stages.put("processing-principal-section", new Stage("Processing principal section") {
+ @Override
+ public void execute() {
+ principal = sections.get(props.getPrincipalSectionName());
+ if (principal == null) {
+ throw new IllegalArgumentException(String.format(
+ "Could not set principal section, there is no section called \"%s\"",
+ props.getPrincipalSectionName()
+ ));
+ }
+ }
+ });
+
+ stages.put("processing-parents", new SectionStage("Processing parents") {
+ @Override
+ public void execute(String sectionName, SectionProps sectionProps, ServerSection section) throws RuntimeException {
+ if (sectionProps.getParentName() != null) {
+ ServerSection parent = getByName(sectionProps.getParentName());
+ if (principal.equals(section) && parent == null) {
+ throw new IllegalArgumentException(String.format(
+ "The section \"%s\" has an invalid parent set",
+ section.getName()
+ ));
+ } else {
+ section.setParent(parent);
+ }
+ }
+ }
+ });
+
+ stages.put("validating-parents", new SectionStage("Validating parents") {
+ @Override
+ public void execute(String sectionName, SectionProps sectionProps, ServerSection section) throws RuntimeException {
+ ServerSection parent = section.getParent();
+ if (parent != null && parent.getParent() == section) {
+ throw new IllegalStateException(String.format(
+ "The sections \"%s\" and \"%s\" are parents of each other",
+ section.getName(),
+ parent.getName()
+ ));
+ }
+ }
+ });
+
+ stages.put("validating-providers", new SectionStage("Validating providers") {
+ @Override
+ public void execute(String sectionName, SectionProps sectionProps, ServerSection section) throws RuntimeException {
+ if (sectionProps.getProvider() == null) {
+ ServerSection current = section.getParent();
+ if (current != null) {
+ while (current.getProps().getProvider() == null) {
+ current = current.getParent();
+ }
+
+ plugin.getLogger().info(String.format(
+ "The section \"%s\" inherits the provider from the section \"%s\"",
+ section.getName(),
+ current.getName()
+ ));
+
+ section.setInherited(true);
+ }
+ } else {
+ section.setInherited(false);
+ }
+ }
+ });
+
+ stages.put("calculating-positions", new SectionStage("Calculating positions") {
+ @Override
+ public void execute(String sectionName, SectionProps sectionProps, ServerSection section) throws RuntimeException {
+ section.setPosition(calculatePosition(section));
+ }
+ });
+
+ stages.put("resolving-servers", new SectionStage("Resolving servers") {
+ @Override
+ public void execute(String sectionName, SectionProps sectionProps, ServerSection section) throws RuntimeException {
+ calculateServers(section);
+ }
+ });
+
+ stages.put("section-server-processing", new SectionStage("Section server processing") {
+ @Override
+ public void execute(String sectionName, SectionProps sectionProps, ServerSection section) throws RuntimeException {
+ if (sectionProps.getServerName() != null) {
+ ServerInfo serverInfo = new ServerInfo("@" + sectionProps.getServerName(), new InetSocketAddress("0.0.0.0", (int) Math.floor(Math.random() * (0xFFFF + 1))));
+ RegisteredServer sectionServer = plugin.getProxyServer().registerServer(serverInfo);
+ plugin.getSectionManager().registerServer(sectionServer, section);
+ section.setServer(sectionServer);
+ }
+ }
+ });
+
+ stages.put("section-command-processing", new SectionStage("Section command processing") {
+ @Override
+ public void execute(String sectionName, SectionProps sectionProps, ServerSection section) throws RuntimeException {
+ if (sectionProps.getCommandProps() != null) {
+ SectionCommand command = new SectionCommand(plugin, section);
+ section.setCommand(command);
+ CommandMeta sectionCommandMeta = plugin.getProxyServer().getCommandManager()
+ .metaBuilder(command.getCommandProps().getName())
+ .aliases(command.getCommandProps().getAliasesArray())
+ .build();
+ plugin.getProxyServer().getCommandManager().register(sectionCommandMeta, command);
+ }
+ }
+ });
+
+ stages.put("finishing-loading", new SectionStage("Finishing loading sections") {
+ @Override
+ public void execute(String sectionName, SectionProps sectionProps, ServerSection section) throws RuntimeException {
+ section.setValid(true);
+ }
+ });
+
+ stages.forEach((name, stage) -> {
+ plugin.getLogger().info("Executing stage \"" + stage.title + "\"");
+ stage.execute();
+ });
+
+ if (plugin.getSettings().getFeaturesProps().getServerRefreshProps().isEnabled()) {
+ plugin.getLogger().info("Starting automatic server refresh task");
+ refreshTask = plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
+ props.getSectionProps().forEach((name, props) -> {
+ ServerSection section = sections.get(name);
+ calculateServers(section);
+ });
+ }).delay(plugin.getSettings().getFeaturesProps().getServerRefreshProps().getDelay(), TimeUnit.MILLISECONDS)
+ .repeat(plugin.getSettings().getFeaturesProps().getServerRefreshProps().getInterval(), TimeUnit.MILLISECONDS)
+ .schedule();
+ }
+
+ long ending = System.currentTimeMillis() - starting;
+ plugin.getLogger().info(String.format("A total of %s section(s) have been loaded in %sms", sections.size(), ending));
+ }
+
+ public void flush() {
+ plugin.getLogger().info("Flushing section storage because of plugin shutdown");
+ sections.forEach((key, value) -> {
+ value.setValid(false);
+
+ if (value.getCommand() != null) {
+ SectionCommand command = value.getCommand();
+ plugin.getProxyServer().getCommandManager().unregister(command.getCommandProps().getName());
+ }
+
+ if (value.getServer() != null) {
+ ServerInfo server = value.getServer().getServerInfo();
+
+ plugin.getProxyServer().unregisterServer(server);
+ }
+ });
+
+ if (refreshTask != null) {
+ refreshTask.cancel();
+ refreshTask = null;
+ }
+
+ principal = null;
+ sections.clear();
+ servers.clear();
+ }
+
+ public ServerSection getByName(String name) {
+ if (name == null) {
+ return null;
+ }
+
+ return sections.get(name);
+ }
+
+ public ServerSection getByServer(RegisteredServer server) {
+ if (server == null) {
+ return null;
+ }
+
+ return servers.get(server);
+ }
+
+ public ServerSection getByPlayer(Player player) {
+ if (player == null) {
+ return null;
+ }
+
+ Optional server = player.getCurrentServer();
+
+ return server.map(serverConnection -> getByServer(serverConnection.getServer())).orElse(null);
+
+ }
+
+ public void registerServer(RegisteredServer server, ServerSection section) {
+ if (!isDummy(section)) {
+ // Checking for already we already added this server to other section
+ // This can only happen if another non dummy section registers this server
+ if (servers.containsKey(server)) {
+ ServerSection other = servers.get(server);
+ throw new IllegalArgumentException(String.format(
+ "The server \"%s\" is already in the section \"%s\"",
+ server.getServerInfo().getName(),
+ other.getName()
+ ));
+ }
+
+ plugin.getLogger().info(String.format("Registering server \"%s\" to section \"%s\"",
+ server.getServerInfo().getName(),
+ section.getName()
+ ));
+
+ servers.put(server, section);
+ }
+ }
+
+ public void calculateServers(ServerSection section) {
+ Set results = new HashSet<>();
+
+ // Searches for matches
+ section.getProps().getServerEntries().forEach(entry -> {
+ Pattern pattern = Pattern.compile(entry);
+ plugin.getProxyServer().getAllServers().forEach((server) -> {
+ Matcher matcher = pattern.matcher(server.getServerInfo().getName());
+ if (matcher.matches()) {
+ results.add(server);
+ }
+ });
+ });
+
+ // Checks if there are servers previously matched that are no longer valid
+ section.getServers().forEach(server -> {
+ ServerInfo info = server.getServerInfo();
+ if (!results.contains(server)) {
+ servers.remove(info);
+ section.getServers().remove(server);
+ plugin.getLogger().info(String.format("Removed the server %s from %s as it does no longer exist",
+ info.getName(), section.getName()
+ ));
+ }
+ });
+
+ // Add matched servers to the section
+ int addedServers = 0;
+ for (RegisteredServer server : results) {
+ if (!section.getServers().contains(server)) {
+ section.getServers().add(server);
+ registerServer(server, section);
+ addedServers++;
+ plugin.getLogger().info(String.format("Added the server %s to %s",
+ server.getServerInfo().getName(), section.getName()
+ ));
+ }
+ }
+
+ if (addedServers > 0) {
+ plugin.getLogger().info(String.format("Recognized %s server%s in the section \"%s\"",
+ addedServers,
+ addedServers != 1 ? "s" : "",
+ section.getName()
+ ));
+ }
+ }
+
+ public int calculatePosition(ServerSection section) {
+ ServerSection current = section;
+
+ // Calculate above principal
+ int iterations = 0;
+ while (current != null) {
+ if (current == principal) {
+ return iterations;
+ }
+
+ current = current.getParent();
+ iterations++;
+ }
+
+ // Calculate below principal
+ if (principal != null) {
+ iterations = 0;
+ current = principal;
+ while (current != null) {
+ if (current.equals(section)) {
+ return iterations;
+ }
+
+ current = current.getParent();
+ iterations--;
+ }
+ }
+
+ return iterations;
+ }
+
+ public boolean isPrincipal(ServerSection section) {
+ return principal.equals(section);
+ }
+
+ public boolean isDummy(ServerSection section) {
+ List dummySections = props.getDummySectionNames();
+ return dummySections.contains(section.getName());
+ }
+
+ public boolean isReiterative(ServerSection section) {
+ List reiterativeSections = props.getReiterativeSectionNames();
+ return reiterativeSections.contains(section.getName());
+ }
+
+ public Stage getStage(String name) {
+ return stages.get(name);
+ }
+
+ private abstract class Stage {
+ private final String title;
+
+ private Stage(String title) {
+ this.title = title;
+ }
+
+ public abstract void execute() throws RuntimeException;
+ }
+
+ private abstract class SectionStage extends Stage {
+ private SectionStage(String title) {
+ super(title);
+ }
+
+ @Override
+ public void execute() throws RuntimeException {
+ props.getSectionProps().forEach((name, props) -> {
+ execute(name, props, sections.get(name));
+ });
+ }
+
+ public abstract void execute(
+ String sectionName,
+ SectionProps sectionProps,
+ ServerSection section
+ ) throws RuntimeException;
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/section/ServerSection.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/section/ServerSection.java
new file mode 100644
index 0000000..c96b601
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/section/ServerSection.java
@@ -0,0 +1,46 @@
+package com.jaimemartz.playerbalancer.velocity.section;
+
+import com.jaimemartz.playerbalancer.velocity.connection.ProviderType;
+import com.jaimemartz.playerbalancer.velocity.connection.provider.AbstractProvider;
+import com.jaimemartz.playerbalancer.velocity.settings.props.shared.SectionProps;
+import com.jaimemartz.playerbalancer.velocity.utils.AlphanumComparator;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+import lombok.Data;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.TreeSet;
+
+@Data
+public class ServerSection {
+ private final String name;
+ private final SectionProps props;
+
+ private boolean inherited = false;
+ private ServerSection parent;
+ private int position;
+
+ private transient RegisteredServer server;
+ private transient SectionCommand command;
+ private transient AbstractProvider externalProvider;
+ private Set servers;
+
+ private boolean valid = false;
+
+ public ServerSection(String name, SectionProps props) {
+ this.name = name;
+ this.props = props;
+
+ this.servers = Collections.synchronizedNavigableSet(new TreeSet<>((lhs, rhs) ->
+ AlphanumComparator.getInstance().compare(lhs.getServerInfo().getName(), rhs.getServerInfo().getName())
+ ));
+ }
+
+ public ProviderType getImplicitProvider() {
+ if (inherited) {
+ return parent.getImplicitProvider();
+ } else {
+ return props.getProvider();
+ }
+ }
+}
\ No newline at end of file
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/SettingsHolder.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/SettingsHolder.java
new file mode 100644
index 0000000..8c005c3
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/SettingsHolder.java
@@ -0,0 +1,21 @@
+package com.jaimemartz.playerbalancer.velocity.settings;
+
+import com.jaimemartz.playerbalancer.velocity.settings.props.FeaturesProps;
+import com.jaimemartz.playerbalancer.velocity.settings.props.GeneralProps;
+import com.jaimemartz.playerbalancer.velocity.settings.props.MessagesProps;
+import lombok.Data;
+import ninja.leaping.configurate.objectmapping.Setting;
+import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;
+
+@ConfigSerializable
+@Data
+public class SettingsHolder {
+ @Setting(value = "general")
+ private GeneralProps generalProps;
+
+ @Setting(value = "messages")
+ private MessagesProps messagesProps;
+
+ @Setting(value = "features")
+ private FeaturesProps featuresProps;
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/FeaturesProps.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/FeaturesProps.java
new file mode 100644
index 0000000..00c7ab7
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/FeaturesProps.java
@@ -0,0 +1,33 @@
+package com.jaimemartz.playerbalancer.velocity.settings.props;
+
+import com.jaimemartz.playerbalancer.velocity.settings.props.features.BalancerProps;
+import com.jaimemartz.playerbalancer.velocity.settings.props.features.FallbackCommandProps;
+import com.jaimemartz.playerbalancer.velocity.settings.props.features.KickHandlerProps;
+import com.jaimemartz.playerbalancer.velocity.settings.props.features.PermissionRouterProps;
+import com.jaimemartz.playerbalancer.velocity.settings.props.features.ServerCheckerProps;
+import com.jaimemartz.playerbalancer.velocity.settings.props.features.ServerRefreshProps;
+import lombok.Data;
+import ninja.leaping.configurate.objectmapping.Setting;
+import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;
+
+@ConfigSerializable
+@Data
+public class FeaturesProps {
+ @Setting(value = "balancer")
+ private BalancerProps balancerProps;
+
+ @Setting(value = "fallback-command")
+ private FallbackCommandProps fallbackCommandProps;
+
+ @Setting(value = "server-checker")
+ private ServerCheckerProps serverCheckerProps;
+
+ @Setting(value = "kick-handler")
+ private KickHandlerProps kickHandlerProps;
+
+ @Setting(value = "server-refresh")
+ private ServerRefreshProps serverRefreshProps;
+
+ @Setting(value = "permission-router")
+ private PermissionRouterProps permissionRouterProps;
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/GeneralProps.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/GeneralProps.java
new file mode 100644
index 0000000..550f2ca
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/GeneralProps.java
@@ -0,0 +1,27 @@
+package com.jaimemartz.playerbalancer.velocity.settings.props;
+
+import lombok.Data;
+import ninja.leaping.configurate.objectmapping.Setting;
+import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;
+
+@ConfigSerializable
+@Data
+public class GeneralProps {
+ @Setting
+ private boolean enabled;
+
+ @Setting
+ private boolean silent;
+
+ @Setting(value = "auto-reload")
+ private boolean autoReload;
+
+ @Setting(value = "plugin-messaging")
+ private boolean pluginMessaging;
+
+ @Setting(value = "redis-bungee")
+ private boolean redisBungee;
+
+ @Setting
+ private String version;
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/MessagesProps.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/MessagesProps.java
new file mode 100644
index 0000000..90a9726
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/MessagesProps.java
@@ -0,0 +1,36 @@
+package com.jaimemartz.playerbalancer.velocity.settings.props;
+
+import lombok.Data;
+import ninja.leaping.configurate.objectmapping.Setting;
+import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;
+
+@ConfigSerializable
+@Data
+public class MessagesProps {
+ @Setting(value = "connecting-server")
+ private String connectingMessage;
+
+ @Setting(value = "connected-server")
+ private String connectedMessage;
+
+ @Setting(value = "misc-failure")
+ private String failureMessage;
+
+ @Setting(value = "unknown-section")
+ private String unknownSectionMessage;
+
+ @Setting(value = "invalid-input")
+ private String invalidInputMessage;
+
+ @Setting(value = "unavailable-server")
+ private String unavailableServerMessage;
+
+ @Setting(value = "player-kicked")
+ private String kickMessage;
+
+ @Setting(value = "player-bypass")
+ private String bypassMessage;
+
+ @Setting(value = "same-section")
+ private String sameSectionMessage;
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/BalancerProps.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/BalancerProps.java
new file mode 100644
index 0000000..6da34e7
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/BalancerProps.java
@@ -0,0 +1,31 @@
+package com.jaimemartz.playerbalancer.velocity.settings.props.features;
+
+import com.jaimemartz.playerbalancer.velocity.settings.props.shared.SectionProps;
+import lombok.Data;
+import ninja.leaping.configurate.objectmapping.Setting;
+import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;
+
+import java.util.List;
+import java.util.Map;
+
+@ConfigSerializable
+@Data
+public class BalancerProps {
+ @Setting(value = "principal-section")
+ private String principalSectionName;
+
+ @Setting(value = "default-principal")
+ private boolean defaultPrincipal;
+
+ @Setting(value = "dummy-sections")
+ private List dummySectionNames;
+
+ @Setting(value = "reiterative-sections")
+ private List reiterativeSectionNames;
+
+ @Setting(value = "sections")
+ private Map sectionProps;
+
+ @Setting(value = "show-players")
+ private boolean showPlayers;
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/FallbackCommandProps.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/FallbackCommandProps.java
new file mode 100644
index 0000000..b3f98e9
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/FallbackCommandProps.java
@@ -0,0 +1,31 @@
+package com.jaimemartz.playerbalancer.velocity.settings.props.features;
+
+import com.jaimemartz.playerbalancer.velocity.settings.props.shared.CommandProps;
+import lombok.Data;
+import ninja.leaping.configurate.objectmapping.Setting;
+import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;
+
+import java.util.List;
+import java.util.Map;
+
+@ConfigSerializable
+@Data
+public class FallbackCommandProps {
+ @Setting
+ private boolean enabled;
+
+ @Setting
+ private CommandProps command;
+
+ @Setting(value = "excluded-sections")
+ private List excludedSections;
+
+ @Setting
+ private boolean restrictive;
+
+ @Setting(value = "prevent-same-section")
+ private boolean preventSameSection;
+
+ @Setting
+ private Map rules;
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/KickHandlerProps.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/KickHandlerProps.java
new file mode 100644
index 0000000..40acf25
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/KickHandlerProps.java
@@ -0,0 +1,36 @@
+package com.jaimemartz.playerbalancer.velocity.settings.props.features;
+
+import lombok.Data;
+import ninja.leaping.configurate.objectmapping.Setting;
+import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;
+
+import java.util.List;
+import java.util.Map;
+
+@ConfigSerializable
+@Data
+public class KickHandlerProps {
+ @Setting
+ private boolean enabled;
+
+ @Setting
+ private boolean inverted;
+
+ @Setting
+ private List reasons;
+
+ @Setting(value = "excluded-sections")
+ private List excludedSections;
+
+ @Setting
+ private boolean restrictive;
+
+ @Setting(value = "force-principal")
+ private boolean forcePrincipal;
+
+ @Setting
+ private Map rules;
+
+ @Setting(value = "debug-info")
+ private boolean debug;
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/PermissionRouterProps.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/PermissionRouterProps.java
new file mode 100644
index 0000000..5179d7c
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/PermissionRouterProps.java
@@ -0,0 +1,17 @@
+package com.jaimemartz.playerbalancer.velocity.settings.props.features;
+
+import lombok.Data;
+import ninja.leaping.configurate.objectmapping.Setting;
+import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;
+
+import java.util.Map;
+
+@ConfigSerializable
+@Data
+public class PermissionRouterProps {
+ @Setting
+ private boolean enabled;
+
+ @Setting
+ private Map> rules;
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/ServerCheckerProps.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/ServerCheckerProps.java
new file mode 100644
index 0000000..f4a7898
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/ServerCheckerProps.java
@@ -0,0 +1,33 @@
+package com.jaimemartz.playerbalancer.velocity.settings.props.features;
+
+import com.jaimemartz.playerbalancer.velocity.ping.PingTactic;
+import lombok.Data;
+import ninja.leaping.configurate.objectmapping.Setting;
+import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;
+
+import java.util.List;
+
+@ConfigSerializable
+@Data
+public class ServerCheckerProps {
+ @Setting
+ private boolean enabled;
+
+ @Setting
+ private PingTactic tactic;
+
+ @Setting
+ private int attempts;
+
+ @Setting
+ private int interval;
+
+ @Setting
+ private int timeout;
+
+ @Setting(value = "marker-descs")
+ private List markerDescs;
+
+ @Setting(value = "debug-info")
+ private boolean debug;
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/ServerRefreshProps.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/ServerRefreshProps.java
new file mode 100644
index 0000000..1e85d2a
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/features/ServerRefreshProps.java
@@ -0,0 +1,18 @@
+package com.jaimemartz.playerbalancer.velocity.settings.props.features;
+
+import lombok.Data;
+import ninja.leaping.configurate.objectmapping.Setting;
+import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;
+
+@ConfigSerializable
+@Data
+public class ServerRefreshProps {
+ @Setting
+ private boolean enabled;
+
+ @Setting
+ private int delay;
+
+ @Setting
+ private int interval;
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/shared/CommandProps.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/shared/CommandProps.java
new file mode 100644
index 0000000..2742c98
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/shared/CommandProps.java
@@ -0,0 +1,45 @@
+package com.jaimemartz.playerbalancer.velocity.settings.props.shared;
+
+import lombok.Data;
+import ninja.leaping.configurate.objectmapping.Setting;
+import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;
+
+import java.util.Collections;
+import java.util.List;
+
+@ConfigSerializable
+@Data
+public class CommandProps {
+ @Setting
+ private String name;
+
+ @Setting
+ private String permission;
+
+ @Setting
+ private List aliases;
+
+ public String getPermission() {
+ if (permission != null) {
+ return permission;
+ } else {
+ return "";
+ }
+ }
+
+ public List getAliases() {
+ if (aliases != null) {
+ return aliases;
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ public String[] getAliasesArray() {
+ if (aliases != null) {
+ return aliases.toArray(new String[aliases.size()]);
+ } else {
+ return new String[] {};
+ }
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/shared/SectionProps.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/shared/SectionProps.java
new file mode 100644
index 0000000..57d3ddb
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/settings/props/shared/SectionProps.java
@@ -0,0 +1,30 @@
+package com.jaimemartz.playerbalancer.velocity.settings.props.shared;
+
+import com.jaimemartz.playerbalancer.velocity.connection.ProviderType;
+import lombok.Data;
+import ninja.leaping.configurate.objectmapping.Setting;
+import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;
+
+import java.util.List;
+
+@ConfigSerializable
+@Data
+public class SectionProps {
+ @Setting
+ private ProviderType provider;
+
+ @Setting
+ private String alias;
+
+ @Setting(value = "parent")
+ private String parentName;
+
+ @Setting(value = "servers")
+ private List serverEntries;
+
+ @Setting(value = "section-command")
+ private CommandProps commandProps;
+
+ @Setting(value = "section-server")
+ private String serverName;
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/AlphanumComparator.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/AlphanumComparator.java
new file mode 100644
index 0000000..da7aae8
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/AlphanumComparator.java
@@ -0,0 +1,137 @@
+package com.jaimemartz.playerbalancer.velocity.utils;
+
+/*
+ * The Alphanum Algorithm is an improved sorting algorithm for strings
+ * containing numbers. Instead of sorting numbers in ASCII order like
+ * a standard sort, this algorithm sorts numbers in numeric order.
+ *
+ * The Alphanum Algorithm is discussed at http://www.DaveKoelle.com
+ *
+ * Released under the MIT License - https://opensource.org/licenses/MIT
+ *
+ * Copyright 2007-2017 David Koelle
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+ * USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import java.util.Comparator;
+
+/**
+ * This is an updated version with enhancements made by Daniel Migowski,
+ * Andre Bogus, and David Koelle. Updated by David Koelle in 2017.
+ *
+ * To use this class:
+ * Use the static "sort" method from the java.util.Collections class:
+ * Collections.sort(your list, new AlphanumComparator());
+ */
+public class AlphanumComparator implements Comparator
+{
+ private final boolean isDigit(char ch)
+ {
+ return ((ch >= 48) && (ch <= 57));
+ }
+
+ /** Length of string is passed in for improved efficiency (only need to calculate it once) **/
+ private final String getChunk(String s, int slength, int marker)
+ {
+ StringBuilder chunk = new StringBuilder();
+ char c = s.charAt(marker);
+ chunk.append(c);
+ marker++;
+ if (isDigit(c))
+ {
+ while (marker < slength)
+ {
+ c = s.charAt(marker);
+ if (!isDigit(c))
+ break;
+ chunk.append(c);
+ marker++;
+ }
+ } else
+ {
+ while (marker < slength)
+ {
+ c = s.charAt(marker);
+ if (isDigit(c))
+ break;
+ chunk.append(c);
+ marker++;
+ }
+ }
+ return chunk.toString();
+ }
+
+ public int compare(String s1, String s2)
+ {
+ if ((s1 == null) || (s2 == null))
+ {
+ return 0;
+ }
+
+ int thisMarker = 0;
+ int thatMarker = 0;
+ int s1Length = s1.length();
+ int s2Length = s2.length();
+
+ while (thisMarker < s1Length && thatMarker < s2Length)
+ {
+ String thisChunk = getChunk(s1, s1Length, thisMarker);
+ thisMarker += thisChunk.length();
+
+ String thatChunk = getChunk(s2, s2Length, thatMarker);
+ thatMarker += thatChunk.length();
+
+ // If both chunks contain numeric characters, sort them numerically
+ int result = 0;
+ if (isDigit(thisChunk.charAt(0)) && isDigit(thatChunk.charAt(0)))
+ {
+ // Simple chunk comparison by length.
+ int thisChunkLength = thisChunk.length();
+ result = thisChunkLength - thatChunk.length();
+ // If equal, the first different number counts
+ if (result == 0)
+ {
+ for (int i = 0; i < thisChunkLength; i++)
+ {
+ result = thisChunk.charAt(i) - thatChunk.charAt(i);
+ if (result != 0)
+ {
+ return result;
+ }
+ }
+ }
+ }
+ else
+ {
+ result = thisChunk.compareTo(thatChunk);
+ }
+
+ if (result != 0)
+ return result;
+ }
+
+ return s1Length - s2Length;
+ }
+
+ private static final AlphanumComparator instance = new AlphanumComparator();
+ public static AlphanumComparator getInstance() {
+ return instance;
+ }
+}
\ No newline at end of file
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/BuildInfo.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/BuildInfo.java
new file mode 100644
index 0000000..aeb078e
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/BuildInfo.java
@@ -0,0 +1,15 @@
+package com.jaimemartz.playerbalancer.velocity.utils;
+
+public class BuildInfo {
+ public static String getUserId() {
+ return "%%__USER__%%";
+ }
+
+ public static String getResourceId() {
+ return "%%__RESOURCE__%%";
+ }
+
+ public static String getNonceId() {
+ return "%%__NONCE__%%";
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/CustomFormatter.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/CustomFormatter.java
new file mode 100644
index 0000000..af7494e
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/CustomFormatter.java
@@ -0,0 +1,29 @@
+package com.jaimemartz.playerbalancer.velocity.utils;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.logging.Formatter;
+import java.util.logging.LogRecord;
+
+public class CustomFormatter extends Formatter {
+ private final DateFormat format = new SimpleDateFormat("HH:mm:ss");
+
+ @Override
+ public String format(LogRecord record) {
+ StringBuilder builder = new StringBuilder(String.format("[%s %s] %s\n",
+ format.format(record.getMillis()),
+ record.getLevel().getName(),
+ formatMessage(record)
+ ));
+
+ if (record.getThrown() != null) {
+ StringWriter writer = new StringWriter();
+ record.getThrown().printStackTrace(new PrintWriter(writer));
+ builder.append(writer);
+ }
+
+ return builder.toString();
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/DigitUtils.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/DigitUtils.java
new file mode 100644
index 0000000..d95c903
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/DigitUtils.java
@@ -0,0 +1,37 @@
+package com.jaimemartz.playerbalancer.velocity.utils;
+
+public final class DigitUtils {
+ public static int getDigits(String string, int digits) {
+ StringBuilder builder = new StringBuilder();
+
+ for (char character : string.toCharArray()) {
+ if (Character.isDigit(character)) {
+ if (builder.length() >= digits) {
+ break;
+ }
+
+ builder.append(character);
+ }
+ }
+
+ while (builder.length() < digits) {
+ builder.append("0");
+ }
+
+ return Integer.parseInt(builder.toString());
+ }
+
+ public static int getDigits(String string) {
+ StringBuilder builder = new StringBuilder();
+
+ for (char character : string.toCharArray()) {
+ if (Character.isDigit(character)) {
+ builder.append(character);
+ }
+ }
+
+ return Integer.parseInt(builder.toString());
+ }
+
+ private DigitUtils() {}
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/GuestPaste.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/GuestPaste.java
new file mode 100644
index 0000000..dc5d53b
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/GuestPaste.java
@@ -0,0 +1,174 @@
+package com.jaimemartz.playerbalancer.velocity.utils;
+
+import javax.net.ssl.HttpsURLConnection;
+import java.io.BufferedReader;
+import java.io.DataOutputStream;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.AbstractMap.SimpleEntry;
+import java.util.LinkedList;
+import java.util.List;
+
+public final class GuestPaste {
+ private final String key;
+ private final String code;
+
+ private String name = null;
+ private String format = null;
+ private Expiration expiration = null;
+ private Exposure exposure = null;
+
+ public GuestPaste(String key, String code) {
+ this.key = key;
+ this.code = code;
+ }
+
+ public URL paste() throws Exception {
+ URL url = new URL("https://pastebin.com/api/api_post.php");
+ HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
+
+ con.setRequestMethod("POST");
+ con.setRequestProperty("User-Agent", "Mozilla/5.0");
+
+ List> params = new LinkedList<>();
+ params.add(new SimpleEntry<>("api_dev_key", key));
+ params.add(new SimpleEntry<>("api_option", "paste"));
+ params.add(new SimpleEntry<>("api_paste_code", code));
+
+ if (name != null) {
+ params.add(new SimpleEntry<>("api_paste_name", name));
+ }
+
+ if (format != null) {
+ params.add(new SimpleEntry<>("api_paste_format", format));
+ }
+
+ if (expiration != null) {
+ params.add(new SimpleEntry<>("api_paste_expire_date", expiration.value));
+ }
+
+ if (exposure != null) {
+ params.add(new SimpleEntry<>("api_paste_private", String.valueOf(exposure.value)));
+ }
+
+ StringBuilder output = new StringBuilder();
+ for (SimpleEntry entry : params) {
+ if (output.length() > 0)
+ output.append('&');
+
+ output.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
+ output.append('=');
+ output.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
+ }
+
+ con.setDoOutput(true);
+ try (DataOutputStream dos = new DataOutputStream(con.getOutputStream())) {
+ dos.writeBytes(output.toString());
+ dos.flush();
+ }
+
+ int status = con.getResponseCode();
+ if (status >= 200 && status < 300) {
+ try (BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
+ String inputLine;
+ StringBuilder response = new StringBuilder();
+
+ while ((inputLine = br.readLine()) != null) {
+ response.append(inputLine);
+ }
+
+ try {
+ return new URL(response.toString());
+ } catch (MalformedURLException e) {
+ throw new PasteException(response.toString());
+ }
+ }
+ } else {
+ throw new PasteException("Unexpected response code " + status);
+ }
+ }
+
+ public enum Expiration {
+ NEVER("N"),
+ TEN_MINUTES("10M"),
+ ONE_HOUR("1H"),
+ ONE_DAY("1D"),
+ ONE_WEEK("1W"),
+ TWO_WEEKS("2W"),
+ ONE_MONTH("1M"),
+ SIX_MONTHS("6M"),
+ ONE_YEAR("1Y");
+
+ private final String value;
+ Expiration(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+ }
+
+ public enum Exposure {
+ PUBLIC(0),
+ UNLISTED(1),
+ PRIVATE(2);
+
+ private final int value;
+ Exposure(int value) {
+ this.value = value;
+ }
+
+ public int getValue() {
+ return value;
+ }
+ }
+
+ public class PasteException extends Exception {
+ public PasteException(String response) {
+ super(response);
+ }
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getFormat() {
+ return format;
+ }
+
+ public void setFormat(String format) {
+ this.format = format;
+ }
+
+ public Expiration getExpiration() {
+ return expiration;
+ }
+
+ public void setExpiration(Expiration expiration) {
+ this.expiration = expiration;
+ }
+
+ public Exposure getExposure() {
+ return exposure;
+ }
+
+ public void setExposure(Exposure exposure) {
+ this.exposure = exposure;
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/HastebinPaste.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/HastebinPaste.java
new file mode 100644
index 0000000..a4f91d5
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/HastebinPaste.java
@@ -0,0 +1,44 @@
+package com.jaimemartz.playerbalancer.velocity.utils;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+import javax.net.ssl.HttpsURLConnection;
+import java.io.BufferedReader;
+import java.io.DataOutputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+
+public class HastebinPaste {
+ private final String site;
+ private final String code;
+
+ public HastebinPaste(String site, String code) {
+ this.site = site.endsWith("/") ? site : site + "/";
+ this.code = code;
+ }
+
+ public URL paste() throws Exception {
+ URL url = new URL(site + "documents");
+ HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
+
+ con.setRequestMethod("POST");
+ con.setRequestProperty("User-Agent", "Mozilla/5.0");
+
+ con.setDoOutput(true);
+ try (DataOutputStream dos = new DataOutputStream(con.getOutputStream())) {
+ dos.writeBytes(code);
+ dos.flush();
+ }
+
+ int status = con.getResponseCode();
+ if (status >= 200 && status < 300) {
+ try (BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
+ JsonObject root = new JsonParser().parse(br.readLine()).getAsJsonObject();
+ return new URL(site + root.getAsJsonPrimitive("key").getAsString());
+ }
+ } else {
+ throw new Exception("Unexpected response code " + status);
+ }
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/LevenshteinDistance.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/LevenshteinDistance.java
new file mode 100644
index 0000000..757ac71
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/LevenshteinDistance.java
@@ -0,0 +1,59 @@
+package com.jaimemartz.playerbalancer.velocity.utils;
+
+public final class LevenshteinDistance {
+ public static T closest(Iterable collection, T target) {
+ int distance = Integer.MAX_VALUE;
+ T closest = null;
+
+ for (T object : collection) {
+ int current = distance(object.toString(), target.toString());
+ if (current < distance) {
+ distance = current;
+ closest = object;
+ }
+ }
+
+ return closest;
+ }
+
+ private static int distance(CharSequence lhs, CharSequence rhs) {
+ int len0 = lhs.length() + 1;
+ int len1 = rhs.length() + 1;
+
+ // the array of distances
+ int[] cost = new int[len0];
+ int[] newcost = new int[len0];
+
+ // initial cost of skipping prefix in String s0
+ for (int i = 0; i < len0; i++) cost[i] = i;
+
+ // dynamically computing the array of distances
+
+ // transformation cost for each letter in s1
+ for (int j = 1; j < len1; j++) {
+ // initial cost of skipping prefix in String s1
+ newcost[0] = j;
+
+ // transformation cost for each letter in s0
+ for (int i = 1; i < len0; i++) {
+ // matching current letters in both strings
+ int match = (lhs.charAt(i - 1) == rhs.charAt(j - 1)) ? 0 : 1;
+
+ // computing cost for each transformation
+ int cost_replace = cost[i - 1] + match;
+ int cost_insert = cost[i] + 1;
+ int cost_delete = newcost[i - 1] + 1;
+
+ // keep minimum cost
+ newcost[i] = Math.min(Math.min(cost_insert, cost_delete), cost_replace);
+ }
+
+ // swap cost/newcost arrays
+ int[] swap = cost;
+ cost = newcost;
+ newcost = swap;
+ }
+
+ return cost[len0 - 1];
+ }
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/MessageUtils.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/MessageUtils.java
new file mode 100644
index 0000000..46d4058
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/MessageUtils.java
@@ -0,0 +1,35 @@
+package com.jaimemartz.playerbalancer.velocity.utils;
+
+import com.velocitypowered.api.command.CommandSource;
+import net.kyori.adventure.text.minimessage.MiniMessage;
+
+import java.util.function.Function;
+
+public final class MessageUtils {
+ public static void send(CommandSource sender, String text) {
+ if (text != null) {
+ sender.sendMessage(MiniMessage.miniMessage().deserialize(text));
+ }
+ }
+
+ public static void send(CommandSource sender, String text, Function postProcess) {
+ if (text != null) {
+ text = postProcess.apply(text);
+ }
+
+ send(sender, text);
+ }
+
+ public static String revertColor(String string) {
+ return string.replace('§', '&');
+ }
+
+ public static String safeNull(String string) {
+ if (string == null) {
+ return "Undefined";
+ }
+ return string;
+ }
+
+ private MessageUtils() {}
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/RandomUtils.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/RandomUtils.java
new file mode 100644
index 0000000..069902a
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/RandomUtils.java
@@ -0,0 +1,14 @@
+package com.jaimemartz.playerbalancer.velocity.utils;
+
+import java.security.SecureRandom;
+import java.util.List;
+
+public final class RandomUtils {
+ private static final SecureRandom instance = new SecureRandom();
+
+ public static T random(List list) {
+ return list.get(instance.nextInt(list.size()));
+ }
+
+ private RandomUtils() {}
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/RegExUtils.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/RegExUtils.java
new file mode 100644
index 0000000..348e2e2
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/RegExUtils.java
@@ -0,0 +1,37 @@
+package com.jaimemartz.playerbalancer.velocity.utils;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+
+import java.util.concurrent.ExecutionException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class RegExUtils {
+ private static final LoadingCache COMPILED_PATTERNS = CacheBuilder.newBuilder().build(new CacheLoader() {
+ @Override
+ public Pattern load(String string) throws Exception {
+ return Pattern.compile(string);
+ }
+ });
+
+ public static Pattern getPattern(String regexp) {
+ try {
+ return COMPILED_PATTERNS.get(regexp);
+ } catch (ExecutionException e) {
+ throw new RuntimeException("Error while getting a pattern from the cache");
+ }
+ }
+
+ public static boolean matches(String string, String expression) {
+ return getMatcher(string, expression).matches();
+ }
+
+ private static Matcher getMatcher(String string, String expression) {
+ Pattern pattern = getPattern(expression);
+ return pattern.matcher(string);
+ }
+
+ private RegExUtils() {}
+}
diff --git a/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/ServerListPing.java b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/ServerListPing.java
new file mode 100644
index 0000000..2c7d0ca
--- /dev/null
+++ b/balancer-velocity/src/main/java/com/jaimemartz/playerbalancer/velocity/utils/ServerListPing.java
@@ -0,0 +1,227 @@
+package com.jaimemartz.playerbalancer.velocity.utils;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import lombok.extern.slf4j.Slf4j;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.List;
+
+@Slf4j
+public final class ServerListPing {
+
+ private static GsonComponentSerializer gson = GsonComponentSerializer.gson();
+
+ private static int readVarInt(DataInputStream in) throws IOException {
+ int i = 0;
+ int j = 0;
+ while (true) {
+ int k = in.readByte();
+ i |= (k & 0x7F) << j++ * 7;
+ if (j > 5) throw new RuntimeException("VarInt too big");
+ if ((k & 0x80) != 128) break;
+ }
+ return i;
+ }
+
+ private static void writeVarInt(DataOutputStream out, int paramInt) throws IOException {
+ while (true) {
+ if ((paramInt & 0xFFFFFF80) == 0) {
+ out.writeByte(paramInt);
+ return;
+ }
+
+ out.writeByte(paramInt & 0x7F | 0x80);
+ paramInt >>>= 7;
+ }
+ }
+
+ public StatusResponse ping(InetSocketAddress host, int timeout) throws IOException {
+ try (Socket socket = new Socket()) {
+ socket.setSoTimeout(timeout);
+ socket.connect(host, timeout);
+
+ try (DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
+ DataInputStream dataInputStream = new DataInputStream(socket.getInputStream())) {
+ ByteArrayOutputStream b = new ByteArrayOutputStream();
+ DataOutputStream handshake = new DataOutputStream(b);
+ handshake.writeByte(0x00); // packet id for handshake
+ writeVarInt(handshake, 4); // protocol version
+ writeVarInt(handshake, host.getHostString().length()); // host length
+ handshake.writeBytes(host.getHostString()); // host string
+ handshake.writeShort(host.getPort()); // port
+ writeVarInt(handshake, 1); // state (1 for handshake)
+
+ writeVarInt(dataOutputStream, b.size()); // prepend size
+ dataOutputStream.write(b.toByteArray()); // write handshake packet
+
+
+ dataOutputStream.writeByte(0x01); // size is only 1
+ dataOutputStream.writeByte(0x00); // packet id for ping
+ int size = readVarInt(dataInputStream); // size of packet
+ int id = readVarInt(dataInputStream); // packet id
+
+ if (id == -1) {
+ throw new IOException("Premature end of stream.");
+ }
+
+ if (id != 0x00) { // we want a status response
+ throw new IOException("Invalid packetID");
+ }
+
+ int length = readVarInt(dataInputStream); // length of json string
+ if (length == -1) {
+ throw new IOException("Premature end of stream.");
+ }
+
+ if (length == 0) {
+ throw new IOException("Invalid string length.");
+ }
+
+ byte[] in = new byte[length];
+ dataInputStream.readFully(in); // read json string
+ String json = new String(in);
+
+
+ long now = System.currentTimeMillis();
+ dataOutputStream.writeByte(0x09); // size of packet
+ dataOutputStream.writeByte(0x01); // 0x01 for ping
+ dataOutputStream.writeLong(now); // time!?
+
+ readVarInt(dataInputStream);
+ id = readVarInt(dataInputStream);
+ if (id == -1) {
+ throw new IOException("Premature end of stream.");
+ }
+
+ if (id != 0x01) {
+ throw new IOException("Invalid packetID");
+ }
+
+ long pingTime = dataInputStream.readLong(); // read response
+
+ StatusResponse response = StatusResponse.fromJson(json);
+
+ response.time = (int) (now - pingTime);
+ return response;
+ }
+ }
+ }
+
+ public static class StatusResponse {
+ private Component description;
+ private Players players;
+ private Version version;
+ private String favicon;
+ private int time;
+
+ public Component getDescription() {
+ return description;
+ }
+
+ public Players getPlayers() {
+ return players;
+ }
+
+ public Version getVersion() {
+ return version;
+ }
+
+ public String getFavicon() {
+ return favicon;
+ }
+
+ public int getTime() {
+ return time;
+ }
+
+ public static class Players {
+ private int max;
+ private int online;
+ private List sample;
+
+ public int getMax() {
+ return max;
+ }
+
+ public int getOnline() {
+ return online;
+ }
+
+ public List getSample() {
+ return sample;
+ }
+ }
+
+ public static class Player {
+ private String name;
+ private String id;
+
+ public String getName() {
+ return name;
+ }
+
+ public String getId() {
+ return id;
+ }
+ }
+
+ public static class Version {
+ private String name;
+ private String protocol;
+
+ public String getName() {
+ return name;
+ }
+
+ public String getProtocol() {
+ return protocol;
+ }
+ }
+
+ public static StatusResponse fromJson(String json) {
+ StatusResponse response = new StatusResponse();
+
+ JsonObject jsonObject = new JsonParser().parse(json).getAsJsonObject();
+
+ // Extract and set the "description" field as an Adventure Component
+ JsonElement descriptionElement = jsonObject.get("description");
+ if (descriptionElement != null) {
+ response.description = gson.deserialize(descriptionElement.toString());
+ }
+
+ // Extract and set the "players" field
+ JsonObject playersObject = jsonObject.getAsJsonObject("players");
+ if (playersObject != null) {
+ response.players = new Players();
+ response.players.max = playersObject.get("max").getAsInt();
+ response.players.online = playersObject.get("online").getAsInt();
+ // You can also extract the "sample" field if needed
+ }
+
+ // Extract and set the "version" field
+ JsonObject versionObject = jsonObject.getAsJsonObject("version");
+ if (versionObject != null) {
+ response.version = new Version();
+ response.version.name = versionObject.get("name").getAsString();
+ response.version.protocol = versionObject.get("protocol").getAsString();
+ }
+
+ // Extract and set the "favicon" field
+ JsonElement faviconElement = jsonObject.get("favicon");
+ if (faviconElement != null) {
+ response.favicon = faviconElement.getAsString();
+ }
+
+ return response;
+ }
+ }
+}
diff --git a/balancer-velocity/src/main/resources/velocity.conf b/balancer-velocity/src/main/resources/velocity.conf
new file mode 100644
index 0000000..eac52cf
--- /dev/null
+++ b/balancer-velocity/src/main/resources/velocity.conf
@@ -0,0 +1,236 @@
+# PlayerBalancer Configuration (https://www.spigotmc.org/resources/10788/)
+# Read the comments, they are a very important part of the configuration
+# To get support please message us on Discord (https://bghddevelopment.com/discord) or on the "Issues"
+# section of the GitHub repository.
+# To easily paste the config file (and other relevant files) use the command /balancer paste
+# If the plugin has issues loading the configuration, try putting quotes around text
+
+general {
+ # IMPORTANT! Set this to true after configuring the plugin!
+ enabled=false
+
+ # When true, the plugin will reload when you execute /greload
+ auto-reload=true
+
+ # When true, the plugin will get player counts from RedisBungee (Limework Fork: https://www.spigotmc.org/resources/87700/)
+ redis-bungee=false
+
+ # When true, this plugin will print less messages when loading
+ silent=false
+
+ # When true, spigot plugins will be able to contact with this plugin
+ # Do not disable if you are using the addon!
+ plugin-messaging=true
+
+ # Do not modify this
+ version="${project.version}"
+}
+
+# Effectively remove (i.e comment) a message to disable it
+# Supported variables are shown in the default messages
+messages {
+ # connecting-server="&aConnecting to an {section} ({alias}) server" # this message is disabled by default!
+ connected-server="Connected to {server} (an {alias} server)"
+ invalid-input="This is an invalid input type for this command"
+ misc-failure="Could not find a server to get connected to"
+ player-bypass="You have not been moved because you have the playerbalancer.bypass permission"
+ player-kicked="You have been kicked from {from} so you are being moved to {to}\nReason: {reason}"
+ same-section="You are already connected to a server on {alias}!"
+ unavailable-server="This command cannot be executed on this server"
+ unknown-section="Could not find a section with that name"
+}
+
+features {
+ balancer {
+ # Here you have an example of what you can do with the sections
+ # The best way to understand this is to play around with it
+ # You can have as many sections as you want, there is no limit here
+ # If a section does not have a provider it will try to inherit it from the parent
+ # When connecting to a server on a section while not being on it already, you get distributed
+ # You can use regex to match a set of servers instead of adding each server individually
+
+ # Providers you can use: (you can request more!)
+ # NONE: Returns no server (no one will be able to connect to this section)
+ # RANDOM: Returns a random server, generated by SecureRandom
+ # RANDOM_LOWEST: Returns a random server between the ones with the least players online
+ # RANDOM_FILLER: Returns a random server between the ones with the most players online that is not full
+ # PROGRESSIVE: Returns the first server that is not full
+ # PROGRESSIVE_LOWEST: Returns the first server with the least players online
+ # PROGRESSIVE_FILLER: Returns the first server with the most players online that is not full
+ # EXTERNAL: Returns the server calculated by a provider created by other plugin
+
+ sections {
+ auth-lobbies {
+ provider=RANDOM
+ servers=[
+ "Auth1",
+ "Auth2",
+ "Auth3"
+ ]
+ }
+
+ general-lobbies {
+ parent="auth-lobbies"
+ alias="General Lobbies"
+ servers=[
+ "Lobby[1-3]"
+ ]
+ }
+
+ skywars-lobbies {
+ parent="general-lobbies"
+ provider=PROGRESSIVE_LOWEST
+ servers=[
+ "SWLobby1",
+ "SWLobby2",
+ "SWLobby3"
+ ]
+ }
+
+ skywars-games {
+ parent="skywars-lobbies"
+ provider=RANDOM_FILLER
+ servers=["SW_A[1-5]", "SW_B[1-5]"]
+ section-command {
+ name=skywars
+ permission=""
+ aliases=[]
+ }
+ }
+ }
+
+ # The principal section is very important for other features
+ # Normally set this to the section that has your main lobbies
+ principal-section="general-lobbies"
+
+ # When a player is not in any section, the player will go to the principal section
+ # This affects both the fallback command and kick handler features
+ default-principal=true
+
+ # Dummy sections can have servers from other non-dummy sections
+ # When a player connects to a dummy section, nothing will happen
+ dummy-sections=[]
+
+ # Reiterative sections remember the server the player connected to previously
+ # The plugin will keep connecting the player to that server repeatedly
+ reiterative-sections=[]
+
+ # When true, section servers will show the sum of the players on all servers on that section
+ # Important: This will make some plugins think that your proxy has more players than it really does
+ show-players=true
+ }
+
+ # Pings servers to see if they are online or not and if they are accessible
+ server-checker {
+ enabled=true
+
+ # Use either CUSTOM or GENERIC, the first one generally works the best
+ tactic=CUSTOM
+
+ # The attempts before giving up on getting a server for a player
+ attempts=5
+
+ # The interval between every round of checks (in milliseconds)
+ interval=10000
+
+ # The timeout of a ping, only applied to the CUSTOM tactic
+ timeout=7000
+
+ # When true, the plugin will print useful info when a server gets checked
+ debug-info=false
+
+ # When the description of a server matches these, it will be set as non accessible
+ # Be aware of colors, it is recommended to use the "contains" rule below or some others
+ marker-descs=[
+ "(?i).*maintenance*", # match if contains (regex)
+ "Game in progress" # match if exactly equal
+ ]
+ }
+
+ # Connects a player to the parent of current section the player is connected to
+ fallback-command {
+ enabled=true
+
+ # Leave permission empty for no permission
+ command {
+ name=fallback
+ permission=""
+ aliases=[
+ lobby,
+ hub,
+ back
+ ]
+ }
+
+ # Add sections here where you do not want this feature to work
+ excluded-sections=[]
+
+ # When true, players will not be able to get connected to sections that are parents of the principal section
+ restrictive=true
+
+ # When true, players will not be able get connected within servers of the same section
+ # This does not affect the parametized variant of the command (/command [number])
+ # This also affects section commands
+ prevent-same-section=true
+
+ # You can override the behavior with rules, overriding the parent section
+ # This will set the section to go when you come from the section specified
+ rules {
+ section-from=section-to
+ }
+ }
+
+ # Connects a player to other section when kicked
+ kick-handler {
+ enabled=true
+
+ # When true, the reasons will work as a blacklist instead of a whitelist
+ # Blacklist: A player must be kicked with a reason that is NOT in the reasons
+ # Whitelist: A player must be kicked with a reason that is in the reasons
+ inverted=true
+
+ # The reasons that determine if a player is reconnected or not, supports regex
+ reasons=[]
+
+ # When true, players that are kicked while connecting to the proxy will be forced to reconnect to the principal section
+ force-principal=false
+
+ # Add sections here where you do not want this feature to work
+ excluded-sections=[]
+
+ # When true, players will not be able to get connected to sections that are parents of the principal section
+ restrictive=true
+
+ # When true, the plugin will print useful info when a player gets kicked
+ debug-info=false
+
+ # You can override the behavior with rules, overriding the parent section
+ # When you get kicked from the section on the left, you go to the one on the right
+ rules {
+ section-from=section-to
+ }
+ }
+
+ # Periodically adds servers that weren't there before the plugin loaded
+ server-refresh {
+ enabled=false
+
+ # The delay to the first refresh (in milliseconds)
+ delay=2000
+
+ # The interval between every refresh (in milliseconds)
+ interval=5000
+ }
+
+ # Redirect players when connecting to a section in case they have a permission, useful for special lobbies
+ # Players will not get redirected if they are connected to a server where they were previously redirected to
+ permission-router {
+ enabled=false
+
+ rules {
+ general-lobbies {
+ "special.permission"=other-lobby-section
+ }
+ }
+ }
+}
diff --git a/balancer-velocity/src/test/java/DefaultConfigLoadTest.java b/balancer-velocity/src/test/java/DefaultConfigLoadTest.java
new file mode 100644
index 0000000..f394dde
--- /dev/null
+++ b/balancer-velocity/src/test/java/DefaultConfigLoadTest.java
@@ -0,0 +1,33 @@
+import com.google.common.reflect.TypeToken;
+import com.jaimemartz.playerbalancer.velocity.settings.SettingsHolder;
+import ninja.leaping.configurate.commented.CommentedConfigurationNode;
+import ninja.leaping.configurate.hocon.HoconConfigurationLoader;
+import ninja.leaping.configurate.objectmapping.ObjectMappingException;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+
+public class DefaultConfigLoadTest {
+ private URL file;
+
+ @Before
+ public void before() throws IOException {
+ file = getClass().getResource("default.conf");
+ }
+
+ @Test
+ public void test() throws IOException, ObjectMappingException {
+ HoconConfigurationLoader loader = HoconConfigurationLoader
+ .builder()
+ .setURL(file)
+ .build();
+
+ CommentedConfigurationNode node = loader.load();
+ SettingsHolder settings = node.getValue(TypeToken.of(SettingsHolder.class));
+
+ System.out.println(settings);
+ }
+}
diff --git a/balancer-velocity/src/test/java/HastebinPasteTest.java b/balancer-velocity/src/test/java/HastebinPasteTest.java
new file mode 100644
index 0000000..c5e03b5
--- /dev/null
+++ b/balancer-velocity/src/test/java/HastebinPasteTest.java
@@ -0,0 +1,25 @@
+import com.jaimemartz.playerbalancer.velocity.utils.HastebinPaste;
+import org.junit.Test;
+
+import java.net.URL;
+import static org.junit.Assert.*;
+
+public class HastebinPasteTest {
+ @Test
+ public void test() throws Exception {
+ HastebinPaste paste = new HastebinPaste("https://haste.zneix.eu/",
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed " +
+ "iaculis, sapien et vehicula tristique, diam libero bibendum " +
+ "nunc, et rutrum nisl nulla quis diam. Cras ipsum enim, molestie" +
+ " eget bibendum nec, porta quis ex. Nunc ac sem lorem. Duis eget" +
+ " vestibulum libero. Phasellus vitae venenatis arcu, ac volutpat " +
+ "sem. Nunc porttitor lacus nulla, vitae dictum justo porta at. " +
+ "Aliquam erat volutpat. Vestibulum aliquet eget diam eget commodo." +
+ " Integer facilisis ipsum sit amet sem pharetra ultrices. Nulla diam" +
+ " orci, posuere malesuada ante non, elementum vehicula libero."
+ );
+
+ URL pasteUrl = paste.paste();
+ assertNotNull(pasteUrl);
+ }
+}
diff --git a/pom.xml b/pom.xml
index e4988cb..729eb30 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,6 +11,7 @@
balancer
+ balancer-velocity
addon
partyandfriendsaddon