Now fully reliable and added support for health, max health, etc

feat/data-edit-commands
William 3 years ago
parent ec6f85250d
commit ba8e4ee175

@ -11,8 +11,6 @@ dependencies {
shadowJar { shadowJar {
relocate 'redis.clients', 'me.William278.crossserversync.libraries.jedis' relocate 'redis.clients', 'me.William278.crossserversync.libraries.jedis'
relocate 'org.bstats', 'me.William278.crossserversync.libraries.plan' 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'
} }
tasks.register('prepareKotlinBuildScriptModel'){} tasks.register('prepareKotlinBuildScriptModel'){}

@ -1,4 +1,4 @@
package me.william278.crossserversync.bukkit; package me.william278.crossserversync;
import me.william278.crossserversync.bukkit.config.ConfigLoader; import me.william278.crossserversync.bukkit.config.ConfigLoader;
import me.william278.crossserversync.bukkit.data.LastDataUpdateUUIDCache; import me.william278.crossserversync.bukkit.data.LastDataUpdateUUIDCache;
@ -34,12 +34,12 @@ public final class CrossServerSyncBukkit extends JavaPlugin {
// Initialize last data update UUID cache // Initialize last data update UUID cache
lastDataUpdateUUIDCache = new LastDataUpdateUUIDCache(); lastDataUpdateUUIDCache = new LastDataUpdateUUIDCache();
// Initialize the redis listener
new BukkitRedisListener();
// Initialize event listener // Initialize event listener
getServer().getPluginManager().registerEvents(new EventListener(), this); getServer().getPluginManager().registerEvents(new EventListener(), this);
// Initialize the redis listener
new BukkitRedisListener();
// Log to console // Log to console
getLogger().info("Enabled CrossServerSync (" + getServer().getName() + ") v" + getDescription().getVersion()); getLogger().info("Enabled CrossServerSync (" + getServer().getName() + ") v" + getDescription().getVersion());
} }

@ -1,8 +1,8 @@
package me.william278.crossserversync.bukkit; package me.william278.crossserversync.bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import org.bukkit.util.io.BukkitObjectInputStream; import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream; import org.bukkit.util.io.BukkitObjectOutputStream;
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
@ -19,8 +19,9 @@ import java.util.Map;
* *
* @author efindus * @author efindus
* @author graywolf336 * @author graywolf336
* @author William278
*/ */
public final class InventorySerializer { public final class DataSerializer {
/** /**
* Converts the player inventory to a Base64 encoded string. * Converts the player inventory to a Base64 encoded string.
@ -46,35 +47,33 @@ public final class InventorySerializer {
return itemStackArrayToBase64(player.getEnderChest().getContents()); return itemStackArrayToBase64(player.getEnderChest().getContents());
} }
/** public static String getSerializedEffectData(Player player) {
* Sets a player's inventory from a set of {@link ItemStack}s PotionEffect[] potionEffects = new PotionEffect[player.getActivePotionEffects().size()];
* int x = 0;
* @param player The player to set the inventory of for (PotionEffect effect : player.getActivePotionEffects()) {
* @param items The array of {@link ItemStack}s to set potionEffects[x] = effect;
*/ x++;
public static void setPlayerItems(Player player, ItemStack[] items) { }
setInventoryItems(player.getInventory(), items); return effectArrayToBase64(potionEffects);
} }
/** public static String effectArrayToBase64(PotionEffect[] effects) {
* Sets a player's ender chest from a set of {@link ItemStack}s try {
* ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
* @param player The player to set the inventory of try (BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream)) {
* @param items The array of {@link ItemStack}s to set dataOutput.writeInt(effects.length);
*/
public static void setPlayerEnderChest(Player player, ItemStack[] items) {
setInventoryItems(player.getEnderChest(), items);
}
// Clears, then fills an inventory's items correctly. for (PotionEffect effect : effects) {
private static void setInventoryItems(Inventory inventory, ItemStack[] items) { if (effect != null) {
inventory.clear(); dataOutput.writeObject(effect.serialize());
int index = 0; } else {
for (ItemStack item : items) { dataOutput.writeObject(null);
if (item != null) { }
inventory.setItem(index, item); }
} }
index++; return Base64Coder.encodeLines(outputStream.toByteArray());
} catch (Exception e) {
throw new IllegalStateException("Unable to save potion effects.", e);
} }
} }
@ -137,4 +136,30 @@ public final class InventorySerializer {
throw new IOException("Unable to decode class type.", e); throw new IOException("Unable to decode class type.", e);
} }
} }
public static PotionEffect[] potionEffectArrayFromBase64(String data) throws IOException {
// Return an empty PotionEffect[] if the data is empty
if (data.isEmpty()) {
return new PotionEffect[0];
}
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(data))) {
BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream);
PotionEffect[] items = new PotionEffect[dataInput.readInt()];
for (int Index = 0; Index < items.length; Index++) {
@SuppressWarnings("unchecked") // Ignore the unchecked cast here
Map<String, Object> effect = (Map<String, Object>) dataInput.readObject();
if (effect != null) {
items[Index] = new PotionEffect(effect);
} else {
items[Index] = null;
}
}
return items;
} catch (ClassNotFoundException e) {
throw new IOException("Unable to decode class type.", e);
}
}
} }

