diff --git a/.gitignore b/.gitignore index d7b144fa..b1946b65 100644 --- a/.gitignore +++ b/.gitignore @@ -106,7 +106,7 @@ build/ # Ignore Gradle GUI config gradle-app.setting -# me.william278.crossserversync.bungeecord.PlayerDataCache of project +# me.william278.crossserversync.bungeecord.data.DataManager.PlayerDataCache of project .gradletasknamecache **/build/ diff --git a/bukkit/src/main/java/me/william278/crossserversync/bukkit/BukkitRedisListener.java b/bukkit/src/main/java/me/william278/crossserversync/bukkit/BukkitRedisListener.java deleted file mode 100644 index a0315e04..00000000 --- a/bukkit/src/main/java/me/william278/crossserversync/bukkit/BukkitRedisListener.java +++ /dev/null @@ -1,50 +0,0 @@ -package me.william278.crossserversync.bukkit; - -import me.william278.crossserversync.Settings; -import me.william278.crossserversync.redis.RedisListener; -import me.william278.crossserversync.redis.RedisMessage; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import java.util.logging.Level; - -public class BukkitRedisListener extends RedisListener { - - private static final CrossServerSyncBukkit plugin = CrossServerSyncBukkit.getInstance(); - - // Initialize the listener on the bukkit server - public BukkitRedisListener() { - listen(); - } - - /** - * Handle an incoming {@link RedisMessage} - * - * @param message The {@link RedisMessage} to handle - */ - @Override - public void handleMessage(RedisMessage message) { - // Ignore messages for proxy servers - if (message.getMessageTarget().targetServerType() != Settings.ServerType.BUKKIT) { - return; - } - // Handle the message for the player - for (Player player : Bukkit.getOnlinePlayers()) { - if (player.getUniqueId() == message.getMessageTarget().targetPlayerName()) { - - return; - } - } - } - - /** - * Log to console - * - * @param level The {@link Level} to log - * @param message Message to log - */ - @Override - public void log(Level level, String message) { - plugin.getLogger().log(level, message); - } -} diff --git a/bukkit/src/main/java/me/william278/crossserversync/bukkit/CrossServerSyncBukkit.java b/bukkit/src/main/java/me/william278/crossserversync/bukkit/CrossServerSyncBukkit.java index a4cd3f88..4c7fbd7b 100644 --- a/bukkit/src/main/java/me/william278/crossserversync/bukkit/CrossServerSyncBukkit.java +++ b/bukkit/src/main/java/me/william278/crossserversync/bukkit/CrossServerSyncBukkit.java @@ -1,6 +1,9 @@ package me.william278.crossserversync.bukkit; import me.william278.crossserversync.bukkit.config.ConfigLoader; +import me.william278.crossserversync.bukkit.data.LastDataUpdateUUIDCache; +import me.william278.crossserversync.bukkit.listener.BukkitRedisListener; +import me.william278.crossserversync.bukkit.listener.EventListener; import org.bukkit.plugin.java.JavaPlugin; public final class CrossServerSyncBukkit extends JavaPlugin { @@ -10,6 +13,8 @@ public final class CrossServerSyncBukkit extends JavaPlugin { return instance; } + public static LastDataUpdateUUIDCache lastDataUpdateUUIDCache; + @Override public void onLoad() { instance = this; @@ -26,12 +31,22 @@ public final class CrossServerSyncBukkit extends JavaPlugin { reloadConfig(); ConfigLoader.loadSettings(getConfig()); + // Initialize last data update UUID cache + lastDataUpdateUUIDCache = new LastDataUpdateUUIDCache(); + // Initialize the redis listener new BukkitRedisListener(); + + // Initialize event listener + getServer().getPluginManager().registerEvents(new EventListener(), this); + + // Log to console + getLogger().info("Enabled CrossServerSync (" + getServer().getName() + ") v" + getDescription().getVersion()); } @Override public void onDisable() { // Plugin shutdown logic + getLogger().info("Disabled CrossServerSync (" + getServer().getName() + ") v" + getDescription().getVersion()); } } diff --git a/bukkit/src/main/java/me/william278/crossserversync/bukkit/InventorySerializer.java b/bukkit/src/main/java/me/william278/crossserversync/bukkit/InventorySerializer.java index ae00c954..eec8f1eb 100644 --- a/bukkit/src/main/java/me/william278/crossserversync/bukkit/InventorySerializer.java +++ b/bukkit/src/main/java/me/william278/crossserversync/bukkit/InventorySerializer.java @@ -3,7 +3,6 @@ package me.william278.crossserversync.bukkit; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; import org.bukkit.util.io.BukkitObjectInputStream; import org.bukkit.util.io.BukkitObjectOutputStream; import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; @@ -107,13 +106,17 @@ public final class InventorySerializer { } /** - * Gets an array of ItemStacks from Base64 string. + * Gets an array of ItemStacks from a Base64 string. * * @param data Base64 string to convert to ItemStack array. * @return ItemStack array created from the Base64 string. * @throws IOException in the event the class type cannot be decoded */ public static ItemStack[] itemStackArrayFromBase64(String data) throws IOException { + // Return an empty ItemStack[] if the data is empty + if (data.isEmpty()) { + return new ItemStack[0]; + } try (ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(data))) { BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream); ItemStack[] items = new ItemStack[dataInput.readInt()]; diff --git a/bukkit/src/main/java/me/william278/crossserversync/bukkit/data/LastDataUpdateUUIDCache.java b/bukkit/src/main/java/me/william278/crossserversync/bukkit/data/LastDataUpdateUUIDCache.java new file mode 100644 index 00000000..c1247000 --- /dev/null +++ b/bukkit/src/main/java/me/william278/crossserversync/bukkit/data/LastDataUpdateUUIDCache.java @@ -0,0 +1,25 @@ +package me.william278.crossserversync.bukkit.data; + +import java.util.HashMap; +import java.util.UUID; + +public class LastDataUpdateUUIDCache { + + /** + * Map of Player UUIDs to last-updated PlayerData version UUIDs + */ + private static HashMap lastUpdatedPlayerDataUUIDs; + + public LastDataUpdateUUIDCache() { + lastUpdatedPlayerDataUUIDs = new HashMap<>(); + } + + public UUID getVersionUUID(UUID playerUUID) { + return lastUpdatedPlayerDataUUIDs.get(playerUUID); + } + + public void setVersionUUID(UUID playerUUID, UUID dataVersionUUID) { + lastUpdatedPlayerDataUUIDs.put(playerUUID, dataVersionUUID); + } + +} diff --git a/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/BukkitRedisListener.java b/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/BukkitRedisListener.java new file mode 100644 index 00000000..8bef6f34 --- /dev/null +++ b/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/BukkitRedisListener.java @@ -0,0 +1,69 @@ +package me.william278.crossserversync.bukkit.listener; + +import me.william278.crossserversync.bukkit.InventorySerializer; +import me.william278.crossserversync.PlayerData; +import me.william278.crossserversync.Settings; +import me.william278.crossserversync.bukkit.CrossServerSyncBukkit; +import me.william278.crossserversync.redis.RedisListener; +import me.william278.crossserversync.redis.RedisMessage; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.IOException; +import java.util.logging.Level; + +public class BukkitRedisListener extends RedisListener { + + private static final CrossServerSyncBukkit plugin = CrossServerSyncBukkit.getInstance(); + + // Initialize the listener on the bukkit server + public BukkitRedisListener() { + listen(); + } + + /** + * Handle an incoming {@link RedisMessage} + * + * @param message The {@link RedisMessage} to handle + */ + @Override + public void handleMessage(RedisMessage message) { + // Ignore messages for proxy servers + if (message.getMessageTarget().targetServerType() != Settings.ServerType.BUKKIT) { + return; + } + // Handle the message for the player + for (Player player : Bukkit.getOnlinePlayers()) { + if (player.getUniqueId() == message.getMessageTarget().targetPlayerName()) { + if (message.getMessageType() == RedisMessage.MessageType.PLAYER_DATA_REPLY) { + try { + // Deserialize the received PlayerData + PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageData()); + + // Set the player's data //todo do more stuff like health etc + InventorySerializer.setPlayerItems(player, InventorySerializer.itemStackArrayFromBase64(data.getSerializedInventory())); + InventorySerializer.setPlayerEnderChest(player, InventorySerializer.itemStackArrayFromBase64(data.getSerializedEnderChest())); + + // Update last loaded data UUID + CrossServerSyncBukkit.lastDataUpdateUUIDCache.setVersionUUID(player.getUniqueId(), data.getDataVersionUUID()); + } catch (IOException | ClassNotFoundException e) { + log(Level.SEVERE, "Failed to deserialize PlayerData when handling a reply from the proxy with PlayerData"); + e.printStackTrace(); + } + } + return; + } + } + } + + /** + * Log to console + * + * @param level The {@link Level} to log + * @param message Message to log + */ + @Override + public void log(Level level, String message) { + plugin.getLogger().log(level, message); + } +} diff --git a/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/EventListener.java b/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/EventListener.java new file mode 100644 index 00000000..a2a00cec --- /dev/null +++ b/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/EventListener.java @@ -0,0 +1,67 @@ +package me.william278.crossserversync.bukkit.listener; + +import me.william278.crossserversync.PlayerData; +import me.william278.crossserversync.Settings; +import me.william278.crossserversync.bukkit.CrossServerSyncBukkit; +import me.william278.crossserversync.bukkit.InventorySerializer; +import me.william278.crossserversync.redis.RedisMessage; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +import java.io.IOException; +import java.util.UUID; +import java.util.logging.Level; + +public class EventListener implements Listener { + + private static final CrossServerSyncBukkit plugin = CrossServerSyncBukkit.getInstance(); + + /** + * Returns the new serialized PlayerData for a player. + * @param player The {@link Player} to get the new serialized PlayerData for + * @return The {@link PlayerData}, serialized as a {@link String} + * @throws IOException If the serialization fails + */ + private static String getNewSerializedPlayerData(Player player) throws IOException { + return RedisMessage.serialize(new PlayerData(player.getUniqueId(), + InventorySerializer.getSerializedInventoryContents(player), + InventorySerializer.getSerializedEnderChestContents(player))); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + // When a player leaves a Bukkit server + final Player player = event.getPlayer(); + + try { + // Get the player's last updated PlayerData version UUID + final UUID lastUpdatedDataVersion = CrossServerSyncBukkit.lastDataUpdateUUIDCache.getVersionUUID(player.getUniqueId()); + if (lastUpdatedDataVersion == null) return; // Return if the player has not been properly updated. + + // Send a redis message with the player's last updated PlayerData version UUID and their new PlayerData + new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE, + new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null), + lastUpdatedDataVersion.toString(), getNewSerializedPlayerData(player)).send(); + } catch (IOException e) { + plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData update to the proxy", e); + } + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + // When a player joins a Bukkit server + final Player player = event.getPlayer(); + + try { + // Send a redis message requesting the player data + new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_REQUEST, + new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null), + player.getUniqueId().toString()).send(); + } catch (IOException e) { + plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData fetch request", e); + } + } +} diff --git a/bungeecord/build.gradle b/bungeecord/build.gradle index 93b05103..76e401ec 100644 --- a/bungeecord/build.gradle +++ b/bungeecord/build.gradle @@ -3,12 +3,14 @@ dependencies { implementation project(path: ':common', configuration: 'shadow') implementation 'redis.clients:jedis:3.7.0' + implementation 'com.zaxxer:HikariCP:5.0.0' compileOnly 'net.md-5:bungeecord-api:1.16-R0.5-SNAPSHOT' } shadowJar { relocate 'redis.clients', 'me.William278.crossserversync.libraries.jedis' + relocate 'com.zaxxer', 'me.William278.crossserversync.libraries.hikari' relocate 'org.bstats', 'me.William278.crossserversync.libraries.plan' relocate 'org.apache.commons', 'me.William278.crossserversync.libraries.apache-commons' relocate 'org.slf4j', 'me.William278.crossserversync.libraries.slf4j' diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/BungeeRedisListener.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/BungeeRedisListener.java deleted file mode 100644 index 6edf42b5..00000000 --- a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/BungeeRedisListener.java +++ /dev/null @@ -1,41 +0,0 @@ -package me.william278.crossserversync.bungeecord; - -import me.william278.crossserversync.Settings; -import me.william278.crossserversync.redis.RedisListener; -import me.william278.crossserversync.redis.RedisMessage; - -import java.util.logging.Level; - -public class BungeeRedisListener extends RedisListener { - - private static final CrossServerSyncBungeeCord plugin = CrossServerSyncBungeeCord.getInstance(); - - // Initialize the listener on the bungee - public BungeeRedisListener() { - listen(); - } - - /** - * Handle an incoming {@link RedisMessage} - * - * @param message The {@link RedisMessage} to handle - */ - @Override - public void handleMessage(RedisMessage message) { - // Ignore messages destined for Bukkit servers - if (message.getMessageTarget().targetServerType() != Settings.ServerType.BUNGEECORD) { - return; - } - } - - /** - * Log to console - * - * @param level The {@link Level} to log - * @param message Message to log - */ - @Override - public void log(Level level, String message) { - plugin.getLogger().log(level, message); - } -} diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/CrossServerSyncBungeeCord.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/CrossServerSyncBungeeCord.java index 93ed42e5..0d387493 100644 --- a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/CrossServerSyncBungeeCord.java +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/CrossServerSyncBungeeCord.java @@ -1,9 +1,18 @@ package me.william278.crossserversync.bungeecord; +import me.william278.crossserversync.Settings; import me.william278.crossserversync.bungeecord.config.ConfigLoader; import me.william278.crossserversync.bungeecord.config.ConfigManager; +import me.william278.crossserversync.bungeecord.data.DataManager; +import me.william278.crossserversync.bungeecord.data.sql.Database; +import me.william278.crossserversync.bungeecord.data.sql.MySQL; +import me.william278.crossserversync.bungeecord.data.sql.SQLite; +import me.william278.crossserversync.bungeecord.listener.BungeeEventListener; +import me.william278.crossserversync.bungeecord.listener.BungeeRedisListener; import net.md_5.bungee.api.plugin.Plugin; +import java.sql.Connection; +import java.sql.SQLException; import java.util.Objects; public final class CrossServerSyncBungeeCord extends Plugin { @@ -13,7 +22,10 @@ public final class CrossServerSyncBungeeCord extends Plugin { return instance; } - public PlayerDataCache cache; + private static Database database; + public static Connection getConnection() throws SQLException { + return database.getConnection(); + } @Override public void onLoad() { @@ -30,15 +42,34 @@ public final class CrossServerSyncBungeeCord extends Plugin { // Load settings from config ConfigLoader.loadSettings(Objects.requireNonNull(ConfigManager.getConfig())); + // Initialize the database + database = switch (Settings.dataStorageType) { + case SQLITE -> new SQLite(this); + case MYSQL -> new MySQL(this); + }; + database.load(); + // Setup player data cache - cache = new PlayerDataCache(); + DataManager.setupCache(); + + // Initialize PreLoginEvent listener + getProxy().getPluginManager().registerListener(this, new BungeeEventListener()); // Initialize the redis listener new BungeeRedisListener(); + + // Log to console + getLogger().info("Enabled CrossServerSync (" + getProxy().getName() + ") v" + getDescription().getVersion()); } @Override public void onDisable() { // Plugin shutdown logic + + // Close the database + database.close(); + + // Log to console + getLogger().info("Disabled CrossServerSync (" + getProxy().getName() + ") v" + getDescription().getVersion()); } } diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/PlayerDataCache.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/PlayerDataCache.java deleted file mode 100644 index 27a5bf8a..00000000 --- a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/PlayerDataCache.java +++ /dev/null @@ -1,51 +0,0 @@ -package me.william278.crossserversync.bungeecord; - -import me.william278.crossserversync.PlayerData; - -import java.util.HashSet; -import java.util.UUID; - -public class PlayerDataCache { - - // The cached player data - public HashSet playerData; - - public PlayerDataCache() { - playerData = new HashSet<>(); - } - - /** - * Update ar add data for a player to the cache - * @param newData The player's new/updated {@link PlayerData} - */ - public void updatePlayer(PlayerData newData) { - // Remove the old data if it exists - PlayerData oldData = null; - for (PlayerData data : playerData) { - if (data.getPlayerUUID() == newData.getPlayerUUID()) { - oldData = data; - } - } - if (oldData != null) { - playerData.remove(oldData); - } - - // Add the new data - playerData.add(newData); - } - - /** - * Get a player's {@link PlayerData} by their {@link UUID} - * @param playerUUID The {@link UUID} of the player to check - * @return The player's {@link PlayerData} - */ - public PlayerData getPlayer(UUID playerUUID) { - for (PlayerData data : playerData) { - if (data.getPlayerUUID() == playerUUID) { - return data; - } - } - return null; - } - -} diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/DataManager.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/DataManager.java new file mode 100644 index 00000000..7d811fa3 --- /dev/null +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/DataManager.java @@ -0,0 +1,243 @@ +package me.william278.crossserversync.bungeecord.data; + +import me.william278.crossserversync.PlayerData; +import me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord; +import me.william278.crossserversync.bungeecord.data.sql.Database; + +import java.sql.*; +import java.time.Instant; +import java.util.HashSet; +import java.util.UUID; +import java.util.logging.Level; + +public class DataManager { + + private static final CrossServerSyncBungeeCord plugin = CrossServerSyncBungeeCord.getInstance(); + public static PlayerDataCache playerDataCache; + + public static void setupCache() { + playerDataCache = new PlayerDataCache(); + } + + /** + * Checks if the player is registered on the database; register them if not. + * + * @param playerUUID The UUID of the player to register + */ + public static void ensurePlayerExists(UUID playerUUID) { + if (!playerExists(playerUUID)) { + createPlayerEntry(playerUUID); + } + } + + /** + * Returns whether the player is registered in SQL (an entry in the PLAYER_TABLE) + * + * @param playerUUID The UUID of the player + * @return {@code true} if the player is on the player table + */ + private static boolean playerExists(UUID playerUUID) { + try (Connection connection = CrossServerSyncBungeeCord.getConnection()) { + try (PreparedStatement statement = connection.prepareStatement( + "SELECT * FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?;")) { + statement.setString(1, playerUUID.toString()); + ResultSet resultSet = statement.executeQuery(); + return resultSet.next(); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "An SQL exception occurred", e); + return false; + } + } + + private static void createPlayerEntry(UUID playerUUID) { + try (Connection connection = CrossServerSyncBungeeCord.getConnection()) { + try (PreparedStatement statement = connection.prepareStatement( + "INSERT INTO " + Database.PLAYER_TABLE_NAME + " (`uuid`) VALUES(?);")) { + statement.setString(1, playerUUID.toString()); + statement.executeUpdate(); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "An SQL exception occurred", e); + } + } + + public static PlayerData getPlayerData(UUID playerUUID) { + try (Connection connection = CrossServerSyncBungeeCord.getConnection()) { + try (PreparedStatement statement = connection.prepareStatement( + "SELECT * FROM " + Database.DATA_TABLE_NAME + " WHERE `player_id`=(SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?);")) { + statement.setString(1, playerUUID.toString()); + ResultSet resultSet = statement.executeQuery(); + if (resultSet.next()) { + final UUID dataVersionUUID = UUID.fromString(resultSet.getString("version_uuid")); + final Timestamp dataSaveTimestamp = resultSet.getTimestamp("timestamp"); + final String serializedInventory = resultSet.getString("inventory"); + final String serializedEnderChest = resultSet.getString("ender_chest"); + final double health = resultSet.getDouble("health"); + final double maxHealth = resultSet.getDouble("max_health"); + final double hunger = resultSet.getDouble("hunger"); + final double saturation = resultSet.getDouble("saturation"); + final String serializedStatusEffects = resultSet.getString("status_effects"); + + return new PlayerData(playerUUID, dataVersionUUID, serializedInventory, serializedEnderChest, health, maxHealth, hunger, saturation, serializedStatusEffects); + } else { + return PlayerData.EMPTY_PLAYER_DATA(playerUUID); + } + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "An SQL exception occurred", e); + return null; + } + } + + public static void updatePlayerData(PlayerData playerData, UUID lastDataUUID) { + // Ignore if the Spigot server didn't properly sync the previous data + PlayerData oldPlayerData = playerDataCache.getPlayer(playerData.getPlayerUUID()); + if (oldPlayerData != null) { + if (oldPlayerData.getDataVersionUUID() != lastDataUUID) { + return; + } + } + + // Add the new player data to the cache + playerDataCache.updatePlayer(playerData); + + // SQL: If the player has cached data, update it, otherwise insert new data. + if (playerHasCachedData(playerData.getPlayerUUID())) { + updatePlayerData(playerData); + } else { + insertPlayerData(playerData); + } + } + + private static void updatePlayerData(PlayerData playerData) { + try (Connection connection = CrossServerSyncBungeeCord.getConnection()) { + try (PreparedStatement statement = connection.prepareStatement( + "UPDATE " + Database.DATA_TABLE_NAME + " SET `version_uuid`=?, `timestamp`=?, `inventory`=?, `ender_chest`=?, `health`=?, `max_health`=?, `hunger`=?, `saturation`=?, `status_effects`=? WHERE `player_id`=(SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?);")) { + statement.setString(1, playerData.getDataVersionUUID().toString()); + statement.setTimestamp(2, new Timestamp(Instant.now().getEpochSecond())); + statement.setString(3, playerData.getSerializedInventory()); + statement.setString(4, playerData.getSerializedEnderChest()); + statement.setDouble(5, 20D); // Health + statement.setDouble(6, 20D); // Max health + statement.setDouble(7, 20D); // Hunger + statement.setDouble(8, 20D); // Saturation + statement.setString(9, ""); // Status effects + + statement.setString(10, playerData.getPlayerUUID().toString()); + statement.executeUpdate(); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "An SQL exception occurred", e); + } + } + + private static void insertPlayerData(PlayerData playerData) { + try (Connection connection = CrossServerSyncBungeeCord.getConnection()) { + try (PreparedStatement statement = connection.prepareStatement( + "INSERT INTO " + Database.DATA_TABLE_NAME + " (`player_id`,`version_uuid`,`timestamp`,`inventory`,`ender_chest`,`health`,`max_health`,`hunger`,`saturation`,`status_effects`) VALUES((SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?),?,?,?,?,?,?,?,?,?);")) { + statement.setString(1, playerData.getPlayerUUID().toString()); + statement.setString(2, playerData.getDataVersionUUID().toString()); + statement.setTimestamp(3, new Timestamp(Instant.now().getEpochSecond())); + statement.setString(4, playerData.getSerializedInventory()); + statement.setString(5, playerData.getSerializedEnderChest()); + statement.setDouble(6, 20D); // Health + statement.setDouble(7, 20D); // Max health + statement.setDouble(8, 20D); // Hunger + statement.setDouble(9, 20D); // Saturation + statement.setString(10, ""); // Status effects + + statement.executeUpdate(); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "An SQL exception occurred", e); + } + } + + /** + * Returns whether the player has cached data saved in SQL (an entry in the DATA_TABLE) + * + * @param playerUUID The UUID of the player + * @return {@code true} if the player has an entry in the data table + */ + private static boolean playerHasCachedData(UUID playerUUID) { + try (Connection connection = CrossServerSyncBungeeCord.getConnection()) { + try (PreparedStatement statement = connection.prepareStatement( + "SELECT * FROM " + Database.DATA_TABLE_NAME + " WHERE `player_id`=(SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?);")) { + statement.setString(1, playerUUID.toString()); + ResultSet resultSet = statement.executeQuery(); + return resultSet.next(); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "An SQL exception occurred", e); + return false; + } + } + + /** + * A cache of PlayerData + */ + public static class PlayerDataCache { + + // The cached player data + public HashSet playerData; + + public PlayerDataCache() { + playerData = new HashSet<>(); + } + + /** + * Update ar add data for a player to the cache + * + * @param newData The player's new/updated {@link PlayerData} + */ + public void updatePlayer(PlayerData newData) { + // Remove the old data if it exists + PlayerData oldData = null; + for (PlayerData data : playerData) { + if (data.getPlayerUUID() == newData.getPlayerUUID()) { + oldData = data; + } + } + if (oldData != null) { + playerData.remove(oldData); + } + + // Add the new data + playerData.add(newData); + } + + /** + * Get a player's {@link PlayerData} by their {@link UUID} + * + * @param playerUUID The {@link UUID} of the player to check + * @return The player's {@link PlayerData} + */ + public PlayerData getPlayer(UUID playerUUID) { + for (PlayerData data : playerData) { + if (data.getPlayerUUID() == playerUUID) { + return data; + } + } + return null; + } + + /** + * Remove a player's {@link PlayerData} from the cache + * @param playerUUID The UUID of the player to remove + */ + public void removePlayer(UUID playerUUID) { + PlayerData dataToRemove = null; + for (PlayerData data : playerData) { + if (data.getPlayerUUID() == playerUUID) { + dataToRemove = data; + break; + } + } + if (dataToRemove != null) { + playerData.remove(dataToRemove); + } + } + + } +} diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/Database.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/Database.java new file mode 100644 index 00000000..9db43303 --- /dev/null +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/Database.java @@ -0,0 +1,33 @@ +package me.william278.crossserversync.bungeecord.data.sql; + +import me.william278.crossserversync.Settings; +import me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord; + +import java.sql.Connection; +import java.sql.SQLException; + +public abstract class Database { + protected CrossServerSyncBungeeCord plugin; + + public final static String DATA_POOL_NAME = "CrossServerSyncHikariPool"; + public final static String PLAYER_TABLE_NAME = "crossserversync_players"; + public final static String DATA_TABLE_NAME = "crossserversync_data"; + + public Database(CrossServerSyncBungeeCord instance) { + plugin = instance; + } + + public abstract Connection getConnection() throws SQLException; + + public abstract void load(); + + public abstract void backup(); + + public abstract void close(); + + public final int hikariMaximumPoolSize = Settings.hikariMaximumPoolSize; + public final int hikariMinimumIdle = Settings.hikariMinimumIdle; + public final long hikariMaximumLifetime = Settings.hikariMaximumLifetime; + public final long hikariKeepAliveTime = Settings.hikariKeepAliveTime; + public final long hikariConnectionTimeOut = Settings.hikariConnectionTimeOut; +} diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/MySQL.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/MySQL.java new file mode 100644 index 00000000..8842b19b --- /dev/null +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/MySQL.java @@ -0,0 +1,99 @@ +package me.william278.crossserversync.bungeecord.data.sql; + +import com.zaxxer.hikari.HikariDataSource; +import me.william278.crossserversync.Settings; +import me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.logging.Level; + +public class MySQL extends Database { + + final static String[] SQL_SETUP_STATEMENTS = { + "CREATE TABLE IF NOT EXISTS " + PLAYER_TABLE_NAME + " (" + + "`id` integer NOT NULL AUTO_INCREMENT," + + "`uuid` char(36) NOT NULL UNIQUE," + + + "PRIMARY KEY (`id`)" + + ");", + + "CREATE TABLE IF NOT EXISTS " + DATA_TABLE_NAME + " (" + + "`player_id` integer NOT NULL," + + "`version_uuid` char(36) NOT NULL UNIQUE," + + "`timestamp` datetime NOT NULL," + + "`inventory` longtext NOT NULL," + + "`ender_chest` longtext NOT NULL," + + "`health` double NOT NULL," + + "`max_health` double NOT NULL," + + "`hunger` double NOT NULL," + + "`saturation` double NOT NULL," + + "`status_effects` longtext NOT NULL," + + + "PRIMARY KEY (`player_id`,`uuid`)," + + "FOREIGN KEY (`player_id`) REFERENCES " + PLAYER_TABLE_NAME + " (`id`)" + + ");" + + }; + + final String host = Settings.mySQLHost; + final int port = Settings.mySQLPort; + final String database = Settings.mySQLDatabase; + final String username = Settings.mySQLUsername; + final String password = Settings.mySQLPassword; + final String params = Settings.mySQLParams; + + private HikariDataSource dataSource; + + public MySQL(CrossServerSyncBungeeCord instance) { + super(instance); + } + + @Override + public Connection getConnection() throws SQLException { + return dataSource.getConnection(); + } + + @Override + public void load() { + // Create new HikariCP data source + final String jdbcUrl = "jdbc:mysql://" + host + ":" + port + "/" + database + params; + dataSource = new HikariDataSource(); + dataSource.setJdbcUrl(jdbcUrl); + + dataSource.setUsername(username); + dataSource.setPassword(password); + + // Set various additional parameters + dataSource.setMaximumPoolSize(hikariMaximumPoolSize); + dataSource.setMinimumIdle(hikariMinimumIdle); + dataSource.setMaxLifetime(hikariMaximumLifetime); + dataSource.setKeepaliveTime(hikariKeepAliveTime); + dataSource.setConnectionTimeout(hikariConnectionTimeOut); + dataSource.setPoolName(DATA_POOL_NAME); + + // Create tables + try (Connection connection = dataSource.getConnection()) { + try (Statement statement = connection.createStatement()) { + for (String tableCreationStatement : SQL_SETUP_STATEMENTS) { + statement.execute(tableCreationStatement); + } + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "An error occurred creating tables on the MySQL database: ", e); + } + } + + @Override + public void close() { + if (dataSource != null) { + dataSource.close(); + } + } + + @Override + public void backup() { + plugin.getLogger().info("Remember to make backups of your HuskHomes Database before updating the plugin!"); + } +} diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/SQLite.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/SQLite.java new file mode 100644 index 00000000..2e89e1aa --- /dev/null +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/SQLite.java @@ -0,0 +1,133 @@ +package me.william278.crossserversync.bungeecord.data.sql; + +import com.zaxxer.hikari.HikariDataSource; +import me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.logging.Level; + +public class SQLite extends Database { + + final static String[] SQL_SETUP_STATEMENTS = { + "PRAGMA foreign_keys = ON;", + "PRAGMA encoding = 'UTF-8';", + + "CREATE TABLE IF NOT EXISTS " + PLAYER_TABLE_NAME + " (" + + "`id` integer NOT NULL AUTO_INCREMENT," + + "`uuid` char(36) NOT NULL UNIQUE," + + + "PRIMARY KEY (`id`)" + + ");", + + "CREATE TABLE IF NOT EXISTS " + DATA_TABLE_NAME + " (" + + "`player_id` integer NOT NULL," + + "`version_uuid` char(36) NOT NULL UNIQUE," + + "`timestamp` datetime NOT NULL," + + "`inventory` longtext NOT NULL," + + "`ender_chest` longtext NOT NULL," + + "`health` double NOT NULL," + + "`max_health` double NOT NULL," + + "`hunger` double NOT NULL," + + "`saturation` double NOT NULL," + + "`status_effects` longtext NOT NULL," + + + "PRIMARY KEY (`player_id`,`uuid`)," + + "FOREIGN KEY (`player_id`) REFERENCES " + PLAYER_TABLE_NAME + "(`id`)" + + ");" + + }; + + private static final String DATABASE_NAME = "CrossServerSyncData"; + + private HikariDataSource dataSource; + + public SQLite(CrossServerSyncBungeeCord instance) { + super(instance); + } + + // Create the database file if it does not exist yet + private void createDatabaseFileIfNotExist() { + File databaseFile = new File(plugin.getDataFolder(), DATABASE_NAME + ".db"); + if (!databaseFile.exists()) { + try { + if (!databaseFile.createNewFile()) { + plugin.getLogger().log(Level.SEVERE, "Failed to write new file: " + DATABASE_NAME + ".db (file already exists)"); + } + } catch (IOException e) { + plugin.getLogger().log(Level.SEVERE, "An error occurred writing a file: " + DATABASE_NAME + ".db (" + e.getCause() + ")"); + } + } + } + + @Override + public Connection getConnection() throws SQLException { + return dataSource.getConnection(); + } + + @Override + public void load() { + // Make SQLite database file + createDatabaseFileIfNotExist(); + + // Create new HikariCP data source + final String jdbcUrl = "jdbc:sqlite:" + plugin.getDataFolder().getAbsolutePath() + "/" + DATABASE_NAME + ".db"; + dataSource = new HikariDataSource(); + dataSource.setJdbcUrl(jdbcUrl); + + // Set various additional parameters + dataSource.setMaximumPoolSize(hikariMaximumPoolSize); + dataSource.setMinimumIdle(hikariMinimumIdle); + dataSource.setMaxLifetime(hikariMaximumLifetime); + dataSource.setKeepaliveTime(hikariKeepAliveTime); + dataSource.setConnectionTimeout(hikariConnectionTimeOut); + dataSource.setPoolName(DATA_POOL_NAME); + + // Create tables + try (Connection connection = dataSource.getConnection()) { + try (Statement statement = connection.createStatement()) { + for (String tableCreationStatement : SQL_SETUP_STATEMENTS) { + statement.execute(tableCreationStatement); + } + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "An error occurred creating tables on the SQLite database: ", e); + } + } + + @Override + public void close() { + if (dataSource != null) { + dataSource.close(); + } + } + + @Override + public void backup() { + final String BACKUPS_FOLDER_NAME = "database-backups"; + final String backupFileName = DATABASE_NAME + "Backup_" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss-SS") + .withLocale(Locale.getDefault()) + .withZone(ZoneId.systemDefault()) + .format(Instant.now()).replaceAll(" ", "-") + ".db"; + final File databaseFile = new File(plugin.getDataFolder(), DATABASE_NAME + ".db"); + if (new File(plugin.getDataFolder(), BACKUPS_FOLDER_NAME).mkdirs()) { + plugin.getLogger().info("Created backups directory in CrossServerSync plugin data folder."); + } + final File backUpFile = new File(plugin.getDataFolder(), BACKUPS_FOLDER_NAME + File.separator + backupFileName); + try { + Files.copy(databaseFile.toPath(), backUpFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + plugin.getLogger().info("Created a backup of your database."); + } catch (IOException iox) { + plugin.getLogger().log(Level.WARNING, "An error occurred making a database backup", iox); + } + } +} diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeEventListener.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeEventListener.java new file mode 100644 index 00000000..24ba7ec8 --- /dev/null +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeEventListener.java @@ -0,0 +1,35 @@ +package me.william278.crossserversync.bungeecord.listener; + +import me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord; +import me.william278.crossserversync.bungeecord.data.DataManager; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.PlayerDisconnectEvent; +import net.md_5.bungee.api.event.PostLoginEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.event.EventHandler; + +public class BungeeEventListener implements Listener { + + private static final CrossServerSyncBungeeCord plugin = CrossServerSyncBungeeCord.getInstance(); + + @EventHandler + public void onPostLogin(PostLoginEvent event) { + final ProxiedPlayer player = event.getPlayer(); + ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> { + // Ensure the player has data on SQL + DataManager.ensurePlayerExists(player.getUniqueId()); + + // Update the player's data from SQL onto the cache + DataManager.playerDataCache.updatePlayer(DataManager.getPlayerData(player.getUniqueId())); + }); + } + + @EventHandler + public void onDisconnect(PlayerDisconnectEvent event) { + final ProxiedPlayer player = event.getPlayer(); + + // Remove the player's data from the cache + DataManager.playerDataCache.removePlayer(player.getUniqueId()); + } +} diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeRedisListener.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeRedisListener.java new file mode 100644 index 00000000..12faaeb4 --- /dev/null +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeRedisListener.java @@ -0,0 +1,102 @@ +package me.william278.crossserversync.bungeecord.listener; + +import me.william278.crossserversync.PlayerData; +import me.william278.crossserversync.Settings; +import me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord; +import me.william278.crossserversync.bungeecord.data.DataManager; +import me.william278.crossserversync.redis.RedisListener; +import me.william278.crossserversync.redis.RedisMessage; +import net.md_5.bungee.api.ProxyServer; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.UUID; +import java.util.logging.Level; + +public class BungeeRedisListener extends RedisListener { + + private static final CrossServerSyncBungeeCord plugin = CrossServerSyncBungeeCord.getInstance(); + + // Initialize the listener on the bungee + public BungeeRedisListener() { + listen(); + } + + private PlayerData getPlayerCachedData(UUID uuid) { + for (PlayerData data : DataManager.playerDataCache.playerData) { + if (data.getPlayerUUID() == uuid) { + return data; + } + } + // If the cache does not contain player data: + DataManager.ensurePlayerExists(uuid); // Make sure the player is registered on MySQL + + final PlayerData data = DataManager.getPlayerData(uuid); // Get their player data from MySQL + DataManager.playerDataCache.updatePlayer(data); // Update the cache + return data; // Return the data + } + + /** + * Handle an incoming {@link RedisMessage} + * + * @param message The {@link RedisMessage} to handle + */ + @Override + public void handleMessage(RedisMessage message) { + // Ignore messages destined for Bukkit servers + if (message.getMessageTarget().targetServerType() != Settings.ServerType.BUNGEECORD) { + return; + } + + switch (message.getMessageType()) { + case PLAYER_DATA_REQUEST -> { + // Get the UUID of the requesting player + final UUID requestingPlayerUUID = UUID.fromString(message.getMessageData()); + ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> { + try { + // Send the reply, serializing the message data + new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_REPLY, + new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, requestingPlayerUUID), + RedisMessage.serialize(getPlayerCachedData(requestingPlayerUUID))).send(); + } catch (IOException e) { + log(Level.SEVERE, "Failed to serialize data when replying to a data request"); + e.printStackTrace(); + } + }); + } + case PLAYER_DATA_UPDATE -> { + // Get the update data + final String[] updateData = message.getMessageDataSeparated(); + + // Get UUID of the last-updated data on the spigot + final UUID lastDataUpdateUUID = UUID.fromString(updateData[0]); + + // Deserialize the PlayerData + PlayerData playerData; + final String serializedPlayerData = updateData[1]; + try (ObjectInputStream stream = new ObjectInputStream(new ByteArrayInputStream(serializedPlayerData.getBytes()))) { + playerData = (PlayerData) stream.readObject(); + } catch (IOException | ClassNotFoundException e) { + log(Level.SEVERE, "Failed to deserialize PlayerData when handling a player update request"); + e.printStackTrace(); + return; + } + + // Update the data in the cache and SQL + DataManager.updatePlayerData(playerData, lastDataUpdateUUID); + } + } + } + + /** + * Log to console + * + * @param level The {@link Level} to log + * @param message Message to log + */ + @Override + public void log(Level level, String message) { + plugin.getLogger().log(level, message); + } +} \ No newline at end of file diff --git a/common/src/main/java/me/william278/crossserversync/PlayerData.java b/common/src/main/java/me/william278/crossserversync/PlayerData.java index fccba6b4..5ef72469 100644 --- a/common/src/main/java/me/william278/crossserversync/PlayerData.java +++ b/common/src/main/java/me/william278/crossserversync/PlayerData.java @@ -1,6 +1,6 @@ package me.william278.crossserversync; -import java.io.Serializable; +import java.io.*; import java.util.UUID; public class PlayerData implements Serializable { @@ -25,13 +25,13 @@ public class PlayerData implements Serializable { */ private final String serializedEnderChest; - //todo add more stuff, like ender chest, player health, max health, hunger and status effects, et cetera - /** * Create a new PlayerData object; a random data version UUID will be selected. - * @param playerUUID The UUID of the player + * + * @param playerUUID The UUID of the player * @param serializedInventory The player's serialized inventory data */ + //todo add more stuff, like player health, max health, hunger, saturation and status effects public PlayerData(UUID playerUUID, String serializedInventory, String serializedEnderChest) { this.dataVersionUUID = UUID.randomUUID(); this.playerUUID = playerUUID; @@ -39,6 +39,19 @@ public class PlayerData implements Serializable { this.serializedEnderChest = serializedEnderChest; } + public PlayerData(UUID playerUUID, UUID dataVersionUUID, String serializedInventory, String serializedEnderChest, double health, double maxHealth, double hunger, double saturation, String serializedStatusEffects) { + this.playerUUID = playerUUID; + this.dataVersionUUID = dataVersionUUID; + this.serializedInventory = serializedInventory; + this.serializedEnderChest = serializedEnderChest; + + //todo Incorporate more of these + } + + public static PlayerData EMPTY_PLAYER_DATA(UUID playerUUID) { + return new PlayerData(playerUUID, "", ""); + } + public UUID getPlayerUUID() { return playerUUID; } diff --git a/common/src/main/java/me/william278/crossserversync/redis/RedisListener.java b/common/src/main/java/me/william278/crossserversync/redis/RedisListener.java index 077e08b1..3ae2ebcb 100644 --- a/common/src/main/java/me/william278/crossserversync/redis/RedisListener.java +++ b/common/src/main/java/me/william278/crossserversync/redis/RedisListener.java @@ -4,6 +4,7 @@ import me.william278.crossserversync.Settings; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPubSub; +import java.io.IOException; import java.util.logging.Level; public abstract class RedisListener { @@ -42,7 +43,11 @@ public abstract class RedisListener { } // Handle the message - handleMessage(new RedisMessage(message)); + try { + handleMessage(new RedisMessage(message)); + } catch (IOException | ClassNotFoundException e) { + log(Level.SEVERE, "Failed to deserialize message target"); + } } }, RedisMessage.REDIS_CHANNEL), "Redis Subscriber").start(); } else { diff --git a/common/src/main/java/me/william278/crossserversync/redis/RedisMessage.java b/common/src/main/java/me/william278/crossserversync/redis/RedisMessage.java index 60896796..8a74a584 100644 --- a/common/src/main/java/me/william278/crossserversync/redis/RedisMessage.java +++ b/common/src/main/java/me/william278/crossserversync/redis/RedisMessage.java @@ -1,12 +1,11 @@ package me.william278.crossserversync.redis; +import me.william278.crossserversync.PlayerData; import me.william278.crossserversync.Settings; import redis.clients.jedis.Jedis; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.Serializable; +import java.io.*; +import java.util.Base64; import java.util.StringJoiner; import java.util.UUID; @@ -41,32 +40,27 @@ public class RedisMessage { * Get a new RedisMessage from an incoming message string * @param messageString The message string to parse */ - public RedisMessage(String messageString) { + public RedisMessage(String messageString) throws IOException, ClassNotFoundException { String[] messageMetaElements = messageString.split(MESSAGE_META_SEPARATOR); messageType = MessageType.valueOf(messageMetaElements[0]); + messageTarget = (MessageTarget) RedisMessage.deserialize(messageMetaElements[1]); messageData = messageMetaElements[2]; - - try (ObjectInputStream stream = new ObjectInputStream(new ByteArrayInputStream(messageMetaElements[1].getBytes()))) { - messageTarget = (MessageTarget) stream.readObject(); - } catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); - } } /** * Returns the full, formatted message string with type, target & data * @return The fully formatted message */ - private String getFullMessage() { + private String getFullMessage() throws IOException { return new StringJoiner(MESSAGE_META_SEPARATOR) - .add(messageType.toString()).add(messageTarget.toString()).add(messageData) + .add(messageType.toString()).add(RedisMessage.serialize(messageTarget)).add(messageData) .toString(); } /** * Send the redis message */ - public void send() { + public void send() throws IOException { try (Jedis publisher = new Jedis(Settings.redisHost, Settings.redisPort)) { final String jedisPassword = Settings.redisPassword; if (!jedisPassword.equals("")) { @@ -77,6 +71,10 @@ public class RedisMessage { } } + public String[] getMessageDataSeparated() { + return messageData.split(MESSAGE_DATA_SEPARATOR); + } + public String getMessageData() { return messageData; } @@ -94,17 +92,17 @@ public class RedisMessage { */ public enum MessageType { /** - * Sent by Bukkit servers to proxy when a player disconnects with a player's updated data, alongside the UUID of the last loaded {@link me.william278.crossserversync.PlayerData} for the user + * Sent by Bukkit servers to proxy when a player disconnects with a player's updated data, alongside the UUID of the last loaded {@link PlayerData} for the user */ PLAYER_DATA_UPDATE, /** - * Sent by Bukkit servers to proxy to request {@link me.william278.crossserversync.PlayerData} from the proxy. + * Sent by Bukkit servers to proxy to request {@link PlayerData} from the proxy. */ PLAYER_DATA_REQUEST, /** - * Sent by the Proxy to reply to a {@code MessageType.PLAYER_DATA_REQUEST}, contains the latest {@link me.william278.crossserversync.PlayerData} for the requester. + * Sent by the Proxy to reply to a {@code MessageType.PLAYER_DATA_REQUEST}, contains the latest {@link PlayerData} for the requester. */ PLAYER_DATA_REPLY } @@ -114,4 +112,25 @@ public class RedisMessage { * For Bukkit servers, the name of the server must also be specified */ public record MessageTarget(Settings.ServerType targetServerType, UUID targetPlayerName) implements Serializable { } + + /** + * Deserialize an object from a Base64 string + */ + public static Object deserialize(String s) throws IOException, ClassNotFoundException { + byte[] data = Base64.getDecoder().decode(s); + try (ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(data))) { + return objectInputStream.readObject(); + } + } + + /** + * Serialize an object to a Base64 string + */ + public static String serialize(Serializable o) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) { + objectOutputStream.writeObject(o); + } + return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); + } } \ No newline at end of file diff --git a/images/flow-chart.png b/images/flow-chart.png index 818d4315..3e9527d6 100644 Binary files a/images/flow-chart.png and b/images/flow-chart.png differ