mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-19 14:59:21 +00:00
Now fully reliable and added support for health, max health, etc
This commit is contained in:
@@ -11,8 +11,6 @@ dependencies {
|
||||
shadowJar {
|
||||
relocate 'redis.clients', 'me.William278.crossserversync.libraries.jedis'
|
||||
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'){}
|
||||
@@ -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.data.LastDataUpdateUUIDCache;
|
||||
@@ -34,12 +34,12 @@ public final class CrossServerSyncBukkit extends JavaPlugin {
|
||||
// Initialize last data update UUID cache
|
||||
lastDataUpdateUUIDCache = new LastDataUpdateUUIDCache();
|
||||
|
||||
// Initialize the redis listener
|
||||
new BukkitRedisListener();
|
||||
|
||||
// Initialize event listener
|
||||
getServer().getPluginManager().registerEvents(new EventListener(), this);
|
||||
|
||||
// Initialize the redis listener
|
||||
new BukkitRedisListener();
|
||||
|
||||
// Log to console
|
||||
getLogger().info("Enabled CrossServerSync (" + getServer().getName() + ") v" + getDescription().getVersion());
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package me.william278.crossserversync.bukkit;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
|
||||
@@ -19,8 +19,9 @@ import java.util.Map;
|
||||
*
|
||||
* @author efindus
|
||||
* @author graywolf336
|
||||
* @author William278
|
||||
*/
|
||||
public final class InventorySerializer {
|
||||
public final class DataSerializer {
|
||||
|
||||
/**
|
||||
* Converts the player inventory to a Base64 encoded string.
|
||||
@@ -46,35 +47,33 @@ public final class InventorySerializer {
|
||||
return itemStackArrayToBase64(player.getEnderChest().getContents());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static void setPlayerItems(Player player, ItemStack[] items) {
|
||||
setInventoryItems(player.getInventory(), items);
|
||||
public static String getSerializedEffectData(Player player) {
|
||||
PotionEffect[] potionEffects = new PotionEffect[player.getActivePotionEffects().size()];
|
||||
int x = 0;
|
||||
for (PotionEffect effect : player.getActivePotionEffects()) {
|
||||
potionEffects[x] = effect;
|
||||
x++;
|
||||
}
|
||||
return effectArrayToBase64(potionEffects);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static void setPlayerEnderChest(Player player, ItemStack[] items) {
|
||||
setInventoryItems(player.getEnderChest(), items);
|
||||
}
|
||||
public static String effectArrayToBase64(PotionEffect[] effects) {
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
try (BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream)) {
|
||||
dataOutput.writeInt(effects.length);
|
||||
|
||||
// Clears, then fills an inventory's items correctly.
|
||||
private static void setInventoryItems(Inventory inventory, ItemStack[] items) {
|
||||
inventory.clear();
|
||||
int index = 0;
|
||||
for (ItemStack item : items) {
|
||||
if (item != null) {
|
||||
inventory.setItem(index, item);
|
||||
for (PotionEffect effect : effects) {
|
||||
if (effect != null) {
|
||||
dataOutput.writeObject(effect.serialize());
|
||||
} else {
|
||||
dataOutput.writeObject(null);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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.CrossServerSyncBukkit;
|
||||
import me.william278.crossserversync.bukkit.PlayerSetter;
|
||||
import me.william278.crossserversync.redis.RedisListener;
|
||||
import me.william278.crossserversync.redis.RedisMessage;
|
||||
import org.bukkit.Bukkit;
|
||||
@@ -29,20 +29,19 @@ public class BukkitRedisListener extends RedisListener {
|
||||
@Override
|
||||
public void handleMessage(RedisMessage message) {
|
||||
// Ignore messages for proxy servers
|
||||
if (message.getMessageTarget().targetServerType() != Settings.ServerType.BUKKIT) {
|
||||
if (!message.getMessageTarget().targetServerType().equals(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) {
|
||||
if (player.getUniqueId().equals(message.getMessageTarget().targetPlayerUUID())) {
|
||||
if (message.getMessageType().equals(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()));
|
||||
// Set the player's data
|
||||
PlayerSetter.setPlayerFrom(player, data);
|
||||
|
||||
// Update last loaded data UUID
|
||||
CrossServerSyncBukkit.lastDataUpdateUUIDCache.setVersionUUID(player.getUniqueId(), data.getDataVersionUUID());
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package me.william278.crossserversync.bukkit.listener;
|
||||
|
||||
import me.william278.crossserversync.CrossServerSyncBukkit;
|
||||
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.bukkit.DataSerializer;
|
||||
import me.william278.crossserversync.redis.RedisMessage;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
@@ -27,8 +27,14 @@ public class EventListener implements Listener {
|
||||
*/
|
||||
private static String getNewSerializedPlayerData(Player player) throws IOException {
|
||||
return RedisMessage.serialize(new PlayerData(player.getUniqueId(),
|
||||
InventorySerializer.getSerializedInventoryContents(player),
|
||||
InventorySerializer.getSerializedEnderChestContents(player)));
|
||||
DataSerializer.getSerializedInventoryContents(player),
|
||||
DataSerializer.getSerializedEnderChestContents(player),
|
||||
player.getHealth(),
|
||||
player.getMaxHealth(),
|
||||
player.getFoodLevel(),
|
||||
player.getSaturation(),
|
||||
player.getInventory().getHeldItemSlot(),
|
||||
DataSerializer.getSerializedEffectData(player)));
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
@@ -42,9 +48,10 @@ public class EventListener implements Listener {
|
||||
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
|
||||
final String serializedPlayerData = getNewSerializedPlayerData(player);
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null),
|
||||
lastUpdatedDataVersion.toString(), getNewSerializedPlayerData(player)).send();
|
||||
serializedPlayerData).send();
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData update to the proxy", e);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ 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'
|
||||
}
|
||||
|
||||
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.ConfigManager;
|
||||
import me.william278.crossserversync.bungeecord.data.DataManager;
|
||||
@@ -50,7 +49,7 @@ public final class CrossServerSyncBungeeCord extends Plugin {
|
||||
database.load();
|
||||
|
||||
// Setup player data cache
|
||||
DataManager.setupCache();
|
||||
DataManager.playerDataCache = new DataManager.PlayerDataCache();
|
||||
|
||||
// Initialize PreLoginEvent listener
|
||||
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.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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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.ConfigurationProvider;
|
||||
import net.md_5.bungee.config.YamlConfiguration;
|
||||
@@ -23,7 +23,8 @@ public class ConfigManager {
|
||||
}
|
||||
File configFile = new File(plugin.getDataFolder(), "config.yml");
|
||||
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) {
|
||||
plugin.getLogger().log(Level.CONFIG, "An exception occurred loading the configuration file", e);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package me.william278.crossserversync.bungeecord.data;
|
||||
|
||||
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 java.sql.*;
|
||||
@@ -15,10 +15,6 @@ 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.
|
||||
*
|
||||
@@ -75,11 +71,12 @@ public class DataManager {
|
||||
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 int hunger = resultSet.getInt("hunger");
|
||||
final float saturation = resultSet.getFloat("saturation");
|
||||
final int selectedSlot = resultSet.getInt("selected_slot");
|
||||
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 {
|
||||
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
|
||||
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);
|
||||
updatePlayerSQLData(playerData);
|
||||
} else {
|
||||
insertPlayerData(playerData);
|
||||
}
|
||||
}
|
||||
|
||||
private static void updatePlayerData(PlayerData playerData) {
|
||||
private static void updatePlayerSQLData(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`=?);")) {
|
||||
"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.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.setDouble(5, playerData.getHealth()); // Health
|
||||
statement.setDouble(6, playerData.getMaxHealth()); // Max health
|
||||
statement.setInt(7, playerData.getHunger()); // Hunger
|
||||
statement.setFloat(8, playerData.getSaturation()); // Saturation
|
||||
statement.setInt(9, playerData.getSelectedSlot());
|
||||
statement.setString(10, playerData.getSerializedEffectData()); // Status effects
|
||||
statement.setString(11, playerData.getPlayerUUID().toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
@@ -135,17 +126,18 @@ public class DataManager {
|
||||
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`=?),?,?,?,?,?,?,?,?,?);")) {
|
||||
"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(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.setDouble(6, playerData.getHealth()); // Health
|
||||
statement.setDouble(7, playerData.getMaxHealth()); // Max health
|
||||
statement.setInt(8, playerData.getHunger()); // Hunger
|
||||
statement.setFloat(9, playerData.getSaturation()); // Saturation
|
||||
statement.setInt(10, playerData.getSelectedSlot());
|
||||
statement.setString(11, playerData.getSerializedEffectData()); // Status effects
|
||||
|
||||
statement.executeUpdate();
|
||||
}
|
||||
@@ -178,7 +170,6 @@ public class DataManager {
|
||||
* A cache of PlayerData
|
||||
*/
|
||||
public static class PlayerDataCache {
|
||||
|
||||
// The cached player data
|
||||
public HashSet<PlayerData> playerData;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package me.william278.crossserversync.bungeecord.data.sql;
|
||||
|
||||
import me.william278.crossserversync.Settings;
|
||||
import me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord;
|
||||
import me.william278.crossserversync.CrossServerSyncBungeeCord;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
@@ -2,7 +2,7 @@ package me.william278.crossserversync.bungeecord.data.sql;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import me.william278.crossserversync.Settings;
|
||||
import me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord;
|
||||
import me.william278.crossserversync.CrossServerSyncBungeeCord;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
@@ -27,8 +27,9 @@ public class MySQL extends Database {
|
||||
"`ender_chest` longtext NOT NULL," +
|
||||
"`health` double NOT NULL," +
|
||||
"`max_health` double NOT NULL," +
|
||||
"`hunger` double NOT NULL," +
|
||||
"`saturation` double NOT NULL," +
|
||||
"`hunger` integer NOT NULL," +
|
||||
"`saturation` float NOT NULL," +
|
||||
"`selected_slot` integer NOT NULL," +
|
||||
"`status_effects` longtext NOT NULL," +
|
||||
|
||||
"PRIMARY KEY (`player_id`,`uuid`)," +
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package me.william278.crossserversync.bungeecord.data.sql;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord;
|
||||
import me.william278.crossserversync.CrossServerSyncBungeeCord;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -23,28 +23,25 @@ public class SQLite extends Database {
|
||||
"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`)" +
|
||||
"`id` integer PRIMARY KEY," +
|
||||
"`uuid` char(36) NOT NULL UNIQUE" +
|
||||
");",
|
||||
|
||||
"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," +
|
||||
"`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," +
|
||||
"`hunger` integer NOT NULL," +
|
||||
"`saturation` float NOT NULL," +
|
||||
"`selected_slot` integer NOT NULL," +
|
||||
"`status_effects` longtext NOT NULL," +
|
||||
|
||||
"PRIMARY KEY (`player_id`,`uuid`)," +
|
||||
"FOREIGN KEY (`player_id`) REFERENCES " + PLAYER_TABLE_NAME + "(`id`)" +
|
||||
"PRIMARY KEY (`player_id`,`version_uuid`)" +
|
||||
");"
|
||||
|
||||
};
|
||||
|
||||
private static final String DATABASE_NAME = "CrossServerSyncData";
|
||||
@@ -80,9 +77,10 @@ public class SQLite extends Database {
|
||||
createDatabaseFileIfNotExist();
|
||||
|
||||
// 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.setJdbcUrl(jdbcUrl);
|
||||
dataSource.setDataSourceClassName("org.sqlite.SQLiteDataSource");
|
||||
dataSource.addDataSourceProperty("url", jdbcUrl);
|
||||
|
||||
// Set various additional parameters
|
||||
dataSource.setMaximumPoolSize(hikariMaximumPoolSize);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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 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;
|
||||
@@ -20,16 +20,12 @@ public class BungeeEventListener implements Listener {
|
||||
// Ensure the player has data on SQL
|
||||
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
|
||||
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;
|
||||
|
||||
import me.william278.crossserversync.CrossServerSyncBungeeCord;
|
||||
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;
|
||||
|
||||
@@ -66,17 +64,11 @@ public class BungeeRedisListener extends RedisListener {
|
||||
});
|
||||
}
|
||||
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
|
||||
// Deserialize the PlayerData received
|
||||
PlayerData playerData;
|
||||
final String serializedPlayerData = updateData[1];
|
||||
try (ObjectInputStream stream = new ObjectInputStream(new ByteArrayInputStream(serializedPlayerData.getBytes()))) {
|
||||
playerData = (PlayerData) stream.readObject();
|
||||
final String serializedPlayerData = message.getMessageData();
|
||||
try {
|
||||
playerData = (PlayerData) RedisMessage.deserialize(serializedPlayerData);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to deserialize PlayerData when handling a player update request");
|
||||
e.printStackTrace();
|
||||
@@ -84,7 +76,7 @@ public class BungeeRedisListener extends RedisListener {
|
||||
}
|
||||
|
||||
// Update the data in the cache and SQL
|
||||
DataManager.updatePlayerData(playerData, lastDataUpdateUUID);
|
||||
DataManager.updatePlayerData(playerData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,4 @@ shadowJar {
|
||||
|
||||
// Relocations
|
||||
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;
|
||||
|
||||
/**
|
||||
* Serialized inventory data
|
||||
*/
|
||||
private final String serializedInventory;
|
||||
|
||||
/**
|
||||
* Serialized ender chest data
|
||||
*/
|
||||
// Player data
|
||||
private final String serializedInventory;
|
||||
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.
|
||||
*
|
||||
* @param playerUUID The UUID of the player
|
||||
* @param serializedInventory The player's serialized inventory data
|
||||
* @param playerUUID UUID of the player
|
||||
* @param serializedInventory 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) {
|
||||
public PlayerData(UUID playerUUID, String serializedInventory, String serializedEnderChest, double health, double maxHealth, int hunger, float saturation, int selectedSlot, String serializedStatusEffects) {
|
||||
this.dataVersionUUID = UUID.randomUUID();
|
||||
this.playerUUID = playerUUID;
|
||||
this.serializedInventory = serializedInventory;
|
||||
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.dataVersionUUID = dataVersionUUID;
|
||||
this.serializedInventory = serializedInventory;
|
||||
this.serializedEnderChest = serializedEnderChest;
|
||||
|
||||
//todo Incorporate more of these
|
||||
this.health = health;
|
||||
this.maxHealth = maxHealth;
|
||||
this.hunger = hunger;
|
||||
this.saturation = saturation;
|
||||
this.selectedSlot = selectedSlot;
|
||||
this.serializedEffectData = serializedStatusEffects;
|
||||
}
|
||||
|
||||
public static PlayerData EMPTY_PLAYER_DATA(UUID playerUUID) {
|
||||
return new PlayerData(playerUUID, "", "");
|
||||
return new PlayerData(playerUUID, "", "", 20,
|
||||
20, 20, 20, 0, "");
|
||||
}
|
||||
|
||||
public UUID getPlayerUUID() {
|
||||
@@ -67,4 +85,28 @@ public class PlayerData implements Serializable {
|
||||
public String getSerializedEnderChest() {
|
||||
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() {
|
||||
return messageData;
|
||||
}
|
||||
@@ -90,7 +86,7 @@ public class RedisMessage {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -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).
|
||||
* 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
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
name: CrossServerSync
|
||||
version: @version@
|
||||
main: me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord
|
||||
main: me.william278.crossserversync.CrossServerSyncBungeeCord
|
||||
author: William278
|
||||
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
|
||||
version: @version@
|
||||
main: me.william278.crossserversync.bukkit.CrossServerSyncBukkit
|
||||
main: me.william278.crossserversync.CrossServerSyncBukkit
|
||||
api-version: 1.16
|
||||
author: William278
|
||||
description: 'Synchronize data cross-server'
|
||||
Reference in New Issue
Block a user