@ -0,0 +1,83 @@
package me.william278.crossserversync.bukkit;
import me.william278.crossserversync.CrossServerSyncBukkit;
import me.william278.crossserversync.PlayerData;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import java.io.IOException;
import java.util.logging.Level;
public class PlayerSetter {
private static final CrossServerSyncBukkit plugin = CrossServerSyncBukkit.getInstance();
/**
* Set a player from their PlayerData
*
* @param player The {@link Player} to set
* @param data The {@link PlayerData} to assign to the player
*/
public static void setPlayerFrom(Player player, PlayerData data) {
try {
setPlayerInventory(player, DataSerializer.itemStackArrayFromBase64(data.getSerializedInventory()));
setPlayerEnderChest(player, DataSerializer.itemStackArrayFromBase64(data.getSerializedEnderChest()));
player.setHealth(data.getHealth());
player.setMaxHealth(data.getMaxHealth());
player.setFoodLevel(data.getHunger());
player.setSaturation(data.getSaturation());
player.getInventory().setHeldItemSlot(data.getSelectedSlot());
//todo potion effects not working
setPlayerPotionEffects(player, DataSerializer.potionEffectArrayFromBase64(data.getSerializedEffectData()));
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to deserialize PlayerData", e);
}
}
/**
* Sets a player's ender chest from a set of {@link ItemStack}s
*
* @param player The player to set the inventory of
* @param items The array of {@link ItemStack}s to set
*/
private static void setPlayerEnderChest(Player player, ItemStack[] items) {
player.getEnderChest().clear();
int index = 0;
for (ItemStack item : items) {
if (item != null) {
player.getEnderChest().setItem(index, item);
}
index++;
}
}
/**
* Sets a player's inventory from a set of {@link ItemStack}s
*
* @param player The player to set the inventory of
* @param items The array of {@link ItemStack}s to set
*/
private static void setPlayerInventory(Player player, ItemStack[] items) {
player.getInventory().clear();
int index = 0;
for (ItemStack item : items) {
if (item != null) {
player.getInventory().setItem(index, item);
}
index++;
}
}
/**
* Set a player's current potion effects from a set of {@link PotionEffect[]}
* @param player The player to set the potion effects of
* @param effects The array of {@link PotionEffect}s to set
*/
private static void setPlayerPotionEffects(Player player, PotionEffect[] effects) {
player.getActivePotionEffects().clear();
for (PotionEffect effect : effects) {
player.getActivePotionEffects().add(effect);
}
}
}

