Make synchronisation much smoother, add Statistics and fix experience syncing

feat/data-edit-commands
William 3 years ago
parent d54de93099
commit bd316c0b8c

@ -1,7 +1,7 @@
package me.william278.crossserversync;
import me.william278.crossserversync.bukkit.config.ConfigLoader;
import me.william278.crossserversync.bukkit.data.LastDataUpdateUUIDCache;
import me.william278.crossserversync.bukkit.data.BukkitDataCache;
import me.william278.crossserversync.bukkit.listener.BukkitRedisListener;
import me.william278.crossserversync.bukkit.listener.EventListener;
import org.bukkit.plugin.java.JavaPlugin;
@ -13,7 +13,7 @@ public final class CrossServerSyncBukkit extends JavaPlugin {
return instance;
}
public static LastDataUpdateUUIDCache lastDataUpdateUUIDCache;
public static BukkitDataCache bukkitCache;
@Override
public void onLoad() {
@ -32,7 +32,7 @@ public final class CrossServerSyncBukkit extends JavaPlugin {
ConfigLoader.loadSettings(getConfig());
// Initialize last data update UUID cache
lastDataUpdateUUIDCache = new LastDataUpdateUUIDCache();
bukkitCache = new BukkitDataCache();
// Initialize event listener
getServer().getPluginManager().registerEvents(new EventListener(), this);

@ -1,5 +1,9 @@
package me.william278.crossserversync.bukkit;
import me.william278.crossserversync.redis.RedisMessage;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
@ -10,7 +14,12 @@ import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Class for serializing and deserializing player inventories and Ender Chests contents ({@link ItemStack[]}) as base64 strings.
@ -162,4 +171,56 @@ public final class DataSerializer {
throw new IOException("Unable to decode class type.", e);
}
}
public static StatisticData deserializeStatisticData(String serializedStatisticData) throws IOException {
if (serializedStatisticData.isEmpty()) {
return new StatisticData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
}
try {
return (StatisticData) RedisMessage.deserialize(serializedStatisticData);
} catch (ClassNotFoundException e) {
throw new IOException("Unable to decode class type.", e);
}
}
public static String getSerializedStatisticData(Player player) throws IOException {
HashMap<Statistic,Integer> untypedStatisticValues = new HashMap<>();
HashMap<Statistic,HashMap<Material,Integer>> blockStatisticValues = new HashMap<>();
HashMap<Statistic,HashMap<Material,Integer>> itemStatisticValues = new HashMap<>();
HashMap<Statistic,HashMap<EntityType,Integer>> entityStatisticValues = new HashMap<>();
for (Statistic statistic : Statistic.values()) {
switch (statistic.getType()) {
case ITEM -> {
HashMap<Material,Integer> itemValues = new HashMap<>();
for (Material itemMaterial : Arrays.stream(Material.values()).filter(Material::isItem).collect(Collectors.toList())) {
itemValues.put(itemMaterial, player.getStatistic(statistic, itemMaterial));
}
itemStatisticValues.put(statistic, itemValues);
}
case BLOCK -> {
HashMap<Material,Integer> blockValues = new HashMap<>();
for (Material blockMaterial : Arrays.stream(Material.values()).filter(Material::isBlock).collect(Collectors.toList())) {
blockValues.put(blockMaterial, player.getStatistic(statistic, blockMaterial));
}
blockStatisticValues.put(statistic, blockValues);
}
case ENTITY -> {
HashMap<EntityType,Integer> entityValues = new HashMap<>();
for (EntityType type : Arrays.stream(EntityType.values()).filter(EntityType::isAlive).collect(Collectors.toList())) {
entityValues.put(type, player.getStatistic(statistic, type));
}
entityStatisticValues.put(statistic, entityValues);
}
case UNTYPED -> untypedStatisticValues.put(statistic, player.getStatistic(statistic));
}
}
StatisticData statisticData = new StatisticData(untypedStatisticValues, blockStatisticValues, itemStatisticValues, entityStatisticValues);
return RedisMessage.serialize(statisticData);
}
public record StatisticData(HashMap<Statistic,Integer> untypedStatisticValues,
HashMap<Statistic,HashMap<Material,Integer>> blockStatisticValues,
HashMap<Statistic,HashMap<Material,Integer>> itemStatisticValues,
HashMap<Statistic,HashMap<EntityType,Integer>> entityStatisticValues) implements Serializable { }
}

