feat: initial commit for velocity support

This commit is contained in:
HappyAreaBean 2023-10-05 17:03:49 +08:00
parent 0847b0febd
commit 16293403f0
No known key found for this signature in database
GPG Key ID: A027DED558B69688
57 changed files with 4468 additions and 0 deletions

107
balancer-velocity/pom.xml Normal file
View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.jaimemartz</groupId>
<version>2.3.3</version>
<artifactId>playerbalancer-parent</artifactId>
</parent>
<artifactId>playerbalancer-velocity</artifactId>
<name>PlayerBalancer Velocity</name>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<configuration>
<finalName>PlayerBalancer-Velocity-${project.version}</finalName>
<relocations>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>com.jaimemartz.playerbalancer.metrics</shadedPattern>
</relocation>
<relocation>
<pattern>ninja.leaping.configurate</pattern>
<shadedPattern>com.jaimemartz.playerbalancer.libs.ninja.leaping.configurate</shadedPattern>
</relocation>
<relocation>
<pattern>com.typesafe.config</pattern>
<shadedPattern>com.jaimemartz.playerbalancer.libs.com.typesafe.config</shadedPattern>
</relocation>
</relocations>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>CodeMC</id>
<url>https://repo.codemc.org/repository/maven-public</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.velocitypowered</groupId>
<artifactId>velocity-api</artifactId>
<version>3.2.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>25.1-jre</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.imaginarycode.minecraft</groupId>
<artifactId>RedisBungee</artifactId>
<version>0.6.5-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.spongepowered</groupId>
<artifactId>configurate-hocon</artifactId>
<version>3.7.3</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<!-- Already shaded in velocity -->
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-velocity</artifactId>
<version>3.0.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -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<CommentedConfigurationNode> 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<String> 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;
}
}

View File

@ -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<ServerConnection> 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;
}
}

View File

@ -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));
}
}
}

View File

@ -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> 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 <section> [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 <section>", 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 <section>", AQUA).append(text(" - ", GRAY)).append(text("Tells you info about the specified section", RED)));
sender.sendMessage(text("/section connect <section> [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));
}
}
}

View File

@ -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<ServerInfo> 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<ServerInfo> 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<RegisteredServer> 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<RegisteredServer> 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<String, String> routes = plugin.getSettings().getFeaturesProps().getPermissionRouterProps().getRules().get(section.getName());
ServerSection current = plugin.getSectionManager().getByPlayer(player);
if (routes != null) {
for (Map.Entry<String, String> 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<Boolean> callback);
public static void simple(PlayerBalancer plugin, Player player, ServerSection section) {
new ConnectionIntent(plugin, player, section) {
@Override
public void connect(ServerInfo server, Consumer<Boolean> callback) {
ConnectionIntent.direct(plugin, player, server, callback);
}
}.execute();
}
public static void direct(PlayerBalancer plugin, Player player, ServerInfo server, Consumer<Boolean> 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());
});
});
}
}

View File

@ -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<RegisteredServer> servers, Player player) {
return provider.requestTarget(plugin, section, servers, player);
}
},
RANDOM {
RandomProvider provider = new RandomProvider();
@Override
public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List<RegisteredServer> 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<RegisteredServer> 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<RegisteredServer> servers, Player player) {
return provider.requestTarget(plugin, section, servers, player);
}
},
PROGRESSIVE {
ProgressiveProvider provider = new ProgressiveProvider();
@Override
public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List<RegisteredServer> 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<RegisteredServer> 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<RegisteredServer> servers, Player player) {
return provider.requestTarget(plugin, section, servers, player);
}
},
EXTERNAL {
@Override
public RegisteredServer requestTarget(PlayerBalancer plugin, ServerSection section, List<RegisteredServer> 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<RegisteredServer> servers,
Player player
);
}

View File

@ -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<Player, ServerSection, ServerInfo> 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<ServerSection, ServerInfo> 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<Player, ServerSection, ServerInfo> getTable() {
synchronized (table) {
return table;
}
}
}

View File

@ -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<RegisteredServer> servers,
Player player
);
}

View File

@ -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<RegisteredServer> servers, Player player) {
return null;
}
}

View File

@ -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<RegisteredServer> 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;
}
}

View File

@ -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<RegisteredServer> 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;
}
}

View File

@ -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<RegisteredServer> 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;
}
}

View File

@ -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<RegisteredServer> servers, Player player) {
List<RegisteredServer> 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);
}
}

View File

@ -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<RegisteredServer> servers, Player player) {
List<RegisteredServer> 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);
}
}

View File

@ -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<RegisteredServer> servers, Player player) {
return random(servers);
}
}

View File

@ -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<RegisteredServer> serverConnection = plugin.getProxyServer().getServer(server.getName());
return serverConnection.map(registeredServer -> registeredServer.getPlayersConnected().size()).orElse(0);
}
}

View File