@ -1,9 +1,9 @@
package me.william278.crossserversync.bukkit.listener; package me.william278.crossserversync.bukkit.listener;
import me.william278.crossserversync.bukkit.InventorySerializer;
import me.william278.crossserversync.PlayerData; import me.william278.crossserversync.PlayerData;
import me.william278.crossserversync.Settings; import me.william278.crossserversync.Settings;
import me.william278.crossserversync.bukkit.CrossServerSyncBukkit; import me.william278.crossserversync.CrossServerSyncBukkit;
import me.william278.crossserversync.bukkit.PlayerSetter;
import me.william278.crossserversync.redis.RedisListener; import me.william278.crossserversync.redis.RedisListener;
import me.william278.crossserversync.redis.RedisMessage; import me.william278.crossserversync.redis.RedisMessage;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@ -29,20 +29,19 @@ public class BukkitRedisListener extends RedisListener {
@Override @Override
public void handleMessage(RedisMessage message) { public void handleMessage(RedisMessage message) {
// Ignore messages for proxy servers // Ignore messages for proxy servers
if (message.getMessageTarget().targetServerType() != Settings.ServerType.BUKKIT) { if (!message.getMessageTarget().targetServerType().equals(Settings.ServerType.BUKKIT)) {
return; return;
} }
// Handle the message for the player // Handle the message for the player
for (Player player : Bukkit.getOnlinePlayers()) { for (Player player : Bukkit.getOnlinePlayers()) {
if (player.getUniqueId() == message.getMessageTarget().targetPlayerName()) { if (player.getUniqueId().equals(message.getMessageTarget().targetPlayerUUID())) {
if (message.getMessageType() == RedisMessage.MessageType.PLAYER_DATA_REPLY) { if (message.getMessageType().equals(RedisMessage.MessageType.PLAYER_DATA_REPLY)) {
try { try {
// Deserialize the received PlayerData // Deserialize the received PlayerData
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageData()); PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageData());
// Set the player's data //todo do more stuff like health etc // Set the player's data
InventorySerializer.setPlayerItems(player, InventorySerializer.itemStackArrayFromBase64(data.getSerializedInventory())); PlayerSetter.setPlayerFrom(player, data);
InventorySerializer.setPlayerEnderChest(player, InventorySerializer.itemStackArrayFromBase64(data.getSerializedEnderChest()));
// Update last loaded data UUID // Update last loaded data UUID
CrossServerSyncBukkit.lastDataUpdateUUIDCache.setVersionUUID(player.getUniqueId(), data.getDataVersionUUID()); CrossServerSyncBukkit.lastDataUpdateUUIDCache.setVersionUUID(player.getUniqueId(), data.getDataVersionUUID());

@ -1,9 +1,9 @@
package me.william278.crossserversync.bukkit.listener; package me.william278.crossserversync.bukkit.listener;
import me.william278.crossserversync.CrossServerSyncBukkit;
import me.william278.crossserversync.PlayerData; import me.william278.crossserversync.PlayerData;
import me.william278.crossserversync.Settings; import me.william278.crossserversync.Settings;
import me.william278.crossserversync.bukkit.CrossServerSyncBukkit; import me.william278.crossserversync.bukkit.DataSerializer;
import me.william278.crossserversync.bukkit.InventorySerializer;
import me.william278.crossserversync.redis.RedisMessage; import me.william278.crossserversync.redis.RedisMessage;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
@ -27,8 +27,14 @@ public class EventListener implements Listener {
*/ */
private static String getNewSerializedPlayerData(Player player) throws IOException { private static String getNewSerializedPlayerData(Player player) throws IOException {
return RedisMessage.serialize(new PlayerData(player.getUniqueId(), return RedisMessage.serialize(new PlayerData(player.getUniqueId(),
InventorySerializer.getSerializedInventoryContents(player), DataSerializer.getSerializedInventoryContents(player),
InventorySerializer.getSerializedEnderChestContents(player))); DataSerializer.getSerializedEnderChestContents(player),
player.getHealth(),
player.getMaxHealth(),
player.getFoodLevel(),
player.getSaturation(),
player.getInventory().getHeldItemSlot(),
DataSerializer.getSerializedEffectData(player)));
} }
@EventHandler @EventHandler
@ -42,15 +48,16 @@ public class EventListener implements Listener {
if (lastUpdatedDataVersion == null) return; // Return if the player has not been properly updated. 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 // Send a redis message with the player's last updated PlayerData version UUID and their new PlayerData
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),
lastUpdatedDataVersion.toString(), getNewSerializedPlayerData(player)).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);
} }
} }
@EventHandler @EventHandler
public void onPlayerJoin(PlayerJoinEvent event) { public void onPlayerJoin(PlayerJoinEvent event) {
// When a player joins a Bukkit server // When a player joins a Bukkit server
final Player player = event.getPlayer(); final Player player = event.getPlayer();

@ -12,8 +12,6 @@ shadowJar {
relocate 'redis.clients', 'me.William278.crossserversync.libraries.jedis' relocate 'redis.clients', 'me.William278.crossserversync.libraries.jedis'
relocate 'com.zaxxer', 'me.William278.crossserversync.libraries.hikari' relocate 'com.zaxxer', 'me.William278.crossserversync.libraries.hikari'
relocate 'org.bstats', 'me.William278.crossserversync.libraries.plan' 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'
} }
tasks.register('prepareKotlinBuildScriptModel'){} tasks.register('prepareKotlinBuildScriptModel'){}

@ -1,6 +1,5 @@
package me.william278.crossserversync.bungeecord; package me.william278.crossserversync;
import me.william278.crossserversync.Settings;
import me.william278.crossserversync.bungeecord.config.ConfigLoader; import me.william278.crossserversync.bungeecord.config.ConfigLoader;
import me.william278.crossserversync.bungeecord.config.ConfigManager; import me.william278.crossserversync.bungeecord.config.ConfigManager;
import me.william278.crossserversync.bungeecord.data.DataManager; import me.william278.crossserversync.bungeecord.data.DataManager;
@ -50,7 +49,7 @@ public final class CrossServerSyncBungeeCord extends Plugin {
database.load(); database.load();
// Setup player data cache // Setup player data cache
DataManager.setupCache(); DataManager.playerDataCache = new DataManager.PlayerDataCache();
// Initialize PreLoginEvent listener // Initialize PreLoginEvent listener
getProxy().getPluginManager().registerListener(this, new BungeeEventListener()); getProxy().getPluginManager().registerListener(this, new BungeeEventListener());

@ -26,7 +26,6 @@ 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", 10); Settings.hikariKeepAliveTime = config.getLong("data_storage_settings.hikari_pool_settings.keepalive_time", 10);
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);
} }
} }

@ -1,6 +1,6 @@
package me.william278.crossserversync.bungeecord.config; package me.william278.crossserversync.bungeecord.config;
import me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord; import me.william278.crossserversync.CrossServerSyncBungeeCord;
import net.md_5.bungee.config.Configuration; import net.md_5.bungee.config.Configuration;
import net.md_5.bungee.config.ConfigurationProvider; import net.md_5.bungee.config.ConfigurationProvider;
import net.md_5.bungee.config.YamlConfiguration; import net.md_5.bungee.config.YamlConfiguration;
@ -23,7 +23,8 @@ public class ConfigManager {
} }
File configFile = new File(plugin.getDataFolder(), "config.yml"); File configFile = new File(plugin.getDataFolder(), "config.yml");
if (!configFile.exists()) { if (!configFile.exists()) {
Files.copy(plugin.getResourceAsStream("bungee_config.yml"), configFile.toPath()); Files.copy(plugin.getResourceAsStream("bungee-config.yml"), configFile.toPath());
plugin.getLogger().info("Created CrossServerSync bungee-config.yml file");
} }
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().log(Level.CONFIG, "An exception occurred loading the configuration file", e); plugin.getLogger().log(Level.CONFIG, "An exception occurred loading the configuration file", e);