@ -1,13 +1,23 @@
package me.william278.crossserversync.bukkit;
import de.themoep.minedown.MineDown;
import me.william278.crossserversync.CrossServerSyncBukkit;
import me.william278.crossserversync.MessageStrings;
import me.william278.crossserversync.PlayerData;
import me.william278.crossserversync.Settings;
import net.md_5.bungee.api.ChatMessageType;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.attribute.Attribute;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import java.io.IOException;
import java.util.Objects;
import java.util.logging.Level;
public class PlayerSetter {
@ -15,39 +25,57 @@ public class PlayerSetter {
private static final CrossServerSyncBukkit plugin = CrossServerSyncBukkit.getInstance();
/**
* Set a player from their PlayerData
* Set a player from their PlayerData, based on settings
*
* @param player The {@link Player} to set
* @param data The {@link PlayerData} to assign to the player
*/
public static void setPlayerFrom(Player player, PlayerData data) {
try {
if (Settings.syncInventories) {
setPlayerInventory(player, DataSerializer.itemStackArrayFromBase64(data.getSerializedInventory()));
player.getInventory().setHeldItemSlot(data.getSelectedSlot());
}
if (Settings.syncEnderChests) {
setPlayerEnderChest(player, DataSerializer.itemStackArrayFromBase64(data.getSerializedEnderChest()));
}
if (Settings.syncHealth) {
player.setMaxHealth(data.getMaxHealth());
player.setHealth(data.getHealth());
}
if (Settings.syncHunger) {
player.setFoodLevel(data.getHunger());
player.setSaturation(data.getSaturation());
player.setExhaustion(data.getSaturationExhaustion());
}
if (Settings.syncExperience) {
player.setTotalExperience(data.getExperience());
}
if (Settings.syncPotionEffects) {
// todo not working ?
setPlayerPotionEffects(player, DataSerializer.potionEffectArrayFromBase64(data.getSerializedEffectData()));
}
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to deserialize PlayerData", e);
// If the data is flagged as being default data, skip setting
if (data.isUseDefaultData()) {
return;
}
// Set the player's data from the PlayerData
Bukkit.getScheduler().runTask(plugin, () -> {
try {
if (Settings.syncInventories) {
setPlayerInventory(player, DataSerializer.itemStackArrayFromBase64(data.getSerializedInventory()));
player.getInventory().setHeldItemSlot(data.getSelectedSlot());
}
if (Settings.syncEnderChests) {
setPlayerEnderChest(player, DataSerializer.itemStackArrayFromBase64(data.getSerializedEnderChest()));
}
if (Settings.syncHealth) {
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).setBaseValue(data.getMaxHealth());
player.setHealth(data.getHealth());
}
if (Settings.syncHunger) {
player.setFoodLevel(data.getHunger());
player.setSaturation(data.getSaturation());
player.setExhaustion(data.getSaturationExhaustion());
}
if (Settings.syncExperience) {
player.setTotalExperience(data.getTotalExperience());
player.setLevel(data.getExpLevel());
player.setExp(data.getExpProgress());
}
if (Settings.syncPotionEffects) {
setPlayerPotionEffects(player, DataSerializer.potionEffectArrayFromBase64(data.getSerializedEffectData()));
}
if (Settings.syncStatistics) {
setPlayerStatistics(player, DataSerializer.deserializeStatisticData(data.getSerializedStatistics()));
}
if (Settings.syncGameMode) {
player.setGameMode(GameMode.valueOf(data.getGameMode()));
}
// Send action bar synchronisation message
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, new MineDown(MessageStrings.SYNCHRONISATION_COMPLETE).toComponent());
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to deserialize PlayerData", e);
}
});
}
/**
@ -90,9 +118,44 @@ public class PlayerSetter {
* @param effects The array of {@link PotionEffect}s to set
*/
private static void setPlayerPotionEffects(Player player, PotionEffect[] effects) {
player.getActivePotionEffects().clear();
for (PotionEffect effect : player.getActivePotionEffects()) {
player.removePotionEffect(effect.getType());
}
for (PotionEffect effect : effects) {
player.getActivePotionEffects().add(effect);
player.addPotionEffect(effect);
}
}
/**
* Set a player's statistics (in the Statistic menu)
* @param player The player to set the statistics of
* @param statisticData The {@link DataSerializer.StatisticData} to set
*/
private static void setPlayerStatistics(Player player, DataSerializer.StatisticData statisticData) {
// Set untyped statistics
for (Statistic statistic : statisticData.untypedStatisticValues().keySet()) {
player.setStatistic(statistic, statisticData.untypedStatisticValues().get(statistic));
}
// Set block statistics
for (Statistic statistic : statisticData.blockStatisticValues().keySet()) {
for (Material blockMaterial : statisticData.blockStatisticValues().get(statistic).keySet()) {
player.setStatistic(statistic, blockMaterial, statisticData.blockStatisticValues().get(statistic).get(blockMaterial));
}
}
// Set item statistics
for (Statistic statistic : statisticData.itemStatisticValues().keySet()) {
for (Material itemMaterial : statisticData.itemStatisticValues().get(statistic).keySet()) {
player.setStatistic(statistic, itemMaterial, statisticData.itemStatisticValues().get(statistic).get(itemMaterial));
}
}
// Set entity statistics
for (Statistic statistic : statisticData.entityStatisticValues().keySet()) {
for (EntityType entityType : statisticData.entityStatisticValues().get(statistic).keySet()) {
player.setStatistic(statistic, entityType, statisticData.entityStatisticValues().get(statistic).get(entityType));
}
}
}
}

