9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-27 02:29:10 +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;
import me.william278.crossserversync.redis.RedisMessage;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.*;
import org.bukkit.advancement.Advancement;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
@@ -15,10 +16,7 @@ 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.*;
import java.util.stream.Collectors;
/**
@@ -120,6 +118,7 @@ public final class DataSerializer {
* @return ItemStack array created from the Base64 string.
* @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 {
// Return an empty ItemStack[] if the data is empty
if (data.isEmpty()) {
@@ -130,7 +129,6 @@ public final class DataSerializer {
ItemStack[] items = new ItemStack[dataInput.readInt()];
for (int Index = 0; Index < items.length; Index++) {
@SuppressWarnings("unchecked") // Ignore the unchecked cast here
Map<String, Object> stack = (Map<String, Object>) dataInput.readObject();
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 {
// Return an empty PotionEffect[] if the data is empty
if (data.isEmpty()) {
@@ -156,7 +155,6 @@ public final class DataSerializer {
PotionEffect[] items = new PotionEffect[dataInput.readInt()];
for (int Index = 0; Index < items.length; Index++) {
@SuppressWarnings("unchecked") // Ignore the unchecked cast here
Map<String, Object> effect = (Map<String, Object>) dataInput.readObject();
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 {
if (serializedStatisticData.isEmpty()) {
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 {
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<>();
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<>();
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<>();
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<>();
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));
}
@@ -219,8 +268,9 @@ public final class DataSerializer {
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 { }
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

@@ -6,10 +6,9 @@ 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.*;
import org.bukkit.advancement.Advancement;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.attribute.Attribute;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
@@ -17,6 +16,8 @@ import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
import java.util.logging.Level;
@@ -39,6 +40,10 @@ public class PlayerSetter {
// Set the player's data from the PlayerData
Bukkit.getScheduler().runTask(plugin, () -> {
try {
if (Settings.syncAdvancements) {
// Sync advancements first so that any rewards will be overridden
setPlayerAdvancements(player, DataSerializer.deserializeAdvancementData(data.getSerializedAdvancements()));
}
if (Settings.syncInventories) {
setPlayerInventory(player, DataSerializer.itemStackArrayFromBase64(data.getSerializedInventory()));
player.getInventory().setHeldItemSlot(data.getSelectedSlot());
@@ -69,6 +74,10 @@ public class PlayerSetter {
if (Settings.syncGameMode) {
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
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, new MineDown(MessageStrings.SYNCHRONISATION_COMPLETE).toComponent());
@@ -114,7 +123,8 @@ public class PlayerSetter {
/**
* 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
*/
private static void setPlayerPotionEffects(Player player, PotionEffect[] effects) {
@@ -126,9 +136,70 @@ 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)
* @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
*/
private static void setPlayerStatistics(Player player, DataSerializer.StatisticData statisticData) {
@@ -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.syncStatistics = config.getBoolean("synchronisation_settings.statistics", 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)) {
return;
}
// Handle the message for the player
if (message.getMessageTarget().targetPlayerUUID() == null) {
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.util.Objects;
import java.util.UUID;
import java.util.logging.Level;
public class EventListener implements Listener {
@@ -23,6 +22,7 @@ public class EventListener implements Listener {
/**
* Returns the new serialized PlayerData for a player.
*
* @param player The {@link Player} to get the new serialized PlayerData for
* @return The {@link PlayerData}, serialized as a {@link String}
* @throws IOException If the serialization fails
@@ -42,7 +42,10 @@ public class EventListener implements Listener {
player.getLevel(),
player.getExp(),
player.getGameMode().toString(),
DataSerializer.getSerializedStatisticData(player)));
DataSerializer.getSerializedStatisticData(player),
player.isFlying(),
DataSerializer.getSerializedAdvancements(player),
DataSerializer.getSerializedLocation(player)));
}
@EventHandler
@@ -52,8 +55,8 @@ public class EventListener implements Listener {
try {
// Get the player's last updated PlayerData version UUID
final UUID lastUpdatedDataVersion = CrossServerSyncBukkit.bukkitCache.getVersionUUID(player.getUniqueId());
if (lastUpdatedDataVersion == null) return; // Return if the player has not been properly updated.
//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
final String serializedPlayerData = getNewSerializedPlayerData(player);
@@ -63,13 +66,24 @@ public class EventListener implements Listener {
} catch (IOException 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
public void onPlayerJoin(PlayerJoinEvent event) {
// When a player joins a Bukkit server
final Player player = event.getPlayer();
// Clear player inventory and ender chest
player.getInventory().clear();
player.getEnderChest().clear();
if (CrossServerSyncBukkit.bukkitCache.isPlayerRequestingOnJoin(player.getUniqueId())) {
try {
// Send a redis message requesting the player data