diff --git a/bukkit/src/main/java/me/william278/crossserversync/CrossServerSyncBukkit.java b/bukkit/src/main/java/me/william278/crossserversync/CrossServerSyncBukkit.java index 4453512e..51d007ed 100644 --- a/bukkit/src/main/java/me/william278/crossserversync/CrossServerSyncBukkit.java +++ b/bukkit/src/main/java/me/william278/crossserversync/CrossServerSyncBukkit.java @@ -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); diff --git a/bukkit/src/main/java/me/william278/crossserversync/bukkit/DataSerializer.java b/bukkit/src/main/java/me/william278/crossserversync/bukkit/DataSerializer.java index c38bc499..06ab3798 100644 --- a/bukkit/src/main/java/me/william278/crossserversync/bukkit/DataSerializer.java +++ b/bukkit/src/main/java/me/william278/crossserversync/bukkit/DataSerializer.java @@ -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 untypedStatisticValues = new HashMap<>(); + HashMap> blockStatisticValues = new HashMap<>(); + HashMap> itemStatisticValues = new HashMap<>(); + HashMap> entityStatisticValues = new HashMap<>(); + for (Statistic statistic : Statistic.values()) { + switch (statistic.getType()) { + case ITEM -> { + HashMap 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 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 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 untypedStatisticValues, + HashMap> blockStatisticValues, + HashMap> itemStatisticValues, + HashMap> entityStatisticValues) implements Serializable { } } \ No newline at end of file diff --git a/bukkit/src/main/java/me/william278/crossserversync/bukkit/PlayerSetter.java b/bukkit/src/main/java/me/william278/crossserversync/bukkit/PlayerSetter.java index 85e7eadc..f577b4e4 100644 --- a/bukkit/src/main/java/me/william278/crossserversync/bukkit/PlayerSetter.java +++ b/bukkit/src/main/java/me/william278/crossserversync/bukkit/PlayerSetter.java @@ -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)); + } } } } diff --git a/bukkit/src/main/java/me/william278/crossserversync/bukkit/config/ConfigLoader.java b/bukkit/src/main/java/me/william278/crossserversync/bukkit/config/ConfigLoader.java index 330f6208..23791df7 100644 --- a/bukkit/src/main/java/me/william278/crossserversync/bukkit/config/ConfigLoader.java +++ b/bukkit/src/main/java/me/william278/crossserversync/bukkit/config/ConfigLoader.java @@ -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); + } } diff --git a/bukkit/src/main/java/me/william278/crossserversync/bukkit/data/BukkitDataCache.java b/bukkit/src/main/java/me/william278/crossserversync/bukkit/data/BukkitDataCache.java new file mode 100644 index 00000000..f5a2cff2 --- /dev/null +++ b/bukkit/src/main/java/me/william278/crossserversync/bukkit/data/BukkitDataCache.java @@ -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 bukkitDataCache; + + /** + * Map of Player UUIDs to request on join + */ + private static HashSet 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); + } +} diff --git a/bukkit/src/main/java/me/william278/crossserversync/bukkit/data/LastDataUpdateUUIDCache.java b/bukkit/src/main/java/me/william278/crossserversync/bukkit/data/LastDataUpdateUUIDCache.java deleted file mode 100644 index c1247000..00000000 --- a/bukkit/src/main/java/me/william278/crossserversync/bukkit/data/LastDataUpdateUUIDCache.java +++ /dev/null @@ -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 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); - } - -} diff --git a/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/BukkitRedisListener.java b/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/BukkitRedisListener.java index 2742016a..7fed9ced 100644 --- a/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/BukkitRedisListener.java +++ b/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/BukkitRedisListener.java @@ -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; } } } diff --git a/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/EventListener.java b/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/EventListener.java index 7382b6fb..991a9041 100644 --- a/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/EventListener.java +++ b/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/EventListener.java @@ -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); + } } } } diff --git a/bukkit/src/main/resources/config.yml b/bukkit/src/main/resources/config.yml index 5d60f385..5d4f05bf 100644 --- a/bukkit/src/main/resources/config.yml +++ b/bukkit/src/main/resources/config.yml @@ -8,4 +8,6 @@ synchronisation_settings: health: true hunger: true experience: true - potion_effects: true \ No newline at end of file + potion_effects: true + statistics: true + game_mode: true \ No newline at end of file diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/command/CrossServerSyncCommand.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/command/CrossServerSyncCommand.java index 8a588707..cb8786e6 100644 --- a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/command/CrossServerSyncCommand.java +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/command/CrossServerSyncCommand.java @@ -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) { diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/DataManager.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/DataManager.java index a51e3b94..bce9e464 100644 --- a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/DataManager.java +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/DataManager.java @@ -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(); } diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/MySQL.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/MySQL.java index 28f79148..3ca7a794 100644 --- a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/MySQL.java +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/MySQL.java @@ -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`)" + diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/SQLite.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/SQLite.java index 8794999b..7bf38af5 100644 --- a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/SQLite.java +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/SQLite.java @@ -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`)" + ");" diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeEventListener.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeEventListener.java index db6fc8f1..060f6e11 100644 --- a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeEventListener.java +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeEventListener.java @@ -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(); + } }); } diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeRedisListener.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeRedisListener.java index 3a2ecae1..e9ef671a 100644 --- a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeRedisListener.java +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeRedisListener.java @@ -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(); + } + } + } } } } diff --git a/common/src/main/java/me/william278/crossserversync/MessageStrings.java b/common/src/main/java/me/william278/crossserversync/MessageStrings.java index ae9709b7..b8d4d1d5 100644 --- a/common/src/main/java/me/william278/crossserversync/MessageStrings.java +++ b/common/src/main/java/me/william278/crossserversync/MessageStrings.java @@ -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)"; + } \ No newline at end of file diff --git a/common/src/main/java/me/william278/crossserversync/PlayerData.java b/common/src/main/java/me/william278/crossserversync/PlayerData.java index 36742712..5b172dc3 100644 --- a/common/src/main/java/me/william278/crossserversync/PlayerData.java +++ b/common/src/main/java/me/william278/crossserversync/PlayerData.java @@ -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; + } } diff --git a/common/src/main/java/me/william278/crossserversync/Settings.java b/common/src/main/java/me/william278/crossserversync/Settings.java index a8ede679..510d9fc4 100644 --- a/common/src/main/java/me/william278/crossserversync/Settings.java +++ b/common/src/main/java/me/william278/crossserversync/Settings.java @@ -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 diff --git a/common/src/main/java/me/william278/crossserversync/redis/RedisMessage.java b/common/src/main/java/me/william278/crossserversync/redis/RedisMessage.java index dca892af..177129f5 100644 --- a/common/src/main/java/me/william278/crossserversync/redis/RedisMessage.java +++ b/common/src/main/java/me/william278/crossserversync/redis/RedisMessage.java @@ -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