@ -17,6 +17,9 @@ public class ConfigLoader {
Settings.syncHunger = config.getBoolean("synchronisation_settings.hunger", true);
Settings.syncExperience = config.getBoolean("synchronisation_settings.experience", true);
Settings.syncPotionEffects = config.getBoolean("synchronisation_settings.potion_effects", true);
Settings.syncStatistics = config.getBoolean("synchronisation_settings.statistics", true);
Settings.syncGameMode = config.getBoolean("synchronisation_settings.game_mode", true);
}
}

@ -0,0 +1,43 @@
package me.william278.crossserversync.bukkit.data;
import java.util.HashMap;
import java.util.HashSet;
import java.util.UUID;
public class BukkitDataCache {
/**
* Map of Player UUIDs to last-updated PlayerData version UUIDs
*/
private static HashMap<UUID, UUID> bukkitDataCache;
/**
* Map of Player UUIDs to request on join
*/
private static HashSet<UUID> requestOnJoin;
public BukkitDataCache() {
bukkitDataCache = new HashMap<>();
requestOnJoin = new HashSet<>();
}
public UUID getVersionUUID(UUID playerUUID) {
return bukkitDataCache.get(playerUUID);
}
public void setVersionUUID(UUID playerUUID, UUID dataVersionUUID) {
bukkitDataCache.put(playerUUID, dataVersionUUID);
}
public boolean isPlayerRequestingOnJoin(UUID uuid) {
return requestOnJoin.contains(uuid);
}
public void setRequestOnJoin(UUID uuid) {
requestOnJoin.add(uuid);
}
public void removeRequestOnJoin(UUID uuid) {
requestOnJoin.remove(uuid);
}
}

@ -1,25 +0,0 @@
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);
}
}

