diff --git a/build.gradle b/build.gradle index 4df5451b..5f83a0f7 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ plugins { allprojects { group 'me.William278' - version '1.0.1' + version '1.0.2' compileJava { options.encoding = 'UTF-8' } tasks.withType(JavaCompile) { options.encoding = 'UTF-8' } diff --git a/bukkit/src/main/java/me/william278/husksync/bukkit/data/DataSerializer.java b/bukkit/src/main/java/me/william278/husksync/bukkit/data/DataSerializer.java deleted file mode 100644 index 8db70550..00000000 --- a/bukkit/src/main/java/me/william278/husksync/bukkit/data/DataSerializer.java +++ /dev/null @@ -1,277 +0,0 @@ -package me.william278.husksync.bukkit.data; - -import me.william278.husksync.redis.RedisMessage; -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.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.potion.PotionEffect; -import org.bukkit.util.io.BukkitObjectInputStream; -import org.bukkit.util.io.BukkitObjectOutputStream; -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.*; -import java.util.stream.Collectors; - -/** - * Class for serializing and deserializing player inventories and Ender Chests contents ({@link ItemStack[]}) as base64 strings. - * Based on https://gist.github.com/graywolf336/8153678 by graywolf336 - * Modified for 1.16 via https://gist.github.com/graywolf336/8153678#gistcomment-3551376 by efindus - * - * @author efindus - * @author graywolf336 - * @author William278 - */ -public final class DataSerializer { - - /** - * Converts the player inventory to a Base64 encoded string. - * - * @param inventory the inventory to convert to Base64. - * @return string with serialized Inventory - * @throws IllegalStateException in the event the item stacks cannot be saved - */ - public static String getSerializedInventoryContents(Inventory inventory) throws IllegalStateException { - // This contains contents, armor and offhand (contents are indexes 0 - 35, armor 36 - 39, offhand - 40) - return itemStackArrayToBase64(inventory.getContents()); - } - - /** - * Converts the player inventory to a Base64 encoded string. - * - * @param player whose Ender Chest will be turned into Base64. - * @return string with serialized Ender Chest - * @throws IllegalStateException in the event the item stacks cannot be saved - */ - public static String getSerializedEnderChestContents(Player player) throws IllegalStateException { - // This contains all slots (0-27) in the player's Ender Chest - return itemStackArrayToBase64(player.getEnderChest().getContents()); - } - - public static String getSerializedEffectData(Player player) { - PotionEffect[] potionEffects = new PotionEffect[player.getActivePotionEffects().size()]; - int x = 0; - for (PotionEffect effect : player.getActivePotionEffects()) { - potionEffects[x] = effect; - x++; - } - return effectArrayToBase64(potionEffects); - } - - public static String effectArrayToBase64(PotionEffect[] effects) { - try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - try (BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream)) { - dataOutput.writeInt(effects.length); - - for (PotionEffect effect : effects) { - if (effect != null) { - dataOutput.writeObject(effect.serialize()); - } else { - dataOutput.writeObject(null); - } - } - } - return Base64Coder.encodeLines(outputStream.toByteArray()); - } catch (Exception e) { - throw new IllegalStateException("Unable to save potion effects.", e); - } - } - - /** - * A method to serialize an {@link ItemStack} array to Base64 String. - * - * @param items to turn into a Base64 String. - * @return Base64 string of the items. - * @throws IllegalStateException in the event the item stacks cannot be saved - */ - public static String itemStackArrayToBase64(ItemStack[] items) throws IllegalStateException { - try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - try (BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream)) { - dataOutput.writeInt(items.length); - - for (ItemStack item : items) { - if (item != null) { - dataOutput.writeObject(item.serialize()); - } else { - dataOutput.writeObject(null); - } - } - } - return Base64Coder.encodeLines(outputStream.toByteArray()); - } catch (Exception e) { - throw new IllegalStateException("Unable to save item stacks.", e); - } - } - - /** - * Gets an array of ItemStacks from a Base64 string. - * - * @param data Base64 string to convert to ItemStack array. - * @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()) { - return new ItemStack[0]; - } - try (ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(data))) { - BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream); - ItemStack[] items = new ItemStack[dataInput.readInt()]; - - for (int Index = 0; Index < items.length; Index++) { - Map stack = (Map) dataInput.readObject(); - - if (stack != null) { - items[Index] = ItemStack.deserialize(stack); - } else { - items[Index] = null; - } - } - - return items; - } catch (ClassNotFoundException e) { - throw new IOException("Unable to decode class type.", e); - } - } - - @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()) { - return new PotionEffect[0]; - } - try (ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(data))) { - BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream); - PotionEffect[] items = new PotionEffect[dataInput.readInt()]; - - for (int Index = 0; Index < items.length; Index++) { - Map effect = (Map) dataInput.readObject(); - - if (effect != null) { - items[Index] = new PotionEffect(effect); - } else { - items[Index] = null; - } - } - - return items; - } catch (ClassNotFoundException e) { - throw new IOException("Unable to decode class type.", e); - } - } - - public static PlayerLocation deserializePlayerLocationData(String serializedLocationData) throws IOException { - if (serializedLocationData.isEmpty()) { - return null; - } - try { - return (PlayerLocation) me.william278.husksync.redis.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 me.william278.husksync.redis.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 deserializeAdvancementData(String serializedAdvancementData) throws IOException { - if (serializedAdvancementData.isEmpty()) { - return new ArrayList<>(); - } - try { - return (ArrayList) me.william278.husksync.redis.RedisMessage.deserialize(serializedAdvancementData); - } catch (ClassNotFoundException e) { - throw new IOException("Unable to decode class type.", e); - } - } - - public static String getSerializedAdvancements(Player player) throws IOException { - Iterator serverAdvancements = Bukkit.getServer().advancementIterator(); - ArrayList advancementData = new ArrayList<>(); - - while (serverAdvancements.hasNext()) { - final AdvancementProgress progress = player.getAdvancementProgress(serverAdvancements.next()); - final NamespacedKey advancementKey = progress.getAdvancement().getKey(); - final ArrayList awardedCriteria = new ArrayList<>(progress.getAwardedCriteria()); - advancementData.add(new AdvancementRecord(advancementKey.getNamespace() + ":" + advancementKey.getKey(), awardedCriteria)); - } - - return me.william278.husksync.redis.RedisMessage.serialize(advancementData); - } - - public record AdvancementRecord(String advancementKey, - ArrayList 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<>()); - } - try { - return (StatisticData) me.william278.husksync.redis.RedisMessage.deserialize(serializedStatisticData); - } catch (ClassNotFoundException e) { - throw new IOException("Unable to decode class type.", e); - } - } - - public static String getSerializedStatisticData(Player player) throws IOException { - HashMap untypedStatisticValues = new HashMap<>(); - HashMap> blockStatisticValues = new HashMap<>(); - HashMap> itemStatisticValues = new HashMap<>(); - HashMap> entityStatisticValues = new HashMap<>(); - for (Statistic statistic : Statistic.values()) { - switch (statistic.getType()) { - case ITEM -> { - HashMap itemValues = new HashMap<>(); - for (Material itemMaterial : Arrays.stream(Material.values()).filter(Material::isItem).collect(Collectors.toList())) { - itemValues.put(itemMaterial, player.getStatistic(statistic, itemMaterial)); - } - itemStatisticValues.put(statistic, itemValues); - } - case BLOCK -> { - HashMap blockValues = new HashMap<>(); - for (Material blockMaterial : Arrays.stream(Material.values()).filter(Material::isBlock).collect(Collectors.toList())) { - blockValues.put(blockMaterial, player.getStatistic(statistic, blockMaterial)); - } - blockStatisticValues.put(statistic, blockValues); - } - case ENTITY -> { - HashMap entityValues = new HashMap<>(); - for (EntityType type : Arrays.stream(EntityType.values()).filter(EntityType::isAlive).collect(Collectors.toList())) { - entityValues.put(type, player.getStatistic(statistic, type)); - } - entityStatisticValues.put(statistic, entityValues); - } - case UNTYPED -> untypedStatisticValues.put(statistic, player.getStatistic(statistic)); - } - } - - StatisticData statisticData = new StatisticData(untypedStatisticValues, blockStatisticValues, itemStatisticValues, entityStatisticValues); - return RedisMessage.serialize(statisticData); - } - - public record StatisticData(HashMap untypedStatisticValues, - HashMap> blockStatisticValues, - HashMap> itemStatisticValues, - HashMap> entityStatisticValues) implements Serializable { - } -} \ No newline at end of file diff --git a/bukkit/src/main/java/me/william278/husksync/bukkit/data/DataViewer.java b/bukkit/src/main/java/me/william278/husksync/bukkit/data/DataViewer.java index 2ffb805c..4074f5f2 100644 --- a/bukkit/src/main/java/me/william278/husksync/bukkit/data/DataViewer.java +++ b/bukkit/src/main/java/me/william278/husksync/bukkit/data/DataViewer.java @@ -24,7 +24,7 @@ public class DataViewer { * @param data The {@link DataView} to show the viewer * @throws IOException If an exception occurred deserializing item data */ - public static void showData(Player viewer, DataView data) throws IOException { + public static void showData(Player viewer, DataView data) throws IOException, ClassNotFoundException { // Show an inventory with the viewer's inventory and equipment viewer.closeInventory(); viewer.openInventory(createInventory(viewer, data)); @@ -49,7 +49,7 @@ public class DataViewer { // Get and update the PlayerData with the new item data PlayerData playerData = dataView.playerData(); - String serializedItemData = DataSerializer.itemStackArrayToBase64(inventory.getContents()); + String serializedItemData = PlayerSerializer.serializeInventory(inventory.getContents()); switch (dataView.inventoryType()) { case INVENTORY -> playerData.setSerializedInventory(serializedItemData); case ENDER_CHEST -> playerData.setSerializedEnderChest(serializedItemData); @@ -70,7 +70,7 @@ public class DataViewer { * @return The {@link Inventory} that the viewer will see * @throws IOException If an exception occurred deserializing item data */ - private static Inventory createInventory(Player viewer, DataView data) throws IOException { + private static Inventory createInventory(Player viewer, DataView data) throws IOException, ClassNotFoundException { Inventory inventory = switch (data.inventoryType) { case INVENTORY -> Bukkit.createInventory(viewer, 45, data.ownerName + "'s Inventory"); case ENDER_CHEST -> Bukkit.createInventory(viewer, 27, data.ownerName + "'s Ender Chest"); @@ -104,10 +104,10 @@ public class DataViewer { * @return The deserialized item data, as an {@link ItemStack[]} array * @throws IOException If an exception occurred deserializing item data */ - public ItemStack[] getDeserializedData() throws IOException { + public ItemStack[] getDeserializedData() throws IOException, ClassNotFoundException { return switch (inventoryType) { - case INVENTORY -> DataSerializer.itemStackArrayFromBase64(playerData.getSerializedInventory()); - case ENDER_CHEST -> DataSerializer.itemStackArrayFromBase64(playerData.getSerializedEnderChest()); + case INVENTORY -> PlayerSerializer.deserializeInventory(playerData.getSerializedInventory()); + case ENDER_CHEST -> PlayerSerializer.deserializeInventory(playerData.getSerializedEnderChest()); }; } } diff --git a/bukkit/src/main/java/me/william278/husksync/bukkit/data/PlayerSerializer.java b/bukkit/src/main/java/me/william278/husksync/bukkit/data/PlayerSerializer.java new file mode 100644 index 00000000..71e7b120 --- /dev/null +++ b/bukkit/src/main/java/me/william278/husksync/bukkit/data/PlayerSerializer.java @@ -0,0 +1,288 @@ +package me.william278.husksync.bukkit.data; + +import me.william278.husksync.redis.RedisMessage; +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; +import org.bukkit.potion.PotionEffect; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.bukkit.util.io.BukkitObjectOutputStream; +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.*; +import java.util.stream.Collectors; + +public class PlayerSerializer { + + /** + * Returns a serialized array of {@link ItemStack}s + * + * @param inventoryContents The contents of the inventory + * @return The serialized inventory contents + */ + public static String serializeInventory(ItemStack[] inventoryContents) { + // Create an output stream that will be encoded into base 64 + ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); + + try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) { + // Define the length of the inventory array to serialize + bukkitOutputStream.writeInt(inventoryContents.length); + + // Write each serialize each ItemStack to the output stream + for (ItemStack inventoryItem : inventoryContents) { + bukkitOutputStream.writeObject(serializeItemStack(inventoryItem)); + } + + // Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion + return Base64Coder.encodeLines(byteOutputStream.toByteArray()); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to serialize item stack data"); + } + } + + /** + * Returns an array of ItemStacks from serialized inventory data + * + * @param inventoryData The serialized {@link ItemStack[]} array + * @return The inventory contents as an array of {@link ItemStack}s + * @throws IOException If the deserialization fails reading data from the InputStream + * @throws ClassNotFoundException If the deserialization class cannot be found + */ + public static ItemStack[] deserializeInventory(String inventoryData) throws IOException, ClassNotFoundException { + // Return empty array if there is no inventory data (set the player as having an empty inventory) + if (inventoryData.isEmpty()) { + return new ItemStack[0]; + } + + // Create a byte input stream to read the serialized data + try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(inventoryData))) { + try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) { + // Read the length of the Bukkit input stream and set the length of the array to this value + ItemStack[] inventoryContents = new ItemStack[bukkitInputStream.readInt()]; + + // Set the ItemStacks in the array from deserialized ItemStack data + int slotIndex = 0; + for (ItemStack ignored : inventoryContents) { + inventoryContents[slotIndex] = deserializeItemStack(bukkitInputStream.readObject()); + slotIndex++; + } + + // Return the finished, serialized inventory contents + return inventoryContents; + } + } + } + + /** + * Returns the serialized version of an {@link ItemStack} as a string to object Map + * + * @param item The {@link ItemStack} to serialize + * @return The serialized {@link ItemStack} + */ + private static Map serializeItemStack(ItemStack item) { + return item != null ? item.serialize() : null; + } + + /** + * Returns the deserialized {@link ItemStack} from the Object read from the {@link BukkitObjectInputStream} + * + * @param serializedItemStack The serialized item stack; a String-Object map + * @return The deserialized {@link ItemStack} + */ + @SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning + private static ItemStack deserializeItemStack(Object serializedItemStack) { + return serializedItemStack != null ? ItemStack.deserialize((Map) serializedItemStack) : null; + } + + /** + * Returns a serialized array of {@link PotionEffect}s + * + * @param potionEffects The potion effect array + * @return The serialized potion effects + */ + public static String serializePotionEffects(PotionEffect[] potionEffects) { + // Create an output stream that will be encoded into base 64 + ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); + + try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) { + // Define the length of the potion effect array to serialize + bukkitOutputStream.writeInt(potionEffects.length); + + // Write each serialize each PotionEffect to the output stream + for (PotionEffect potionEffect : potionEffects) { + bukkitOutputStream.writeObject(serializePotionEffect(potionEffect)); + } + + // Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion + return Base64Coder.encodeLines(byteOutputStream.toByteArray()); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to serialize potion effect data"); + } + } + + /** + * Returns an array of ItemStacks from serialized potion effect data + * + * @param potionEffectData The serialized {@link PotionEffect[]} array + * @return The {@link PotionEffect}s + * @throws IOException If the deserialization fails reading data from the InputStream + * @throws ClassNotFoundException If the deserialization class cannot be found + */ + public static PotionEffect[] deserializePotionEffects(String potionEffectData) throws IOException, ClassNotFoundException { + // Return empty array if there is no potion effect data (don't apply any effects to the player) + if (potionEffectData.isEmpty()) { + return new PotionEffect[0]; + } + + // Create a byte input stream to read the serialized data + try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(potionEffectData))) { + try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) { + // Read the length of the Bukkit input stream and set the length of the array to this value + PotionEffect[] potionEffects = new PotionEffect[bukkitInputStream.readInt()]; + + // Set the potion effects in the array from deserialized PotionEffect data + int potionIndex = 0; + for (PotionEffect ignored : potionEffects) { + potionEffects[potionIndex] = deserializePotionEffect(bukkitInputStream.readObject()); + potionIndex++; + } + + // Return the finished, serialized potion effect array + return potionEffects; + } + } + } + + /** + * Returns the serialized version of an {@link ItemStack} as a string to object Map + * + * @param potionEffect The {@link ItemStack} to serialize + * @return The serialized {@link ItemStack} + */ + private static Map serializePotionEffect(PotionEffect potionEffect) { + return potionEffect != null ? potionEffect.serialize() : null; + } + + /** + * Returns the deserialized {@link PotionEffect} from the Object read from the {@link BukkitObjectInputStream} + * + * @param serializedPotionEffect The serialized potion effect; a String-Object map + * @return The deserialized {@link PotionEffect} + */ + @SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning + private static PotionEffect deserializePotionEffect(Object serializedPotionEffect) { + return serializedPotionEffect != null ? new PotionEffect((Map) serializedPotionEffect) : null; + } + + public static PlayerSerializer.PlayerLocation deserializePlayerLocationData(String serializedLocationData) throws IOException { + if (serializedLocationData.isEmpty()) { + return null; + } + try { + return (PlayerSerializer.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 PlayerSerializer.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 deserializeAdvancementData(String serializedAdvancementData) throws IOException { + if (serializedAdvancementData.isEmpty()) { + return new ArrayList<>(); + } + try { + return (ArrayList) RedisMessage.deserialize(serializedAdvancementData); + } catch (ClassNotFoundException e) { + throw new IOException("Unable to decode class type.", e); + } + } + + public static String getSerializedAdvancements(Player player) throws IOException { + Iterator serverAdvancements = Bukkit.getServer().advancementIterator(); + ArrayList advancementData = new ArrayList<>(); + + while (serverAdvancements.hasNext()) { + final AdvancementProgress progress = player.getAdvancementProgress(serverAdvancements.next()); + final NamespacedKey advancementKey = progress.getAdvancement().getKey(); + final ArrayList awardedCriteria = new ArrayList<>(progress.getAwardedCriteria()); + advancementData.add(new PlayerSerializer.AdvancementRecord(advancementKey.getNamespace() + ":" + advancementKey.getKey(), awardedCriteria)); + } + + return RedisMessage.serialize(advancementData); + } + + public record AdvancementRecord(String advancementKey, + ArrayList awardedAdvancementCriteria) implements Serializable { + } + + public static PlayerSerializer.StatisticData deserializeStatisticData(String serializedStatisticData) throws IOException { + if (serializedStatisticData.isEmpty()) { + return new PlayerSerializer.StatisticData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()); + } + try { + return (PlayerSerializer.StatisticData) RedisMessage.deserialize(serializedStatisticData); + } catch (ClassNotFoundException e) { + throw new IOException("Unable to decode class type.", e); + } + } + + public static String getSerializedStatisticData(Player player) throws IOException { + HashMap untypedStatisticValues = new HashMap<>(); + HashMap> blockStatisticValues = new HashMap<>(); + HashMap> itemStatisticValues = new HashMap<>(); + HashMap> entityStatisticValues = new HashMap<>(); + for (Statistic statistic : Statistic.values()) { + switch (statistic.getType()) { + case ITEM -> { + HashMap itemValues = new HashMap<>(); + for (Material itemMaterial : Arrays.stream(Material.values()).filter(Material::isItem).collect(Collectors.toList())) { + itemValues.put(itemMaterial, player.getStatistic(statistic, itemMaterial)); + } + itemStatisticValues.put(statistic, itemValues); + } + case BLOCK -> { + HashMap blockValues = new HashMap<>(); + for (Material blockMaterial : Arrays.stream(Material.values()).filter(Material::isBlock).collect(Collectors.toList())) { + blockValues.put(blockMaterial, player.getStatistic(statistic, blockMaterial)); + } + blockStatisticValues.put(statistic, blockValues); + } + case ENTITY -> { + HashMap entityValues = new HashMap<>(); + for (EntityType type : Arrays.stream(EntityType.values()).filter(EntityType::isAlive).collect(Collectors.toList())) { + entityValues.put(type, player.getStatistic(statistic, type)); + } + entityStatisticValues.put(statistic, entityValues); + } + case UNTYPED -> untypedStatisticValues.put(statistic, player.getStatistic(statistic)); + } + } + + PlayerSerializer.StatisticData statisticData = new PlayerSerializer.StatisticData(untypedStatisticValues, blockStatisticValues, itemStatisticValues, entityStatisticValues); + return RedisMessage.serialize(statisticData); + } + + public record StatisticData(HashMap untypedStatisticValues, + HashMap> blockStatisticValues, + HashMap> itemStatisticValues, + HashMap> entityStatisticValues) implements Serializable { + } + +} diff --git a/bukkit/src/main/java/me/william278/husksync/bukkit/migrator/MPDBDeserializer.java b/bukkit/src/main/java/me/william278/husksync/bukkit/migrator/MPDBDeserializer.java index 424e066f..807a6db3 100644 --- a/bukkit/src/main/java/me/william278/husksync/bukkit/migrator/MPDBDeserializer.java +++ b/bukkit/src/main/java/me/william278/husksync/bukkit/migrator/MPDBDeserializer.java @@ -3,7 +3,7 @@ package me.william278.husksync.bukkit.migrator; import me.william278.husksync.HuskSyncBukkit; import me.william278.husksync.PlayerData; import me.william278.husksync.bukkit.util.PlayerSetter; -import me.william278.husksync.bukkit.data.DataSerializer; +import me.william278.husksync.bukkit.data.PlayerSerializer; import me.william278.husksync.migrator.MPDBPlayerData; import net.craftersland.data.bridge.PD; import org.bukkit.Bukkit; @@ -61,7 +61,7 @@ public class MPDBDeserializer { } // Now apply the contents and clear the temporary inventory variable - playerData.setSerializedInventory(DataSerializer.getSerializedInventoryContents(inventory)); + playerData.setSerializedInventory(PlayerSerializer.serializeInventory(inventory.getContents())); // Set ender chest (again, if there is data) ItemStack[] enderChestData; @@ -70,7 +70,7 @@ public class MPDBDeserializer { } else { enderChestData = new ItemStack[0]; } - playerData.setSerializedEnderChest(DataSerializer.itemStackArrayToBase64(enderChestData)); + playerData.setSerializedEnderChest(PlayerSerializer.serializeInventory(enderChestData)); // Set experience playerData.setExpLevel(mpdbPlayerData.expLevel); diff --git a/bukkit/src/main/java/me/william278/husksync/bukkit/util/PlayerSetter.java b/bukkit/src/main/java/me/william278/husksync/bukkit/util/PlayerSetter.java index 7dfd048a..9bf2b7fc 100644 --- a/bukkit/src/main/java/me/william278/husksync/bukkit/util/PlayerSetter.java +++ b/bukkit/src/main/java/me/william278/husksync/bukkit/util/PlayerSetter.java @@ -5,7 +5,7 @@ import me.william278.husksync.PlayerData; import me.william278.husksync.Settings; import me.william278.husksync.api.events.SyncCompleteEvent; import me.william278.husksync.api.events.SyncEvent; -import me.william278.husksync.bukkit.data.DataSerializer; +import me.william278.husksync.bukkit.data.PlayerSerializer; import me.william278.husksync.redis.RedisMessage; import org.bukkit.*; import org.bukkit.advancement.Advancement; @@ -37,8 +37,8 @@ public class PlayerSetter { */ private static String getNewSerializedPlayerData(Player player) throws IOException { return RedisMessage.serialize(new PlayerData(player.getUniqueId(), - DataSerializer.getSerializedInventoryContents(player.getInventory()), - DataSerializer.getSerializedEnderChestContents(player), + PlayerSerializer.serializeInventory(player.getInventory().getContents()), + PlayerSerializer.serializeInventory(player.getEnderChest().getContents()), player.getHealth(), Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).getBaseValue(), player.getHealthScale(), @@ -46,15 +46,31 @@ public class PlayerSetter { player.getSaturation(), player.getExhaustion(), player.getInventory().getHeldItemSlot(), - DataSerializer.getSerializedEffectData(player), + PlayerSerializer.serializePotionEffects(getPlayerPotionEffects(player)), player.getTotalExperience(), player.getLevel(), player.getExp(), player.getGameMode().toString(), - DataSerializer.getSerializedStatisticData(player), + PlayerSerializer.getSerializedStatisticData(player), player.isFlying(), - DataSerializer.getSerializedAdvancements(player), - DataSerializer.getSerializedLocation(player))); + PlayerSerializer.getSerializedAdvancements(player), + PlayerSerializer.getSerializedLocation(player))); + } + + /** + * Returns a {@link Player}'s active potion effects in a {@link PotionEffect} array + * + * @param player The {@link Player} to get the effects of + * @return The {@link PotionEffect} array + */ + private static PotionEffect[] getPlayerPotionEffects(Player player) { + PotionEffect[] potionEffects = new PotionEffect[player.getActivePotionEffects().size()]; + int arrayIndex = 0; + for (PotionEffect effect : player.getActivePotionEffects()) { + potionEffects[arrayIndex] = effect; + arrayIndex++; + } + return potionEffects; } /** @@ -123,14 +139,14 @@ public class PlayerSetter { // Set the player's data from the PlayerData try { if (Settings.syncAdvancements) { - setPlayerAdvancements(player, DataSerializer.deserializeAdvancementData(data.getSerializedAdvancements()), data); + setPlayerAdvancements(player, PlayerSerializer.deserializeAdvancementData(data.getSerializedAdvancements()), data); } if (Settings.syncInventories) { - setPlayerInventory(player, DataSerializer.itemStackArrayFromBase64(data.getSerializedInventory())); + setPlayerInventory(player, PlayerSerializer.deserializeInventory(data.getSerializedInventory())); player.getInventory().setHeldItemSlot(data.getSelectedSlot()); } if (Settings.syncEnderChests) { - setPlayerEnderChest(player, DataSerializer.itemStackArrayFromBase64(data.getSerializedEnderChest())); + setPlayerEnderChest(player, PlayerSerializer.deserializeInventory(data.getSerializedEnderChest())); } if (Settings.syncHealth) { player.setHealthScale(data.getHealthScale() <= 0 ? data.getHealthScale() : 20D); @@ -147,22 +163,22 @@ public class PlayerSetter { setPlayerExperience(player, data); } if (Settings.syncPotionEffects) { - setPlayerPotionEffects(player, DataSerializer.potionEffectArrayFromBase64(data.getSerializedEffectData())); + setPlayerPotionEffects(player, PlayerSerializer.deserializePotionEffects(data.getSerializedEffectData())); } if (Settings.syncStatistics) { - setPlayerStatistics(player, DataSerializer.deserializeStatisticData(data.getSerializedStatistics())); + setPlayerStatistics(player, PlayerSerializer.deserializeStatisticData(data.getSerializedStatistics())); } if (Settings.syncGameMode) { player.setGameMode(GameMode.valueOf(data.getGameMode())); } if (Settings.syncLocation) { player.setFlying(player.getAllowFlight() && data.isFlying()); - setPlayerLocation(player, DataSerializer.deserializePlayerLocationData(data.getSerializedLocation())); + setPlayerLocation(player, PlayerSerializer.deserializePlayerLocationData(data.getSerializedLocation())); } // Handle the SyncCompleteEvent Bukkit.getPluginManager().callEvent(new SyncCompleteEvent(player, data)); - } catch (IOException e) { + } catch (IOException | ClassNotFoundException e) { plugin.getLogger().log(Level.SEVERE, "Failed to deserialize PlayerData", e); } }); @@ -224,9 +240,9 @@ 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 + * @param advancementData The ArrayList of {@link PlayerSerializer.AdvancementRecord}s to set */ - private static void setPlayerAdvancements(Player player, ArrayList advancementData, PlayerData data) { + private static void setPlayerAdvancements(Player player, ArrayList advancementData, PlayerData data) { // Temporarily disable advancement announcing if needed boolean announceAdvancementUpdate = false; if (Boolean.TRUE.equals(player.getWorld().getGameRuleValue(GameRule.ANNOUNCE_ADVANCEMENTS))) { @@ -244,7 +260,7 @@ public class PlayerSetter { boolean correctExperienceCheck = false; // Determines whether the experience might have changed warranting an update Advancement advancement = serverAdvancements.next(); AdvancementProgress playerProgress = player.getAdvancementProgress(advancement); - for (DataSerializer.AdvancementRecord record : advancementData) { + for (PlayerSerializer.AdvancementRecord record : advancementData) { // If the advancement is one on the data if (record.advancementKey().equals(advancement.getKey().getNamespace() + ":" + advancement.getKey().getKey())) { @@ -287,9 +303,9 @@ public class PlayerSetter { * 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 + * @param statisticData The {@link PlayerSerializer.StatisticData} to set */ - private static void setPlayerStatistics(Player player, DataSerializer.StatisticData statisticData) { + private static void setPlayerStatistics(Player player, PlayerSerializer.StatisticData statisticData) { // Set untyped statistics for (Statistic statistic : statisticData.untypedStatisticValues().keySet()) { player.setStatistic(statistic, statisticData.untypedStatisticValues().get(statistic)); @@ -330,12 +346,12 @@ public class PlayerSetter { } /** - * Set a player's location from {@link DataSerializer.PlayerLocation} data + * Set a player's location from {@link PlayerSerializer.PlayerLocation} data * * @param player The {@link Player} to teleport - * @param location The {@link DataSerializer.PlayerLocation} + * @param location The {@link PlayerSerializer.PlayerLocation} */ - private static void setPlayerLocation(Player player, DataSerializer.PlayerLocation location) { + private static void setPlayerLocation(Player player, PlayerSerializer.PlayerLocation location) { // Don't teleport if the location is invalid if (location == null) { return; diff --git a/common/src/main/java/me/william278/husksync/redis/RedisListener.java b/common/src/main/java/me/william278/husksync/redis/RedisListener.java index d085c3ae..547e20f5 100644 --- a/common/src/main/java/me/william278/husksync/redis/RedisListener.java +++ b/common/src/main/java/me/william278/husksync/redis/RedisListener.java @@ -3,6 +3,7 @@ package me.william278.husksync.redis; import me.william278.husksync.Settings; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPubSub; +import redis.clients.jedis.exceptions.JedisException; import java.io.IOException; import java.util.logging.Level; @@ -31,34 +32,37 @@ public abstract class RedisListener { * Start the Redis listener */ public final void listen() { - Jedis jedis = new Jedis(Settings.redisHost, Settings.redisPort); - final String jedisPassword = Settings.redisPassword; - if (!jedisPassword.equals("")) { - jedis.auth(jedisPassword); - } - jedis.connect(); - if (jedis.isConnected()) { - isActiveAndEnabled = true; - log(Level.INFO,"Enabled Redis listener successfully!"); - new Thread(() -> jedis.subscribe(new JedisPubSub() { - @Override - public void onMessage(String channel, String message) { - // Only accept messages to the HuskSync channel - if (!channel.equals(RedisMessage.REDIS_CHANNEL)) { - return; - } + try(Jedis jedis = new Jedis(Settings.redisHost, Settings.redisPort)) { + final String jedisPassword = Settings.redisPassword; + if (!jedisPassword.equals("")) { + jedis.auth(jedisPassword); + } + jedis.connect(); + if (jedis.isConnected()) { + isActiveAndEnabled = true; + log(Level.INFO,"Enabled Redis listener successfully!"); + new Thread(() -> jedis.subscribe(new JedisPubSub() { + @Override + public void onMessage(String channel, String message) { + // Only accept messages to the HuskSync channel + if (!channel.equals(RedisMessage.REDIS_CHANNEL)) { + return; + } - // Handle the message - try { - handleMessage(new RedisMessage(message)); - } catch (IOException | ClassNotFoundException e) { - log(Level.SEVERE, "Failed to deserialize message target"); + // Handle the message + try { + handleMessage(new RedisMessage(message)); + } catch (IOException | ClassNotFoundException e) { + log(Level.SEVERE, "Failed to deserialize message target"); + } } - } - }, RedisMessage.REDIS_CHANNEL), "Redis Subscriber").start(); - } else { - isActiveAndEnabled = false; - log(Level.SEVERE, "Failed to initialize the redis listener!"); + }, RedisMessage.REDIS_CHANNEL), "Redis Subscriber").start(); + } else { + isActiveAndEnabled = false; + log(Level.SEVERE, "Failed to initialize the redis listener!"); + } + } catch (JedisException e) { + log(Level.SEVERE, "Failed to establish a connection to the Redis server!"); } } }