diff --git a/bungeecord/src/main/java/me/william278/husksync/HuskSyncBungeeCord.java b/bungeecord/src/main/java/me/william278/husksync/HuskSyncBungeeCord.java index 8f6f2f50..9aa7e1bc 100644 --- a/bungeecord/src/main/java/me/william278/husksync/HuskSyncBungeeCord.java +++ b/bungeecord/src/main/java/me/william278/husksync/HuskSyncBungeeCord.java @@ -1,12 +1,12 @@ package me.william278.husksync; -import me.william278.husksync.bungeecord.command.HuskSyncCommand; +import me.william278.husksync.bungeecord.command.BungeeCommand; import me.william278.husksync.bungeecord.config.ConfigLoader; import me.william278.husksync.bungeecord.config.ConfigManager; import me.william278.husksync.proxy.data.DataManager; import me.william278.husksync.bungeecord.listener.BungeeEventListener; import me.william278.husksync.bungeecord.listener.BungeeRedisListener; -import me.william278.husksync.bungeecord.migrator.MPDBMigrator; +import me.william278.husksync.migrator.MPDBMigrator; import me.william278.husksync.bungeecord.util.BungeeLogger; import me.william278.husksync.bungeecord.util.BungeeUpdateChecker; import me.william278.husksync.redis.RedisMessage; @@ -104,10 +104,10 @@ public final class HuskSyncBungeeCord extends Plugin { getProxy().getPluginManager().registerListener(this, new BungeeEventListener()); // Register command - getProxy().getPluginManager().registerCommand(this, new HuskSyncCommand()); + getProxy().getPluginManager().registerCommand(this, new BungeeCommand()); // Prepare the migrator for use if needed - mpdbMigrator = new MPDBMigrator(); + mpdbMigrator = new MPDBMigrator(getBungeeLogger()); // Initialize bStats metrics try { diff --git a/bungeecord/src/main/java/me/william278/husksync/bungeecord/command/HuskSyncCommand.java b/bungeecord/src/main/java/me/william278/husksync/bungeecord/command/BungeeCommand.java similarity index 88% rename from bungeecord/src/main/java/me/william278/husksync/bungeecord/command/HuskSyncCommand.java rename to bungeecord/src/main/java/me/william278/husksync/bungeecord/command/BungeeCommand.java index 8fc3e823..63dcce89 100644 --- a/bungeecord/src/main/java/me/william278/husksync/bungeecord/command/HuskSyncCommand.java +++ b/bungeecord/src/main/java/me/william278/husksync/bungeecord/command/BungeeCommand.java @@ -4,12 +4,13 @@ import de.themoep.minedown.MineDown; import me.william278.husksync.HuskSyncBungeeCord; import me.william278.husksync.Server; import me.william278.husksync.bungeecord.util.BungeeUpdateChecker; +import me.william278.husksync.proxy.command.HuskSyncCommand; import me.william278.husksync.util.MessageManager; import me.william278.husksync.PlayerData; import me.william278.husksync.Settings; import me.william278.husksync.bungeecord.config.ConfigLoader; import me.william278.husksync.bungeecord.config.ConfigManager; -import me.william278.husksync.bungeecord.migrator.MPDBMigrator; +import me.william278.husksync.migrator.MPDBMigrator; import me.william278.husksync.redis.RedisMessage; import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.ProxyServer; @@ -25,17 +26,11 @@ import java.util.Objects; import java.util.logging.Level; import java.util.stream.Collectors; -public class HuskSyncCommand extends Command implements TabExecutor { +public class BungeeCommand extends Command implements TabExecutor, HuskSyncCommand { private final static HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance(); - private final static SubCommand[] SUB_COMMANDS = {new SubCommand("about", null), - new SubCommand("status", "husksync.command.admin"), - new SubCommand("reload", "husksync.command.admin"), - new SubCommand("update", "husksync.command.admin"), - new SubCommand("invsee", "husksync.command.inventory"), - new SubCommand("echest", "husksync.command.ender_chest")}; - public HuskSyncCommand() { + public BungeeCommand() { super("husksync", null, "hs"); } @@ -195,6 +190,7 @@ public class HuskSyncCommand extends Command implements TabExecutor { // Database migration wizard if (args.length >= 1) { if (args[0].equalsIgnoreCase("migrate")) { + MPDBMigrator migrator = HuskSyncBungeeCord.mpdbMigrator; if (args.length == 1) { sender.sendMessage(new MineDown( """ @@ -210,7 +206,7 @@ public class HuskSyncCommand extends Command implements TabExecutor { Other non-vital data, such as current health, hunger & potion effects will not be migrated to ensure that migration does not take an excessive amount of time. - + To do this, you need to have MySqlPlayerDataBridge and HuskSync installed on one Spigot server as well as HuskSync installed on the proxy (which you have) @@ -234,7 +230,7 @@ public class HuskSyncCommand extends Command implements TabExecutor { sourceInventoryTableName: %6% sourceEnderChestTableName: %7% sourceExperienceTableName: %8% - + targetCluster: %9% To change a setting, type: @@ -256,37 +252,37 @@ public class HuskSyncCommand extends Command implements TabExecutor { data from the source MySQLPlayerDataBridge database. >When done, type: husksync migrate start""" - .replaceAll("%1%", MPDBMigrator.migrationSettings.sourceHost) - .replaceAll("%2%", String.valueOf(MPDBMigrator.migrationSettings.sourcePort)) - .replaceAll("%3%", MPDBMigrator.migrationSettings.sourceDatabase) - .replaceAll("%4%", MPDBMigrator.migrationSettings.sourceUsername) - .replaceAll("%5%", MPDBMigrator.migrationSettings.sourcePassword) - .replaceAll("%6%", MPDBMigrator.migrationSettings.inventoryDataTable) - .replaceAll("%7%", MPDBMigrator.migrationSettings.enderChestDataTable) - .replaceAll("%8%", MPDBMigrator.migrationSettings.expDataTable) - .replaceAll("%9%", MPDBMigrator.migrationSettings.targetCluster) + .replaceAll("%1%", migrator.migrationSettings.sourceHost) + .replaceAll("%2%", String.valueOf(migrator.migrationSettings.sourcePort)) + .replaceAll("%3%", migrator.migrationSettings.sourceDatabase) + .replaceAll("%4%", migrator.migrationSettings.sourceUsername) + .replaceAll("%5%", migrator.migrationSettings.sourcePassword) + .replaceAll("%6%", migrator.migrationSettings.inventoryDataTable) + .replaceAll("%7%", migrator.migrationSettings.enderChestDataTable) + .replaceAll("%8%", migrator.migrationSettings.expDataTable) + .replaceAll("%9%", migrator.migrationSettings.targetCluster) .replaceAll("%10%", Settings.dataStorageType.toString()) ).toComponent()); case "setting" -> { if (args.length == 4) { String value = args[3]; switch (args[2]) { - case "sourceHost", "host" -> MPDBMigrator.migrationSettings.sourceHost = value; + case "sourceHost", "host" -> migrator.migrationSettings.sourceHost = value; case "sourcePort", "port" -> { try { - MPDBMigrator.migrationSettings.sourcePort = Integer.parseInt(value); + migrator.migrationSettings.sourcePort = Integer.parseInt(value); } catch (NumberFormatException e) { sender.sendMessage(new MineDown("Error: Invalid value; port must be a number").toComponent()); return; } } - case "sourceDatabase", "database" -> MPDBMigrator.migrationSettings.sourceDatabase = value; - case "sourceUsername", "username" -> MPDBMigrator.migrationSettings.sourceUsername = value; - case "sourcePassword", "password" -> MPDBMigrator.migrationSettings.sourcePassword = value; - case "sourceInventoryTableName", "inventoryTableName", "inventoryTable" -> MPDBMigrator.migrationSettings.inventoryDataTable = value; - case "sourceEnderChestTableName", "enderChestTableName", "enderChestTable" -> MPDBMigrator.migrationSettings.enderChestDataTable = value; - case "sourceExperienceTableName", "experienceTableName", "experienceTable" -> MPDBMigrator.migrationSettings.expDataTable = value; - case "targetCluster", "cluster" -> MPDBMigrator.migrationSettings.targetCluster = value; + case "sourceDatabase", "database" -> migrator.migrationSettings.sourceDatabase = value; + case "sourceUsername", "username" -> migrator.migrationSettings.sourceUsername = value; + case "sourcePassword", "password" -> migrator.migrationSettings.sourcePassword = value; + case "sourceInventoryTableName", "inventoryTableName", "inventoryTable" -> migrator.migrationSettings.inventoryDataTable = value; + case "sourceEnderChestTableName", "enderChestTableName", "enderChestTable" -> migrator.migrationSettings.enderChestDataTable = value; + case "sourceExperienceTableName", "experienceTableName", "experienceTable" -> migrator.migrationSettings.expDataTable = value; + case "targetCluster", "cluster" -> migrator.migrationSettings.targetCluster = value; default -> { sender.sendMessage(new MineDown("Error: Invalid setting; please use \"husksync migrate setup\" to view a list").toComponent()); return; @@ -299,7 +295,14 @@ public class HuskSyncCommand extends Command implements TabExecutor { } case "start" -> { sender.sendMessage(new MineDown("Starting MySQLPlayerDataBridge migration!...").toComponent()); - HuskSyncBungeeCord.mpdbMigrator.start(); + + // If the migrator is ready, execute the migration asynchronously + if (HuskSyncBungeeCord.mpdbMigrator.readyToMigrate(ProxyServer.getInstance().getOnlineCount(), + HuskSyncBungeeCord.synchronisedServers)) { + ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> + HuskSyncBungeeCord.mpdbMigrator.executeMigrationOperations(HuskSyncBungeeCord.dataManager, + HuskSyncBungeeCord.synchronisedServers)); + } } default -> sender.sendMessage(new MineDown("Error: Invalid argument for migration. Use \"husksync migrate\" to start the process").toComponent()); } @@ -346,7 +349,7 @@ public class HuskSyncCommand extends Command implements TabExecutor { } // View the ender chest of a player specified by their name - private void openEnderChest(ProxiedPlayer viewer, String targetPlayerName, String clusterId) { + public void openEnderChest(ProxiedPlayer viewer, String targetPlayerName, String clusterId) { if (viewer.getName().equalsIgnoreCase(targetPlayerName)) { viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_ender_chest")).toComponent()); return; @@ -401,9 +404,12 @@ public class HuskSyncCommand extends Command implements TabExecutor { if (args.length == 1) { final ArrayList subCommands = new ArrayList<>(); for (SubCommand subCommand : SUB_COMMANDS) { - if (subCommand.doesPlayerHavePermission(player)) { - subCommands.add(subCommand.command()); + if (subCommand.permission() != null) { + if (!player.hasPermission(subCommand.permission())) { + continue; + } } + subCommands.add(subCommand.command()); } // Automatically filter the sub commands' order in tab completion by what the player has typed return subCommands.stream().filter(val -> val.startsWith(args[0])) @@ -415,19 +421,4 @@ public class HuskSyncCommand extends Command implements TabExecutor { return Collections.emptyList(); } - /** - * A sub command, that may require a permission - */ - public record SubCommand(String command, String permission) { - /** - * Returns if the player can use the sub command - * - * @param player The {@link ProxiedPlayer} to check - * @return {@code true} if the player can use the sub command; {@code false} otherwise - */ - public boolean doesPlayerHavePermission(ProxiedPlayer player) { - return permission == null || player.hasPermission(permission); - } - } - } diff --git a/bungeecord/src/main/java/me/william278/husksync/bungeecord/listener/BungeeRedisListener.java b/bungeecord/src/main/java/me/william278/husksync/bungeecord/listener/BungeeRedisListener.java index 1e47ac96..270e3ce7 100644 --- a/bungeecord/src/main/java/me/william278/husksync/bungeecord/listener/BungeeRedisListener.java +++ b/bungeecord/src/main/java/me/william278/husksync/bungeecord/listener/BungeeRedisListener.java @@ -6,7 +6,7 @@ import me.william278.husksync.Server; import me.william278.husksync.util.MessageManager; import me.william278.husksync.PlayerData; import me.william278.husksync.Settings; -import me.william278.husksync.bungeecord.migrator.MPDBMigrator; +import me.william278.husksync.migrator.MPDBMigrator; import me.william278.husksync.redis.RedisListener; import me.william278.husksync.redis.RedisMessage; import net.md_5.bungee.api.ChatMessageType; @@ -181,16 +181,20 @@ public class BungeeRedisListener extends RedisListener { return; } + // Get the migrator + MPDBMigrator migrator = HuskSyncBungeeCord.mpdbMigrator; + // Add the incoming data to the data to be saved - MPDBMigrator.incomingPlayerData.put(playerData, playerName); + migrator.incomingPlayerData.put(playerData, playerName); // Increment players migrated - MPDBMigrator.playersMigrated++; - plugin.getBungeeLogger().log(Level.INFO, "Migrated " + MPDBMigrator.playersMigrated + "/" + MPDBMigrator.migratedDataSent + " players."); + migrator.playersMigrated++; + plugin.getBungeeLogger().log(Level.INFO, "Migrated " + migrator.playersMigrated + "/" + migrator.migratedDataSent + " players."); // When all the data has been received, save it - if (MPDBMigrator.migratedDataSent == MPDBMigrator.playersMigrated) { - MPDBMigrator.loadIncomingData(MPDBMigrator.incomingPlayerData); + if (migrator.migratedDataSent == migrator.playersMigrated) { + ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> migrator.loadIncomingData(migrator.incomingPlayerData, + HuskSyncBungeeCord.dataManager)); } } } diff --git a/bungeecord/src/main/java/me/william278/husksync/bungeecord/migrator/MPDBMigrator.java b/common/src/main/java/me/william278/husksync/migrator/MPDBMigrator.java similarity index 56% rename from bungeecord/src/main/java/me/william278/husksync/bungeecord/migrator/MPDBMigrator.java rename to common/src/main/java/me/william278/husksync/migrator/MPDBMigrator.java index 2f355d8e..6c6b2b79 100644 --- a/bungeecord/src/main/java/me/william278/husksync/bungeecord/migrator/MPDBMigrator.java +++ b/common/src/main/java/me/william278/husksync/migrator/MPDBMigrator.java @@ -1,14 +1,13 @@ -package me.william278.husksync.bungeecord.migrator; +package me.william278.husksync.migrator; -import me.william278.husksync.HuskSyncBungeeCord; import me.william278.husksync.PlayerData; import me.william278.husksync.Server; import me.william278.husksync.Settings; -import me.william278.husksync.migrator.MPDBPlayerData; +import me.william278.husksync.proxy.data.DataManager; import me.william278.husksync.proxy.data.sql.Database; import me.william278.husksync.proxy.data.sql.MySQL; import me.william278.husksync.redis.RedisMessage; -import net.md_5.bungee.api.ProxyServer; +import me.william278.husksync.util.Logger; import java.io.IOException; import java.sql.Connection; @@ -28,36 +27,40 @@ import java.util.logging.Level; */ public class MPDBMigrator { - public static int migratedDataSent = 0; - public static int playersMigrated = 0; + public int migratedDataSent = 0; + public int playersMigrated = 0; - private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance(); + public HashMap incomingPlayerData; - public static HashMap incomingPlayerData; + public MigrationSettings migrationSettings = new MigrationSettings(); + private Settings.SynchronisationCluster targetCluster; + private Database sourceDatabase; - public static MigrationSettings migrationSettings = new MigrationSettings(); - private static Settings.SynchronisationCluster targetCluster; - private static Database sourceDatabase; + private HashSet mpdbPlayerData; - private static HashSet mpdbPlayerData; + private final Logger logger; - public void start() { - if (ProxyServer.getInstance().getPlayers().size() > 0) { - plugin.getBungeeLogger().log(Level.WARNING, "Failed to start migration because there are players online. " + + public MPDBMigrator(Logger logger) { + this.logger = logger; + } + + public boolean readyToMigrate(int networkPlayerCount, HashSet synchronisedServers) { + if (networkPlayerCount > 0) { + logger.log(Level.WARNING, "Failed to start migration because there are players online. " + "Your network has to be empty to migrate data for safety reasons."); - return; + return false; } int synchronisedServersWithMpdb = 0; - for (Server server : HuskSyncBungeeCord.synchronisedServers) { + for (Server server : synchronisedServers) { if (server.hasMySqlPlayerDataBridge()) { synchronisedServersWithMpdb++; } } if (synchronisedServersWithMpdb < 1) { - plugin.getBungeeLogger().log(Level.WARNING, "Failed to start migration because at least one Spigot server with both HuskSync and MySqlPlayerDataBridge installed is not online. " + + logger.log(Level.WARNING, "Failed to start migration because at least one Spigot server with both HuskSync and MySqlPlayerDataBridge installed is not online. " + "Please start one Spigot server with HuskSync installed to begin migration."); - return; + return false; } for (Settings.SynchronisationCluster cluster : Settings.clusters) { @@ -67,9 +70,9 @@ public class MPDBMigrator { } } if (targetCluster == null) { - plugin.getBungeeLogger().log(Level.WARNING, "Failed to start migration because the target cluster could not be found. " + - "Please ensure the target cluster is correct, configured in the proxy config file, then try again"); - return; + logger.log(Level.WARNING, "Failed to start migration because the target cluster could not be found. " + + "Please ensure the target cluster is correct, configured in the proxy config file, then try again"); + return false; } migratedDataSent = 0; @@ -79,32 +82,40 @@ public class MPDBMigrator { final MigrationSettings settings = migrationSettings; // Get connection to source database - sourceDatabase = new MigratorMySQL(plugin, settings.sourceHost, settings.sourcePort, + sourceDatabase = new MigratorMySQL(logger, settings.sourceHost, settings.sourcePort, settings.sourceDatabase, settings.sourceUsername, settings.sourcePassword, targetCluster); sourceDatabase.load(); if (sourceDatabase.isInactive()) { - plugin.getBungeeLogger().log(Level.WARNING, "Failed to establish connection to the origin MySQL database. " + + logger.log(Level.WARNING, "Failed to establish connection to the origin MySQL database. " + "Please check you have input the correct connection details and try again."); - return; + return false; } - ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> { - prepareTargetDatabase(); + return true; + } - getInventoryData(); + // Carry out the migration + public void executeMigrationOperations(DataManager dataManager, HashSet synchronisedServers) { + // Prepare the target database for insertion + prepareTargetDatabase(dataManager); - getEnderChestData(); + // Fetch inventory data from MPDB + getInventoryData(); - getExperienceData(); + // Fetch ender chest data from MPDB + getEnderChestData(); - sendEncodedData(); - }); + // Fetch experience data from MPDB + getExperienceData(); + + // Send the encoded data to the Bukkit servers for conversion + sendEncodedData(synchronisedServers); } // Clear the new database out of current data - private void prepareTargetDatabase() { - plugin.getBungeeLogger().log(Level.INFO, "Preparing target database..."); - try (Connection connection = HuskSyncBungeeCord.dataManager.getConnection(targetCluster.clusterId())) { + private void prepareTargetDatabase(DataManager dataManager) { + logger.log(Level.INFO, "Preparing target database..."); + try (Connection connection = dataManager.getConnection(targetCluster.clusterId())) { try (PreparedStatement statement = connection.prepareStatement("DELETE FROM " + targetCluster.playerTableName() + ";")) { statement.executeUpdate(); } @@ -112,14 +123,14 @@ public class MPDBMigrator { statement.executeUpdate(); } } catch (SQLException e) { - plugin.getBungeeLogger().log(Level.SEVERE, "An exception occurred preparing the target database", e); + logger.log(Level.SEVERE, "An exception occurred preparing the target database", e); } finally { - plugin.getBungeeLogger().log(Level.INFO, "Finished preparing target database!"); + logger.log(Level.INFO, "Finished preparing target database!"); } } private void getInventoryData() { - plugin.getBungeeLogger().log(Level.INFO, "Getting inventory data from MySQLPlayerDataBridge..."); + logger.log(Level.INFO, "Getting inventory data from MySQLPlayerDataBridge..."); try (Connection connection = sourceDatabase.getConnection()) { try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM " + migrationSettings.inventoryDataTable + ";")) { ResultSet resultSet = statement.executeQuery(); @@ -135,14 +146,14 @@ public class MPDBMigrator { } } } catch (SQLException e) { - plugin.getBungeeLogger().log(Level.SEVERE, "An exception occurred getting inventory data", e); + logger.log(Level.SEVERE, "An exception occurred getting inventory data", e); } finally { - plugin.getBungeeLogger().log(Level.INFO, "Finished getting inventory data from MySQLPlayerDataBridge"); + logger.log(Level.INFO, "Finished getting inventory data from MySQLPlayerDataBridge"); } } private void getEnderChestData() { - plugin.getBungeeLogger().log(Level.INFO, "Getting ender chest data from MySQLPlayerDataBridge..."); + logger.log(Level.INFO, "Getting ender chest data from MySQLPlayerDataBridge..."); try (Connection connection = sourceDatabase.getConnection()) { try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM " + migrationSettings.enderChestDataTable + ";")) { ResultSet resultSet = statement.executeQuery(); @@ -158,14 +169,14 @@ public class MPDBMigrator { } } } catch (SQLException e) { - plugin.getBungeeLogger().log(Level.SEVERE, "An exception occurred getting ender chest data", e); + logger.log(Level.SEVERE, "An exception occurred getting ender chest data", e); } finally { - plugin.getBungeeLogger().log(Level.INFO, "Finished getting ender chest data from MySQLPlayerDataBridge"); + logger.log(Level.INFO, "Finished getting ender chest data from MySQLPlayerDataBridge"); } } private void getExperienceData() { - plugin.getBungeeLogger().log(Level.INFO, "Getting experience data from MySQLPlayerDataBridge..."); + logger.log(Level.INFO, "Getting experience data from MySQLPlayerDataBridge..."); try (Connection connection = sourceDatabase.getConnection()) { try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM " + migrationSettings.expDataTable + ";")) { ResultSet resultSet = statement.executeQuery(); @@ -183,14 +194,14 @@ public class MPDBMigrator { } } } catch (SQLException e) { - plugin.getBungeeLogger().log(Level.SEVERE, "An exception occurred getting experience data", e); + logger.log(Level.SEVERE, "An exception occurred getting experience data", e); } finally { - plugin.getBungeeLogger().log(Level.INFO, "Finished getting experience data from MySQLPlayerDataBridge"); + logger.log(Level.INFO, "Finished getting experience data from MySQLPlayerDataBridge"); } } - private void sendEncodedData() { - for (Server processingServer : HuskSyncBungeeCord.synchronisedServers) { + private void sendEncodedData(HashSet synchronisedServers) { + for (Server processingServer : synchronisedServers) { if (processingServer.hasMySqlPlayerDataBridge()) { for (MPDBPlayerData data : mpdbPlayerData) { try { @@ -201,10 +212,10 @@ public class MPDBMigrator { .send(); migratedDataSent++; } catch (IOException e) { - plugin.getBungeeLogger().log(Level.SEVERE, "Failed to serialize encoded MPDB data", e); + logger.log(Level.SEVERE, "Failed to serialize encoded MPDB data", e); } } - plugin.getBungeeLogger().log(Level.INFO, "Finished dispatching encoded data for " + migratedDataSent + " players; please wait for conversion to finish"); + logger.log(Level.INFO, "Finished dispatching encoded data for " + migratedDataSent + " players; please wait for conversion to finish"); } return; } @@ -215,41 +226,39 @@ public class MPDBMigrator { * * @param dataToLoad HashMap of the {@link PlayerData} to player Usernames that will be loaded */ - public static void loadIncomingData(HashMap dataToLoad) { - ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> { - int playersSaved = 0; - plugin.getBungeeLogger().log(Level.INFO, "Saving data for " + playersMigrated + " players..."); - - for (PlayerData playerData : dataToLoad.keySet()) { - String playerName = dataToLoad.get(playerData); + public void loadIncomingData(HashMap dataToLoad, DataManager dataManager) { + int playersSaved = 0; + logger.log(Level.INFO, "Saving data for " + playersMigrated + " players..."); - // Add the player to the MySQL table - HuskSyncBungeeCord.dataManager.ensurePlayerExists(playerData.getPlayerUUID(), playerName); + for (PlayerData playerData : dataToLoad.keySet()) { + String playerName = dataToLoad.get(playerData); - // Update the data in the cache and SQL - for (Settings.SynchronisationCluster cluster : Settings.clusters) { - HuskSyncBungeeCord.dataManager.updatePlayerData(playerData, cluster); - break; - } + // Add the player to the MySQL table + dataManager.ensurePlayerExists(playerData.getPlayerUUID(), playerName); - playersSaved++; - plugin.getBungeeLogger().log(Level.INFO, "Saved data for " + playersSaved + "/" + playersMigrated + " players"); + // Update the data in the cache and SQL + for (Settings.SynchronisationCluster cluster : Settings.clusters) { + dataManager.updatePlayerData(playerData, cluster); + break; } - // Mark as done when done - plugin.getBungeeLogger().log(Level.INFO, """ - === MySQLPlayerDataBridge Migration Wizard ========== - - Migration complete! - - Successfully migrated data for %1%/%2% players. - - You should now uninstall MySQLPlayerDataBridge from - the rest of the Spigot servers, then restart them. - """.replaceAll("%1%", Integer.toString(MPDBMigrator.playersMigrated)) - .replaceAll("%2%", Integer.toString(MPDBMigrator.migratedDataSent))); - sourceDatabase.close(); // Close source database - }); + playersSaved++; + logger.log(Level.INFO, "Saved data for " + playersSaved + "/" + playersMigrated + " players"); + } + + // Mark as done when done + logger.log(Level.INFO, """ + === MySQLPlayerDataBridge Migration Wizard ========== + + Migration complete! + + Successfully migrated data for %1%/%2% players. + + You should now uninstall MySQLPlayerDataBridge from + the rest of the Spigot servers, then restart them. + """.replaceAll("%1%", Integer.toString(playersMigrated)) + .replaceAll("%2%", Integer.toString(migratedDataSent))); + sourceDatabase.close(); // Close source database } /** @@ -287,8 +296,8 @@ public class MPDBMigrator { * MySQL class used for importing data from MPDB */ public static class MigratorMySQL extends MySQL { - public MigratorMySQL(HuskSyncBungeeCord instance, String host, int port, String database, String username, String password, Settings.SynchronisationCluster cluster) { - super(cluster, instance.getBungeeLogger()); + public MigratorMySQL(Logger logger, String host, int port, String database, String username, String password, Settings.SynchronisationCluster cluster) { + super(cluster, logger); super.host = host; super.port = port; super.database = database; diff --git a/common/src/main/java/me/william278/husksync/proxy/command/HuskSyncCommand.java b/common/src/main/java/me/william278/husksync/proxy/command/HuskSyncCommand.java new file mode 100644 index 00000000..bb62d9d0 --- /dev/null +++ b/common/src/main/java/me/william278/husksync/proxy/command/HuskSyncCommand.java @@ -0,0 +1,17 @@ +package me.william278.husksync.proxy.command; + +public interface HuskSyncCommand { + + SubCommand[] SUB_COMMANDS = {new SubCommand("about", null), + new SubCommand("status", "husksync.command.admin"), + new SubCommand("reload", "husksync.command.admin"), + new SubCommand("update", "husksync.command.admin"), + new SubCommand("invsee", "husksync.command.inventory"), + new SubCommand("echest", "husksync.command.ender_chest")}; + + /** + * A sub command, that may require a permission + */ + record SubCommand(String command, String permission) { } + +} diff --git a/velocity/src/main/java/me/william278/husksync/HuskSyncVelocity.java b/velocity/src/main/java/me/william278/husksync/HuskSyncVelocity.java index 9983926f..130419fd 100644 --- a/velocity/src/main/java/me/william278/husksync/HuskSyncVelocity.java +++ b/velocity/src/main/java/me/william278/husksync/HuskSyncVelocity.java @@ -9,10 +9,11 @@ import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.plugin.Plugin; import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; +import me.william278.husksync.migrator.MPDBMigrator; import me.william278.husksync.proxy.data.DataManager; import me.william278.husksync.redis.RedisMessage; import me.william278.husksync.velocity.VelocityUpdateChecker; -import me.william278.husksync.velocity.command.HuskSyncCommand; +import me.william278.husksync.velocity.command.VelocityCommand; import me.william278.husksync.velocity.config.ConfigLoader; import me.william278.husksync.velocity.config.ConfigManager; import me.william278.husksync.velocity.listener.VelocityEventListener; @@ -65,7 +66,7 @@ public class HuskSyncVelocity { public static DataManager dataManager; - //public static MPDBMigrator mpdbMigrator; + public static MPDBMigrator mpdbMigrator; private final Logger logger; private final ProxyServer server; @@ -108,13 +109,13 @@ public class HuskSyncVelocity { ConfigManager.loadConfig(); // Load settings from config - ConfigLoader.loadSettings(ConfigManager.getConfig()); + ConfigLoader.loadSettings(Objects.requireNonNull(ConfigManager.getConfig())); // Load messages ConfigManager.loadMessages(); // Load locales from messages - ConfigLoader.loadMessageStrings(ConfigManager.getMessages()); + ConfigLoader.loadMessageStrings(Objects.requireNonNull(ConfigManager.getMessages())); // Do update checker if (Settings.automaticUpdateChecks) { @@ -143,10 +144,10 @@ public class HuskSyncVelocity { CommandMeta meta = commandManager.metaBuilder("husksync") .aliases("hs") .build(); - commandManager.register(meta, new HuskSyncCommand()); + commandManager.register(meta, new VelocityCommand()); // Prepare the migrator for use if needed - //todo migrator + mpdbMigrator = new MPDBMigrator(getVelocityLogger()); // Initialize bStats metrics try { diff --git a/velocity/src/main/java/me/william278/husksync/velocity/command/HuskSyncCommand.java b/velocity/src/main/java/me/william278/husksync/velocity/command/HuskSyncCommand.java deleted file mode 100644 index 1e21df94..00000000 --- a/velocity/src/main/java/me/william278/husksync/velocity/command/HuskSyncCommand.java +++ /dev/null @@ -1,34 +0,0 @@ -package me.william278.husksync.velocity.command; - -import com.velocitypowered.api.command.CommandSource; -import com.velocitypowered.api.command.SimpleCommand; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -public class HuskSyncCommand implements SimpleCommand { - - /** - * Executes the command for the specified invocation. - * - * @param invocation the invocation context - */ - @Override - public void execute(Invocation invocation) { - final String[] args = invocation.arguments(); - final CommandSource source = invocation.source(); - - } - - /** - * Provides tab complete suggestions for the specified invocation. - * - * @param invocation the invocation context - * @return the tab complete suggestions - */ - @Override - public List suggest(Invocation invocation) { - return new ArrayList<>(); - } -} \ No newline at end of file diff --git a/velocity/src/main/java/me/william278/husksync/velocity/command/VelocityCommand.java b/velocity/src/main/java/me/william278/husksync/velocity/command/VelocityCommand.java new file mode 100644 index 00000000..818de9a7 --- /dev/null +++ b/velocity/src/main/java/me/william278/husksync/velocity/command/VelocityCommand.java @@ -0,0 +1,418 @@ +package me.william278.husksync.velocity.command; + +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.command.SimpleCommand; +import com.velocitypowered.api.proxy.Player; +import de.themoep.minedown.adventure.MineDown; +import me.william278.husksync.HuskSyncVelocity; +import me.william278.husksync.PlayerData; +import me.william278.husksync.Server; +import me.william278.husksync.Settings; +import me.william278.husksync.migrator.MPDBMigrator; +import me.william278.husksync.proxy.command.HuskSyncCommand; +import me.william278.husksync.redis.RedisMessage; +import me.william278.husksync.util.MessageManager; +import me.william278.husksync.velocity.VelocityUpdateChecker; +import me.william278.husksync.velocity.config.ConfigLoader; +import me.william278.husksync.velocity.config.ConfigManager; + +import java.io.IOException; +import java.util.*; +import java.util.logging.Level; +import java.util.stream.Collectors; + +public class VelocityCommand implements SimpleCommand, HuskSyncCommand { + + private static final HuskSyncVelocity plugin = HuskSyncVelocity.getInstance(); + + @Override + public void execute(Invocation invocation) { + final String[] args = invocation.arguments(); + final CommandSource sender = invocation.source(); + if (sender instanceof Player player) { + if (HuskSyncVelocity.synchronisedServers.size() == 0) { + player.sendMessage(new MineDown(MessageManager.getMessage("error_no_servers_proxied")).toComponent()); + return; + } + if (args.length >= 1) { + switch (args[0].toLowerCase(Locale.ROOT)) { + case "about", "info" -> sendAboutInformation(player); + case "update" -> { + if (!player.hasPermission("husksync.command.inventory")) { + sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent()); + return; + } + sender.sendMessage(new MineDown("[Checking for HuskSync updates...](gray)").toComponent()); + plugin.getProxyServer().getScheduler().buildTask(plugin, () -> { + // Check Bukkit servers needing updates + int updatesNeeded = 0; + String bukkitBrand = "Spigot"; + String bukkitVersion = "1.0"; + for (Server server : HuskSyncVelocity.synchronisedServers) { + VelocityUpdateChecker updateChecker = new VelocityUpdateChecker(server.huskSyncVersion()); + if (!updateChecker.isUpToDate()) { + updatesNeeded++; + bukkitBrand = server.serverBrand(); + bukkitVersion = server.huskSyncVersion(); + } + } + + // Check Velocity servers needing updates and send message + VelocityUpdateChecker proxyUpdateChecker = new VelocityUpdateChecker(HuskSyncVelocity.VERSION); + if (proxyUpdateChecker.isUpToDate() && updatesNeeded == 0) { + sender.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date, running Version " + proxyUpdateChecker.getLatestVersion() + "](#00fb9a)").toComponent()); + } else { + sender.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| Your server(s) are not up-to-date:](#00fb9a)").toComponent()); + if (!proxyUpdateChecker.isUpToDate()) { + sender.sendMessage(new MineDown("[•](white) [HuskSync on the Velocity proxy is outdated (Latest: " + proxyUpdateChecker.getLatestVersion() + ", Running: " + proxyUpdateChecker.getCurrentVersion() + ")](#00fb9a)").toComponent()); + } + if (updatesNeeded > 0) { + sender.sendMessage(new MineDown("[•](white) [HuskSync on " + updatesNeeded + " connected " + bukkitBrand + " server(s) are outdated (Latest: " + proxyUpdateChecker.getLatestVersion() + ", Running: " + bukkitVersion + ")](#00fb9a)").toComponent()); + } + sender.sendMessage(new MineDown("[•](white) [Download links:](#00fb9a) [[⏩ Spigot]](gray open_url=https://www.spigotmc.org/resources/husktowns.92672/updates) [•](#262626) [[⏩ Polymart]](gray open_url=https://polymart.org/resource/husktowns.1056/updates)").toComponent()); + } + }); + } + case "invsee", "openinv", "inventory" -> { + if (!player.hasPermission("husksync.command.inventory")) { + sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent()); + return; + } + String clusterId; + if (Settings.clusters.size() > 1) { + if (args.length == 3) { + clusterId = args[2]; + } else { + sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent()); + return; + } + } else { + clusterId = "main"; + for (Settings.SynchronisationCluster cluster : Settings.clusters) { + clusterId = cluster.clusterId(); + break; + } + } + if (args.length == 2 || args.length == 3) { + String playerName = args[1]; + openInventory(player, playerName, clusterId); + } else { + sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax").replaceAll("%1%", + "/husksync invsee ")).toComponent()); + } + } + case "echest", "enderchest" -> { + if (!player.hasPermission("husksync.command.ender_chest")) { + sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent()); + return; + } + String clusterId; + if (Settings.clusters.size() > 1) { + if (args.length == 3) { + clusterId = args[2]; + } else { + sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent()); + return; + } + } else { + clusterId = "main"; + for (Settings.SynchronisationCluster cluster : Settings.clusters) { + clusterId = cluster.clusterId(); + break; + } + } + if (args.length == 2 || args.length == 3) { + String playerName = args[1]; + openEnderChest(player, playerName, clusterId); + } else { + sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax") + .replaceAll("%1%", "/husksync echest ")).toComponent()); + } + } + case "migrate" -> { + if (!player.hasPermission("husksync.command.admin")) { + sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent()); + return; + } + sender.sendMessage(new MineDown(MessageManager.getMessage("error_console_command_only") + .replaceAll("%1%", "Velocity")).toComponent()); + } + case "status" -> { + if (!player.hasPermission("husksync.command.admin")) { + sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent()); + return; + } + int playerDataSize = 0; + for (Settings.SynchronisationCluster cluster : Settings.clusters) { + playerDataSize += HuskSyncVelocity.dataManager.playerDataCache.get(cluster).playerData.size(); + } + sender.sendMessage(new MineDown(MessageManager.PLUGIN_STATUS.toString() + .replaceAll("%1%", String.valueOf(HuskSyncVelocity.synchronisedServers.size())) + .replaceAll("%2%", String.valueOf(playerDataSize))).toComponent()); + } + case "reload" -> { + if (!player.hasPermission("husksync.command.admin")) { + sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent()); + return; + } + ConfigManager.loadConfig(); + ConfigLoader.loadSettings(Objects.requireNonNull(ConfigManager.getConfig())); + + ConfigManager.loadMessages(); + ConfigLoader.loadMessageStrings(Objects.requireNonNull(ConfigManager.getMessages())); + + // Send reload request to all bukkit servers + try { + new RedisMessage(RedisMessage.MessageType.RELOAD_CONFIG, + new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null), + "reload") + .send(); + } catch (IOException e) { + plugin.getVelocityLogger().log(Level.WARNING, "Failed to serialize reload notification message data"); + } + + sender.sendMessage(new MineDown(MessageManager.getMessage("reload_complete")).toComponent()); + } + default -> sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax").replaceAll("%1%", + "/husksync ")).toComponent()); + } + } else { + sendAboutInformation(player); + } + } else { + // Database migration wizard + if (args.length >= 1) { + if (args[0].equalsIgnoreCase("migrate")) { + MPDBMigrator migrator = HuskSyncVelocity.mpdbMigrator; + if (args.length == 1) { + sender.sendMessage(new MineDown( + """ + === MySQLPlayerDataBridge Migration Wizard ========== + This will migrate data from the MySQLPlayerDataBridge + plugin to HuskSync. + + Data that will be migrated: + - Inventories + - Ender Chests + - Experience points + + Other non-vital data, such as current health, hunger + & potion effects will not be migrated to ensure that + migration does not take an excessive amount of time. + + To do this, you need to have MySqlPlayerDataBridge + and HuskSync installed on one Spigot server as well + as HuskSync installed on the proxy (which you have) + + >To proceed, type: husksync migrate setup""").toComponent()); + } else { + switch (args[1].toLowerCase()) { + case "setup" -> sender.sendMessage(new MineDown( + """ + === MySQLPlayerDataBridge Migration Wizard ========== + The following database settings will be used. + Please make sure they match the correct settings to + access your MySQLPlayerDataBridge Data + + sourceHost: %1% + sourcePort: %2% + sourceDatabase: %3% + sourceUsername: %4% + sourcePassword: %5% + + sourceInventoryTableName: %6% + sourceEnderChestTableName: %7% + sourceExperienceTableName: %8% + + targetCluster: %9% + + To change a setting, type: + husksync migrate setting + + Please ensure no players are logged in to the network + and that at least one Spigot server is online with + both HuskSync AND MySqlPlayerDataBridge installed AND + that the server has been configured with the correct + Redis credentials. + + Warning: Data will be saved to your configured data + source, which is currently a %10% database. + Please make sure you are happy with this, or stop + the proxy server and edit this in config.yml + + Warning: Migration will overwrite any current data + saved by HuskSync. It will not, however, delete any + data from the source MySQLPlayerDataBridge database. + + >When done, type: husksync migrate start""" + .replaceAll("%1%", migrator.migrationSettings.sourceHost) + .replaceAll("%2%", String.valueOf(migrator.migrationSettings.sourcePort)) + .replaceAll("%3%", migrator.migrationSettings.sourceDatabase) + .replaceAll("%4%", migrator.migrationSettings.sourceUsername) + .replaceAll("%5%", migrator.migrationSettings.sourcePassword) + .replaceAll("%6%", migrator.migrationSettings.inventoryDataTable) + .replaceAll("%7%", migrator.migrationSettings.enderChestDataTable) + .replaceAll("%8%", migrator.migrationSettings.expDataTable) + .replaceAll("%9%", migrator.migrationSettings.targetCluster) + .replaceAll("%10%", Settings.dataStorageType.toString()) + ).toComponent()); + case "setting" -> { + if (args.length == 4) { + String value = args[3]; + switch (args[2]) { + case "sourceHost", "host" -> migrator.migrationSettings.sourceHost = value; + case "sourcePort", "port" -> { + try { + migrator.migrationSettings.sourcePort = Integer.parseInt(value); + } catch (NumberFormatException e) { + sender.sendMessage(new MineDown("Error: Invalid value; port must be a number").toComponent()); + return; + } + } + case "sourceDatabase", "database" -> migrator.migrationSettings.sourceDatabase = value; + case "sourceUsername", "username" -> migrator.migrationSettings.sourceUsername = value; + case "sourcePassword", "password" -> migrator.migrationSettings.sourcePassword = value; + case "sourceInventoryTableName", "inventoryTableName", "inventoryTable" -> migrator.migrationSettings.inventoryDataTable = value; + case "sourceEnderChestTableName", "enderChestTableName", "enderChestTable" -> migrator.migrationSettings.enderChestDataTable = value; + case "sourceExperienceTableName", "experienceTableName", "experienceTable" -> migrator.migrationSettings.expDataTable = value; + case "targetCluster", "cluster" -> migrator.migrationSettings.targetCluster = value; + default -> { + sender.sendMessage(new MineDown("Error: Invalid setting; please use \"husksync migrate setup\" to view a list").toComponent()); + return; + } + } + sender.sendMessage(new MineDown("Successfully updated setting: \"" + args[2] + "\" --> \"" + value + "\"").toComponent()); + } else { + sender.sendMessage(new MineDown("Error: Invalid usage. Syntax: husksync migrate setting ").toComponent()); + } + } + case "start" -> { + sender.sendMessage(new MineDown("Starting MySQLPlayerDataBridge migration!...").toComponent()); + + // If the migrator is ready, execute the migration asynchronously + if (HuskSyncVelocity.mpdbMigrator.readyToMigrate(plugin.getProxyServer().getPlayerCount(), + HuskSyncVelocity.synchronisedServers)) { + plugin.getProxyServer().getScheduler().buildTask(plugin, () -> + HuskSyncVelocity.mpdbMigrator.executeMigrationOperations(HuskSyncVelocity.dataManager, + HuskSyncVelocity.synchronisedServers)); + } + } + default -> sender.sendMessage(new MineDown("Error: Invalid argument for migration. Use \"husksync migrate\" to start the process").toComponent()); + } + } + return; + } + } + sender.sendMessage(new MineDown("Error: Invalid syntax. Usage: husksync migrate ").toComponent()); + } + } + + // View the inventory of a player specified by their name + private void openInventory(Player viewer, String targetPlayerName, String clusterId) { + if (viewer.getUsername().equalsIgnoreCase(targetPlayerName)) { + viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_ender_chest")).toComponent()); + return; + } + if (plugin.getProxyServer().getPlayer(targetPlayerName).isPresent()) { + viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_inventory_online")).toComponent()); + return; + } + plugin.getProxyServer().getScheduler().buildTask(plugin, () -> { + for (Settings.SynchronisationCluster cluster : Settings.clusters) { + if (!cluster.clusterId().equals(clusterId)) continue; + PlayerData playerData = HuskSyncVelocity.dataManager.getPlayerDataByName(targetPlayerName, cluster.clusterId()); + if (playerData == null) { + viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent()); + return; + } + try { + new RedisMessage(RedisMessage.MessageType.OPEN_INVENTORY, + new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId(), null), + targetPlayerName, RedisMessage.serialize(playerData)) + .send(); + viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_inventory_of").replaceAll("%1%", + targetPlayerName)).toComponent()); + } catch (IOException e) { + plugin.getVelocityLogger().log(Level.WARNING, "Failed to serialize inventory-see player data", e); + } + return; + } + viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent()); + }); + } + + // View the ender chest of a player specified by their name + public void openEnderChest(Player viewer, String targetPlayerName, String clusterId) { + if (viewer.getUsername().equalsIgnoreCase(targetPlayerName)) { + viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_ender_chest")).toComponent()); + return; + } + if (plugin.getProxyServer().getPlayer(targetPlayerName).isPresent()) { + viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_ender_chest_online")).toComponent()); + return; + } + plugin.getProxyServer().getScheduler().buildTask(plugin, () -> { + for (Settings.SynchronisationCluster cluster : Settings.clusters) { + if (!cluster.clusterId().equals(clusterId)) continue; + PlayerData playerData = HuskSyncVelocity.dataManager.getPlayerDataByName(targetPlayerName, cluster.clusterId()); + if (playerData == null) { + viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent()); + return; + } + try { + new RedisMessage(RedisMessage.MessageType.OPEN_ENDER_CHEST, + new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId(), null), + targetPlayerName, RedisMessage.serialize(playerData)) + .send(); + viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_ender_chest_of").replaceAll("%1%", + targetPlayerName)).toComponent()); + } catch (IOException e) { + plugin.getVelocityLogger().log(Level.WARNING, "Failed to serialize inventory-see player data", e); + } + return; + } + viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent()); + }); + } + + /** + * Send information about the plugin + * + * @param player The player to send it to + */ + private void sendAboutInformation(Player player) { + try { + new RedisMessage(RedisMessage.MessageType.SEND_PLUGIN_INFORMATION, + new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, player.getUniqueId(), null), + "Velocity", HuskSyncVelocity.VERSION).send(); + } catch (IOException e) { + plugin.getVelocityLogger().log(Level.WARNING, "Failed to serialize plugin information to send", e); + } + } + + @Override + public List suggest(Invocation invocation) { + final CommandSource sender = invocation.source(); + final String[] args = invocation.arguments(); + + if (sender instanceof Player player) { + if (args.length == 1) { + final ArrayList subCommands = new ArrayList<>(); + for (SubCommand subCommand : SUB_COMMANDS) { + if (subCommand.permission() != null) { + if (!player.hasPermission(subCommand.permission())) { + continue; + } + } + subCommands.add(subCommand.command()); + } + // Automatically filter the sub commands' order in tab completion by what the player has typed + return subCommands.stream().filter(val -> val.startsWith(args[0])) + .sorted().collect(Collectors.toList()); + } else { + return Collections.emptyList(); + } + } + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/velocity/src/main/java/me/william278/husksync/velocity/config/ConfigManager.java b/velocity/src/main/java/me/william278/husksync/velocity/config/ConfigManager.java index 940df485..05081861 100644 --- a/velocity/src/main/java/me/william278/husksync/velocity/config/ConfigManager.java +++ b/velocity/src/main/java/me/william278/husksync/velocity/config/ConfigManager.java @@ -3,7 +3,6 @@ package me.william278.husksync.velocity.config; import me.william278.husksync.HuskSyncVelocity; import me.william278.husksync.Settings; import ninja.leaping.configurate.ConfigurationNode; -import ninja.leaping.configurate.commented.CommentedConfigurationNode; import ninja.leaping.configurate.yaml.YAMLConfigurationLoader; import java.io.File;