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