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:
@@ -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 {
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user