@ -12,6 +12,7 @@ import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.IOException;
import java.util.UUID;
import java.util.logging.Level;
public class BukkitRedisListener extends RedisListener {
@ -35,38 +36,48 @@ public class BukkitRedisListener extends RedisListener {
return;
}
// Handle the message for the player
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.getUniqueId().equals(message.getMessageTarget().targetPlayerUUID())) {
switch (message.getMessageType()) {
case PLAYER_DATA_SET -> {
try {
// Deserialize the received PlayerData
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageData());
if (message.getMessageTarget().targetPlayerUUID() == null) {
if (message.getMessageType() == RedisMessage.MessageType.REQUEST_DATA_ON_JOIN) {
UUID playerUUID = UUID.fromString(message.getMessageDataElements()[1]);
switch (RedisMessage.RequestOnJoinUpdateType.valueOf(message.getMessageDataElements()[0])) {
case ADD_REQUESTER -> CrossServerSyncBukkit.bukkitCache.setRequestOnJoin(playerUUID);
case REMOVE_REQUESTER -> CrossServerSyncBukkit.bukkitCache.removeRequestOnJoin(playerUUID);
}
}
} else {
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.getUniqueId().equals(message.getMessageTarget().targetPlayerUUID())) {
switch (message.getMessageType()) {
case PLAYER_DATA_SET -> {
try {
// Deserialize the received PlayerData
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageData());
// Set the player's data
PlayerSetter.setPlayerFrom(player, data);
// Update last loaded data UUID
CrossServerSyncBukkit.bukkitCache.setVersionUUID(player.getUniqueId(), data.getDataVersionUUID());
// 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();
// Set the player's data
PlayerSetter.setPlayerFrom(player, data);
} catch (IOException | ClassNotFoundException e) {
log(Level.SEVERE, "Failed to deserialize PlayerData when handling a reply from the proxy with PlayerData");
e.printStackTrace();
}
}
case SEND_PLUGIN_INFORMATION -> {
String proxyBrand = message.getMessageDataElements()[0];
String proxyVersion = message.getMessageDataElements()[1];
assert plugin.getDescription().getDescription() != null;
player.spigot().sendMessage(new MineDown(MessageStrings.PLUGIN_INFORMATION.toString()
.replaceAll("%plugin_description%", plugin.getDescription().getDescription())
.replaceAll("%proxy_brand%", proxyBrand)
.replaceAll("%proxy_version%", proxyVersion)
.replaceAll("%bukkit_brand%", Bukkit.getName())
.replaceAll("%bukkit_version%", plugin.getDescription().getVersion()))
.toComponent());
}
}
case SEND_PLUGIN_INFORMATION -> {
String proxyBrand = message.getMessageDataElements()[0];
String proxyVersion = message.getMessageDataElements()[1];
assert plugin.getDescription().getDescription() != null;
player.spigot().sendMessage(new MineDown(MessageStrings.PLUGIN_INFORMATION.toString()
.replaceAll("%plugin_description%", plugin.getDescription().getDescription())
.replaceAll("%proxy_brand%", proxyBrand)
.replaceAll("%proxy_version%", proxyVersion)
.replaceAll("%bukkit_brand%", Bukkit.getName())
.replaceAll("%bukkit_version%", plugin.getDescription().getVersion()))
.toComponent());
}
return;
}
return;
}
}
}

@ -5,6 +5,7 @@ import me.william278.crossserversync.PlayerData;
import me.william278.crossserversync.Settings;
import me.william278.crossserversync.bukkit.DataSerializer;
import me.william278.crossserversync.redis.RedisMessage;
import org.bukkit.attribute.Attribute;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@ -12,6 +13,7 @@ import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.io.IOException;
import java.util.Objects;
import java.util.UUID;
import java.util.logging.Level;
@ -30,13 +32,17 @@ public class EventListener implements Listener {
DataSerializer.getSerializedInventoryContents(player),
DataSerializer.getSerializedEnderChestContents(player),
player.getHealth(),
player.getMaxHealth(),
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).getBaseValue(),
player.getFoodLevel(),
player.getSaturation(),
player.getExhaustion(),
player.getInventory().getHeldItemSlot(),
DataSerializer.getSerializedEffectData(player),
player.getTotalExperience()));
player.getTotalExperience(),
player.getLevel(),
player.getExp(),
player.getGameMode().toString(),
DataSerializer.getSerializedStatisticData(player)));
}
@EventHandler
@ -46,7 +52,7 @@ public class EventListener implements Listener {
try {
// Get the player's last updated PlayerData version UUID
final UUID lastUpdatedDataVersion = CrossServerSyncBukkit.lastDataUpdateUUIDCache.getVersionUUID(player.getUniqueId());
final UUID lastUpdatedDataVersion = CrossServerSyncBukkit.bukkitCache.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
@ -64,13 +70,15 @@ public class EventListener implements Listener {
// 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);
if (CrossServerSyncBukkit.bukkitCache.isPlayerRequestingOnJoin(player.getUniqueId())) {
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);
}
}
}
}

@ -8,4 +8,6 @@ synchronisation_settings:
health: true
hunger: true
experience: true
potion_effects: true
potion_effects: true
statistics: true
game_mode: true