@ -1,7 +1,7 @@
package me.william278.crossserversync.bungeecord.data; package me.william278.crossserversync.bungeecord.data;
import me.william278.crossserversync.PlayerData; import me.william278.crossserversync.PlayerData;
import me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord; import me.william278.crossserversync.CrossServerSyncBungeeCord;
import me.william278.crossserversync.bungeecord.data.sql.Database; import me.william278.crossserversync.bungeecord.data.sql.Database;
import java.sql.*; import java.sql.*;
@ -15,10 +15,6 @@ public class DataManager {
private static final CrossServerSyncBungeeCord plugin = CrossServerSyncBungeeCord.getInstance(); private static final CrossServerSyncBungeeCord plugin = CrossServerSyncBungeeCord.getInstance();
public static PlayerDataCache playerDataCache; public static PlayerDataCache playerDataCache;
public static void setupCache() {
playerDataCache = new PlayerDataCache();
}
/** /**
* Checks if the player is registered on the database; register them if not. * Checks if the player is registered on the database; register them if not.
* *
@ -75,11 +71,12 @@ public class DataManager {
final String serializedEnderChest = resultSet.getString("ender_chest"); final String serializedEnderChest = resultSet.getString("ender_chest");
final double health = resultSet.getDouble("health"); final double health = resultSet.getDouble("health");
final double maxHealth = resultSet.getDouble("max_health"); final double maxHealth = resultSet.getDouble("max_health");
final double hunger = resultSet.getDouble("hunger"); final int hunger = resultSet.getInt("hunger");
final double saturation = resultSet.getDouble("saturation"); final float saturation = resultSet.getFloat("saturation");
final int selectedSlot = resultSet.getInt("selected_slot");
final String serializedStatusEffects = resultSet.getString("status_effects"); final String serializedStatusEffects = resultSet.getString("status_effects");
return new PlayerData(playerUUID, dataVersionUUID, serializedInventory, serializedEnderChest, health, maxHealth, hunger, saturation, serializedStatusEffects); return new PlayerData(playerUUID, dataVersionUUID, serializedInventory, serializedEnderChest, health, maxHealth, hunger, saturation, selectedSlot, serializedStatusEffects);
} else { } else {
return PlayerData.EMPTY_PLAYER_DATA(playerUUID); return PlayerData.EMPTY_PLAYER_DATA(playerUUID);
} }
@ -90,41 +87,35 @@ public class DataManager {
} }
} }
public static void updatePlayerData(PlayerData playerData, UUID lastDataUUID) { public static void updatePlayerData(PlayerData playerData) {
// Ignore if the Spigot server didn't properly sync the previous data // 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 // Add the new player data to the cache
playerDataCache.updatePlayer(playerData); playerDataCache.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())) {
updatePlayerData(playerData); updatePlayerSQLData(playerData);
} else { } else {
insertPlayerData(playerData); insertPlayerData(playerData);
} }
} }
private static void updatePlayerData(PlayerData playerData) { private static void updatePlayerSQLData(PlayerData playerData) {
try (Connection connection = CrossServerSyncBungeeCord.getConnection()) { try (Connection connection = CrossServerSyncBungeeCord.getConnection()) {
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`=?, `hunger`=?, `saturation`=?, `status_effects`=? WHERE `player_id`=(SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?);")) { "UPDATE " + Database.DATA_TABLE_NAME + " SET `version_uuid`=?, `timestamp`=?, `inventory`=?, `ender_chest`=?, `health`=?, `max_health`=?, `hunger`=?, `saturation`=?, `selected_slot`=?, `status_effects`=? WHERE `player_id`=(SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " 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());
statement.setString(4, playerData.getSerializedEnderChest()); statement.setString(4, playerData.getSerializedEnderChest());
statement.setDouble(5, 20D); // Health statement.setDouble(5, playerData.getHealth()); // Health
statement.setDouble(6, 20D); // Max health statement.setDouble(6, playerData.getMaxHealth()); // Max health
statement.setDouble(7, 20D); // Hunger statement.setInt(7, playerData.getHunger()); // Hunger
statement.setDouble(8, 20D); // Saturation statement.setFloat(8, playerData.getSaturation()); // Saturation
statement.setString(9, ""); // Status effects statement.setInt(9, playerData.getSelectedSlot());
statement.setString(10, playerData.getSerializedEffectData()); // Status effects
statement.setString(10, playerData.getPlayerUUID().toString()); statement.setString(11, playerData.getPlayerUUID().toString());
statement.executeUpdate(); statement.executeUpdate();
} }
} catch (SQLException e) { } catch (SQLException e) {
@ -135,17 +126,18 @@ public class DataManager {
private static void insertPlayerData(PlayerData playerData) { private static void insertPlayerData(PlayerData playerData) {
try (Connection connection = CrossServerSyncBungeeCord.getConnection()) { try (Connection connection = CrossServerSyncBungeeCord.getConnection()) {
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`,`hunger`,`saturation`,`status_effects`) VALUES((SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?),?,?,?,?,?,?,?,?,?);")) { "INSERT INTO " + Database.DATA_TABLE_NAME + " (`player_id`,`version_uuid`,`timestamp`,`inventory`,`ender_chest`,`health`,`max_health`,`hunger`,`saturation`,`selected_slot`,`status_effects`) VALUES((SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " 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()));
statement.setString(4, playerData.getSerializedInventory()); statement.setString(4, playerData.getSerializedInventory());
statement.setString(5, playerData.getSerializedEnderChest()); statement.setString(5, playerData.getSerializedEnderChest());
statement.setDouble(6, 20D); // Health statement.setDouble(6, playerData.getHealth()); // Health
statement.setDouble(7, 20D); // Max health statement.setDouble(7, playerData.getMaxHealth()); // Max health
statement.setDouble(8, 20D); // Hunger statement.setInt(8, playerData.getHunger()); // Hunger
statement.setDouble(9, 20D); // Saturation statement.setFloat(9, playerData.getSaturation()); // Saturation
statement.setString(10, ""); // Status effects statement.setInt(10, playerData.getSelectedSlot());
statement.setString(11, playerData.getSerializedEffectData()); // Status effects
statement.executeUpdate(); statement.executeUpdate();
} }
@ -178,7 +170,6 @@ public class DataManager {
* A cache of PlayerData * A cache of PlayerData
*/ */
public static class PlayerDataCache { public static class PlayerDataCache {
// The cached player data // The cached player data
public HashSet<PlayerData> playerData; public HashSet<PlayerData> playerData;

@ -1,7 +1,7 @@
package me.william278.crossserversync.bungeecord.data.sql; package me.william278.crossserversync.bungeecord.data.sql;
import me.william278.crossserversync.Settings; import me.william278.crossserversync.Settings;
import me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord; import me.william278.crossserversync.CrossServerSyncBungeeCord;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;

@ -2,7 +2,7 @@ package me.william278.crossserversync.bungeecord.data.sql;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import me.william278.crossserversync.Settings; import me.william278.crossserversync.Settings;
import me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord; import me.william278.crossserversync.CrossServerSyncBungeeCord;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
@ -27,8 +27,9 @@ public class MySQL extends Database {
"`ender_chest` longtext NOT NULL," + "`ender_chest` longtext NOT NULL," +
"`health` double NOT NULL," + "`health` double NOT NULL," +
"`max_health` double NOT NULL," + "`max_health` double NOT NULL," +
"`hunger` double NOT NULL," + "`hunger` integer NOT NULL," +
"`saturation` double NOT NULL," + "`saturation` float NOT NULL," +
"`selected_slot` integer NOT NULL," +
"`status_effects` longtext NOT NULL," + "`status_effects` longtext NOT NULL," +
"PRIMARY KEY (`player_id`,`uuid`)," + "PRIMARY KEY (`player_id`,`uuid`)," +

@ -1,7 +1,7 @@
package me.william278.crossserversync.bungeecord.data.sql; package me.william278.crossserversync.bungeecord.data.sql;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord; import me.william278.crossserversync.CrossServerSyncBungeeCord;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -23,28 +23,25 @@ public class SQLite extends Database {
"PRAGMA encoding = 'UTF-8';", "PRAGMA encoding = 'UTF-8';",
"CREATE TABLE IF NOT EXISTS " + PLAYER_TABLE_NAME + " (" + "CREATE TABLE IF NOT EXISTS " + PLAYER_TABLE_NAME + " (" +
"`id` integer NOT NULL AUTO_INCREMENT," + "`id` integer PRIMARY KEY," +
"`uuid` char(36) NOT NULL UNIQUE," + "`uuid` char(36) NOT NULL UNIQUE" +
"PRIMARY KEY (`id`)" +
");", ");",
"CREATE TABLE IF NOT EXISTS " + DATA_TABLE_NAME + " (" + "CREATE TABLE IF NOT EXISTS " + DATA_TABLE_NAME + " (" +
"`player_id` integer NOT NULL," + "`player_id` integer NOT NULL REFERENCES " + PLAYER_TABLE_NAME + "(`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," +
"`ender_chest` longtext NOT NULL," + "`ender_chest` longtext NOT NULL," +
"`health` double NOT NULL," + "`health` double NOT NULL," +
"`max_health` double NOT NULL," + "`max_health` double NOT NULL," +
"`hunger` double NOT NULL," + "`hunger` integer NOT NULL," +
"`saturation` double NOT NULL," + "`saturation` float NOT NULL," +
"`selected_slot` integer NOT NULL," +
"`status_effects` longtext NOT NULL," + "`status_effects` longtext NOT NULL," +
"PRIMARY KEY (`player_id`,`uuid`)," + "PRIMARY KEY (`player_id`,`version_uuid`)" +
"FOREIGN KEY (`player_id`) REFERENCES " + PLAYER_TABLE_NAME + "(`id`)" +
");" ");"
}; };
private static final String DATABASE_NAME = "CrossServerSyncData"; private static final String DATABASE_NAME = "CrossServerSyncData";
@ -80,9 +77,10 @@ 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() + "/" + DATABASE_NAME + ".db"; final String jdbcUrl = "jdbc:sqlite:" + plugin.getDataFolder().getAbsolutePath() + File.separator + DATABASE_NAME + ".db";
dataSource = new HikariDataSource(); dataSource = new HikariDataSource();
dataSource.setJdbcUrl(jdbcUrl); dataSource.setDataSourceClassName("org.sqlite.SQLiteDataSource");
dataSource.addDataSourceProperty("url", jdbcUrl);
// Set various additional parameters // Set various additional parameters
dataSource.setMaximumPoolSize(hikariMaximumPoolSize); dataSource.setMaximumPoolSize(hikariMaximumPoolSize);

@ -1,10 +1,10 @@
package me.william278.crossserversync.bungeecord.listener; package me.william278.crossserversync.bungeecord.listener;
import me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord; import me.william278.crossserversync.CrossServerSyncBungeeCord;
import me.william278.crossserversync.PlayerData;
import me.william278.crossserversync.bungeecord.data.DataManager; import me.william278.crossserversync.bungeecord.data.DataManager;
import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer; 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.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler; import net.md_5.bungee.event.EventHandler;
@ -20,16 +20,12 @@ public class BungeeEventListener implements Listener {
// Ensure the player has data on SQL // Ensure the player has data on SQL
DataManager.ensurePlayerExists(player.getUniqueId()); DataManager.ensurePlayerExists(player.getUniqueId());
// Get the player's data from SQL
final 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(DataManager.getPlayerData(player.getUniqueId())); DataManager.playerDataCache.updatePlayer(data);
}); });
} }
@EventHandler
public void onDisconnect(PlayerDisconnectEvent event) {
final ProxiedPlayer player = event.getPlayer();
// Remove the player's data from the cache
DataManager.playerDataCache.removePlayer(player.getUniqueId());
}
} }

