From e35dcf3aadf01f90a3ee377b620daa9b42e0b062 Mon Sep 17 00:00:00 2001 From: William Date: Wed, 1 May 2024 12:08:42 +0100 Subject: [PATCH] feat: Minecraft 1.20.5/6 support (#295) * feat: start 1.20.5 update testing nbt-api seems to work great already :) * feat: add DFU support for legacy upgrade Adds an optional overload to `deserialize` to support passing the MC Version of the snapshot data * refactor: `clone` ItemStack[] bukkit data arrays, close #294 Don't perform async operations on mutable player data --- bukkit/build.gradle | 2 +- .../william278/husksync/data/BukkitData.java | 7 +- .../husksync/data/BukkitSerializer.java | 88 +++++++++++++++++-- .../husksync/data/DataSnapshot.java | 2 +- .../william278/husksync/data/Serializer.java | 11 ++- gradle.properties | 2 +- test/spin_network.py | 14 +-- 7 files changed, 101 insertions(+), 25 deletions(-) diff --git a/bukkit/build.gradle b/bukkit/build.gradle index bc78f7c7..37242d65 100644 --- a/bukkit/build.gradle +++ b/bukkit/build.gradle @@ -10,7 +10,7 @@ dependencies { implementation 'net.kyori:adventure-platform-bukkit:4.3.2' implementation 'dev.triumphteam:triumph-gui:3.1.7' implementation 'space.arim.morepaperlib:morepaperlib:0.4.4' - implementation 'de.tr7zw:item-nbt-api:2.12.3' + implementation 'de.tr7zw:item-nbt-api:2.12.4' compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT' compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0' diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitData.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitData.java index 3edd97c6..d5c23d43 100644 --- a/bukkit/src/main/java/net/william278/husksync/data/BukkitData.java +++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitData.java @@ -66,7 +66,8 @@ public abstract class BukkitData implements Data { private final @Nullable ItemStack @NotNull [] contents; private Items(@Nullable ItemStack @NotNull [] contents) { - this.contents = Arrays.stream(contents) + + this.contents = Arrays.stream(contents.clone()) .map(i -> i == null || i.getType() == Material.AIR ? null : i) .toArray(ItemStack[]::new); } @@ -128,13 +129,13 @@ public abstract class BukkitData implements Data { @Range(from = 0, to = 8) private int heldItemSlot; - private Inventory(@NotNull ItemStack[] contents, int heldItemSlot) { + private Inventory(@Nullable ItemStack @NotNull [] contents, int heldItemSlot) { super(contents); this.heldItemSlot = heldItemSlot; } @NotNull - public static BukkitData.Items.Inventory from(@NotNull ItemStack[] contents, int heldItemSlot) { + public static BukkitData.Items.Inventory from(@Nullable ItemStack @NotNull [] contents, int heldItemSlot) { return new BukkitData.Items.Inventory(contents, heldItemSlot); } diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java index bae767be..ef3bd752 100644 --- a/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java +++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java @@ -21,16 +21,22 @@ package net.william278.husksync.data; import com.google.gson.reflect.TypeToken; import de.tr7zw.changeme.nbtapi.NBT; +import de.tr7zw.changeme.nbtapi.NBTCompound; import de.tr7zw.changeme.nbtapi.NBTContainer; import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT; +import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBTCompoundList; +import de.tr7zw.changeme.nbtapi.utils.DataFixerUtil; import lombok.AccessLevel; import lombok.AllArgsConstructor; +import net.william278.desertwell.util.Version; import net.william278.husksync.HuskSync; import net.william278.husksync.adapter.Adaptable; import net.william278.husksync.api.HuskSyncAPI; +import org.bukkit.Material; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; @@ -52,7 +58,8 @@ public class BukkitSerializer { return plugin; } - public static class Inventory extends BukkitSerializer implements Serializer { + public static class Inventory extends BukkitSerializer implements Serializer, + ItemDeserializer { private static final String ITEMS_TAG = "items"; private static final String HELD_ITEM_SLOT_TAG = "held_item_slot"; @@ -61,16 +68,21 @@ public class BukkitSerializer { } @Override - public BukkitData.Items.Inventory deserialize(@NotNull String serialized) throws DeserializationException { + public BukkitData.Items.Inventory deserialize(@NotNull String serialized, @NotNull Version dataMcVersion) + throws DeserializationException { final ReadWriteNBT root = NBT.parseNBT(serialized); - final ItemStack[] items = root.getItemStackArray(ITEMS_TAG); - final int heldItemSlot = root.getInteger(HELD_ITEM_SLOT_TAG); + final ReadWriteNBT items = root.hasTag(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null; return BukkitData.Items.Inventory.from( - items == null ? new ItemStack[INVENTORY_SLOT_COUNT] : items, - heldItemSlot + items != null ? getItems(items, dataMcVersion) : new ItemStack[INVENTORY_SLOT_COUNT], + root.getInteger(HELD_ITEM_SLOT_TAG) ); } + @Override + public BukkitData.Items.Inventory deserialize(@NotNull String serialized) { + return deserialize(serialized, plugin.getMinecraftVersion()); + } + @NotNull @Override public String serialize(@NotNull BukkitData.Items.Inventory data) throws SerializationException { @@ -82,18 +94,25 @@ public class BukkitSerializer { } - public static class EnderChest extends BukkitSerializer implements Serializer { + public static class EnderChest extends BukkitSerializer implements Serializer, + ItemDeserializer { public EnderChest(@NotNull HuskSync plugin) { super(plugin); } @Override - public BukkitData.Items.EnderChest deserialize(@NotNull String serialized) throws DeserializationException { - final ItemStack[] items = NBT.itemStackArrayFromNBT(NBT.parseNBT(serialized)); + public BukkitData.Items.EnderChest deserialize(@NotNull String serialized, @NotNull Version dataMcVersion) + throws DeserializationException { + final ItemStack[] items = getItems(NBT.parseNBT(serialized), dataMcVersion); return items == null ? BukkitData.Items.EnderChest.empty() : BukkitData.Items.EnderChest.adapt(items); } + @Override + public BukkitData.Items.EnderChest deserialize(@NotNull String serialized) { + return deserialize(serialized, plugin.getMinecraftVersion()); + } + @NotNull @Override public String serialize(@NotNull BukkitData.Items.EnderChest data) throws SerializationException { @@ -101,6 +120,57 @@ public class BukkitSerializer { } } + // Utility interface for deserializing and upgrading item stacks from legacy versions + private interface ItemDeserializer { + + @Nullable + default ItemStack[] getItems(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion) { + if (mcVersion.compareTo(getPlugin().getMinecraftVersion()) < 0) { + return upgradeItemStack((NBTCompound) tag, mcVersion); + } + return NBT.itemStackArrayFromNBT(tag); + } + + @NotNull + private ItemStack @NotNull [] upgradeItemStack(@NotNull NBTCompound compound, @NotNull Version mcVersion) { + final ReadWriteNBTCompoundList items = compound.getCompoundList("items"); + final ItemStack[] itemStacks = new ItemStack[compound.getInteger("size")]; + for (int i = 0; i < items.size(); i++) { + if (items.get(i) == null) { + itemStacks[i] = new ItemStack(Material.AIR); + continue; + } + try { + itemStacks[i] = NBT.itemStackFromNBT(upgradeItemData(items.get(i), mcVersion)); + } catch (Throwable e) { + itemStacks[i] = new ItemStack(Material.AIR); + } + } + return itemStacks; + } + + @NotNull + private ReadWriteNBT upgradeItemData(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion) + throws NoSuchFieldException, IllegalAccessException { + return DataFixerUtil.fixUpItemData(tag, getDataVersion(mcVersion), DataFixerUtil.getCurrentVersion()); + } + + private int getDataVersion(@NotNull Version mcVersion) { + return switch (mcVersion.toStringWithoutMetadata()) { + case "1.16", "1.16.1", "1.16.2", "1.16.3", "1.16.4", "1.16.5" -> DataFixerUtil.VERSION1_16_5; + case "1.17", "1.17.1" -> DataFixerUtil.VERSION1_17_1; + case "1.18", "1.18.1", "1.18.2" -> DataFixerUtil.VERSION1_18_2; + case "1.19", "1.19.1", "1.19.2" -> DataFixerUtil.VERSION1_19_2; + case "1.20", "1.20.1", "1.20.2" -> DataFixerUtil.VERSION1_20_2; + case "1.20.3", "1.20.4" -> DataFixerUtil.VERSION1_20_4; + default -> DataFixerUtil.getCurrentVersion(); + }; + } + + @NotNull + HuskSync getPlugin(); + } + public static class PotionEffects extends BukkitSerializer implements Serializer { private static final TypeToken> TYPE = new TypeToken<>() { diff --git a/common/src/main/java/net/william278/husksync/data/DataSnapshot.java b/common/src/main/java/net/william278/husksync/data/DataSnapshot.java index 9c77371d..447e196d 100644 --- a/common/src/main/java/net/william278/husksync/data/DataSnapshot.java +++ b/common/src/main/java/net/william278/husksync/data/DataSnapshot.java @@ -392,7 +392,7 @@ public class DataSnapshot { private Map deserializeData(@NotNull HuskSync plugin) { return data.entrySet().stream() .map((entry) -> plugin.getIdentifier(entry.getKey()).map(id -> Map.entry( - id, plugin.getSerializers().get(id).deserialize(entry.getValue()) + id, plugin.getSerializers().get(id).deserialize(entry.getValue(), getMinecraftVersion()) )).orElse(null)) .filter(Objects::nonNull) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); diff --git a/common/src/main/java/net/william278/husksync/data/Serializer.java b/common/src/main/java/net/william278/husksync/data/Serializer.java index 2da2b14e..9b4a12b0 100644 --- a/common/src/main/java/net/william278/husksync/data/Serializer.java +++ b/common/src/main/java/net/william278/husksync/data/Serializer.java @@ -19,22 +19,27 @@ package net.william278.husksync.data; +import net.william278.desertwell.util.Version; import org.jetbrains.annotations.NotNull; public interface Serializer { - T deserialize(@NotNull String serialized) throws DeserializationException; + T deserialize(@NotNull String serialized); + + default T deserialize(@NotNull String serialized, @NotNull Version dataMcVersion) throws DeserializationException { + return deserialize(serialized); + } @NotNull String serialize(@NotNull T element) throws SerializationException; - static final class DeserializationException extends IllegalStateException { + final class DeserializationException extends IllegalStateException { DeserializationException(@NotNull String message, @NotNull Throwable cause) { super(message, cause); } } - static final class SerializationException extends IllegalStateException { + final class SerializationException extends IllegalStateException { SerializationException(@NotNull String message, @NotNull Throwable cause) { super(message, cause); } diff --git a/gradle.properties b/gradle.properties index dd106028..b6ee74c0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8' org.gradle.daemon=true javaVersion=17 -plugin_version=3.5 +plugin_version=3.5.1 plugin_archive=husksync plugin_description=A modern, cross-server player data synchronization system diff --git a/test/spin_network.py b/test/spin_network.py index 4ffffa93..9d3d3aaf 100644 --- a/test/spin_network.py +++ b/test/spin_network.py @@ -13,7 +13,7 @@ from tqdm import tqdm class Parameters: root_dir = './servers/' proxy_version = "1.20" - minecraft_version = '1.20.4' + minecraft_version = '1.20.5' eula_agreement = 'true' backend_names = ['alpha', 'beta'] @@ -101,9 +101,9 @@ def create_backend_server(name, port, parameters): # Download the latest paper for the version and place it in the server folder server_jar = "paper.jar" - download_paper_build("paper", parameters.minecraft_version, - get_latest_paper_build_number("paper", parameters.minecraft_version), - f"{server_dir}/{server_jar}") + #download_paper_build("paper", parameters.minecraft_version, + # get_latest_paper_build_number("paper", parameters.minecraft_version), + # f"{server_dir}/{server_jar}") # Create eula.text and set eula=true with open(server_dir + "/eula.txt", "w") as file: @@ -175,9 +175,9 @@ def create_proxy_server(parameters): # Download the latest paper for the version and place it in the server folder proxy_jar = "waterfall.jar" - download_paper_build("waterfall", parameters.proxy_version, - get_latest_paper_build_number("waterfall", parameters.proxy_version), - f"{server_dir}/{proxy_jar}") + #download_paper_build("waterfall", parameters.proxy_version, + # get_latest_paper_build_number("waterfall", parameters.proxy_version), + # f"{server_dir}/{proxy_jar}") # Create the config.yml with open(server_dir + "/config.yml", "w") as file: