Finish initial implementation of plan

feat/data-edit-commands
William 3 years ago
parent 2a7371be31
commit 861477f5ae

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 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
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

Loading…
Cancel
Save