@ -1,16 +1,14 @@
package me.william278.crossserversync.bungeecord.listener; package me.william278.crossserversync.bungeecord.listener;
import me.william278.crossserversync.CrossServerSyncBungeeCord;
import me.william278.crossserversync.PlayerData; import me.william278.crossserversync.PlayerData;
import me.william278.crossserversync.Settings; import me.william278.crossserversync.Settings;
import me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord;
import me.william278.crossserversync.bungeecord.data.DataManager; import me.william278.crossserversync.bungeecord.data.DataManager;
import me.william278.crossserversync.redis.RedisListener; import me.william278.crossserversync.redis.RedisListener;
import me.william278.crossserversync.redis.RedisMessage; import me.william278.crossserversync.redis.RedisMessage;
import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.ProxyServer;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Level; import java.util.logging.Level;
@ -66,17 +64,11 @@ public class BungeeRedisListener extends RedisListener {
}); });
} }
case PLAYER_DATA_UPDATE -> { case PLAYER_DATA_UPDATE -> {
// Get the update data // Deserialize the PlayerData received
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; PlayerData playerData;
final String serializedPlayerData = updateData[1]; final String serializedPlayerData = message.getMessageData();
try (ObjectInputStream stream = new ObjectInputStream(new ByteArrayInputStream(serializedPlayerData.getBytes()))) { try {
playerData = (PlayerData) stream.readObject(); playerData = (PlayerData) RedisMessage.deserialize(serializedPlayerData);
} catch (IOException | ClassNotFoundException e) { } catch (IOException | ClassNotFoundException e) {
log(Level.SEVERE, "Failed to deserialize PlayerData when handling a player update request"); log(Level.SEVERE, "Failed to deserialize PlayerData when handling a player update request");
e.printStackTrace(); e.printStackTrace();
@ -84,7 +76,7 @@ public class BungeeRedisListener extends RedisListener {
} }
// Update the data in the cache and SQL // Update the data in the cache and SQL
DataManager.updatePlayerData(playerData, lastDataUpdateUUID); DataManager.updatePlayerData(playerData);
} }
} }
} }

