mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-19 14:59:21 +00:00
Finish initial implementation of plan
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()];
|
||||
|
||||
@@ -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<UUID, UUID> 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 174 KiB |
Reference in New Issue
Block a user