Cluster support

feat/data-edit-commands
William 3 years ago
parent 2367b14738
commit cf6b81200b

@ -11,7 +11,7 @@ plugins {
allprojects { allprojects {
group 'me.William278' group 'me.William278'
version '1.0.4' version '1.1'
compileJava { options.encoding = 'UTF-8' } compileJava { options.encoding = 'UTF-8' }
tasks.withType(JavaCompile) { options.encoding = 'UTF-8' } tasks.withType(JavaCompile) { options.encoding = 'UTF-8' }

@ -21,6 +21,7 @@ import java.util.logging.Level;
public final class HuskSyncBukkit extends JavaPlugin { public final class HuskSyncBukkit extends JavaPlugin {
// Bukkit bStats ID (Different to BungeeCord)
private static final int METRICS_ID = 13140; private static final int METRICS_ID = 13140;
private static HuskSyncBukkit instance; private static HuskSyncBukkit instance;
@ -36,7 +37,7 @@ public final class HuskSyncBukkit extends JavaPlugin {
// Has a handshake been established with the Bungee? // Has a handshake been established with the Bungee?
public static boolean handshakeCompleted = false; public static boolean handshakeCompleted = false;
// THe handshake task to execute // The handshake task to execute
private static BukkitTask handshakeTask; private static BukkitTask handshakeTask;
// Whether MySqlPlayerDataBridge is installed // Whether MySqlPlayerDataBridge is installed
@ -54,7 +55,7 @@ public final class HuskSyncBukkit extends JavaPlugin {
} }
try { try {
new RedisMessage(RedisMessage.MessageType.CONNECTION_HANDSHAKE, new RedisMessage(RedisMessage.MessageType.CONNECTION_HANDSHAKE,
new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null), new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null, Settings.cluster),
serverUUID.toString(), serverUUID.toString(),
Boolean.toString(isMySqlPlayerDataBridgeInstalled), Boolean.toString(isMySqlPlayerDataBridgeInstalled),
Bukkit.getName(), Bukkit.getName(),
@ -74,7 +75,7 @@ public final class HuskSyncBukkit extends JavaPlugin {
if (!handshakeCompleted) return; if (!handshakeCompleted) return;
try { try {
new RedisMessage(RedisMessage.MessageType.TERMINATE_HANDSHAKE, new RedisMessage(RedisMessage.MessageType.TERMINATE_HANDSHAKE,
new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null), new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null, Settings.cluster),
serverUUID.toString(), serverUUID.toString(),
Bukkit.getName()).send(); Bukkit.getName()).send();
} catch (IOException e) { } catch (IOException e) {

@ -8,6 +8,7 @@ public class ConfigLoader {
public static void loadSettings(FileConfiguration config) throws IllegalArgumentException { public static void loadSettings(FileConfiguration config) throws IllegalArgumentException {
Settings.serverType = Settings.ServerType.BUKKIT; Settings.serverType = Settings.ServerType.BUKKIT;
Settings.automaticUpdateChecks = config.getBoolean("check_for_updates", true); Settings.automaticUpdateChecks = config.getBoolean("check_for_updates", true);
Settings.cluster = config.getString("cluster_id", "main");
Settings.redisHost = config.getString("redis_settings.host", "localhost"); Settings.redisHost = config.getString("redis_settings.host", "localhost");
Settings.redisPort = config.getInt("redis_settings.port", 6379); Settings.redisPort = config.getInt("redis_settings.port", 6379);
Settings.redisPassword = config.getString("redis_settings.password", ""); Settings.redisPassword = config.getString("redis_settings.password", "");

@ -57,7 +57,7 @@ public class DataViewer {
// Send a redis message with the updated data after the viewing // Send a redis message with the updated data after the viewing
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE, new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null), new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null, Settings.cluster),
RedisMessage.serialize(playerData)) RedisMessage.serialize(playerData))
.send(); .send();
} }

@ -43,6 +43,13 @@ public class BukkitRedisListener extends RedisListener {
if (!plugin.isEnabled()) { if (!plugin.isEnabled()) {
return; return;
} }
// Ignore messages for other clusters if applicable
final String targetClusterId = message.getMessageTarget().targetClusterId();
if (targetClusterId != null) {
if (!targetClusterId.equalsIgnoreCase(Settings.cluster)) {
return;
}
}
// Handle the incoming redis message; either for a specific player or the system // Handle the incoming redis message; either for a specific player or the system
if (message.getMessageTarget().targetPlayerUUID() == null) { if (message.getMessageTarget().targetPlayerUUID() == null) {
@ -90,7 +97,7 @@ public class BukkitRedisListener extends RedisListener {
try { try {
MPDBPlayerData data = (MPDBPlayerData) RedisMessage.deserialize(encodedData); MPDBPlayerData data = (MPDBPlayerData) RedisMessage.deserialize(encodedData);
new RedisMessage(RedisMessage.MessageType.DECODED_MPDB_DATA_SET, new RedisMessage(RedisMessage.MessageType.DECODED_MPDB_DATA_SET,
new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null), new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null, Settings.cluster),
RedisMessage.serialize(MPDBDeserializer.convertMPDBData(data)), RedisMessage.serialize(MPDBDeserializer.convertMPDBData(data)),
data.playerName) data.playerName)
.send(); .send();

@ -104,7 +104,7 @@ public class PlayerSetter {
try { try {
final String serializedPlayerData = getNewSerializedPlayerData(player); final String serializedPlayerData = getNewSerializedPlayerData(player);
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE, new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null), new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null, Settings.cluster),
serializedPlayerData).send(); serializedPlayerData).send();
} catch (IOException e) { } catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData update to the proxy", e); plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData update to the proxy", e);
@ -123,7 +123,7 @@ public class PlayerSetter {
*/ */
public static void requestPlayerData(UUID playerUUID) throws IOException { public static void requestPlayerData(UUID playerUUID) throws IOException {
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_REQUEST, new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_REQUEST,
new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null), new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null, Settings.cluster),
playerUUID.toString()).send(); playerUUID.toString()).send();
} }

@ -13,4 +13,5 @@ synchronisation_settings:
game_mode: true game_mode: true
advancements: true advancements: true
location: false location: false
cluster_id: 'main'
check_for_updates: true check_for_updates: true

@ -19,6 +19,7 @@ import org.bstats.bungeecord.Metrics;
import java.io.IOException; import java.io.IOException;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
@ -26,6 +27,7 @@ import java.util.logging.Level;
public final class HuskSyncBungeeCord extends Plugin { public final class HuskSyncBungeeCord extends Plugin {
// BungeeCord bStats ID (different to Bukkit)
private static final int METRICS_ID = 13141; private static final int METRICS_ID = 13141;
private static HuskSyncBungeeCord instance; private static HuskSyncBungeeCord instance;
@ -41,9 +43,9 @@ public final class HuskSyncBungeeCord extends Plugin {
*/ */
public static HashSet<Server> synchronisedServers; public static HashSet<Server> synchronisedServers;
private static Database database; private static HashMap<String,Database> clusterDatabases;
public static Connection getConnection() throws SQLException { public static Connection getConnection(String clusterId) throws SQLException {
return database.getConnection(); return clusterDatabases.get(clusterId).getConnection();
} }
public static MPDBMigrator mpdbMigrator; public static MPDBMigrator mpdbMigrator;
@ -76,21 +78,29 @@ public final class HuskSyncBungeeCord extends Plugin {
} }
// Initialize the database // Initialize the database
database = switch (Settings.dataStorageType) { for (Settings.SynchronisationCluster cluster : Settings.clusters) {
case SQLITE -> new SQLite(this); Database clusterDatabase = switch (Settings.dataStorageType) {
case MYSQL -> new MySQL(this); case SQLITE -> new SQLite(this, cluster);
case MYSQL -> new MySQL(this, cluster);
}; };
database.load(); clusterDatabase.load();
database.createTables(); clusterDatabase.createTables();
clusterDatabases.put(cluster.clusterId(), clusterDatabase);
}
// Abort loading if the database failed to initialize // Abort loading if the database failed to initialize
for (Database database : clusterDatabases.values()) {
if (database.isInactive()) { if (database.isInactive()) {
getLogger().severe("Failed to initialize the database; HuskSync will now abort loading itself (" + getProxy().getName() + ") v" + getDescription().getVersion()); getLogger().severe("Failed to initialize the database(s); HuskSync will now abort loading itself (" + getProxy().getName() + ") v" + getDescription().getVersion());
return; return;
} }
}
// Setup player data cache // Setup player data cache
DataManager.playerDataCache = new DataManager.PlayerDataCache(); for (Settings.SynchronisationCluster cluster : Settings.clusters) {
DataManager.playerDataCache.put(cluster, new DataManager.PlayerDataCache());
}
// Initialize the redis listener // Initialize the redis listener
if (!new BungeeRedisListener().isActiveAndEnabled) { if (!new BungeeRedisListener().isActiveAndEnabled) {
@ -129,7 +139,7 @@ public final class HuskSyncBungeeCord extends Plugin {
for (Server server: synchronisedServers) { for (Server server: synchronisedServers) {
try { try {
new RedisMessage(RedisMessage.MessageType.TERMINATE_HANDSHAKE, new RedisMessage(RedisMessage.MessageType.TERMINATE_HANDSHAKE,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null), new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, server.clusterId()),
server.serverUUID().toString(), server.serverUUID().toString(),
ProxyServer.getInstance().getName()).send(); ProxyServer.getInstance().getName()).send();
} catch (IOException e) { } catch (IOException e) {
@ -138,7 +148,9 @@ public final class HuskSyncBungeeCord extends Plugin {
} }
// Close the database // Close the database
for (Database database : clusterDatabases.values()) {
database.close(); database.close();
}
// Log to console // Log to console
getLogger().info("Disabled HuskSync (" + getProxy().getName() + ") v" + getDescription().getVersion()); getLogger().info("Disabled HuskSync (" + getProxy().getName() + ") v" + getDescription().getVersion());
@ -147,5 +159,5 @@ public final class HuskSyncBungeeCord extends Plugin {
/** /**
* A record representing a server synchronised on the network and whether it has MySqlPlayerDataBridge installed * A record representing a server synchronised on the network and whether it has MySqlPlayerDataBridge installed
*/ */
public record Server(UUID serverUUID, boolean hasMySqlPlayerDataBridge, String huskSyncVersion, String serverBrand) { } public record Server(UUID serverUUID, boolean hasMySqlPlayerDataBridge, String huskSyncVersion, String serverBrand, String clusterId) { }
} }

@ -90,9 +90,24 @@ public class HuskSyncCommand extends Command implements TabExecutor {
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent()); sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
return; return;
} }
if (args.length == 2) { 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]; String playerName = args[1];
openInventory(player, playerName); openInventory(player, playerName, clusterId);
} else { } else {
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax").replaceAll("%1%", sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax").replaceAll("%1%",
"/husksync invsee <player>")).toComponent()); "/husksync invsee <player>")).toComponent());
@ -103,9 +118,24 @@ public class HuskSyncCommand extends Command implements TabExecutor {
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent()); sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
return; return;
} }
if (args.length == 2) { 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]; String playerName = args[1];
openEnderChest(player, playerName); openEnderChest(player, playerName, clusterId);
} else { } else {
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax") sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax")
.replaceAll("%1%", "/husksync echest <player>")).toComponent()); .replaceAll("%1%", "/husksync echest <player>")).toComponent());
@ -124,9 +154,13 @@ public class HuskSyncCommand extends Command implements TabExecutor {
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent()); sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
return; return;
} }
int playerDataSize = 0;
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
playerDataSize += DataManager.playerDataCache.get(cluster).playerData.size();
}
sender.sendMessage(new MineDown(MessageManager.PLUGIN_STATUS.toString() sender.sendMessage(new MineDown(MessageManager.PLUGIN_STATUS.toString()
.replaceAll("%1%", String.valueOf(HuskSyncBungeeCord.synchronisedServers.size())) .replaceAll("%1%", String.valueOf(HuskSyncBungeeCord.synchronisedServers.size()))
.replaceAll("%2%", String.valueOf(DataManager.playerDataCache.playerData.size()))).toComponent()); .replaceAll("%2%", String.valueOf(playerDataSize))).toComponent());
} }
case "reload" -> { case "reload" -> {
if (!player.hasPermission("husksync.command.admin")) { if (!player.hasPermission("husksync.command.admin")) {
@ -142,7 +176,7 @@ public class HuskSyncCommand extends Command implements TabExecutor {
// Send reload request to all bukkit servers // Send reload request to all bukkit servers
try { try {
new RedisMessage(RedisMessage.MessageType.RELOAD_CONFIG, new RedisMessage(RedisMessage.MessageType.RELOAD_CONFIG,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null), new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null),
"reload") "reload")
.send(); .send();
} catch (IOException e) { } catch (IOException e) {
@ -201,6 +235,8 @@ public class HuskSyncCommand extends Command implements TabExecutor {
sourceEnderChestTableName: %7% sourceEnderChestTableName: %7%
sourceExperienceTableName: %8% sourceExperienceTableName: %8%
targetCluster: %9%
To change a setting, type: To change a setting, type:
husksync migrate setting <settingName> <value> husksync migrate setting <settingName> <value>
@ -211,7 +247,7 @@ public class HuskSyncCommand extends Command implements TabExecutor {
Redis credentials. Redis credentials.
Warning: Data will be saved to your configured data Warning: Data will be saved to your configured data
source, which is currently a %9% database. source, which is currently a %10% database.
Please make sure you are happy with this, or stop Please make sure you are happy with this, or stop
the proxy server and edit this in config.yml the proxy server and edit this in config.yml
@ -228,7 +264,8 @@ public class HuskSyncCommand extends Command implements TabExecutor {
.replaceAll("%6%", MPDBMigrator.migrationSettings.inventoryDataTable) .replaceAll("%6%", MPDBMigrator.migrationSettings.inventoryDataTable)
.replaceAll("%7%", MPDBMigrator.migrationSettings.enderChestDataTable) .replaceAll("%7%", MPDBMigrator.migrationSettings.enderChestDataTable)
.replaceAll("%8%", MPDBMigrator.migrationSettings.expDataTable) .replaceAll("%8%", MPDBMigrator.migrationSettings.expDataTable)
.replaceAll("%9%", Settings.dataStorageType.toString()) .replaceAll("%9%", MPDBMigrator.migrationSettings.targetCluster)
.replaceAll("%10%", Settings.dataStorageType.toString())
).toComponent()); ).toComponent());
case "setting" -> { case "setting" -> {
if (args.length == 4) { if (args.length == 4) {
@ -249,6 +286,7 @@ public class HuskSyncCommand extends Command implements TabExecutor {
case "sourceInventoryTableName", "inventoryTableName", "inventoryTable" -> MPDBMigrator.migrationSettings.inventoryDataTable = value; case "sourceInventoryTableName", "inventoryTableName", "inventoryTable" -> MPDBMigrator.migrationSettings.inventoryDataTable = value;
case "sourceEnderChestTableName", "enderChestTableName", "enderChestTable" -> MPDBMigrator.migrationSettings.enderChestDataTable = value; case "sourceEnderChestTableName", "enderChestTableName", "enderChestTable" -> MPDBMigrator.migrationSettings.enderChestDataTable = value;
case "sourceExperienceTableName", "experienceTableName", "experienceTable" -> MPDBMigrator.migrationSettings.expDataTable = value; case "sourceExperienceTableName", "experienceTableName", "experienceTable" -> MPDBMigrator.migrationSettings.expDataTable = value;
case "targetCluster", "cluster" -> MPDBMigrator.migrationSettings.targetCluster = value;
default -> { default -> {
sender.sendMessage(new MineDown("Error: Invalid setting; please use \"husksync migrate setup\" to view a list").toComponent()); sender.sendMessage(new MineDown("Error: Invalid setting; please use \"husksync migrate setup\" to view a list").toComponent());
return; return;
@ -274,7 +312,7 @@ public class HuskSyncCommand extends Command implements TabExecutor {
} }
// View the inventory of a player specified by their name // View the inventory of a player specified by their name
private void openInventory(ProxiedPlayer viewer, String targetPlayerName) { private void openInventory(ProxiedPlayer viewer, String targetPlayerName, String clusterId) {
if (viewer.getName().equalsIgnoreCase(targetPlayerName)) { if (viewer.getName().equalsIgnoreCase(targetPlayerName)) {
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_ender_chest")).toComponent()); viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_ender_chest")).toComponent());
return; return;
@ -284,14 +322,16 @@ public class HuskSyncCommand extends Command implements TabExecutor {
return; return;
} }
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> { ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
PlayerData playerData = DataManager.getPlayerDataByName(targetPlayerName); for (Settings.SynchronisationCluster cluster : Settings.clusters) {
if (!cluster.clusterId().equals(clusterId)) continue;
PlayerData playerData = DataManager.getPlayerDataByName(targetPlayerName, cluster.clusterId());
if (playerData == null) { if (playerData == null) {
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent()); viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent());
return; return;
} }
try { try {
new RedisMessage(RedisMessage.MessageType.OPEN_INVENTORY, new RedisMessage(RedisMessage.MessageType.OPEN_INVENTORY,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId()), new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId(), null),
targetPlayerName, RedisMessage.serialize(playerData)) targetPlayerName, RedisMessage.serialize(playerData))
.send(); .send();
viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_inventory_of").replaceAll("%1%", viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_inventory_of").replaceAll("%1%",
@ -299,11 +339,14 @@ public class HuskSyncCommand extends Command implements TabExecutor {
} catch (IOException e) { } catch (IOException e) {
plugin.getLogger().log(Level.WARNING, "Failed to serialize inventory-see player data", e); plugin.getLogger().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 // View the ender chest of a player specified by their name
private void openEnderChest(ProxiedPlayer viewer, String targetPlayerName) { private void openEnderChest(ProxiedPlayer viewer, String targetPlayerName, String clusterId) {
if (viewer.getName().equalsIgnoreCase(targetPlayerName)) { if (viewer.getName().equalsIgnoreCase(targetPlayerName)) {
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_ender_chest")).toComponent()); viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_ender_chest")).toComponent());
return; return;
@ -313,14 +356,16 @@ public class HuskSyncCommand extends Command implements TabExecutor {
return; return;
} }
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> { ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
PlayerData playerData = DataManager.getPlayerDataByName(targetPlayerName); for (Settings.SynchronisationCluster cluster : Settings.clusters) {
if (!cluster.clusterId().equals(clusterId)) continue;
PlayerData playerData = DataManager.getPlayerDataByName(targetPlayerName, cluster.clusterId());
if (playerData == null) { if (playerData == null) {
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent()); viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent());
return; return;
} }
try { try {
new RedisMessage(RedisMessage.MessageType.OPEN_ENDER_CHEST, new RedisMessage(RedisMessage.MessageType.OPEN_ENDER_CHEST,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId()), new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId(), null),
targetPlayerName, RedisMessage.serialize(playerData)) targetPlayerName, RedisMessage.serialize(playerData))
.send(); .send();
viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_ender_chest_of").replaceAll("%1%", viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_ender_chest_of").replaceAll("%1%",
@ -328,6 +373,9 @@ public class HuskSyncCommand extends Command implements TabExecutor {
} catch (IOException e) { } catch (IOException e) {
plugin.getLogger().log(Level.WARNING, "Failed to serialize inventory-see player data", e); plugin.getLogger().log(Level.WARNING, "Failed to serialize inventory-see player data", e);
} }
return;
}
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
}); });
} }
@ -339,7 +387,7 @@ public class HuskSyncCommand extends Command implements TabExecutor {
private void sendAboutInformation(ProxiedPlayer player) { private void sendAboutInformation(ProxiedPlayer player) {
try { try {
new RedisMessage(RedisMessage.MessageType.SEND_PLUGIN_INFORMATION, new RedisMessage(RedisMessage.MessageType.SEND_PLUGIN_INFORMATION,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, player.getUniqueId()), new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, player.getUniqueId(), null),
plugin.getProxy().getName(), plugin.getDescription().getVersion()).send(); plugin.getProxy().getName(), plugin.getDescription().getVersion()).send();
} catch (IOException e) { } catch (IOException e) {
plugin.getLogger().log(Level.WARNING, "Failed to serialize plugin information to send", e); plugin.getLogger().log(Level.WARNING, "Failed to serialize plugin information to send", e);

@ -32,6 +32,15 @@ public class ConfigLoader {
Settings.hikariMaximumLifetime = config.getLong("data_storage_settings.hikari_pool_settings.maximum_lifetime", 1800000); Settings.hikariMaximumLifetime = config.getLong("data_storage_settings.hikari_pool_settings.maximum_lifetime", 1800000);
Settings.hikariKeepAliveTime = config.getLong("data_storage_settings.hikari_pool_settings.keepalive_time", 0); Settings.hikariKeepAliveTime = config.getLong("data_storage_settings.hikari_pool_settings.keepalive_time", 0);
Settings.hikariConnectionTimeOut = config.getLong("data_storage_settings.hikari_pool_settings.connection_timeout", 5000); Settings.hikariConnectionTimeOut = config.getLong("data_storage_settings.hikari_pool_settings.connection_timeout", 5000);
// Read cluster data
Configuration section = config.getSection("clusters");
for (String clusterId : section.getKeys()) {
final String playerTableName = config.getString("clusters." + clusterId + ".player_table", "husksync_players");
final String dataTableName = config.getString("clusters." + clusterId + ".data_table", "husksync_data");
final String databaseName = config.getString("clusters." + clusterId + ".database", Settings.mySQLDatabase);
Settings.clusters.add(new Settings.SynchronisationCluster(clusterId, databaseName, playerTableName, dataTableName));
}
} }
public static void loadMessageStrings(Configuration config) { public static void loadMessageStrings(Configuration config) {

@ -2,18 +2,22 @@ package me.william278.husksync.bungeecord.data;
import me.william278.husksync.PlayerData; import me.william278.husksync.PlayerData;
import me.william278.husksync.HuskSyncBungeeCord; import me.william278.husksync.HuskSyncBungeeCord;
import me.william278.husksync.Settings;
import me.william278.husksync.bungeecord.data.sql.Database; import me.william278.husksync.bungeecord.data.sql.Database;
import java.sql.*; import java.sql.*;
import java.time.Instant; import java.time.Instant;
import java.util.HashSet; import java.util.*;
import java.util.UUID;
import java.util.logging.Level; import java.util.logging.Level;
public class DataManager { public class DataManager {
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance(); private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
public static PlayerDataCache playerDataCache;
/**
* The player data cache for each cluster ID
*/
public static HashMap<Settings.SynchronisationCluster, PlayerDataCache> playerDataCache = new HashMap<>();
/** /**
* Checks if the player is registered on the database. * Checks if the player is registered on the database.
@ -23,10 +27,12 @@ public class DataManager {
* @param playerUUID The UUID of the player to register * @param playerUUID The UUID of the player to register
*/ */
public static void ensurePlayerExists(UUID playerUUID, String playerName) { public static void ensurePlayerExists(UUID playerUUID, String playerName) {
if (!playerExists(playerUUID)) { for (Settings.SynchronisationCluster cluster : Settings.clusters) {
createPlayerEntry(playerUUID, playerName); if (!playerExists(playerUUID, cluster)) {
createPlayerEntry(playerUUID, playerName, cluster);
} else { } else {
updatePlayerName(playerUUID, playerName); updatePlayerName(playerUUID, playerName, cluster);
}
} }
} }
@ -36,10 +42,10 @@ public class DataManager {
* @param playerUUID The UUID of the player * @param playerUUID The UUID of the player
* @return {@code true} if the player is on the player table * @return {@code true} if the player is on the player table
*/ */
private static boolean playerExists(UUID playerUUID) { private static boolean playerExists(UUID playerUUID, Settings.SynchronisationCluster cluster) {
try (Connection connection = HuskSyncBungeeCord.getConnection()) { try (Connection connection = HuskSyncBungeeCord.getConnection(cluster.clusterId())) {
try (PreparedStatement statement = connection.prepareStatement( try (PreparedStatement statement = connection.prepareStatement(
"SELECT * FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?;")) { "SELECT * FROM " + cluster.playerTableName() + " WHERE `uuid`=?;")) {
statement.setString(1, playerUUID.toString()); statement.setString(1, playerUUID.toString());
ResultSet resultSet = statement.executeQuery(); ResultSet resultSet = statement.executeQuery();
return resultSet.next(); return resultSet.next();
@ -50,10 +56,10 @@ public class DataManager {
} }
} }
private static void createPlayerEntry(UUID playerUUID, String playerName) { private static void createPlayerEntry(UUID playerUUID, String playerName, Settings.SynchronisationCluster cluster) {
try (Connection connection = HuskSyncBungeeCord.getConnection()) { try (Connection connection = HuskSyncBungeeCord.getConnection(cluster.clusterId())) {
try (PreparedStatement statement = connection.prepareStatement( try (PreparedStatement statement = connection.prepareStatement(
"INSERT INTO " + Database.PLAYER_TABLE_NAME + " (`uuid`,`username`) VALUES(?,?);")) { "INSERT INTO " + cluster.playerTableName() + " (`uuid`,`username`) VALUES(?,?);")) {
statement.setString(1, playerUUID.toString()); statement.setString(1, playerUUID.toString());
statement.setString(2, playerName); statement.setString(2, playerName);
statement.executeUpdate(); statement.executeUpdate();
@ -63,10 +69,10 @@ public class DataManager {
} }
} }
public static void updatePlayerName(UUID playerUUID, String playerName) { public static void updatePlayerName(UUID playerUUID, String playerName, Settings.SynchronisationCluster cluster) {
try (Connection connection = HuskSyncBungeeCord.getConnection()) { try (Connection connection = HuskSyncBungeeCord.getConnection(cluster.clusterId())) {
try (PreparedStatement statement = connection.prepareStatement( try (PreparedStatement statement = connection.prepareStatement(
"UPDATE " + Database.PLAYER_TABLE_NAME + " SET `username`=? WHERE `uuid`=?;")) { "UPDATE " + cluster.playerTableName() + " SET `username`=? WHERE `uuid`=?;")) {
statement.setString(1, playerName); statement.setString(1, playerName);
statement.setString(2, playerUUID.toString()); statement.setString(2, playerUUID.toString());
statement.executeUpdate(); statement.executeUpdate();
@ -78,36 +84,47 @@ public class DataManager {
/** /**
* Returns a player's PlayerData by their username * Returns a player's PlayerData by their username
*
* @param playerName The PlayerName of the data to get * @param playerName The PlayerName of the data to get
* @return Their {@link PlayerData}; or {@code null} if the player does not exist * @return Their {@link PlayerData}; or {@code null} if the player does not exist
*/ */
public static PlayerData getPlayerDataByName(String playerName) { public static PlayerData getPlayerDataByName(String playerName, String clusterId) {
PlayerData playerData = null; PlayerData playerData = null;
try (Connection connection = HuskSyncBungeeCord.getConnection()) { for (Settings.SynchronisationCluster cluster : Settings.clusters) {
if (cluster.clusterId().equals(clusterId)) {
try (Connection connection = HuskSyncBungeeCord.getConnection(clusterId)) {
try (PreparedStatement statement = connection.prepareStatement( try (PreparedStatement statement = connection.prepareStatement(
"SELECT * FROM " + Database.PLAYER_TABLE_NAME + " WHERE `username`=? LIMIT 1;")) { "SELECT * FROM " + cluster.playerTableName() + " WHERE `username`=? LIMIT 1;")) {
statement.setString(1, playerName); statement.setString(1, playerName);
ResultSet resultSet = statement.executeQuery(); ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) { if (resultSet.next()) {
final UUID uuid = UUID.fromString(resultSet.getString("uuid")); final UUID uuid = UUID.fromString(resultSet.getString("uuid"));
// Get the player data from the cache if it's there, otherwise pull from SQL // Get the player data from the cache if it's there, otherwise pull from SQL
playerData = playerDataCache.getPlayer(uuid); playerData = playerDataCache.get(cluster).getPlayer(uuid);
if (playerData == null) { if (playerData == null) {
playerData = getPlayerData(uuid); playerData = Objects.requireNonNull(getPlayerData(uuid)).get(cluster);
} }
break;
} }
} }
} catch (SQLException e) { } catch (SQLException e) {
plugin.getLogger().log(Level.SEVERE, "An SQL exception occurred", e); plugin.getLogger().log(Level.SEVERE, "An SQL exception occurred", e);
} }
break;
}
}
return playerData; return playerData;
} }
public static PlayerData getPlayerData(UUID playerUUID) { public static Map<Settings.SynchronisationCluster, PlayerData> getPlayerData(UUID playerUUID) {
try (Connection connection = HuskSyncBungeeCord.getConnection()) { HashMap<Settings.SynchronisationCluster, PlayerData> data = new HashMap<>();
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
try (Connection connection = HuskSyncBungeeCord.getConnection(cluster.clusterId())) {
try (PreparedStatement statement = connection.prepareStatement( try (PreparedStatement statement = connection.prepareStatement(
"SELECT * FROM " + Database.DATA_TABLE_NAME + " WHERE `player_id`=(SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?);")) { "SELECT * FROM " + cluster.dataTableName() + " WHERE `player_id`=(SELECT `id` FROM " + cluster.playerTableName() + " WHERE `uuid`=?);")) {
statement.setString(1, playerUUID.toString()); statement.setString(1, playerUUID.toString());
ResultSet resultSet = statement.executeQuery(); ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) { if (resultSet.next()) {
@ -130,15 +147,14 @@ public class DataManager {
final boolean isFlying = resultSet.getBoolean("is_flying"); final boolean isFlying = resultSet.getBoolean("is_flying");
final String serializedAdvancementData = resultSet.getString("advancements"); final String serializedAdvancementData = resultSet.getString("advancements");
final String serializedLocationData = resultSet.getString("location"); final String serializedLocationData = resultSet.getString("location");
final String serializedStatisticData = resultSet.getString("statistics"); final String serializedStatisticData = resultSet.getString("statistics");
return new PlayerData(playerUUID, dataVersionUUID, serializedInventory, serializedEnderChest, data.put(cluster, new PlayerData(playerUUID, dataVersionUUID, serializedInventory, serializedEnderChest,
health, maxHealth, healthScale, hunger, saturation, saturationExhaustion, selectedSlot, serializedStatusEffects, health, maxHealth, healthScale, hunger, saturation, saturationExhaustion, selectedSlot, serializedStatusEffects,
totalExperience, expLevel, expProgress, gameMode, serializedStatisticData, isFlying, totalExperience, expLevel, expProgress, gameMode, serializedStatisticData, isFlying,
serializedAdvancementData, serializedLocationData); serializedAdvancementData, serializedLocationData));
} else { } else {
return PlayerData.DEFAULT_PLAYER_DATA(playerUUID); data.put(cluster, PlayerData.DEFAULT_PLAYER_DATA(playerUUID));
} }
} }
} catch (SQLException e) { } catch (SQLException e) {
@ -146,25 +162,27 @@ public class DataManager {
return null; return null;
} }
} }
return data;
}
public static void updatePlayerData(PlayerData playerData) { public static void updatePlayerData(PlayerData playerData, Settings.SynchronisationCluster cluster) {
// Ignore if the Spigot server didn't properly sync the previous data // Ignore if the Spigot server didn't properly sync the previous data
// Add the new player data to the cache // Add the new player data to the cache
playerDataCache.updatePlayer(playerData); playerDataCache.get(cluster).updatePlayer(playerData);
// SQL: If the player has cached data, update it, otherwise insert new data. // SQL: If the player has cached data, update it, otherwise insert new data.
if (playerHasCachedData(playerData.getPlayerUUID())) { if (playerHasCachedData(playerData.getPlayerUUID(), cluster)) {
updatePlayerSQLData(playerData); updatePlayerSQLData(playerData, cluster);
} else { } else {
insertPlayerData(playerData); insertPlayerData(playerData, cluster);
} }
} }
private static void updatePlayerSQLData(PlayerData playerData) { private static void updatePlayerSQLData(PlayerData playerData, Settings.SynchronisationCluster cluster) {
try (Connection connection = HuskSyncBungeeCord.getConnection()) { try (Connection connection = HuskSyncBungeeCord.getConnection(cluster.clusterId())) {
try (PreparedStatement statement = connection.prepareStatement( try (PreparedStatement statement = connection.prepareStatement(
"UPDATE " + Database.DATA_TABLE_NAME + " SET `version_uuid`=?, `timestamp`=?, `inventory`=?, `ender_chest`=?, `health`=?, `max_health`=?, `health_scale`=?, `hunger`=?, `saturation`=?, `saturation_exhaustion`=?, `selected_slot`=?, `status_effects`=?, `total_experience`=?, `exp_level`=?, `exp_progress`=?, `game_mode`=?, `statistics`=?, `is_flying`=?, `advancements`=?, `location`=? WHERE `player_id`=(SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?);")) { "UPDATE " + cluster.dataTableName() + " SET `version_uuid`=?, `timestamp`=?, `inventory`=?, `ender_chest`=?, `health`=?, `max_health`=?, `health_scale`=?, `hunger`=?, `saturation`=?, `saturation_exhaustion`=?, `selected_slot`=?, `status_effects`=?, `total_experience`=?, `exp_level`=?, `exp_progress`=?, `game_mode`=?, `statistics`=?, `is_flying`=?, `advancements`=?, `location`=? WHERE `player_id`=(SELECT `id` FROM " + cluster.playerTableName() + " WHERE `uuid`=?);")) {
statement.setString(1, playerData.getDataVersionUUID().toString()); statement.setString(1, playerData.getDataVersionUUID().toString());
statement.setTimestamp(2, new Timestamp(Instant.now().getEpochSecond())); statement.setTimestamp(2, new Timestamp(Instant.now().getEpochSecond()));
statement.setString(3, playerData.getSerializedInventory()); statement.setString(3, playerData.getSerializedInventory());
@ -194,10 +212,10 @@ public class DataManager {
} }
} }
private static void insertPlayerData(PlayerData playerData) { private static void insertPlayerData(PlayerData playerData, Settings.SynchronisationCluster cluster) {
try (Connection connection = HuskSyncBungeeCord.getConnection()) { try (Connection connection = HuskSyncBungeeCord.getConnection(cluster.clusterId())) {
try (PreparedStatement statement = connection.prepareStatement( try (PreparedStatement statement = connection.prepareStatement(
"INSERT INTO " + Database.DATA_TABLE_NAME + " (`player_id`,`version_uuid`,`timestamp`,`inventory`,`ender_chest`,`health`,`max_health`,`health_scale`,`hunger`,`saturation`,`saturation_exhaustion`,`selected_slot`,`status_effects`,`total_experience`,`exp_level`,`exp_progress`,`game_mode`,`statistics`,`is_flying`,`advancements`,`location`) VALUES((SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);")) { "INSERT INTO " + cluster.dataTableName() + " (`player_id`,`version_uuid`,`timestamp`,`inventory`,`ender_chest`,`health`,`max_health`,`health_scale`,`hunger`,`saturation`,`saturation_exhaustion`,`selected_slot`,`status_effects`,`total_experience`,`exp_level`,`exp_progress`,`game_mode`,`statistics`,`is_flying`,`advancements`,`location`) VALUES((SELECT `id` FROM " + cluster.playerTableName() + " WHERE `uuid`=?),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);")) {
statement.setString(1, playerData.getPlayerUUID().toString()); statement.setString(1, playerData.getPlayerUUID().toString());
statement.setString(2, playerData.getDataVersionUUID().toString()); statement.setString(2, playerData.getDataVersionUUID().toString());
statement.setTimestamp(3, new Timestamp(Instant.now().getEpochSecond())); statement.setTimestamp(3, new Timestamp(Instant.now().getEpochSecond()));
@ -233,10 +251,10 @@ public class DataManager {
* @param playerUUID The UUID of the player * @param playerUUID The UUID of the player
* @return {@code true} if the player has an entry in the data table * @return {@code true} if the player has an entry in the data table
*/ */
private static boolean playerHasCachedData(UUID playerUUID) { private static boolean playerHasCachedData(UUID playerUUID, Settings.SynchronisationCluster cluster) {
try (Connection connection = HuskSyncBungeeCord.getConnection()) { try (Connection connection = HuskSyncBungeeCord.getConnection(cluster.clusterId())) {
try (PreparedStatement statement = connection.prepareStatement( try (PreparedStatement statement = connection.prepareStatement(
"SELECT * FROM " + Database.DATA_TABLE_NAME + " WHERE `player_id`=(SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?);")) { "SELECT * FROM " + cluster.dataTableName() + " WHERE `player_id`=(SELECT `id` FROM " + cluster.playerTableName() + " WHERE `uuid`=?);")) {
statement.setString(1, playerUUID.toString()); statement.setString(1, playerUUID.toString());
ResultSet resultSet = statement.executeQuery(); ResultSet resultSet = statement.executeQuery();
return resultSet.next(); return resultSet.next();

@ -9,12 +9,13 @@ import java.sql.SQLException;
public abstract class Database { public abstract class Database {
protected HuskSyncBungeeCord plugin; protected HuskSyncBungeeCord plugin;
public final static String DATA_POOL_NAME = "HuskSyncHikariPool"; public String dataPoolName;
public final static String PLAYER_TABLE_NAME = "husksync_players"; public Settings.SynchronisationCluster cluster;
public final static String DATA_TABLE_NAME = "husksync_data";
public Database(HuskSyncBungeeCord instance) { public Database(HuskSyncBungeeCord instance, Settings.SynchronisationCluster cluster) {
plugin = instance; this.plugin = instance;
this.cluster = cluster;
this.dataPoolName = "HuskSyncHikariPool-" + cluster.clusterId();
} }
public abstract Connection getConnection() throws SQLException; public abstract Connection getConnection() throws SQLException;
@ -33,9 +34,9 @@ public abstract class Database {
public abstract void close(); public abstract void close();
public final int hikariMaximumPoolSize = me.william278.husksync.Settings.hikariMaximumPoolSize; public final int hikariMaximumPoolSize = Settings.hikariMaximumPoolSize;
public final int hikariMinimumIdle = me.william278.husksync.Settings.hikariMinimumIdle; public final int hikariMinimumIdle = Settings.hikariMinimumIdle;
public final long hikariMaximumLifetime = me.william278.husksync.Settings.hikariMaximumLifetime; public final long hikariMaximumLifetime = Settings.hikariMaximumLifetime;
public final long hikariKeepAliveTime = me.william278.husksync.Settings.hikariKeepAliveTime; public final long hikariKeepAliveTime = Settings.hikariKeepAliveTime;
public final long hikariConnectionTimeOut = Settings.hikariConnectionTimeOut; public final long hikariConnectionTimeOut = Settings.hikariConnectionTimeOut;
} }

@ -11,8 +11,8 @@ import java.util.logging.Level;
public class MySQL extends Database { public class MySQL extends Database {
final static String[] SQL_SETUP_STATEMENTS = { final String[] SQL_SETUP_STATEMENTS = {
"CREATE TABLE IF NOT EXISTS " + PLAYER_TABLE_NAME + " (" + "CREATE TABLE IF NOT EXISTS " + cluster.playerTableName() + " (" +
"`id` integer NOT NULL AUTO_INCREMENT," + "`id` integer NOT NULL AUTO_INCREMENT," +
"`uuid` char(36) NOT NULL UNIQUE," + "`uuid` char(36) NOT NULL UNIQUE," +
"`username` varchar(16) NOT NULL," + "`username` varchar(16) NOT NULL," +
@ -20,7 +20,7 @@ public class MySQL extends Database {
"PRIMARY KEY (`id`)" + "PRIMARY KEY (`id`)" +
");", ");",
"CREATE TABLE IF NOT EXISTS " + DATA_TABLE_NAME + " (" + "CREATE TABLE IF NOT EXISTS " + cluster.dataTableName() + " (" +
"`player_id` integer NOT NULL," + "`player_id` integer NOT NULL," +
"`version_uuid` char(36) NOT NULL UNIQUE," + "`version_uuid` char(36) NOT NULL UNIQUE," +
"`timestamp` datetime NOT NULL," + "`timestamp` datetime NOT NULL," +
@ -44,7 +44,7 @@ public class MySQL extends Database {
"`location` text NOT NULL," + "`location` text NOT NULL," +
"PRIMARY KEY (`player_id`,`version_uuid`)," + "PRIMARY KEY (`player_id`,`version_uuid`)," +
"FOREIGN KEY (`player_id`) REFERENCES " + PLAYER_TABLE_NAME + " (`id`)" + "FOREIGN KEY (`player_id`) REFERENCES " + cluster.playerTableName() + " (`id`)" +
");" ");"
}; };
@ -55,12 +55,11 @@ public class MySQL extends Database {
public String username = Settings.mySQLUsername; public String username = Settings.mySQLUsername;
public String password = Settings.mySQLPassword; public String password = Settings.mySQLPassword;
public String params = Settings.mySQLParams; public String params = Settings.mySQLParams;
public String dataPoolName = DATA_POOL_NAME;
private HikariDataSource dataSource; private HikariDataSource dataSource;
public MySQL(HuskSyncBungeeCord instance) { public MySQL(HuskSyncBungeeCord instance, Settings.SynchronisationCluster cluster) {
super(instance); super(instance, cluster);
} }
@Override @Override

@ -2,6 +2,7 @@ package me.william278.husksync.bungeecord.data.sql;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import me.william278.husksync.HuskSyncBungeeCord; import me.william278.husksync.HuskSyncBungeeCord;
import me.william278.husksync.Settings;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -12,18 +13,18 @@ import java.util.logging.Level;
public class SQLite extends Database { public class SQLite extends Database {
final static String[] SQL_SETUP_STATEMENTS = { final String[] SQL_SETUP_STATEMENTS = {
"PRAGMA foreign_keys = ON;", "PRAGMA foreign_keys = ON;",
"PRAGMA encoding = 'UTF-8';", "PRAGMA encoding = 'UTF-8';",
"CREATE TABLE IF NOT EXISTS " + PLAYER_TABLE_NAME + " (" + "CREATE TABLE IF NOT EXISTS " + cluster.playerTableName() + " (" +
"`id` integer PRIMARY KEY," + "`id` integer PRIMARY KEY," +
"`uuid` char(36) NOT NULL UNIQUE," + "`uuid` char(36) NOT NULL UNIQUE," +
"`username` varchar(16) NOT NULL" + "`username` varchar(16) NOT NULL" +
");", ");",
"CREATE TABLE IF NOT EXISTS " + DATA_TABLE_NAME + " (" + "CREATE TABLE IF NOT EXISTS " + cluster.dataTableName() + " (" +
"`player_id` integer NOT NULL REFERENCES " + PLAYER_TABLE_NAME + "(`id`)," + "`player_id` integer NOT NULL REFERENCES " + cluster.playerTableName() + "(`id`)," +
"`version_uuid` char(36) NOT NULL UNIQUE," + "`version_uuid` char(36) NOT NULL UNIQUE," +
"`timestamp` datetime NOT NULL," + "`timestamp` datetime NOT NULL," +
"`inventory` longtext NOT NULL," + "`inventory` longtext NOT NULL," +
@ -49,24 +50,26 @@ public class SQLite extends Database {
");" ");"
}; };
private static final String DATABASE_NAME = "HuskSyncData"; private String getDatabaseName() {
return cluster.databaseName() + "Data";
}
private HikariDataSource dataSource; private HikariDataSource dataSource;
public SQLite(HuskSyncBungeeCord instance) { public SQLite(HuskSyncBungeeCord instance, Settings.SynchronisationCluster cluster) {
super(instance); super(instance, cluster);
} }
// Create the database file if it does not exist yet // Create the database file if it does not exist yet
private void createDatabaseFileIfNotExist() { private void createDatabaseFileIfNotExist() {
File databaseFile = new File(plugin.getDataFolder(), DATABASE_NAME + ".db"); File databaseFile = new File(plugin.getDataFolder(), getDatabaseName() + ".db");
if (!databaseFile.exists()) { if (!databaseFile.exists()) {
try { try {
if (!databaseFile.createNewFile()) { if (!databaseFile.createNewFile()) {
plugin.getLogger().log(Level.SEVERE, "Failed to write new file: " + DATABASE_NAME + ".db (file already exists)"); plugin.getLogger().log(Level.SEVERE, "Failed to write new file: " + getDatabaseName() + ".db (file already exists)");
} }
} catch (IOException e) { } catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "An error occurred writing a file: " + DATABASE_NAME + ".db (" + e.getCause() + ")"); plugin.getLogger().log(Level.SEVERE, "An error occurred writing a file: " + getDatabaseName() + ".db (" + e.getCause() + ")");
} }
} }
} }
@ -82,7 +85,7 @@ public class SQLite extends Database {
createDatabaseFileIfNotExist(); createDatabaseFileIfNotExist();
// Create new HikariCP data source // Create new HikariCP data source
final String jdbcUrl = "jdbc:sqlite:" + plugin.getDataFolder().getAbsolutePath() + File.separator + DATABASE_NAME + ".db"; final String jdbcUrl = "jdbc:sqlite:" + plugin.getDataFolder().getAbsolutePath() + File.separator + getDatabaseName() + ".db";
dataSource = new HikariDataSource(); dataSource = new HikariDataSource();
dataSource.setDataSourceClassName("org.sqlite.SQLiteDataSource"); dataSource.setDataSourceClassName("org.sqlite.SQLiteDataSource");
dataSource.addDataSourceProperty("url", jdbcUrl); dataSource.addDataSourceProperty("url", jdbcUrl);
@ -93,7 +96,7 @@ public class SQLite extends Database {
dataSource.setMaxLifetime(hikariMaximumLifetime); dataSource.setMaxLifetime(hikariMaximumLifetime);
dataSource.setKeepaliveTime(hikariKeepAliveTime); dataSource.setKeepaliveTime(hikariKeepAliveTime);
dataSource.setConnectionTimeout(hikariConnectionTimeOut); dataSource.setConnectionTimeout(hikariConnectionTimeOut);
dataSource.setPoolName(DATA_POOL_NAME); dataSource.setPoolName(dataPoolName);
} }
@Override @Override

@ -12,6 +12,7 @@ import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler; import net.md_5.bungee.event.EventHandler;
import java.io.IOException; import java.io.IOException;
import java.util.Map;
import java.util.logging.Level; import java.util.logging.Level;
public class BungeeEventListener implements Listener { public class BungeeEventListener implements Listener {
@ -26,15 +27,18 @@ public class BungeeEventListener implements Listener {
DataManager.ensurePlayerExists(player.getUniqueId(), player.getName()); DataManager.ensurePlayerExists(player.getUniqueId(), player.getName());
// Get the player's data from SQL // Get the player's data from SQL
final PlayerData data = DataManager.getPlayerData(player.getUniqueId()); final Map<Settings.SynchronisationCluster,PlayerData> data = DataManager.getPlayerData(player.getUniqueId());
// Update the player's data from SQL onto the cache // Update the player's data from SQL onto the cache
DataManager.playerDataCache.updatePlayer(data); assert data != null;
for (Settings.SynchronisationCluster cluster : data.keySet()) {
DataManager.playerDataCache.get(cluster).updatePlayer(data.get(cluster));
}
// Send a message asking the bukkit to request data on join // Send a message asking the bukkit to request data on join
try { try {
new me.william278.husksync.redis.RedisMessage(me.william278.husksync.redis.RedisMessage.MessageType.REQUEST_DATA_ON_JOIN, new RedisMessage(RedisMessage.MessageType.REQUEST_DATA_ON_JOIN,
new me.william278.husksync.redis.RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null), new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null),
RedisMessage.RequestOnJoinUpdateType.ADD_REQUESTER.toString(), player.getUniqueId().toString()).send(); RedisMessage.RequestOnJoinUpdateType.ADD_REQUESTER.toString(), player.getUniqueId().toString()).send();
} catch (IOException e) { } catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to serialize request data on join message data"); plugin.getLogger().log(Level.SEVERE, "Failed to serialize request data on join message data");

@ -14,6 +14,7 @@ import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.connection.ProxiedPlayer;
import java.io.IOException; import java.io.IOException;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Level; import java.util.logging.Level;
@ -26,15 +27,21 @@ public class BungeeRedisListener extends RedisListener {
listen(); listen();
} }
private PlayerData getPlayerCachedData(UUID uuid) { private PlayerData getPlayerCachedData(UUID uuid, String clusterId) {
PlayerData data = null;
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
if (cluster.clusterId().equals(clusterId)) {
// Get the player data from the cache // Get the player data from the cache
PlayerData cachedData = DataManager.playerDataCache.getPlayer(uuid); PlayerData cachedData = DataManager.playerDataCache.get(cluster).getPlayer(uuid);
if (cachedData != null) { if (cachedData != null) {
return cachedData; return cachedData;
} }
final PlayerData data = DataManager.getPlayerData(uuid); // Get their player data from MySQL data = Objects.requireNonNull(DataManager.getPlayerData(uuid)).get(cluster); // Get their player data from MySQL
DataManager.playerDataCache.updatePlayer(data); // Update the cache DataManager.playerDataCache.get(cluster).updatePlayer(data); // Update the cache
break;
}
}
return data; // Return the data return data; // Return the data
} }
@ -62,13 +69,13 @@ public class BungeeRedisListener extends RedisListener {
try { try {
// Send the reply, serializing the message data // Send the reply, serializing the message data
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET, new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, requestingPlayerUUID), new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, requestingPlayerUUID, message.getMessageTarget().targetClusterId()),
RedisMessage.serialize(getPlayerCachedData(requestingPlayerUUID))) RedisMessage.serialize(getPlayerCachedData(requestingPlayerUUID, message.getMessageTarget().targetClusterId())))
.send(); .send();
// Send an update to all bukkit servers removing the player from the requester cache // Send an update to all bukkit servers removing the player from the requester cache
new RedisMessage(RedisMessage.MessageType.REQUEST_DATA_ON_JOIN, new RedisMessage(RedisMessage.MessageType.REQUEST_DATA_ON_JOIN,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null), new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
RedisMessage.RequestOnJoinUpdateType.REMOVE_REQUESTER.toString(), requestingPlayerUUID.toString()) RedisMessage.RequestOnJoinUpdateType.REMOVE_REQUESTER.toString(), requestingPlayerUUID.toString())
.send(); .send();
@ -96,7 +103,12 @@ public class BungeeRedisListener extends RedisListener {
} }
// Update the data in the cache and SQL // Update the data in the cache and SQL
DataManager.updatePlayerData(playerData); for (Settings.SynchronisationCluster cluster : Settings.clusters) {
if (cluster.clusterId().equals(message.getMessageTarget().targetClusterId())) {
DataManager.updatePlayerData(playerData, cluster);
break;
}
}
// Reply with the player data if they are still online (switching server) // Reply with the player data if they are still online (switching server)
try { try {
@ -104,7 +116,7 @@ public class BungeeRedisListener extends RedisListener {
if (player != null) { if (player != null) {
if (player.isConnected()) { if (player.isConnected()) {
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET, new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, playerData.getPlayerUUID()), new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, playerData.getPlayerUUID(), message.getMessageTarget().targetClusterId()),
RedisMessage.serialize(playerData)) RedisMessage.serialize(playerData))
.send(); .send();
@ -125,12 +137,12 @@ public class BungeeRedisListener extends RedisListener {
final String huskSyncVersion = message.getMessageDataElements()[3]; final String huskSyncVersion = message.getMessageDataElements()[3];
try { try {
new RedisMessage(RedisMessage.MessageType.CONNECTION_HANDSHAKE, new RedisMessage(RedisMessage.MessageType.CONNECTION_HANDSHAKE,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null), new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
serverUUID.toString(), plugin.getProxy().getName()) serverUUID.toString(), plugin.getProxy().getName())
.send(); .send();
HuskSyncBungeeCord.synchronisedServers.add( HuskSyncBungeeCord.synchronisedServers.add(
new HuskSyncBungeeCord.Server(serverUUID, hasMySqlPlayerDataBridge, new HuskSyncBungeeCord.Server(serverUUID, hasMySqlPlayerDataBridge,
huskSyncVersion, bukkitBrand)); huskSyncVersion, bukkitBrand, message.getMessageTarget().targetClusterId()));
log(Level.INFO, "Completed handshake with " + bukkitBrand + " server (" + serverUUID + ")"); log(Level.INFO, "Completed handshake with " + bukkitBrand + " server (" + serverUUID + ")");
} catch (IOException e) { } catch (IOException e) {
log(Level.SEVERE, "Failed to serialize handshake message data"); log(Level.SEVERE, "Failed to serialize handshake message data");

@ -36,6 +36,7 @@ public class MPDBMigrator {
public static HashMap<PlayerData, String> incomingPlayerData; public static HashMap<PlayerData, String> incomingPlayerData;
public static MigrationSettings migrationSettings = new MigrationSettings(); public static MigrationSettings migrationSettings = new MigrationSettings();
private static Settings.SynchronisationCluster targetCluster;
private static Database sourceDatabase; private static Database sourceDatabase;
private static HashSet<MPDBPlayerData> mpdbPlayerData; private static HashSet<MPDBPlayerData> mpdbPlayerData;
@ -59,6 +60,18 @@ public class MPDBMigrator {
return; return;
} }
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
if (migrationSettings.targetCluster.equals(cluster.clusterId())) {
targetCluster = cluster;
break;
}
}
if (targetCluster == null) {
plugin.getLogger().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;
}
migratedDataSent = 0; migratedDataSent = 0;
playersMigrated = 0; playersMigrated = 0;
mpdbPlayerData = new HashSet<>(); mpdbPlayerData = new HashSet<>();
@ -91,11 +104,11 @@ public class MPDBMigrator {
// Clear the new database out of current data // Clear the new database out of current data
private void prepareTargetDatabase() { private void prepareTargetDatabase() {
plugin.getLogger().log(Level.INFO, "Preparing target database..."); plugin.getLogger().log(Level.INFO, "Preparing target database...");
try (Connection connection = HuskSyncBungeeCord.getConnection()) { try (Connection connection = HuskSyncBungeeCord.getConnection(targetCluster.clusterId())) {
try (PreparedStatement statement = connection.prepareStatement("DELETE FROM " + Database.PLAYER_TABLE_NAME + ";")) { try (PreparedStatement statement = connection.prepareStatement("DELETE FROM " + targetCluster.playerTableName() + ";")) {
statement.executeUpdate(); statement.executeUpdate();
} }
try (PreparedStatement statement = connection.prepareStatement("DELETE FROM " + Database.DATA_TABLE_NAME + ";")) { try (PreparedStatement statement = connection.prepareStatement("DELETE FROM " + targetCluster.dataTableName() + ";")) {
statement.executeUpdate(); statement.executeUpdate();
} }
} catch (SQLException e) { } catch (SQLException e) {
@ -182,7 +195,7 @@ public class MPDBMigrator {
for (MPDBPlayerData data : mpdbPlayerData) { for (MPDBPlayerData data : mpdbPlayerData) {
try { try {
new RedisMessage(RedisMessage.MessageType.DECODE_MPDB_DATA, new RedisMessage(RedisMessage.MessageType.DECODE_MPDB_DATA,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null), new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null),
processingServer.serverUUID().toString(), processingServer.serverUUID().toString(),
RedisMessage.serialize(data)) RedisMessage.serialize(data))
.send(); .send();
@ -214,7 +227,10 @@ public class MPDBMigrator {
DataManager.ensurePlayerExists(playerData.getPlayerUUID(), playerName); DataManager.ensurePlayerExists(playerData.getPlayerUUID(), playerName);
// Update the data in the cache and SQL // Update the data in the cache and SQL
DataManager.updatePlayerData(playerData); for (Settings.SynchronisationCluster cluster : Settings.clusters) {
DataManager.updatePlayerData(playerData, cluster);
break;
}
playersSaved++; playersSaved++;
plugin.getLogger().log(Level.INFO, "Saved data for " + playersSaved + "/" + playersMigrated + " players"); plugin.getLogger().log(Level.INFO, "Saved data for " + playersSaved + "/" + playersMigrated + " players");
@ -250,6 +266,8 @@ public class MPDBMigrator {
public String enderChestDataTable; public String enderChestDataTable;
public String expDataTable; public String expDataTable;
public String targetCluster;
public MigrationSettings() { public MigrationSettings() {
sourceHost = "localhost"; sourceHost = "localhost";
sourcePort = 3306; sourcePort = 3306;
@ -257,6 +275,8 @@ public class MPDBMigrator {
sourceUsername = "root"; sourceUsername = "root";
sourcePassword = "pa55w0rd"; sourcePassword = "pa55w0rd";
targetCluster = "main";
inventoryDataTable = "mpdb_inventory"; inventoryDataTable = "mpdb_inventory";
enderChestDataTable = "mpdb_enderchest"; enderChestDataTable = "mpdb_enderchest";
expDataTable = "mpdb_experience"; expDataTable = "mpdb_experience";
@ -268,14 +288,14 @@ public class MPDBMigrator {
*/ */
public static class MigratorMySQL extends MySQL { public static class MigratorMySQL extends MySQL {
public MigratorMySQL(HuskSyncBungeeCord instance, String host, int port, String database, String username, String password) { public MigratorMySQL(HuskSyncBungeeCord instance, String host, int port, String database, String username, String password) {
super(instance); super(instance, null);
super.host = host; super.host = host;
super.port = port; super.port = port;
super.database = database; super.database = database;
super.username = username; super.username = username;
super.password = password; super.password = password;
super.params = "?useSSL=false"; super.params = "?useSSL=false";
super.dataPoolName = DATA_POOL_NAME + "Migrator"; super.dataPoolName = super.dataPoolName + "Migrator";
} }
} }

@ -18,5 +18,10 @@ data_storage_settings:
maximum_lifetime: 1800000 maximum_lifetime: 1800000
keepalive_time: 0 keepalive_time: 0
connection_timeout: 5000 connection_timeout: 5000
clusters:
main:
player_table: 'husksync_players'
data_table: 'husksync_data'
check_for_updates: true check_for_updates: true
config_file_version: 1.0.2 config_file_version: 1.1

@ -11,3 +11,4 @@ error_cannot_view_own_inventory: '[Error:](#ff3300) [You can''t access your own
error_cannot_view_own_ender_chest: '[Error:](#ff3300) [You can''t access your own ender chest!](#ff7e5e)' error_cannot_view_own_ender_chest: '[Error:](#ff3300) [You can''t access your own ender chest!](#ff7e5e)'
error_console_command_only: '[Error:](#ff3300) [That command can only be run through the %1% console](#ff7e5e)' error_console_command_only: '[Error:](#ff3300) [That command can only be run through the %1% console](#ff7e5e)'
error_no_servers_proxied: '[Error:](#ff3300) [Failed to process operation; no servers are online that have HuskSync installed. Please ensure HuskSync is installed on both the Proxy server and all servers you wish to synchronise data between.](#ff7e5e)' error_no_servers_proxied: '[Error:](#ff3300) [Failed to process operation; no servers are online that have HuskSync installed. Please ensure HuskSync is installed on both the Proxy server and all servers you wish to synchronise data between.](#ff7e5e)'
error_invalid_cluster: '[Error:](#ff3300) [Please specify the ID of a valid cluster.](#ff7e5e)'

@ -1,5 +1,7 @@
package me.william278.husksync; package me.william278.husksync;
import java.util.ArrayList;
/** /**
* Settings class, holds values loaded from the plugin config (either Bukkit or Bungee) * Settings class, holds values loaded from the plugin config (either Bukkit or Bungee)
*/ */
@ -27,6 +29,9 @@ public class Settings {
// Messages language // Messages language
public static String language; public static String language;
// Cluster IDs
public static ArrayList<SynchronisationCluster> clusters = new ArrayList<>();
// SQL settings // SQL settings
public static DataStorageType dataStorageType; public static DataStorageType dataStorageType;
@ -61,6 +66,9 @@ public class Settings {
public static boolean syncAdvancements; public static boolean syncAdvancements;
public static boolean syncLocation; public static boolean syncLocation;
// This Cluster ID
public static String cluster;
/* /*
* Enum definitions * Enum definitions
*/ */
@ -74,4 +82,10 @@ public class Settings {
MYSQL, MYSQL,
SQLITE SQLITE
} }
/**
* Defines information for a synchronisation cluster as listed on the proxy
*/
public record SynchronisationCluster(String clusterId, String databaseName, String playerTableName, String dataTableName) {
}
} }

@ -159,7 +159,7 @@ public class RedisMessage {
* A record that defines the target of a plugin message; a spigot server or the proxy server(s). * A record that defines the target of a plugin message; a spigot server or the proxy server(s).
* For Bukkit servers, the name of the server must also be specified * For Bukkit servers, the name of the server must also be specified
*/ */
public record MessageTarget(Settings.ServerType targetServerType, UUID targetPlayerUUID) implements Serializable { } public record MessageTarget(Settings.ServerType targetServerType, UUID targetPlayerUUID, String targetClusterId) implements Serializable { }
/** /**
* Deserialize an object from a Base64 string * Deserialize an object from a Base64 string

Loading…
Cancel
Save