@ -27,6 +27,4 @@ shadowJar {
// Relocations // Relocations
relocate 'redis.clients', 'me.William278.crossserversync.libraries.jedis' relocate 'redis.clients', 'me.William278.crossserversync.libraries.jedis'
relocate 'org.apache.commons', 'me.William278.crossserversync.libraries.apache-commons'
relocate 'org.slf4j', 'me.William278.crossserversync.libraries.slf4j'
} }

@ -15,41 +15,59 @@ public class PlayerData implements Serializable {
*/ */
private final UUID dataVersionUUID; private final UUID dataVersionUUID;
/**
* Serialized inventory data
*/
private final String serializedInventory;
/** // Player data
* Serialized ender chest data private final String serializedInventory;
*/
private final String serializedEnderChest; private final String serializedEnderChest;
private final double health;
private final double maxHealth;
private final int hunger;
private final float saturation;
private final int selectedSlot;
private final String serializedEffectData;
/** /**
* Create a new PlayerData object; a random data version UUID will be selected. * Create a new PlayerData object; a random data version UUID will be selected.
* * @param playerUUID UUID of the player
* @param playerUUID The UUID of the player * @param serializedInventory Serialized inventory data
* @param serializedInventory The player's serialized inventory data * @param serializedEnderChest Serialized ender chest data
* @param health Player health
* @param maxHealth Player max health
* @param hunger Player hunger
* @param saturation Player saturation
* @param selectedSlot Player selected slot
* @param serializedStatusEffects Serialized status effect data
*/ */
//todo add more stuff, like player health, max health, hunger, saturation and status effects public PlayerData(UUID playerUUID, String serializedInventory, String serializedEnderChest, double health, double maxHealth, int hunger, float saturation, int selectedSlot, String serializedStatusEffects) {
public PlayerData(UUID playerUUID, String serializedInventory, String serializedEnderChest) {
this.dataVersionUUID = UUID.randomUUID(); this.dataVersionUUID = UUID.randomUUID();
this.playerUUID = playerUUID; this.playerUUID = playerUUID;
this.serializedInventory = serializedInventory; this.serializedInventory = serializedInventory;
this.serializedEnderChest = serializedEnderChest; this.serializedEnderChest = serializedEnderChest;
this.health = health;
this.maxHealth = maxHealth;
this.hunger = hunger;
this.saturation = saturation;
this.selectedSlot = selectedSlot;
this.serializedEffectData = serializedStatusEffects;
} }
public PlayerData(UUID playerUUID, UUID dataVersionUUID, String serializedInventory, String serializedEnderChest, double health, double maxHealth, double hunger, double saturation, String serializedStatusEffects) { public PlayerData(UUID playerUUID, UUID dataVersionUUID, String serializedInventory, String serializedEnderChest, double health, double maxHealth, int hunger, float saturation, int selectedSlot, String serializedStatusEffects) {
this.playerUUID = playerUUID; this.playerUUID = playerUUID;
this.dataVersionUUID = dataVersionUUID; this.dataVersionUUID = dataVersionUUID;
this.serializedInventory = serializedInventory; this.serializedInventory = serializedInventory;
this.serializedEnderChest = serializedEnderChest; this.serializedEnderChest = serializedEnderChest;
this.health = health;
//todo Incorporate more of these this.maxHealth = maxHealth;
this.hunger = hunger;
this.saturation = saturation;
this.selectedSlot = selectedSlot;
this.serializedEffectData = serializedStatusEffects;
} }
public static PlayerData EMPTY_PLAYER_DATA(UUID playerUUID) { public static PlayerData EMPTY_PLAYER_DATA(UUID playerUUID) {
return new PlayerData(playerUUID, "", ""); return new PlayerData(playerUUID, "", "", 20,
20, 20, 20, 0, "");
} }
public UUID getPlayerUUID() { public UUID getPlayerUUID() {
@ -67,4 +85,28 @@ public class PlayerData implements Serializable {
public String getSerializedEnderChest() { public String getSerializedEnderChest() {
return serializedEnderChest; return serializedEnderChest;
} }
public double getHealth() {
return health;
}
public double getMaxHealth() {
return maxHealth;
}
public int getHunger() {
return hunger;
}
public float getSaturation() {
return saturation;
}
public int getSelectedSlot() {
return selectedSlot;
}
public String getSerializedEffectData() {
return serializedEffectData;
}
} }

@ -71,10 +71,6 @@ public class RedisMessage {
} }
} }
public String[] getMessageDataSeparated() {
return messageData.split(MESSAGE_DATA_SEPARATOR);
}
public String getMessageData() { public String getMessageData() {
return messageData; return messageData;
} }
@ -90,7 +86,7 @@ public class RedisMessage {
/** /**
* Defines the type of the message * Defines the type of the message
*/ */
public enum MessageType { public enum MessageType implements Serializable {
/** /**
* 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 * 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
*/ */
@ -111,7 +107,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 targetPlayerName) implements Serializable { } public record MessageTarget(Settings.ServerType targetServerType, UUID targetPlayerUUID) implements Serializable { }
/** /**
* Deserialize an object from a Base64 string * Deserialize an object from a Base64 string

@ -1,5 +1,8 @@
name: CrossServerSync name: CrossServerSync
version: @version@ version: @version@
main: me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord main: me.william278.crossserversync.CrossServerSyncBungeeCord
author: William278 author: William278
description: 'Synchronize data cross-server' description: 'Synchronize data cross-server'
libraries:
- mysql:mysql-connector-java:8.0.25
- org.xerial:sqlite-jdbc:3.36.0.3

@ -1,6 +1,6 @@
name: CrossServerSync name: CrossServerSync
version: @version@ version: @version@
main: me.william278.crossserversync.bukkit.CrossServerSyncBukkit main: me.william278.crossserversync.CrossServerSyncBukkit
api-version: 1.16 api-version: 1.16
author: William278 author: William278
description: 'Synchronize data cross-server' description: 'Synchronize data cross-server'
Loading…
Cancel
Save