@ -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<CommandSource, URL> consumer;
private final boolean cache;
PasteHelper(BiConsumer<CommandSource, URL> 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;
}
}
}

View File

@ -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<UUID> storage = Collections.synchronizedSet(new HashSet<UUID>());
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();
}
}

View File

@ -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);
}
}

View File

@ -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<ServerInfo>) (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> 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<RegisteredServer> 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<RegisteredServer> 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<RegisteredServer> 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> 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;
}
}
}
}
}

View File

@ -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();
}
}

View File

@ -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<Boolean> 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;
}
}

View File

@ -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<Boolean> callback) {
PlayerLocker.lock(player);
Optional<RegisteredServer> 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> 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;
}
}

View File

@ -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<ServerStatus> 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<ServerStatus> 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<ServerStatus> callback, PlayerBalancer plugin);
}

View File

@ -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;
}
}

View File

@ -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<ServerInfo, ServerStatus> storage = new HashMap<>();
private final Map<ServerInfo, Boolean> 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<RegisteredServer> server = plugin.getProxyServer().getServer(in.readUTF());
if (!server.isPresent())
break;
overriders.remove(server.get().getServerInfo());
break;
}
case "SetStatusOverride": {
Optional<RegisteredServer> server = plugin.getProxyServer().getServer(in.readUTF());
if (!server.isPresent())
break;
overriders.put(server.get().getServerInfo(), in.readBoolean());
break;
}
}
}
}
}

View File

@ -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();
}
}

View File

@ -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<String, ServerSection> sections = Collections.synchronizedMap(new HashMap<>());
@Getter
private final Map<RegisteredServer, ServerSection> servers = Collections.synchronizedMap(new HashMap<>());
private static final Map<String, Stage> 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<ServerConnection> 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<RegisteredServer> 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<String> dummySections = props.getDummySectionNames();
return dummySections.contains(section.getName());
}
public boolean isReiterative(ServerSection section) {
List<String> 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;
}
}

View File

@ -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<RegisteredServer> 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();
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<String> dummySectionNames;
@Setting(value = "reiterative-sections")
private List<String> reiterativeSectionNames;
@Setting(value = "sections")
private Map<String, SectionProps> sectionProps;
@Setting(value = "show-players")
private boolean showPlayers;
}

View File

@ -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<String> excludedSections;
@Setting
private boolean restrictive;
@Setting(value = "prevent-same-section")
private boolean preventSameSection;
@Setting
private Map<String, String> rules;
}

View File

@ -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<String> reasons;
@Setting(value = "excluded-sections")
private List<String> excludedSections;
@Setting
private boolean restrictive;
@Setting(value = "force-principal")
private boolean forcePrincipal;
@Setting
private Map<String, String> rules;
@Setting(value = "debug-info")
private boolean debug;
}

View File

@ -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<String, Map<String, String>> rules;
}

View File

@ -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<String> markerDescs;
@Setting(value = "debug-info")
private boolean debug;
}

View File

@ -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;
}

View File

@ -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<String> aliases;
public String getPermission() {
if (permission != null) {
return permission;
} else {
return "";
}
}
public List<String> 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[] {};
}
}
}

View File

@ -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<String> serverEntries;
@Setting(value = "section-command")
private CommandProps commandProps;
@Setting(value = "section-server")
private String serverName;
}

View File

@ -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<String>
{
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;
}
}

View File

@ -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__%%";
}
}

View File

@ -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();
}
}

View File

@ -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() {}
}

View File

@ -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<SimpleEntry<String, String>> 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<String, String> 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;
}
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,59 @@
package com.jaimemartz.playerbalancer.velocity.utils;
public final class LevenshteinDistance {
public static <T> T closest(Iterable<T> 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];
}
}

View File

@ -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<String, String> 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() {}
}

View File

@ -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> T random(List<T> list) {
return list.get(instance.nextInt(list.size()));
}
private RandomUtils() {}
}

View File

@ -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<String, Pattern> COMPILED_PATTERNS = CacheBuilder.newBuilder().build(new CacheLoader<String, Pattern>() {
@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() {}
}

View File

@ -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<Player> sample;
public int getMax() {
return max;
}
public int getOnline() {
return online;
}
public List<Player> 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;
}
}
}

View File

@ -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="<green>Connected to {server} (an {alias} server)"
invalid-input="<red>This is an invalid input type for this command"
misc-failure="<red>Could not find a server to get connected to"
player-bypass="<red>You have not been moved because you have the playerbalancer.bypass permission"
player-kicked="<red>You have been kicked from <green>{from} <red>so you are being moved to <green>{to}\n<red>Reason: <green>{reason}"
same-section="<red>You are already connected to a server on {alias}!"
unavailable-server="<red>This command cannot be executed on this server"
unknown-section="<green>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
}
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -11,6 +11,7 @@
<modules>
<module>balancer</module>
<module>balancer-velocity</module>
<module>addon</module>
<module>partyandfriendsaddon</module>
</modules>