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
feat/data-edit-commands
William 7 months ago committed by GitHub
parent 68ec79add6
commit e35dcf3aad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -10,7 +10,7 @@ dependencies {
implementation 'net.kyori:adventure-platform-bukkit:4.3.2' implementation 'net.kyori:adventure-platform-bukkit:4.3.2'
implementation 'dev.triumphteam:triumph-gui:3.1.7' implementation 'dev.triumphteam:triumph-gui:3.1.7'
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4' 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 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT'
compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0' compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0'

@ -66,7 +66,8 @@ public abstract class BukkitData implements Data {
private final @Nullable ItemStack @NotNull [] contents; private final @Nullable ItemStack @NotNull [] contents;
private Items(@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) .map(i -> i == null || i.getType() == Material.AIR ? null : i)
.toArray(ItemStack[]::new); .toArray(ItemStack[]::new);
} }
@ -128,13 +129,13 @@ public abstract class BukkitData implements Data {
@Range(from = 0, to = 8) @Range(from = 0, to = 8)
private int heldItemSlot; private int heldItemSlot;
private Inventory(@NotNull ItemStack[] contents, int heldItemSlot) { private Inventory(@Nullable ItemStack @NotNull [] contents, int heldItemSlot) {
super(contents); super(contents);
this.heldItemSlot = heldItemSlot; this.heldItemSlot = heldItemSlot;
} }
@NotNull @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); return new BukkitData.Items.Inventory(contents, heldItemSlot);
} }

