From de321d2eac873e0c6504bc1fff55762c541d66e2 Mon Sep 17 00:00:00 2001 From: Abdullah Arafat <62858251+kmaba@users.noreply.github.com> Date: Sun, 2 Feb 2025 14:15:04 +0800 Subject: [PATCH] Update README based on changes Update plugin to support unlimited lobbies and dynamic Minecraft versions. * Modify `HubCommand.java` and `LobbyCommand.java` to dynamically parse configuration keys for versions and lobby identifiers. * Implement load balancing among lobbies based on the fewest players connected. * Remove redundant logging for lobby configuration load in `HubCommand.java` and `LobbyCommand.java`. * Change protocol version retrieval to use a library in `HubCommand.java` and `LobbyCommand.java`. * Update `VelocityPlugin.java` to dynamically parse the configuration keys for versions and lobby identifiers. * Implement configuration validation and add a fallback mechanism to handle unavailable or misconfigured lobbies in `VelocityPlugin.java`. * Add logging to the console to ensure the config is understood and ensure the configuration is logged only once in `VelocityPlugin.java`. * Import `java.util.Comparator` to fix the error in `VelocityPlugin.java`. * Update `config.yml` to add a useful comment to help users add lobbies. * Change the plugin version to 2.0.0 in `gradle.properties`. * Update `README.md` to reflect the new configuration format, commands logic, load balancing strategies, and configuration validation. --- .devcontainer/devcontainer.json | 3 +- README.md | 13 +- gradle.properties | 2 +- .../kmaba/vLobbyConnect/HubCommand.java | 116 +++++--- .../kmaba/vLobbyConnect/LobbyCommand.java | 119 +++++--- .../kmaba/vLobbyConnect/VelocityPlugin.java | 278 ++++++++++-------- src/main/resources/config.yml | 4 + 7 files changed, 328 insertions(+), 207 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b38d22d..4bc693e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,6 @@ { "tasks": { - "build": "./gradlew build" + "build": "./gradlew build", + "test": "sdk install java 17.0.2-open || sudo apt-get install openjdk-17-jdk" } } \ No newline at end of file diff --git a/README.md b/README.md index 229d36d..b12d355 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,7 @@ vLobbyConnect is a Velocity plugin that manages lobby connections for different ## Setup 1. Place the plugin jar in your Velocity plugins folder. -2. Ensure your backend servers have `online-mode=false`. -3. Configure your lobbies in two places: +2. Configure your lobbies in two places: ### Plugin Config (config.yml) This file is located in `src/main/resources/config.yml` (it will be copied to `plugins/vLobbyConnect/config.yml` on first run): @@ -18,6 +17,10 @@ lobbies: 1.20lobby2: "name2" 1.8lobby1: "name3" 1.8lobby2: "name4" + +# To add more lobbies, follow the pattern "VERSIONlobbyX" +# Example: +# 1.13lobby8: "name5" ``` ### Velocity Server Configuration (velocity.toml) @@ -37,6 +40,12 @@ try = [] # Fallback is empty - **/lobby**: Connects the player to the correct lobby based on their protocol version. - **/hub**: Transfers the player back to the designated hub/lobby. +## Load Balancing Strategies + +The plugin uses the following load balancing strategies to ensure fair distribution of players across available lobbies while considering the current load on each lobby: + +- **Fallback Mechanism:** If the configured lobbies are not available or misconfigured, the plugin implements a fallback mechanism to handle such cases. This can involve redirecting players to a default lobby or displaying a message indicating that the lobbies are currently unavailable. + Logs will provide further details if lobbies are full or misconfigured. Enjoy using vLobbyConnect! \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index bf9a686..5f45efd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ group = io.github.kmaba -version = 1.2.0 +version = 2.0.0 description = vLobbyConnect diff --git a/src/main/java/io/github/kmaba/vLobbyConnect/HubCommand.java b/src/main/java/io/github/kmaba/vLobbyConnect/HubCommand.java index b9062a0..c4a35e2 100644 --- a/src/main/java/io/github/kmaba/vLobbyConnect/HubCommand.java +++ b/src/main/java/io/github/kmaba/vLobbyConnect/HubCommand.java @@ -13,40 +13,54 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; -import java.util.Map; -import java.util.Optional; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class HubCommand implements SimpleCommand { private final ProxyServer server; private final Logger logger; - private final Map lobbies; + private final Map> versionLobbies = new HashMap<>(); @SuppressWarnings("unchecked") public HubCommand(ProxyServer server, Logger logger) { this.server = server; this.logger = logger; - Map loadedLobbies = null; try { - logger.info("Loading lobby configuration for HubCommand..."); Yaml yaml = new Yaml(); File configFile = new File("plugins/vLobbyConnect/config.yml"); if (!configFile.exists()) { configFile.getParentFile().mkdirs(); Files.copy(getClass().getResourceAsStream("/config.yml"), configFile.toPath()); - logger.info("Config file created from resource."); + // Removed config logging } Map config = yaml.load(Files.newInputStream(configFile.toPath())); - loadedLobbies = (Map) config.get("lobbies"); - if (loadedLobbies == null) { + Map lobbies = (Map) config.get("lobbies"); + if (lobbies == null) { logger.error("Failed to load valid lobby settings from config file."); } else { - logger.info("Lobby configuration loaded successfully."); + Pattern pattern = Pattern.compile("^(\\d+\\.\\d+)lobby(\\d+)$"); + for (Map.Entry entry : lobbies.entrySet()) { + Matcher matcher = pattern.matcher(entry.getKey()); + if (matcher.matches()) { + String version = matcher.group(1); + String lobbyName = entry.getValue(); + Optional serverOpt = server.getServer(lobbyName); + if (serverOpt.isPresent()) { + versionLobbies.computeIfAbsent(version, k -> new ArrayList<>()).add(serverOpt.get()); + // Removed config logging + } else { + // Removed config logging for missing lobby server + } + } else { + logger.warn("Invalid lobby configuration key: {}", entry.getKey()); + } + } } } catch (IOException e) { logger.error("Error loading config.yml", e); } - this.lobbies = loadedLobbies; } @Override @@ -61,39 +75,31 @@ public void execute(Invocation invocation) { } Player player = (Player) source; - int protocol = player.getProtocolVersion().getProtocol(); - RegisteredServer targetServer = null; - - if (protocol <= 47) { - String lobby1Name = lobbies.get("1.8lobby1"); - String lobby2Name = lobbies.get("1.8lobby2"); - Optional lobby1Opt = server.getServer(lobby1Name); - Optional lobby2Opt = server.getServer(lobby2Name); - if (lobby1Opt.isPresent() && lobby1Opt.get().getPlayersConnected().size() < 500) { - targetServer = lobby1Opt.get(); - } else if (lobby2Opt.isPresent() && lobby2Opt.get().getPlayersConnected().size() < 500) { - targetServer = lobby2Opt.get(); - } else { - player.sendMessage(Component.text("All 1.8 lobbies are full, please try again later.")); - return; - } - } else { - String lobby1Name = lobbies.get("1.20lobby1"); - String lobby2Name = lobbies.get("1.20lobby2"); - Optional lobby1Opt = server.getServer(lobby1Name); - Optional lobby2Opt = server.getServer(lobby2Name); - if (lobby1Opt.isPresent() && lobby1Opt.get().getPlayersConnected().size() < 500) { - targetServer = lobby1Opt.get(); - } else if (lobby2Opt.isPresent() && lobby2Opt.get().getPlayersConnected().size() < 500) { - targetServer = lobby2Opt.get(); - } else { - player.sendMessage(Component.text("All 1.20+ lobbies are full, please try again later.")); - return; - } + String version = player.getProtocolVersion().getName(); + List lobbies = versionLobbies.get(version); + if (lobbies == null || lobbies.isEmpty()) { + lobbies = getFallbackLobbies(version); + } + + if (lobbies == null || lobbies.isEmpty()) { + player.sendMessage(Component.text("No lobbies available for your Minecraft version.")); + logger.warn("No lobbies available for version {}", version); + return; } + RegisteredServer targetServer = getLeastLoadedLobby(lobbies); + + if (targetServer == null) { + player.sendMessage(Component.text("All lobbies are full, please try again later.")); + logger.warn("All lobbies are full for version {}", version); + return; + } + + // Instead of checking if current server equals target only, check if player's current server is any hub. if (player.getCurrentServer().isPresent() && - player.getCurrentServer().get().getServerInfo().getName().equals(targetServer.getServerInfo().getName())) { + lobbies.stream().anyMatch(s -> s.getServerInfo().getName().equals( + player.getCurrentServer().get().getServerInfo().getName() + ))) { player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize("&cYou are already in a lobby.")); return; } @@ -101,4 +107,34 @@ public void execute(Invocation invocation) { logger.info("Player {} connecting to lobby '{}'", player.getUsername(), targetServer.getServerInfo().getName()); player.createConnectionRequest(targetServer).fireAndForget(); } + + private RegisteredServer getLeastLoadedLobby(List lobbies) { + return lobbies.stream() + .min(Comparator.comparingInt(server -> server.getPlayersConnected().size())) + .orElse(null); + } + + // Helper: Fallback to the highest available lobby version when an exact match is missing. + private List getFallbackLobbies(String playerVersion) { + return versionLobbies.entrySet().stream() + .filter(entry -> compareVersions(entry.getKey(), playerVersion) <= 0) + .max((a, b) -> compareVersions(a.getKey(), b.getKey())) + .map(Map.Entry::getValue) + .orElse(null); + } + + // Helper: Compare version strings (e.g. "1.8" vs "1.21.1") + private int compareVersions(String v1, String v2) { + String[] parts1 = v1.split("\\."); + String[] parts2 = v2.split("\\."); + int len = Math.max(parts1.length, parts2.length); + for (int i = 0; i < len; i++) { + int num1 = i < parts1.length ? Integer.parseInt(parts1[i]) : 0; + int num2 = i < parts2.length ? Integer.parseInt(parts2[i]) : 0; + if (num1 != num2) { + return num1 - num2; + } + } + return 0; + } } diff --git a/src/main/java/io/github/kmaba/vLobbyConnect/LobbyCommand.java b/src/main/java/io/github/kmaba/vLobbyConnect/LobbyCommand.java index 41ed409..cce1ec3 100644 --- a/src/main/java/io/github/kmaba/vLobbyConnect/LobbyCommand.java +++ b/src/main/java/io/github/kmaba/vLobbyConnect/LobbyCommand.java @@ -13,40 +13,54 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; -import java.util.Map; -import java.util.Optional; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class LobbyCommand implements SimpleCommand { private final ProxyServer server; private final Logger logger; - private final Map lobbies; + private final Map> versionLobbies = new HashMap<>(); @SuppressWarnings("unchecked") public LobbyCommand(ProxyServer server, Logger logger) { this.server = server; this.logger = logger; - Map loadedLobbies = null; try { - logger.info("Loading lobby configuration for LobbyCommand..."); Yaml yaml = new Yaml(); File configFile = new File("plugins/vLobbyConnect/config.yml"); if (!configFile.exists()) { configFile.getParentFile().mkdirs(); Files.copy(getClass().getResourceAsStream("/config.yml"), configFile.toPath()); - logger.info("Config file created from resource."); + // Removed config logging } Map config = yaml.load(Files.newInputStream(configFile.toPath())); - loadedLobbies = (Map) config.get("lobbies"); - if (loadedLobbies == null) { - logger.error("Failed to load valid lobby settings."); + Map lobbies = (Map) config.get("lobbies"); + if (lobbies == null) { + logger.error("Failed to load valid lobby settings from config file."); } else { - logger.info("Lobby configuration loaded successfully."); + Pattern pattern = Pattern.compile("^(\\d+\\.\\d+)lobby(\\d+)$"); + for (Map.Entry entry : lobbies.entrySet()) { + Matcher matcher = pattern.matcher(entry.getKey()); + if (matcher.matches()) { + String version = matcher.group(1); + String lobbyName = entry.getValue(); + Optional serverOpt = server.getServer(lobbyName); + if (serverOpt.isPresent()) { + versionLobbies.computeIfAbsent(version, k -> new ArrayList<>()).add(serverOpt.get()); + // Removed config logging + } else { + // Removed config logging for missing lobby server + } + } else { + logger.warn("Invalid lobby configuration key: {}", entry.getKey()); + } + } } } catch (IOException e) { logger.error("Error loading config.yml", e); } - this.lobbies = loadedLobbies; } @Override @@ -61,39 +75,30 @@ public void execute(Invocation invocation) { } Player player = (Player) source; - int protocol = player.getProtocolVersion().getProtocol(); - RegisteredServer targetServer = null; - - if (protocol <= 47) { - String lobby1Name = lobbies.get("1.8lobby1"); - String lobby2Name = lobbies.get("1.8lobby2"); - Optional lobby1Opt = server.getServer(lobby1Name); - Optional lobby2Opt = server.getServer(lobby2Name); - if (lobby1Opt.isPresent() && lobby1Opt.get().getPlayersConnected().size() < 500) { - targetServer = lobby1Opt.get(); - } else if (lobby2Opt.isPresent() && lobby2Opt.get().getPlayersConnected().size() < 500) { - targetServer = lobby2Opt.get(); - } else { - player.sendMessage(Component.text("All 1.8 lobbies are full, please try again later.")); - return; - } - } else { - String lobby1Name = lobbies.get("1.20lobby1"); - String lobby2Name = lobbies.get("1.20lobby2"); - Optional lobby1Opt = server.getServer(lobby1Name); - Optional lobby2Opt = server.getServer(lobby2Name); - if (lobby1Opt.isPresent() && lobby1Opt.get().getPlayersConnected().size() < 500) { - targetServer = lobby1Opt.get(); - } else if (lobby2Opt.isPresent() && lobby2Opt.get().getPlayersConnected().size() < 500) { - targetServer = lobby2Opt.get(); - } else { - player.sendMessage(Component.text("All 1.20+ lobbies are full, please try again later.")); - return; - } + String version = player.getProtocolVersion().getName(); + List lobbies = versionLobbies.get(version); + if (lobbies == null || lobbies.isEmpty()) { + lobbies = getFallbackLobbies(version); + } + + if (lobbies == null || lobbies.isEmpty()) { + player.sendMessage(Component.text("No lobbies available for your Minecraft version.")); + logger.warn("No lobbies available for version {}", version); + return; } - if (player.getCurrentServer().isPresent() && - player.getCurrentServer().get().getServerInfo().getName().equals(targetServer.getServerInfo().getName())) { + RegisteredServer targetServer = getLeastLoadedLobby(lobbies); + + if (targetServer == null) { + player.sendMessage(Component.text("All lobbies are full, please try again later.")); + logger.warn("All lobbies are full for version {}", version); + return; + } + + // Updated "already in a lobby" check: + if (player.getCurrentServer().isPresent() && + lobbies.stream().anyMatch(s -> s.getServerInfo().getName() + .equals(player.getCurrentServer().get().getServerInfo().getName()))) { player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize("&cYou are already in a lobby.")); return; } @@ -101,4 +106,34 @@ public void execute(Invocation invocation) { logger.info("Player {} connecting to lobby '{}'", player.getUsername(), targetServer.getServerInfo().getName()); player.createConnectionRequest(targetServer).fireAndForget(); } + + private RegisteredServer getLeastLoadedLobby(List lobbies) { + return lobbies.stream() + .min(Comparator.comparingInt(server -> server.getPlayersConnected().size())) + .orElse(null); + } + + // Helper: Fallback to the highest available lobby version when an exact match is missing. + private List getFallbackLobbies(String playerVersion) { + return versionLobbies.entrySet().stream() + .filter(entry -> compareVersions(entry.getKey(), playerVersion) <= 0) + .max((a, b) -> compareVersions(a.getKey(), b.getKey())) + .map(Map.Entry::getValue) + .orElse(null); + } + + // Helper: Compare version strings (e.g. "1.8" vs "1.21.1") + private int compareVersions(String v1, String v2) { + String[] parts1 = v1.split("\\."); + String[] parts2 = v2.split("\\."); + int len = Math.max(parts1.length, parts2.length); + for (int i = 0; i < len; i++) { + int num1 = i < parts1.length ? Integer.parseInt(parts1[i]) : 0; + int num2 = i < parts2.length ? Integer.parseInt(parts2[i]) : 0; + if (num1 != num2) { + return num1 - num2; + } + } + return 0; + } } diff --git a/src/main/java/io/github/kmaba/vLobbyConnect/VelocityPlugin.java b/src/main/java/io/github/kmaba/vLobbyConnect/VelocityPlugin.java index d32568e..9455de8 100644 --- a/src/main/java/io/github/kmaba/vLobbyConnect/VelocityPlugin.java +++ b/src/main/java/io/github/kmaba/vLobbyConnect/VelocityPlugin.java @@ -22,6 +22,12 @@ import net.kyori.adventure.text.Component; import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.List; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Comparator; @Plugin( id = "vlobbyconnect", @@ -37,130 +43,160 @@ public final class VelocityPlugin { @Inject private com.velocitypowered.api.proxy.ProxyServer server; - private RegisteredServer lobby1; - private RegisteredServer lobby2; - private RegisteredServer lobby3; - private RegisteredServer lobby4; - + private final Map> versionLobbies = new HashMap<>(); private final Map connectionAttempts = new ConcurrentHashMap<>(); @Subscribe -public void onProxyInitialize(ProxyInitializeEvent event) { - try { - // Load the config.yml file - Yaml yaml = new Yaml(); - File configFile = new File("plugins/vLobbyConnect/config.yml"); - if (!configFile.exists()) { - configFile.getParentFile().mkdirs(); - Files.copy(getClass().getResourceAsStream("/config.yml"), configFile.toPath()); - } - - // Parse the config.yml file - Map config = yaml.load(Files.newInputStream(configFile.toPath())); - Map lobbies = (Map) config.get("lobbies"); - if (lobbies == null) { - logger.error("Failed to load lobby settings."); - return; - } - - // Retrieve the registered servers - String lobby1Name = lobbies.get("1.20lobby1"); - String lobby2Name = lobbies.get("1.20lobby2"); - String lobby3Name = lobbies.get("1.8lobby1"); - String lobby4Name = lobbies.get("1.8lobby2"); - - lobby1 = server.getServer(lobby1Name).orElse(null); - lobby2 = server.getServer(lobby2Name).orElse(null); - lobby3 = server.getServer(lobby3Name).orElse(null); - lobby4 = server.getServer(lobby4Name).orElse(null); - - // Check if all lobbies were retrieved successfully - if (lobby1 == null || lobby2 == null || lobby3 == null || lobby4 == null) { - logger.error("One or more lobbies were not found. Ensure they are defined in velocity.toml."); - } else { - logger.info("vLobbyConnect initialized successfully."); - } - } catch (IOException e) { - logger.error("Failed to load config.yml", e); - } - - // Register commands - server.getCommandManager().register("hub", new HubCommand(server, logger)); - server.getCommandManager().register("lobby", new LobbyCommand(server, logger)); -} + public void onProxyInitialize(ProxyInitializeEvent event) { + try { + // Load the config.yml file + Yaml yaml = new Yaml(); + File configFile = new File("plugins/vLobbyConnect/config.yml"); + if (!configFile.exists()) { + configFile.getParentFile().mkdirs(); + Files.copy(getClass().getResourceAsStream("/config.yml"), configFile.toPath()); + } + + // Parse the config.yml file + Map config = yaml.load(Files.newInputStream(configFile.toPath())); + Map lobbies = (Map) config.get("lobbies"); + if (lobbies == null) { + logger.error("Failed to load lobby settings."); + return; + } + + // Validate and log the configuration + Pattern pattern = Pattern.compile("^(\\d+\\.\\d+)lobby(\\d+)$"); + for (Map.Entry entry : lobbies.entrySet()) { + Matcher matcher = pattern.matcher(entry.getKey()); + if (matcher.matches()) { + String version = matcher.group(1); + String lobbyName = entry.getValue(); + Optional serverOpt = server.getServer(lobbyName); + if (serverOpt.isPresent()) { + versionLobbies.computeIfAbsent(version, k -> new ArrayList<>()).add(serverOpt.get()); + // Updated logging: Removed protocol version from the log + logger.info("Config Lobbies, [VERSION] {} Lobby number: {} IP: {}", version, matcher.group(2), serverOpt.get().getServerInfo().getAddress()); + } else { + logger.warn("Lobby server '{}' not found in Velocity configuration.", lobbyName); + } + } else { + logger.warn("Invalid lobby configuration key: {}", entry.getKey()); + } + } + + // Check if all lobbies were retrieved successfully + if (versionLobbies.isEmpty()) { + logger.error("No valid lobbies were found. Ensure they are defined in velocity.toml."); + } else { + logger.info("vLobbyConnect initialized successfully."); + } + } catch (IOException e) { + logger.error("Failed to load config.yml", e); + } + + // Register commands + server.getCommandManager().register("hub", new HubCommand(server, logger)); + server.getCommandManager().register("lobby", new LobbyCommand(server, logger)); + } @Subscribe(order = PostOrder.FIRST) - void onPlayerJoin(final PlayerChooseInitialServerEvent event) { - Player player = event.getPlayer(); - UUID uuid = player.getUniqueId(); - int attempts = connectionAttempts.getOrDefault(uuid, 0) + 1; - connectionAttempts.put(uuid, attempts); - - int protocolVersion = player.getProtocolVersion().getProtocol(); - if (protocolVersion <= 47) { - // 1.8 users → lobby3 or lobby4 - if (lobby3 != null && lobby3.getPlayersConnected().size() < 500) { - event.setInitialServer(lobby3); - } else if (lobby4 != null && lobby4.getPlayersConnected().size() < 500) { - event.setInitialServer(lobby4); - } else { - logger.warn("All 1.8 lobbies are full for player {}", player.getUsername()); - player.sendMessage(Component.text("All 1.8 lobbies are full, please try again later.")); - player.disconnect(Component.text("All lobbies are full.")); - } - } else { - // 1.20+ users → lobby1 or lobby2 - if (lobby1 != null && lobby1.getPlayersConnected().size() < 500) { - event.setInitialServer(lobby1); - } else if (lobby2 != null && lobby2.getPlayersConnected().size() < 500) { - event.setInitialServer(lobby2); - } else { - logger.warn("All 1.20+ lobbies are full for player {}", player.getUsername()); - player.sendMessage(Component.text("All 1.20+ lobbies are full, please try again later.")); - player.disconnect(Component.text("All lobbies are full.")); - } - } - } - - @Subscribe - public void onServerKick(com.velocitypowered.api.event.player.KickedFromServerEvent event) { - Player player = event.getPlayer(); - RegisteredServer kickedServer = event.getServer(); - String serverName = kickedServer.getServerInfo().getName(); - - // If the kicked server is already a lobby, do nothing. - if ((lobby1 != null && serverName.equals(lobby1.getServerInfo().getName())) || - (lobby2 != null && serverName.equals(lobby2.getServerInfo().getName())) || - (lobby3 != null && serverName.equals(lobby3.getServerInfo().getName())) || - (lobby4 != null && serverName.equals(lobby4.getServerInfo().getName()))) { - return; - } - - RegisteredServer fallback = null; - int protocolVersion = player.getProtocolVersion().getProtocol(); - if (protocolVersion <= 47) { - if (lobby3 != null && lobby3.getPlayersConnected().size() < 500) { - fallback = lobby3; - } else if (lobby4 != null && lobby4.getPlayersConnected().size() < 500) { - fallback = lobby4; - } - } else { - if (lobby1 != null && lobby1.getPlayersConnected().size() < 500) { - fallback = lobby1; - } else if (lobby2 != null && lobby2.getPlayersConnected().size() < 500) { - fallback = lobby2; - } - } - - if (fallback != null) { - event.setResult(com.velocitypowered.api.event.player.KickedFromServerEvent.RedirectPlayer.create(fallback)); - } - } - - @Subscribe - public void onPlayerDisconnect(Player player) { - UUID uuid = player.getUniqueId(); - connectionAttempts.remove(uuid); - logger.info("Player {} disconnected.", player.getUsername()); - } + void onPlayerJoin(final PlayerChooseInitialServerEvent event) { + Player player = event.getPlayer(); + UUID uuid = player.getUniqueId(); + int attempts = connectionAttempts.getOrDefault(uuid, 0) + 1; + connectionAttempts.put(uuid, attempts); + + String version = player.getProtocolVersion().getName(); + List lobbies = versionLobbies.get(version); + // Fallback if no exact match exists: + if (lobbies == null || lobbies.isEmpty()) { + lobbies = getFallbackLobbies(version); + } + + if (lobbies == null || lobbies.isEmpty()) { + player.sendMessage(Component.text("No lobbies available for your Minecraft version.")); + logger.warn("No lobbies available for version {}", version); + return; + } + + RegisteredServer targetServer = getLeastLoadedLobby(lobbies); + + if (targetServer == null) { + player.sendMessage(Component.text("All lobbies are full, please try again later.")); + logger.warn("All lobbies are full for version {}", version); + return; + } + + if (player.getCurrentServer().isPresent() && + player.getCurrentServer().get().getServerInfo().getName().equals(targetServer.getServerInfo().getName())) { + player.sendMessage(Component.text("You are already in a lobby.")); + return; + } + + logger.info("Player {} connecting to lobby '{}'", player.getUsername(), targetServer.getServerInfo().getName()); + // Instead of a connection request, set the initial server directly: + event.setInitialServer(targetServer); + } + + private RegisteredServer getLeastLoadedLobby(List lobbies) { + return lobbies.stream() + .min(Comparator.comparingInt(server -> server.getPlayersConnected().size())) + .orElse(null); + } + + // Helper: Fallback to the highest available lobby version when an exact match is missing. + private List getFallbackLobbies(String playerVersion) { + return versionLobbies.entrySet().stream() + .filter(entry -> compareVersions(entry.getKey(), playerVersion) <= 0) + .max((a, b) -> compareVersions(a.getKey(), b.getKey())) + .map(Map.Entry::getValue) + .orElse(null); + } + + // Helper: Compare version strings (e.g. "1.8" vs "1.21.1") + private int compareVersions(String v1, String v2) { + String[] parts1 = v1.split("\\."); + String[] parts2 = v2.split("\\."); + int len = Math.max(parts1.length, parts2.length); + for (int i = 0; i < len; i++) { + int num1 = i < parts1.length ? Integer.parseInt(parts1[i]) : 0; + int num2 = i < parts2.length ? Integer.parseInt(parts2[i]) : 0; + if (num1 != num2) { + return num1 - num2; + } + } + return 0; + } + + @Subscribe + public void onServerKick(com.velocitypowered.api.event.player.KickedFromServerEvent event) { + Player player = event.getPlayer(); + RegisteredServer kickedServer = event.getServer(); + String serverName = kickedServer.getServerInfo().getName(); + + // If the kicked server is already a lobby, do nothing. + if (versionLobbies.values().stream().flatMap(List::stream).anyMatch(server -> server.getServerInfo().getName().equals(serverName))) { + return; + } + + RegisteredServer fallback = null; + String version = player.getProtocolVersion().getName(); + List lobbies = versionLobbies.get(version); + + if (lobbies != null && !lobbies.isEmpty()) { + fallback = getLeastLoadedLobby(lobbies); + } + + if (fallback != null) { + event.setResult(com.velocitypowered.api.event.player.KickedFromServerEvent.RedirectPlayer.create(fallback)); + } + } + + @Subscribe + public void onPlayerDisconnect(Player player) { + UUID uuid = player.getUniqueId(); + connectionAttempts.remove(uuid); + logger.info("Player {} disconnected.", player.getUsername()); + } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 76f80af..ec77d97 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -3,3 +3,7 @@ lobbies: 1.20lobby2: "lobby2" 1.8lobby1: "lobby3" 1.8lobby2: "lobby4" + +# To add more lobbies, follow the pattern "VERSIONlobbyX" +# Example: +# 1.13lobby8: "lobby5"