forked from public-mirrors/HuskSync
Finish initial implementation of plan
parent
2a7371be31
commit
861477f5ae
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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,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);
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 174 KiB |
Loading…
Reference in New Issue