@ -21,16 +21,22 @@ package net.william278.husksync.data;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import de.tr7zw.changeme.nbtapi.NBT; import de.tr7zw.changeme.nbtapi.NBT;
import de.tr7zw.changeme.nbtapi.NBTCompound;
import de.tr7zw.changeme.nbtapi.NBTContainer; import de.tr7zw.changeme.nbtapi.NBTContainer;
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT; 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.AccessLevel;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import net.william278.desertwell.util.Version;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.Adaptable; import net.william278.husksync.adapter.Adaptable;
import net.william278.husksync.api.HuskSyncAPI; import net.william278.husksync.api.HuskSyncAPI;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
@ -52,7 +58,8 @@ public class BukkitSerializer {
return plugin; return plugin;
} }
public static class Inventory extends BukkitSerializer implements Serializer<BukkitData.Items.Inventory> { public static class Inventory extends BukkitSerializer implements Serializer<BukkitData.Items.Inventory>,
ItemDeserializer {
private static final String ITEMS_TAG = "items"; private static final String ITEMS_TAG = "items";
private static final String HELD_ITEM_SLOT_TAG = "held_item_slot"; private static final String HELD_ITEM_SLOT_TAG = "held_item_slot";
@ -61,16 +68,21 @@ public class BukkitSerializer {
} }
@Override @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 ReadWriteNBT root = NBT.parseNBT(serialized);
final ItemStack[] items = root.getItemStackArray(ITEMS_TAG); final ReadWriteNBT items = root.hasTag(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null;
final int heldItemSlot = root.getInteger(HELD_ITEM_SLOT_TAG);
return BukkitData.Items.Inventory.from( return BukkitData.Items.Inventory.from(
items == null ? new ItemStack[INVENTORY_SLOT_COUNT] : items, items != null ? getItems(items, dataMcVersion) : new ItemStack[INVENTORY_SLOT_COUNT],
heldItemSlot root.getInteger(HELD_ITEM_SLOT_TAG)
); );
} }
@Override
public BukkitData.Items.Inventory deserialize(@NotNull String serialized) {
return deserialize(serialized, plugin.getMinecraftVersion());
}
@NotNull @NotNull
@Override @Override
public String serialize(@NotNull BukkitData.Items.Inventory data) throws SerializationException { 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<BukkitData.Items.EnderChest> { public static class EnderChest extends BukkitSerializer implements Serializer<BukkitData.Items.EnderChest>,
ItemDeserializer {
public EnderChest(@NotNull HuskSync plugin) { public EnderChest(@NotNull HuskSync plugin) {
super(plugin); super(plugin);
} }
@Override @Override
public BukkitData.Items.EnderChest deserialize(@NotNull String serialized) throws DeserializationException { public BukkitData.Items.EnderChest deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
final ItemStack[] items = NBT.itemStackArrayFromNBT(NBT.parseNBT(serialized)); throws DeserializationException {
final ItemStack[] items = getItems(NBT.parseNBT(serialized), dataMcVersion);
return items == null ? BukkitData.Items.EnderChest.empty() : BukkitData.Items.EnderChest.adapt(items); 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 @NotNull
@Override @Override
public String serialize(@NotNull BukkitData.Items.EnderChest data) throws SerializationException { 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<BukkitData.PotionEffects> { public static class PotionEffects extends BukkitSerializer implements Serializer<BukkitData.PotionEffects> {
private static final TypeToken<List<Data.PotionEffects.Effect>> TYPE = new TypeToken<>() { private static final TypeToken<List<Data.PotionEffects.Effect>> TYPE = new TypeToken<>() {

@ -392,7 +392,7 @@ public class DataSnapshot {
private Map<Identifier, Data> deserializeData(@NotNull HuskSync plugin) { private Map<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
return data.entrySet().stream() return data.entrySet().stream()
.map((entry) -> plugin.getIdentifier(entry.getKey()).map(id -> Map.entry( .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)) )).orElse(null))
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

@ -19,22 +19,27 @@
package net.william278.husksync.data; package net.william278.husksync.data;
import net.william278.desertwell.util.Version;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public interface Serializer<T extends Data> { public interface Serializer<T extends Data> {
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 @NotNull
String serialize(@NotNull T element) throws SerializationException; 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) { DeserializationException(@NotNull String message, @NotNull Throwable cause) {
super(message, cause); super(message, cause);
} }
} }
static final class SerializationException extends IllegalStateException { final class SerializationException extends IllegalStateException {
SerializationException(@NotNull String message, @NotNull Throwable cause) { SerializationException(@NotNull String message, @NotNull Throwable cause) {
super(message, cause); super(message, cause);
} }

@ -3,7 +3,7 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.daemon=true org.gradle.daemon=true
javaVersion=17 javaVersion=17
plugin_version=3.5 plugin_version=3.5.1
plugin_archive=husksync plugin_archive=husksync
plugin_description=A modern, cross-server player data synchronization system plugin_description=A modern, cross-server player data synchronization system

@ -13,7 +13,7 @@ from tqdm import tqdm
class Parameters: class Parameters:
root_dir = './servers/' root_dir = './servers/'
proxy_version = "1.20" proxy_version = "1.20"
minecraft_version = '1.20.4' minecraft_version = '1.20.5'
eula_agreement = 'true' eula_agreement = 'true'
backend_names = ['alpha', 'beta'] 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 # Download the latest paper for the version and place it in the server folder
server_jar = "paper.jar" server_jar = "paper.jar"
download_paper_build("paper", parameters.minecraft_version, #download_paper_build("paper", parameters.minecraft_version,
get_latest_paper_build_number("paper", parameters.minecraft_version), # get_latest_paper_build_number("paper", parameters.minecraft_version),
f"{server_dir}/{server_jar}") # f"{server_dir}/{server_jar}")
# Create eula.text and set eula=true # Create eula.text and set eula=true
with open(server_dir + "/eula.txt", "w") as file: 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 # Download the latest paper for the version and place it in the server folder
proxy_jar = "waterfall.jar" proxy_jar = "waterfall.jar"
download_paper_build("waterfall", parameters.proxy_version, #download_paper_build("waterfall", parameters.proxy_version,
get_latest_paper_build_number("waterfall", parameters.proxy_version), # get_latest_paper_build_number("waterfall", parameters.proxy_version),
f"{server_dir}/{proxy_jar}") # f"{server_dir}/{proxy_jar}")
# Create the config.yml # Create the config.yml
with open(server_dir + "/config.yml", "w") as file: with open(server_dir + "/config.yml", "w") as file:

Loading…
Cancel
Save