@ -23,7 +23,8 @@ public class CrossServerSyncCommand extends Command implements TabExecutor {
private final static String[] COMMAND_TAB_ARGUMENTS = {"about", "reload"};
private final static String PERMISSION = "crossserversync.command.csc";
public CrossServerSyncCommand() { super("csc", PERMISSION, "crossserversync"); }
//public CrossServerSyncCommand() { super("csc", PERMISSION, "crossserversync"); }
public CrossServerSyncCommand() { super("csc"); }
@Override
public void execute(CommandSender sender, String[] args) {

@ -76,9 +76,14 @@ public class DataManager {
final float saturationExhaustion = resultSet.getFloat("saturation_exhaustion");
final int selectedSlot = resultSet.getInt("selected_slot");
final String serializedStatusEffects = resultSet.getString("status_effects");
final int experience = resultSet.getInt("experience");
final int totalExperience = resultSet.getInt("total_experience");
final int expLevel = resultSet.getInt("exp_level");
final float expProgress = resultSet.getInt("exp_progress");
final String gameMode = resultSet.getString("game_mode");
return new PlayerData(playerUUID, dataVersionUUID, serializedInventory, serializedEnderChest, health, maxHealth, hunger, saturation, saturationExhaustion, selectedSlot, serializedStatusEffects, experience);
final String serializedStatisticData = resultSet.getString("statistics");
return new PlayerData(playerUUID, dataVersionUUID, serializedInventory, serializedEnderChest, health, maxHealth, hunger, saturation, saturationExhaustion, selectedSlot, serializedStatusEffects, totalExperience, expLevel, expProgress, gameMode, serializedStatisticData);
} else {
return PlayerData.DEFAULT_PLAYER_DATA(playerUUID);
}
@ -106,7 +111,7 @@ public class DataManager {
private static void updatePlayerSQLData(PlayerData playerData) {
try (Connection connection = CrossServerSyncBungeeCord.getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(
"UPDATE " + Database.DATA_TABLE_NAME + " SET `version_uuid`=?, `timestamp`=?, `inventory`=?, `ender_chest`=?, `health`=?, `max_health`=?, `hunger`=?, `saturation`=?, `saturation_exhaustion`=?, `selected_slot`=?, `status_effects`=?, `experience`=? WHERE `player_id`=(SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?);")) {
"UPDATE " + Database.DATA_TABLE_NAME + " SET `version_uuid`=?, `timestamp`=?, `inventory`=?, `ender_chest`=?, `health`=?, `max_health`=?, `hunger`=?, `saturation`=?, `saturation_exhaustion`=?, `selected_slot`=?, `status_effects`=?, `total_experience`=?, `exp_level`=?, `exp_progress`=?, `game_mode`=?, `statistics`=? 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());
@ -118,8 +123,13 @@ public class DataManager {
statement.setFloat(9, playerData.getSaturationExhaustion()); // Saturation exhaustion
statement.setInt(10, playerData.getSelectedSlot()); // Current selected slot
statement.setString(11, playerData.getSerializedEffectData()); // Status effects
statement.setInt(12, playerData.getExperience()); // Experience
statement.setString(13, playerData.getPlayerUUID().toString());
statement.setInt(12, playerData.getTotalExperience()); // Total Experience
statement.setInt(13, playerData.getExpLevel()); // Exp level
statement.setFloat(14, playerData.getExpProgress()); // Exp progress
statement.setString(15, playerData.getGameMode()); // GameMode
statement.setString(16, playerData.getSerializedStatistics()); // Statistics
statement.setString(17, playerData.getPlayerUUID().toString());
statement.executeUpdate();
}
} catch (SQLException e) {
@ -130,7 +140,7 @@ public class DataManager {
private static void insertPlayerData(PlayerData playerData) {
try (Connection connection = CrossServerSyncBungeeCord.getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(
"INSERT INTO " + Database.DATA_TABLE_NAME + " (`player_id`,`version_uuid`,`timestamp`,`inventory`,`ender_chest`,`health`,`max_health`,`hunger`,`saturation`,`saturation_exhaustion`,`selected_slot`,`status_effects`,`experience`) VALUES((SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?),?,?,?,?,?,?,?,?,?,?,?,?);")) {
"INSERT INTO " + Database.DATA_TABLE_NAME + " (`player_id`,`version_uuid`,`timestamp`,`inventory`,`ender_chest`,`health`,`max_health`,`hunger`,`saturation`,`saturation_exhaustion`,`selected_slot`,`status_effects`,`total_experience`,`exp_level`,`exp_progress`,`game_mode`,`statistics`) 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()));
@ -143,7 +153,12 @@ public class DataManager {
statement.setFloat(10, playerData.getSaturationExhaustion()); // Saturation exhaustion
statement.setInt(11, playerData.getSelectedSlot()); // Current selected slot
statement.setString(12, playerData.getSerializedEffectData()); // Status effects
statement.setInt(13, playerData.getExperience()); // Experience
statement.setInt(13, playerData.getTotalExperience()); // Total Experience
statement.setInt(14, playerData.getExpLevel()); // Exp level
statement.setFloat(15, playerData.getExpProgress()); // Exp progress
statement.setString(16, playerData.getGameMode()); // GameMode
statement.setString(17, playerData.getSerializedStatistics()); // Statistics
statement.executeUpdate();
}

@ -32,7 +32,11 @@ public class MySQL extends Database {
"`saturation_exhaustion` float NOT NULL," +
"`selected_slot` integer NOT NULL," +
"`status_effects` longtext NOT NULL," +
"`experience` integer NOT NULL," +
"`total_experience` integer NOT NULL," +
"`exp_level` integer NOT NULL," +
"`exp_progress` float NOT NULL," +
"`game_mode` tinytext NOT NULL," +
"`statistics` longtext NOT NULL," +
"PRIMARY KEY (`player_id`,`uuid`)," +
"FOREIGN KEY (`player_id`) REFERENCES " + PLAYER_TABLE_NAME + " (`id`)" +

@ -40,7 +40,11 @@ public class SQLite extends Database {
"`saturation_exhaustion` float NOT NULL," +
"`selected_slot` integer NOT NULL," +
"`status_effects` longtext NOT NULL," +
"`experience` integer NOT NULL," +
"`total_experience` integer NOT NULL," +
"`exp_level` integer NOT NULL," +
"`exp_progress` float NOT NULL," +
"`game_mode` tinytext NOT NULL," +
"`statistics` longtext NOT NULL," +
"PRIMARY KEY (`player_id`,`version_uuid`)" +
");"

@ -2,13 +2,18 @@ package me.william278.crossserversync.bungeecord.listener;
import me.william278.crossserversync.CrossServerSyncBungeeCord;
import me.william278.crossserversync.PlayerData;
import me.william278.crossserversync.Settings;
import me.william278.crossserversync.bungeecord.data.DataManager;
import me.william278.crossserversync.redis.RedisMessage;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
import java.io.IOException;
import java.util.logging.Level;
public class BungeeEventListener implements Listener {
private static final CrossServerSyncBungeeCord plugin = CrossServerSyncBungeeCord.getInstance();
@ -25,6 +30,16 @@ public class BungeeEventListener implements Listener {
// Update the player's data from SQL onto the cache
DataManager.playerDataCache.updatePlayer(data);
// Send a message asking the bukkit to request data on join
try {
new RedisMessage(RedisMessage.MessageType.REQUEST_DATA_ON_JOIN,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null),
RedisMessage.RequestOnJoinUpdateType.ADD_REQUESTER.toString(), player.getUniqueId().toString()).send();
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to serialize request data on join message data");
e.printStackTrace();
}
});
}

@ -1,5 +1,6 @@
package me.william278.crossserversync.bungeecord.listener;
import de.themoep.minedown.MineDown;
import me.william278.crossserversync.CrossServerSyncBungeeCord;
import me.william278.crossserversync.PlayerData;
import me.william278.crossserversync.Settings;
@ -7,6 +8,7 @@ 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 net.md_5.bungee.api.connection.ProxiedPlayer;
import java.io.IOException;
import java.util.UUID;
@ -57,7 +59,14 @@ public class BungeeRedisListener extends RedisListener {
// Send the reply, serializing the message data
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, requestingPlayerUUID),
RedisMessage.serialize(getPlayerCachedData(requestingPlayerUUID))).send();
RedisMessage.serialize(getPlayerCachedData(requestingPlayerUUID)))
.send();
// Send an update to all bukkit servers removing the player from the requester cache
new RedisMessage(RedisMessage.MessageType.REQUEST_DATA_ON_JOIN,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null),
RedisMessage.RequestOnJoinUpdateType.REMOVE_REQUESTER.toString(), requestingPlayerUUID.toString())
.send();
} catch (IOException e) {
log(Level.SEVERE, "Failed to serialize data when replying to a data request");
e.printStackTrace();
@ -78,6 +87,22 @@ public class BungeeRedisListener extends RedisListener {
// Update the data in the cache and SQL
DataManager.updatePlayerData(playerData);
// Reply to set the player's data if they moved servers
ProxiedPlayer player = ProxyServer.getInstance().getPlayer(playerData.getPlayerUUID());
if (player != null) {
if (player.isConnected()) {
// Send the reply, serializing the message data
try {
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, playerData.getPlayerUUID()),
RedisMessage.serialize(playerData)).send();
} catch (IOException e) {
log(Level.SEVERE, "Failed to serialize data when replying to a data update");
e.printStackTrace();
}
}
}
}
}
}

@ -2,7 +2,7 @@ package me.william278.crossserversync;
public class MessageStrings {
public static final StringBuilder PLUGIN_INFORMATION = new StringBuilder().append("[CrossServerSync](#00fb9a bold) [| %proxy_brand% Version %proxy_version% | %bukkit_brand% Version %bukkit_version%](#00fb9a)\n")
public static final StringBuilder PLUGIN_INFORMATION = new StringBuilder().append("[CrossServerSync](#00fb9a bold) [| %proxy_brand% Version %proxy_version% (%bukkit_brand% v%bukkit_version%)](#00fb9a)\n")
.append("[%plugin_description%](gray)\n")
.append("[• Author:](white) [William278](gray show_text=&7Click to pay a visit open_url=https://youtube.com/William27528)\n")
.append("[• Help Wiki:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/CrossServerSync/wiki/)\n")
@ -11,4 +11,6 @@ public class MessageStrings {
public static final String ERROR_INVALID_SYNTAX = "[Error:](#ff3300) [Incorrect syntax. Usage: %1%](#ff7e5e)";
public static final String SYNCHRONISATION_COMPLETE = "[Data synchronised!](#00fb9a)";
}

@ -15,6 +15,8 @@ public class PlayerData implements Serializable {
*/
private final UUID dataVersionUUID;
// Flag to indicate if the Bukkit server should use default data
private boolean useDefaultData = false;
// Player data
private final String serializedInventory;
@ -26,21 +28,31 @@ public class PlayerData implements Serializable {
private final float saturationExhaustion;
private final int selectedSlot;
private final String serializedEffectData;
private final int experience;
private final int totalExperience;
private final int expLevel;
private final float expProgress;
private final String gameMode;
private final String serializedStatistics;
/**
* Create a new PlayerData object; a random data version UUID will be selected.
* @param playerUUID UUID of the player
* @param serializedInventory Serialized inventory data
* @param serializedEnderChest Serialized ender chest data
* @param health Player health
* @param maxHealth Player max health
* @param hunger Player hunger
* @param saturation Player saturation
* @param selectedSlot Player selected slot
* @param serializedStatusEffects Serialized status effect data
* Constructor to create new PlayerData from a bukkit {@code Player}'s data
* @param playerUUID The Player's UUID
* @param serializedInventory Their serialized inventory
* @param serializedEnderChest Their serialized ender chest
* @param health Their health
* @param maxHealth Their max health
* @param hunger Their hunger
* @param saturation Their saturation
* @param saturationExhaustion Their saturation exhaustion
* @param selectedSlot Their selected hot bar slot
* @param serializedStatusEffects Their serialized status effects
* @param totalExperience Their total experience points ("Score")
* @param expLevel Their exp level
* @param expProgress Their exp progress to the next level
* @param gameMode Their game mode ({@code SURVIVAL}, {@code CREATIVE}, etc)
* @param serializedStatistics Their serialized statistics data (Displayed in Statistics menu in ESC menu)
*/
public PlayerData(UUID playerUUID, String serializedInventory, String serializedEnderChest, double health, double maxHealth, int hunger, float saturation, float saturationExhaustion, int selectedSlot, String serializedStatusEffects, int experience) {
public PlayerData(UUID playerUUID, String serializedInventory, String serializedEnderChest, double health, double maxHealth, int hunger, float saturation, float saturationExhaustion, int selectedSlot, String serializedStatusEffects, int totalExperience, int expLevel, float expProgress, String gameMode, String serializedStatistics) {
this.dataVersionUUID = UUID.randomUUID();
this.playerUUID = playerUUID;
this.serializedInventory = serializedInventory;
@ -52,10 +64,33 @@ public class PlayerData implements Serializable {
this.saturationExhaustion = saturationExhaustion;
this.selectedSlot = selectedSlot;
this.serializedEffectData = serializedStatusEffects;
this.experience = experience;
this.totalExperience = totalExperience;
this.expLevel = expLevel;
this.expProgress = expProgress;
this.gameMode = gameMode;
this.serializedStatistics = serializedStatistics;
}
public PlayerData(UUID playerUUID, UUID dataVersionUUID, String serializedInventory, String serializedEnderChest, double health, double maxHealth, int hunger, float saturation, float saturationExhaustion, int selectedSlot, String serializedStatusEffects, int experience) {
/**
* Constructor for a PlayerData object from an existing object that was stored in SQL
* @param playerUUID The player whose data this is' UUID
* @param dataVersionUUID The PlayerData version UUID
* @param serializedInventory Their serialized inventory
* @param serializedEnderChest Their serialized ender chest
* @param health Their health
* @param maxHealth Their max health
* @param hunger Their hunger
* @param saturation Their saturation
* @param saturationExhaustion Their saturation exhaustion
* @param selectedSlot Their selected hot bar slot
* @param serializedStatusEffects Their serialized status effects
* @param totalExperience Their total experience points ("Score")
* @param expLevel Their exp level
* @param expProgress Their exp progress to the next level
* @param gameMode Their game mode ({@code SURVIVAL}, {@code CREATIVE}, etc)
* @param serializedStatistics Their serialized statistics data (Displayed in Statistics menu in ESC menu)
*/
public PlayerData(UUID playerUUID, UUID dataVersionUUID, String serializedInventory, String serializedEnderChest, double health, double maxHealth, int hunger, float saturation, float saturationExhaustion, int selectedSlot, String serializedStatusEffects, int totalExperience, int expLevel, float expProgress, String gameMode, String serializedStatistics) {
this.playerUUID = playerUUID;
this.dataVersionUUID = dataVersionUUID;
this.serializedInventory = serializedInventory;
@ -67,12 +102,24 @@ public class PlayerData implements Serializable {
this.saturationExhaustion = saturationExhaustion;
this.selectedSlot = selectedSlot;
this.serializedEffectData = serializedStatusEffects;
this.experience = experience;
this.totalExperience = totalExperience;
this.expLevel = expLevel;
this.expProgress = expProgress;
this.gameMode = gameMode;
this.serializedStatistics = serializedStatistics;
}
/**
* Get default PlayerData for a new user
* @param playerUUID The bukkit Player's UUID
* @return Default {@link PlayerData}
*/
public static PlayerData DEFAULT_PLAYER_DATA(UUID playerUUID) {
return new PlayerData(playerUUID, "", "", 20,
20, 20, 10, 1, 0, "", 0);
PlayerData data = new PlayerData(playerUUID, "", "", 20,
20, 20, 10, 1, 0,
"", 0, 0, 0, "SURVIVAL", "");
data.useDefaultData = true;
return data;
}
public UUID getPlayerUUID() {
@ -107,7 +154,9 @@ public class PlayerData implements Serializable {
return saturation;
}
public float getSaturationExhaustion() { return saturationExhaustion; }
public float getSaturationExhaustion() {
return saturationExhaustion;
}
public int getSelectedSlot() {
return selectedSlot;
@ -117,5 +166,27 @@ public class PlayerData implements Serializable {
return serializedEffectData;
}
public int getExperience() { return experience; }
public int getTotalExperience() {
return totalExperience;
}
public String getSerializedStatistics() {
return serializedStatistics;
}
public int getExpLevel() {
return expLevel;
}
public float getExpProgress() {
return expProgress;
}
public String getGameMode() {
return gameMode;
}
public boolean isUseDefaultData() {
return useDefaultData;
}
}

@ -47,6 +47,8 @@ public class Settings {
public static boolean syncHunger;
public static boolean syncExperience;
public static boolean syncPotionEffects;
public static boolean syncStatistics;
public static boolean syncGameMode;
/*
* Enum definitions

@ -95,7 +95,7 @@ public class RedisMessage {
PLAYER_DATA_UPDATE,
/**
* Sent by Bukkit servers to proxy to request {@link PlayerData} from the proxy.
* Sent by Bukkit servers to proxy to request {@link PlayerData} from the proxy if they are set as needing to request data on join.
*/
PLAYER_DATA_REQUEST,
@ -104,12 +104,22 @@ public class RedisMessage {
*/
PLAYER_DATA_SET,
/**
* Sent by the proxy to a Bukkit server to have them request data on join; contains no data otherwise
*/
REQUEST_DATA_ON_JOIN,
/**
* Sent by the proxy to ask the Bukkit server to send the full plugin information, contains information about the proxy brand and version
*/
SEND_PLUGIN_INFORMATION
}
public enum RequestOnJoinUpdateType {
ADD_REQUESTER,
REMOVE_REQUESTER
}
/**
* A record that defines the target of a plugin message; a spigot server or the proxy server(s).
* For Bukkit servers, the name of the server must also be specified

Loading…
Cancel
Save