9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-23 16:49:19 +00:00

Add advancement, location and flight syncing, fix an issue that sometimes led to inconsistent syncs

This commit is contained in:
William
2021-10-22 17:53:49 +01:00
parent bd316c0b8c
commit 520f1ea1d7
12 changed files with 304 additions and 97 deletions

View File

@@ -1,8 +1,9 @@
package me.william278.crossserversync.bukkit; package me.william278.crossserversync.bukkit;
import me.william278.crossserversync.redis.RedisMessage; import me.william278.crossserversync.redis.RedisMessage;
import org.bukkit.Material; import org.bukkit.*;
import org.bukkit.Statistic; import org.bukkit.advancement.Advancement;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.entity.EntityType; import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
@@ -15,10 +16,7 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
import java.util.Arrays; import java.util.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -120,6 +118,7 @@ public final class DataSerializer {
* @return ItemStack array created from the Base64 string. * @return ItemStack array created from the Base64 string.
* @throws IOException in the event the class type cannot be decoded * @throws IOException in the event the class type cannot be decoded
*/ */
@SuppressWarnings("unchecked") // Ignore the unchecked cast here
public static ItemStack[] itemStackArrayFromBase64(String data) throws IOException { public static ItemStack[] itemStackArrayFromBase64(String data) throws IOException {
// Return an empty ItemStack[] if the data is empty // Return an empty ItemStack[] if the data is empty
if (data.isEmpty()) { if (data.isEmpty()) {
@@ -130,7 +129,6 @@ public final class DataSerializer {
ItemStack[] items = new ItemStack[dataInput.readInt()]; ItemStack[] items = new ItemStack[dataInput.readInt()];
for (int Index = 0; Index < items.length; Index++) { for (int Index = 0; Index < items.length; Index++) {
@SuppressWarnings("unchecked") // Ignore the unchecked cast here
Map<String, Object> stack = (Map<String, Object>) dataInput.readObject(); Map<String, Object> stack = (Map<String, Object>) dataInput.readObject();
if (stack != null) { if (stack != null) {
@@ -146,6 +144,7 @@ public final class DataSerializer {
} }
} }
@SuppressWarnings("unchecked") // Ignore the unchecked cast here
public static PotionEffect[] potionEffectArrayFromBase64(String data) throws IOException { public static PotionEffect[] potionEffectArrayFromBase64(String data) throws IOException {
// Return an empty PotionEffect[] if the data is empty // Return an empty PotionEffect[] if the data is empty
if (data.isEmpty()) { if (data.isEmpty()) {
@@ -156,7 +155,6 @@ public final class DataSerializer {
PotionEffect[] items = new PotionEffect[dataInput.readInt()]; PotionEffect[] items = new PotionEffect[dataInput.readInt()];
for (int Index = 0; Index < items.length; Index++) { for (int Index = 0; Index < items.length; Index++) {
@SuppressWarnings("unchecked") // Ignore the unchecked cast here
Map<String, Object> effect = (Map<String, Object>) dataInput.readObject(); Map<String, Object> effect = (Map<String, Object>) dataInput.readObject();
if (effect != null) { if (effect != null) {
@@ -172,6 +170,57 @@ public final class DataSerializer {
} }
} }
public static PlayerLocation deserializePlayerLocationData(String serializedLocationData) throws IOException {
if (serializedLocationData.isEmpty()) {
return null;
}
try {
return (PlayerLocation) RedisMessage.deserialize(serializedLocationData);
} catch (ClassNotFoundException e) {
throw new IOException("Unable to decode class type.", e);
}
}
public static String getSerializedLocation(Player player) throws IOException {
final Location playerLocation = player.getLocation();
return RedisMessage.serialize(new PlayerLocation(playerLocation.getX(), playerLocation.getY(), playerLocation.getZ(),
playerLocation.getYaw(), playerLocation.getPitch(), player.getWorld().getName(), player.getWorld().getEnvironment()));
}
public record PlayerLocation(double x, double y, double z, float yaw, float pitch,
String worldName, World.Environment environment) implements Serializable {
}
@SuppressWarnings("unchecked") // Ignore the unchecked cast here
public static ArrayList<AdvancementRecord> deserializeAdvancementData(String serializedAdvancementData) throws IOException {
if (serializedAdvancementData.isEmpty()) {
return new ArrayList<>();
}
try {
return (ArrayList<AdvancementRecord>) RedisMessage.deserialize(serializedAdvancementData);
} catch (ClassNotFoundException e) {
throw new IOException("Unable to decode class type.", e);
}
}
public static String getSerializedAdvancements(Player player) throws IOException {
Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
ArrayList<AdvancementRecord> advancementData = new ArrayList<>();
while (serverAdvancements.hasNext()) {
final AdvancementProgress progress = player.getAdvancementProgress(serverAdvancements.next());
final NamespacedKey advancementKey = progress.getAdvancement().getKey();
final ArrayList<String> awardedCriteria = new ArrayList<>(progress.getAwardedCriteria());
advancementData.add(new AdvancementRecord(advancementKey.getNamespace() + ":" + advancementKey.getKey(), awardedCriteria));
}
return RedisMessage.serialize(advancementData);
}
public record AdvancementRecord(String advancementKey,
ArrayList<String> awardedAdvancementCriteria) implements Serializable {
}
public static StatisticData deserializeStatisticData(String serializedStatisticData) throws IOException { public static StatisticData deserializeStatisticData(String serializedStatisticData) throws IOException {
if (serializedStatisticData.isEmpty()) { if (serializedStatisticData.isEmpty()) {
return new StatisticData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()); return new StatisticData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
@@ -184,28 +233,28 @@ public final class DataSerializer {
} }
public static String getSerializedStatisticData(Player player) throws IOException { public static String getSerializedStatisticData(Player player) throws IOException {
HashMap<Statistic,Integer> untypedStatisticValues = new HashMap<>(); HashMap<Statistic, Integer> untypedStatisticValues = new HashMap<>();
HashMap<Statistic,HashMap<Material,Integer>> blockStatisticValues = new HashMap<>(); HashMap<Statistic, HashMap<Material, Integer>> blockStatisticValues = new HashMap<>();
HashMap<Statistic,HashMap<Material,Integer>> itemStatisticValues = new HashMap<>(); HashMap<Statistic, HashMap<Material, Integer>> itemStatisticValues = new HashMap<>();
HashMap<Statistic,HashMap<EntityType,Integer>> entityStatisticValues = new HashMap<>(); HashMap<Statistic, HashMap<EntityType, Integer>> entityStatisticValues = new HashMap<>();
for (Statistic statistic : Statistic.values()) { for (Statistic statistic : Statistic.values()) {
switch (statistic.getType()) { switch (statistic.getType()) {
case ITEM -> { case ITEM -> {
HashMap<Material,Integer> itemValues = new HashMap<>(); HashMap<Material, Integer> itemValues = new HashMap<>();
for (Material itemMaterial : Arrays.stream(Material.values()).filter(Material::isItem).collect(Collectors.toList())) { for (Material itemMaterial : Arrays.stream(Material.values()).filter(Material::isItem).collect(Collectors.toList())) {
itemValues.put(itemMaterial, player.getStatistic(statistic, itemMaterial)); itemValues.put(itemMaterial, player.getStatistic(statistic, itemMaterial));
} }
itemStatisticValues.put(statistic, itemValues); itemStatisticValues.put(statistic, itemValues);
} }
case BLOCK -> { case BLOCK -> {
HashMap<Material,Integer> blockValues = new HashMap<>(); HashMap<Material, Integer> blockValues = new HashMap<>();
for (Material blockMaterial : Arrays.stream(Material.values()).filter(Material::isBlock).collect(Collectors.toList())) { for (Material blockMaterial : Arrays.stream(Material.values()).filter(Material::isBlock).collect(Collectors.toList())) {
blockValues.put(blockMaterial, player.getStatistic(statistic, blockMaterial)); blockValues.put(blockMaterial, player.getStatistic(statistic, blockMaterial));
} }
blockStatisticValues.put(statistic, blockValues); blockStatisticValues.put(statistic, blockValues);
} }
case ENTITY -> { case ENTITY -> {
HashMap<EntityType,Integer> entityValues = new HashMap<>(); HashMap<EntityType, Integer> entityValues = new HashMap<>();
for (EntityType type : Arrays.stream(EntityType.values()).filter(EntityType::isAlive).collect(Collectors.toList())) { for (EntityType type : Arrays.stream(EntityType.values()).filter(EntityType::isAlive).collect(Collectors.toList())) {
entityValues.put(type, player.getStatistic(statistic, type)); entityValues.put(type, player.getStatistic(statistic, type));
} }
@@ -219,8 +268,9 @@ public final class DataSerializer {
return RedisMessage.serialize(statisticData); return RedisMessage.serialize(statisticData);
} }
public record StatisticData(HashMap<Statistic,Integer> untypedStatisticValues, public record StatisticData(HashMap<Statistic, Integer> untypedStatisticValues,
HashMap<Statistic,HashMap<Material,Integer>> blockStatisticValues, HashMap<Statistic, HashMap<Material, Integer>> blockStatisticValues,
HashMap<Statistic,HashMap<Material,Integer>> itemStatisticValues, HashMap<Statistic, HashMap<Material, Integer>> itemStatisticValues,
HashMap<Statistic,HashMap<EntityType,Integer>> entityStatisticValues) implements Serializable { } HashMap<Statistic, HashMap<EntityType, Integer>> entityStatisticValues) implements Serializable {
}
} }

View File

@@ -6,10 +6,9 @@ import me.william278.crossserversync.MessageStrings;
import me.william278.crossserversync.PlayerData; import me.william278.crossserversync.PlayerData;
import me.william278.crossserversync.Settings; import me.william278.crossserversync.Settings;
import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.ChatMessageType;
import org.bukkit.Bukkit; import org.bukkit.*;
import org.bukkit.GameMode; import org.bukkit.advancement.Advancement;
import org.bukkit.Material; import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.Statistic;
import org.bukkit.attribute.Attribute; import org.bukkit.attribute.Attribute;
import org.bukkit.entity.EntityType; import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@@ -17,6 +16,8 @@ import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffect;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects; import java.util.Objects;
import java.util.logging.Level; import java.util.logging.Level;
@@ -39,6 +40,10 @@ public class PlayerSetter {
// Set the player's data from the PlayerData // Set the player's data from the PlayerData
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
try { try {
if (Settings.syncAdvancements) {
// Sync advancements first so that any rewards will be overridden
setPlayerAdvancements(player, DataSerializer.deserializeAdvancementData(data.getSerializedAdvancements()));
}
if (Settings.syncInventories) { if (Settings.syncInventories) {
setPlayerInventory(player, DataSerializer.itemStackArrayFromBase64(data.getSerializedInventory())); setPlayerInventory(player, DataSerializer.itemStackArrayFromBase64(data.getSerializedInventory()));
player.getInventory().setHeldItemSlot(data.getSelectedSlot()); player.getInventory().setHeldItemSlot(data.getSelectedSlot());
@@ -69,6 +74,10 @@ public class PlayerSetter {
if (Settings.syncGameMode) { if (Settings.syncGameMode) {
player.setGameMode(GameMode.valueOf(data.getGameMode())); player.setGameMode(GameMode.valueOf(data.getGameMode()));
} }
if (Settings.syncLocation) {
player.setFlying(player.getAllowFlight() && data.isFlying());
setPlayerLocation(player, DataSerializer.deserializePlayerLocationData(data.getSerializedLocation()));
}
// Send action bar synchronisation message // Send action bar synchronisation message
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, new MineDown(MessageStrings.SYNCHRONISATION_COMPLETE).toComponent()); player.spigot().sendMessage(ChatMessageType.ACTION_BAR, new MineDown(MessageStrings.SYNCHRONISATION_COMPLETE).toComponent());
@@ -114,6 +123,7 @@ public class PlayerSetter {
/** /**
* Set a player's current potion effects from a set of {@link PotionEffect[]} * Set a player's current potion effects from a set of {@link PotionEffect[]}
*
* @param player The player to set the potion effects of * @param player The player to set the potion effects of
* @param effects The array of {@link PotionEffect}s to set * @param effects The array of {@link PotionEffect}s to set
*/ */
@@ -126,8 +136,69 @@ public class PlayerSetter {
} }
} }
/**
* Update a player's advancements and progress to match the advancementData
*
* @param player The player to set the advancements of
* @param advancementData The ArrayList of {@link DataSerializer.AdvancementRecord}s to set
*/
private static void setPlayerAdvancements(Player player, ArrayList<DataSerializer.AdvancementRecord> advancementData) {
// Temporarily disable advancement announcing if needed
boolean announceAdvancementUpdate = false;
if (Boolean.TRUE.equals(player.getWorld().getGameRuleValue(GameRule.ANNOUNCE_ADVANCEMENTS))) {
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false);
announceAdvancementUpdate = true;
}
final boolean finalAnnounceAdvancementUpdate = announceAdvancementUpdate;
// Run async because advancement loading is very slow
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
// Apply the advancements to the player
Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
while (serverAdvancements.hasNext()) {
Advancement advancement = serverAdvancements.next();
AdvancementProgress playerProgress = player.getAdvancementProgress(advancement);
boolean hasAdvancement = false;
for (DataSerializer.AdvancementRecord record : advancementData) {
if (record.advancementKey().equals(advancement.getKey().getNamespace() + ":" + advancement.getKey().getKey())) {
hasAdvancement = true;
// Save the experience before granting the advancement
final int expLevel = player.getLevel();
final float expProgress = player.getExp();
// Grant advancement criteria if the player does not have it
for (String awardCriteria : record.awardedAdvancementCriteria()) {
if (!playerProgress.getAwardedCriteria().contains(awardCriteria)) {
Bukkit.getScheduler().runTask(plugin, () -> player.getAdvancementProgress(advancement).awardCriteria(awardCriteria));
}
}
// Set experience back to before granting advancement; nullify exp gained from it
player.setLevel(expLevel);
player.setExp(expProgress);
break;
}
}
if (!hasAdvancement) {
for (String awardCriteria : playerProgress.getAwardedCriteria()) {
Bukkit.getScheduler().runTask(plugin, () -> player.getAdvancementProgress(advancement).revokeCriteria(awardCriteria));
}
}
}
// Re-enable announcing advancements (back on main thread again)
Bukkit.getScheduler().runTask(plugin, () -> {
if (finalAnnounceAdvancementUpdate) {
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, true);
}
});
});
}
/** /**
* Set a player's statistics (in the Statistic menu) * Set a player's statistics (in the Statistic menu)
*
* @param player The player to set the statistics of * @param player The player to set the statistics of
* @param statisticData The {@link DataSerializer.StatisticData} to set * @param statisticData The {@link DataSerializer.StatisticData} to set
*/ */
@@ -158,4 +229,37 @@ public class PlayerSetter {
} }
} }
} }
/**
* Set a player's location from {@link DataSerializer.PlayerLocation} data
*
* @param player The {@link Player} to teleport
* @param location The {@link DataSerializer.PlayerLocation}
*/
private static void setPlayerLocation(Player player, DataSerializer.PlayerLocation location) {
// Don't teleport if the location is invalid
if (location == null) {
return;
}
// Determine the world; if the names match, use that
World world = Bukkit.getWorld(location.worldName());
if (world == null) {
// If the names don't match, find the corresponding world with the same dimension environment
for (World worldOnServer : Bukkit.getWorlds()) {
if (worldOnServer.getEnvironment().equals(location.environment())) {
world = worldOnServer;
}
}
// If that still fails, return
if (world == null) {
return;
}
}
// Teleport the player
player.teleport(new Location(world, location.x(), location.y(), location.z(), location.yaw(), location.pitch()));
}
} }

View File

@@ -19,7 +19,8 @@ public class ConfigLoader {
Settings.syncPotionEffects = config.getBoolean("synchronisation_settings.potion_effects", true); Settings.syncPotionEffects = config.getBoolean("synchronisation_settings.potion_effects", true);
Settings.syncStatistics = config.getBoolean("synchronisation_settings.statistics", true); Settings.syncStatistics = config.getBoolean("synchronisation_settings.statistics", true);
Settings.syncGameMode = config.getBoolean("synchronisation_settings.game_mode", true); Settings.syncGameMode = config.getBoolean("synchronisation_settings.game_mode", true);
Settings.syncAdvancements = config.getBoolean("synchronisation_settings.advancements", true);
Settings.syncLocation = config.getBoolean("synchronisation_settings.location", false);
} }
} }

View File

@@ -35,6 +35,8 @@ public class BukkitRedisListener extends RedisListener {
if (!message.getMessageTarget().targetServerType().equals(Settings.ServerType.BUKKIT)) { if (!message.getMessageTarget().targetServerType().equals(Settings.ServerType.BUKKIT)) {
return; return;
} }
// Handle the message for the player // Handle the message for the player
if (message.getMessageTarget().targetPlayerUUID() == null) { if (message.getMessageTarget().targetPlayerUUID() == null) {
if (message.getMessageType() == RedisMessage.MessageType.REQUEST_DATA_ON_JOIN) { if (message.getMessageType() == RedisMessage.MessageType.REQUEST_DATA_ON_JOIN) {

View File

@@ -14,7 +14,6 @@ import org.bukkit.event.player.PlayerQuitEvent;
import java.io.IOException; import java.io.IOException;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
import java.util.logging.Level; import java.util.logging.Level;
public class EventListener implements Listener { public class EventListener implements Listener {
@@ -23,6 +22,7 @@ public class EventListener implements Listener {
/** /**
* Returns the new serialized PlayerData for a player. * Returns the new serialized PlayerData for a player.
*
* @param player The {@link Player} to get the new serialized PlayerData for * @param player The {@link Player} to get the new serialized PlayerData for
* @return The {@link PlayerData}, serialized as a {@link String} * @return The {@link PlayerData}, serialized as a {@link String}
* @throws IOException If the serialization fails * @throws IOException If the serialization fails
@@ -42,7 +42,10 @@ public class EventListener implements Listener {
player.getLevel(), player.getLevel(),
player.getExp(), player.getExp(),
player.getGameMode().toString(), player.getGameMode().toString(),
DataSerializer.getSerializedStatisticData(player))); DataSerializer.getSerializedStatisticData(player),
player.isFlying(),
DataSerializer.getSerializedAdvancements(player),
DataSerializer.getSerializedLocation(player)));
} }
@EventHandler @EventHandler
@@ -52,8 +55,8 @@ public class EventListener implements Listener {
try { try {
// Get the player's last updated PlayerData version UUID // Get the player's last updated PlayerData version UUID
final UUID lastUpdatedDataVersion = CrossServerSyncBukkit.bukkitCache.getVersionUUID(player.getUniqueId()); //final UUID lastUpdatedDataVersion = CrossServerSyncBukkit.bukkitCache.getVersionUUID(player.getUniqueId());
if (lastUpdatedDataVersion == null) return; // Return if the player has not been properly updated. //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 // Send a redis message with the player's last updated PlayerData version UUID and their new PlayerData
final String serializedPlayerData = getNewSerializedPlayerData(player); final String serializedPlayerData = getNewSerializedPlayerData(player);
@@ -63,6 +66,13 @@ public class EventListener implements Listener {
} catch (IOException e) { } catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData update to the proxy", e); plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData update to the proxy", e);
} }
// Clear player inventory and ender chest
player.getInventory().clear();
player.getEnderChest().clear();
// Set data version ID to null
CrossServerSyncBukkit.bukkitCache.setVersionUUID(player.getUniqueId(), null);
} }
@EventHandler @EventHandler
@@ -70,6 +80,10 @@ public class EventListener implements Listener {
// When a player joins a Bukkit server // When a player joins a Bukkit server
final Player player = event.getPlayer(); final Player player = event.getPlayer();
// Clear player inventory and ender chest
player.getInventory().clear();
player.getEnderChest().clear();
if (CrossServerSyncBukkit.bukkitCache.isPlayerRequestingOnJoin(player.getUniqueId())) { if (CrossServerSyncBukkit.bukkitCache.isPlayerRequestingOnJoin(player.getUniqueId())) {
try { try {
// Send a redis message requesting the player data // Send a redis message requesting the player data

View File

@@ -11,3 +11,5 @@ synchronisation_settings:
potion_effects: true potion_effects: true
statistics: true statistics: true
game_mode: true game_mode: true
advancements: true
location: false

View File

@@ -80,10 +80,16 @@ public class DataManager {
final int expLevel = resultSet.getInt("exp_level"); final int expLevel = resultSet.getInt("exp_level");
final float expProgress = resultSet.getInt("exp_progress"); final float expProgress = resultSet.getInt("exp_progress");
final String gameMode = resultSet.getString("game_mode"); final String gameMode = resultSet.getString("game_mode");
final boolean isFlying = resultSet.getBoolean("is_flying");
final String serializedAdvancementData = resultSet.getString("advancements");
final String serializedLocationData = resultSet.getString( "location");
final String serializedStatisticData = resultSet.getString("statistics"); 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); return new PlayerData(playerUUID, dataVersionUUID, serializedInventory, serializedEnderChest,
health, maxHealth, hunger, saturation, saturationExhaustion, selectedSlot, serializedStatusEffects,
totalExperience, expLevel, expProgress, gameMode, serializedStatisticData, isFlying,
serializedAdvancementData, serializedLocationData);
} else { } else {
return PlayerData.DEFAULT_PLAYER_DATA(playerUUID); return PlayerData.DEFAULT_PLAYER_DATA(playerUUID);
} }
@@ -111,7 +117,7 @@ public class DataManager {
private static void updatePlayerSQLData(PlayerData playerData) { private static void updatePlayerSQLData(PlayerData playerData) {
try (Connection connection = CrossServerSyncBungeeCord.getConnection()) { try (Connection connection = CrossServerSyncBungeeCord.getConnection()) {
try (PreparedStatement statement = connection.prepareStatement( 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`=?, `total_experience`=?, `exp_level`=?, `exp_progress`=?, `game_mode`=?, `statistics`=? 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`=?, `is_flying`=?, `advancements`=?, `location`=? WHERE `player_id`=(SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?);")) {
statement.setString(1, playerData.getDataVersionUUID().toString()); statement.setString(1, playerData.getDataVersionUUID().toString());
statement.setTimestamp(2, new Timestamp(Instant.now().getEpochSecond())); statement.setTimestamp(2, new Timestamp(Instant.now().getEpochSecond()));
statement.setString(3, playerData.getSerializedInventory()); statement.setString(3, playerData.getSerializedInventory());
@@ -128,8 +134,11 @@ public class DataManager {
statement.setFloat(14, playerData.getExpProgress()); // Exp progress statement.setFloat(14, playerData.getExpProgress()); // Exp progress
statement.setString(15, playerData.getGameMode()); // GameMode statement.setString(15, playerData.getGameMode()); // GameMode
statement.setString(16, playerData.getSerializedStatistics()); // Statistics statement.setString(16, playerData.getSerializedStatistics()); // Statistics
statement.setBoolean(17, playerData.isFlying()); // Is flying
statement.setString(18, playerData.getSerializedAdvancements()); // Advancements
statement.setString(19, playerData.getSerializedLocation()); // Location
statement.setString(17, playerData.getPlayerUUID().toString()); statement.setString(20, playerData.getPlayerUUID().toString());
statement.executeUpdate(); statement.executeUpdate();
} }
} catch (SQLException e) { } catch (SQLException e) {
@@ -140,7 +149,7 @@ public class DataManager {
private static void insertPlayerData(PlayerData playerData) { private static void insertPlayerData(PlayerData playerData) {
try (Connection connection = CrossServerSyncBungeeCord.getConnection()) { try (Connection connection = CrossServerSyncBungeeCord.getConnection()) {
try (PreparedStatement statement = connection.prepareStatement( 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`,`total_experience`,`exp_level`,`exp_progress`,`game_mode`,`statistics`) 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`,`is_flying`,`advancements`,`location`) VALUES((SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);")) {
statement.setString(1, playerData.getPlayerUUID().toString()); statement.setString(1, playerData.getPlayerUUID().toString());
statement.setString(2, playerData.getDataVersionUUID().toString()); statement.setString(2, playerData.getDataVersionUUID().toString());
statement.setTimestamp(3, new Timestamp(Instant.now().getEpochSecond())); statement.setTimestamp(3, new Timestamp(Instant.now().getEpochSecond()));
@@ -157,8 +166,10 @@ public class DataManager {
statement.setInt(14, playerData.getExpLevel()); // Exp level statement.setInt(14, playerData.getExpLevel()); // Exp level
statement.setFloat(15, playerData.getExpProgress()); // Exp progress statement.setFloat(15, playerData.getExpProgress()); // Exp progress
statement.setString(16, playerData.getGameMode()); // GameMode statement.setString(16, playerData.getGameMode()); // GameMode
statement.setString(17, playerData.getSerializedStatistics()); // Statistics statement.setString(17, playerData.getSerializedStatistics()); // Statistics
statement.setBoolean(18, playerData.isFlying()); // Is flying
statement.setString(19, playerData.getSerializedAdvancements()); // Advancements
statement.setString(20, playerData.getSerializedLocation()); // Location
statement.executeUpdate(); statement.executeUpdate();
} }
@@ -234,22 +245,5 @@ public class DataManager {
return null; 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);
}
}
} }
} }

View File

@@ -37,6 +37,9 @@ public class MySQL extends Database {
"`exp_progress` float NOT NULL," + "`exp_progress` float NOT NULL," +
"`game_mode` tinytext NOT NULL," + "`game_mode` tinytext NOT NULL," +
"`statistics` longtext NOT NULL," + "`statistics` longtext NOT NULL," +
"`is_flying` boolean NOT NULL," +
"`advancements` longtext NOT NULL," +
"`location` text NOT NULL," +
"PRIMARY KEY (`player_id`,`uuid`)," + "PRIMARY KEY (`player_id`,`uuid`)," +
"FOREIGN KEY (`player_id`) REFERENCES " + PLAYER_TABLE_NAME + " (`id`)" + "FOREIGN KEY (`player_id`) REFERENCES " + PLAYER_TABLE_NAME + " (`id`)" +

View File

@@ -45,6 +45,9 @@ public class SQLite extends Database {
"`exp_progress` float NOT NULL," + "`exp_progress` float NOT NULL," +
"`game_mode` tinytext NOT NULL," + "`game_mode` tinytext NOT NULL," +
"`statistics` longtext NOT NULL," + "`statistics` longtext NOT NULL," +
"`is_flying` boolean NOT NULL," +
"`advancements` longtext NOT NULL," +
"`location` text NOT NULL," +
"PRIMARY KEY (`player_id`,`version_uuid`)" + "PRIMARY KEY (`player_id`,`version_uuid`)" +
");" ");"

View File

@@ -1,6 +1,5 @@
package me.william278.crossserversync.bungeecord.listener; package me.william278.crossserversync.bungeecord.listener;
import de.themoep.minedown.MineDown;
import me.william278.crossserversync.CrossServerSyncBungeeCord; import me.william278.crossserversync.CrossServerSyncBungeeCord;
import me.william278.crossserversync.PlayerData; import me.william278.crossserversync.PlayerData;
import me.william278.crossserversync.Settings; import me.william278.crossserversync.Settings;
@@ -88,24 +87,24 @@ public class BungeeRedisListener extends RedisListener {
// Update the data in the cache and SQL // Update the data in the cache and SQL
DataManager.updatePlayerData(playerData); DataManager.updatePlayerData(playerData);
// Reply to set the player's data if they moved servers // Reply with the player data if they are still online (switching server)
try {
ProxiedPlayer player = ProxyServer.getInstance().getPlayer(playerData.getPlayerUUID()); ProxiedPlayer player = ProxyServer.getInstance().getPlayer(playerData.getPlayerUUID());
if (player != null) { if (player != null) {
if (player.isConnected()) { if (player.isConnected()) {
// Send the reply, serializing the message data
try {
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET, new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, playerData.getPlayerUUID()), new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, playerData.getPlayerUUID()),
RedisMessage.serialize(playerData)).send(); RedisMessage.serialize(playerData))
.send();
}
}
} catch (IOException e) { } catch (IOException e) {
log(Level.SEVERE, "Failed to serialize data when replying to a data update"); log(Level.SEVERE, "Failed to re-serialize PlayerData when handling a player update request");
e.printStackTrace(); e.printStackTrace();
} }
} }
} }
} }
}
}
/** /**
* Log to console * Log to console

View File

@@ -33,9 +33,13 @@ public class PlayerData implements Serializable {
private final float expProgress; private final float expProgress;
private final String gameMode; private final String gameMode;
private final String serializedStatistics; private final String serializedStatistics;
private final boolean isFlying;
private final String serializedAdvancements;
private final String serializedLocation;
/** /**
* Constructor to create new PlayerData from a bukkit {@code Player}'s data * Constructor to create new PlayerData from a bukkit {@code Player}'s data
*
* @param playerUUID The Player's UUID * @param playerUUID The Player's UUID
* @param serializedInventory Their serialized inventory * @param serializedInventory Their serialized inventory
* @param serializedEnderChest Their serialized ender chest * @param serializedEnderChest Their serialized ender chest
@@ -52,7 +56,10 @@ public class PlayerData implements Serializable {
* @param gameMode Their game mode ({@code SURVIVAL}, {@code CREATIVE}, etc) * @param gameMode Their game mode ({@code SURVIVAL}, {@code CREATIVE}, etc)
* @param serializedStatistics Their serialized statistics data (Displayed in Statistics menu in ESC menu) * @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 totalExperience, int expLevel, float expProgress, String gameMode, String serializedStatistics) { 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, boolean isFlying, String serializedAdvancements, String serializedLocation) {
this.dataVersionUUID = UUID.randomUUID(); this.dataVersionUUID = UUID.randomUUID();
this.playerUUID = playerUUID; this.playerUUID = playerUUID;
this.serializedInventory = serializedInventory; this.serializedInventory = serializedInventory;
@@ -69,10 +76,14 @@ public class PlayerData implements Serializable {
this.expProgress = expProgress; this.expProgress = expProgress;
this.gameMode = gameMode; this.gameMode = gameMode;
this.serializedStatistics = serializedStatistics; this.serializedStatistics = serializedStatistics;
this.isFlying = isFlying;
this.serializedAdvancements = serializedAdvancements;
this.serializedLocation = serializedLocation;
} }
/** /**
* Constructor for a PlayerData object from an existing object that was stored in SQL * Constructor for a PlayerData object from an existing object that was stored in SQL
*
* @param playerUUID The player whose data this is' UUID * @param playerUUID The player whose data this is' UUID
* @param dataVersionUUID The PlayerData version UUID * @param dataVersionUUID The PlayerData version UUID
* @param serializedInventory Their serialized inventory * @param serializedInventory Their serialized inventory
@@ -90,7 +101,11 @@ public class PlayerData implements Serializable {
* @param gameMode Their game mode ({@code SURVIVAL}, {@code CREATIVE}, etc) * @param gameMode Their game mode ({@code SURVIVAL}, {@code CREATIVE}, etc)
* @param serializedStatistics Their serialized statistics data (Displayed in Statistics menu in ESC menu) * @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) { 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, boolean isFlying, String serializedAdvancements,
String serializedLocation) {
this.playerUUID = playerUUID; this.playerUUID = playerUUID;
this.dataVersionUUID = dataVersionUUID; this.dataVersionUUID = dataVersionUUID;
this.serializedInventory = serializedInventory; this.serializedInventory = serializedInventory;
@@ -107,17 +122,22 @@ public class PlayerData implements Serializable {
this.expProgress = expProgress; this.expProgress = expProgress;
this.gameMode = gameMode; this.gameMode = gameMode;
this.serializedStatistics = serializedStatistics; this.serializedStatistics = serializedStatistics;
this.isFlying = isFlying;
this.serializedAdvancements = serializedAdvancements;
this.serializedLocation = serializedLocation;
} }
/** /**
* Get default PlayerData for a new user * Get default PlayerData for a new user
*
* @param playerUUID The bukkit Player's UUID * @param playerUUID The bukkit Player's UUID
* @return Default {@link PlayerData} * @return Default {@link PlayerData}
*/ */
public static PlayerData DEFAULT_PLAYER_DATA(UUID playerUUID) { public static PlayerData DEFAULT_PLAYER_DATA(UUID playerUUID) {
PlayerData data = new PlayerData(playerUUID, "", "", 20, PlayerData data = new PlayerData(playerUUID, "", "", 20,
20, 20, 10, 1, 0, 20, 20, 10, 1, 0,
"", 0, 0, 0, "SURVIVAL", ""); "", 0, 0, 0, "SURVIVAL",
"", false, "", "");
data.useDefaultData = true; data.useDefaultData = true;
return data; return data;
} }
@@ -186,7 +206,20 @@ public class PlayerData implements Serializable {
return gameMode; return gameMode;
} }
public boolean isFlying() {
return isFlying;
}
public String getSerializedAdvancements() {
return serializedAdvancements;
}
public String getSerializedLocation() {
return serializedLocation;
}
public boolean isUseDefaultData() { public boolean isUseDefaultData() {
return useDefaultData; return useDefaultData;
} }
} }

View File

@@ -49,6 +49,8 @@ public class Settings {
public static boolean syncPotionEffects; public static boolean syncPotionEffects;
public static boolean syncStatistics; public static boolean syncStatistics;
public static boolean syncGameMode; public static boolean syncGameMode;
public static boolean syncAdvancements;
public static boolean syncLocation;
/* /*
* Enum definitions * Enum definitions