9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-26 18:19:10 +00:00

Make synchronisation much smoother, add Statistics and fix experience syncing

This commit is contained in:
William
2021-10-22 02:10:13 +01:00
parent d54de93099
commit bd316c0b8c
19 changed files with 442 additions and 127 deletions

View File

@@ -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);

View File

@@ -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 { }
}

View File

@@ -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));
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}
}