Add command and migrator

feat/data-edit-commands
William 3 years ago
parent e7822baa99
commit 635fefba3d

@ -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 {

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

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

@ -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<PlayerData, String> incomingPlayerData;
public static HashMap<PlayerData, String> 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> mpdbPlayerData;
private static HashSet<MPDBPlayerData> 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<Server> 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<Server> 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<Server> 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<PlayerData, String> 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<PlayerData, String> 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;

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

@ -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 {

@ -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<String> suggest(Invocation invocation) {
return new ArrayList<>();
}
}

@ -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 <player>")).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 <player>")).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 <about/status/invsee/echest>")).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 <settingName> <value>
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 <settingName> <value>").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 <args>").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<String> suggest(Invocation invocation) {
final CommandSource sender = invocation.source();
final String[] args = invocation.arguments();
if (sender instanceof Player player) {
if (args.length == 1) {
final ArrayList<String> 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();
}
}

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

Loading…
Cancel
Save