From c0c04eec1f255f105c8f65bd2c51fd16d475dbf9 Mon Sep 17 00:00:00 2001 From: mworzala Date: Mon, 18 Dec 2023 23:08:58 +0200 Subject: [PATCH] upgrade: NBT chat component serializer implementation (cherry picked from commit ed240d6f14b7c1770cd31a2f45dab20be7434b9e) --- gradle/libs.versions.toml | 2 +- .../net/minestom/server/MinecraftServer.java | 4 +- .../nbt/NbtComponentSerializer.java | 12 + .../nbt/NbtComponentSerializerImpl.java | 378 ++++++++++++++++++ .../server/network/NetworkBufferTypes.java | 10 +- 5 files changed, 398 insertions(+), 8 deletions(-) create mode 100644 src/main/java/net/minestom/server/adventure/serializer/nbt/NbtComponentSerializer.java create mode 100644 src/main/java/net/minestom/server/adventure/serializer/nbt/NbtComponentSerializerImpl.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3afbd8575..763d3a9a9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ metadata.format.version = "1.1" # Important dependencies data = "1.20.2-rv1" -adventure = "4.12.0" +adventure = "4.15.0" kotlin = "1.7.22" dependencyGetter = "v1.0.1" hydrazine = "1.7.2" diff --git a/src/main/java/net/minestom/server/MinecraftServer.java b/src/main/java/net/minestom/server/MinecraftServer.java index 96020a07d..7832a2060 100644 --- a/src/main/java/net/minestom/server/MinecraftServer.java +++ b/src/main/java/net/minestom/server/MinecraftServer.java @@ -46,8 +46,8 @@ public final class MinecraftServer { public static final ComponentLogger LOGGER = ComponentLogger.logger(MinecraftServer.class); - public static final String VERSION_NAME = "1.20.2"; - public static final int PROTOCOL_VERSION = 764; + public static final String VERSION_NAME = "1.20.4"; + public static final int PROTOCOL_VERSION = 765; // Threads public static final String THREAD_NAME_BENCHMARK = "Ms-Benchmark"; diff --git a/src/main/java/net/minestom/server/adventure/serializer/nbt/NbtComponentSerializer.java b/src/main/java/net/minestom/server/adventure/serializer/nbt/NbtComponentSerializer.java new file mode 100644 index 000000000..a397352e9 --- /dev/null +++ b/src/main/java/net/minestom/server/adventure/serializer/nbt/NbtComponentSerializer.java @@ -0,0 +1,12 @@ +package net.minestom.server.adventure.serializer.nbt; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.ComponentSerializer; +import org.jetbrains.annotations.NotNull; +import org.jglrxavpok.hephaistos.nbt.NBT; + +public interface NbtComponentSerializer extends ComponentSerializer { + static @NotNull NbtComponentSerializer nbt() { + return NbtComponentSerializerImpl.INSTANCE; + } +} diff --git a/src/main/java/net/minestom/server/adventure/serializer/nbt/NbtComponentSerializerImpl.java b/src/main/java/net/minestom/server/adventure/serializer/nbt/NbtComponentSerializerImpl.java new file mode 100644 index 000000000..5920920c5 --- /dev/null +++ b/src/main/java/net/minestom/server/adventure/serializer/nbt/NbtComponentSerializerImpl.java @@ -0,0 +1,378 @@ +package net.minestom.server.adventure.serializer.nbt; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.api.BinaryTagHolder; +import net.kyori.adventure.text.*; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.minestom.server.utils.validate.Check; +import org.intellij.lang.annotations.Subst; +import org.jetbrains.annotations.NotNull; +import org.jglrxavpok.hephaistos.nbt.NBT; +import org.jglrxavpok.hephaistos.nbt.NBTCompound; +import org.jglrxavpok.hephaistos.nbt.NBTList; +import org.jglrxavpok.hephaistos.nbt.NBTType; +import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.UUID; + +//todo write tests for me!! +final class NbtComponentSerializerImpl implements NbtComponentSerializer { + static final NbtComponentSerializer INSTANCE = new NbtComponentSerializerImpl(); + + @Override + public @NotNull Component deserialize(@NotNull NBT input) { + return deserializeAnyComponent(input); + } + + @Override + public @NotNull NBT serialize(@NotNull Component component) { + return serializeComponent(component); + } + + // DESERIALIZATION + + private @NotNull Component deserializeAnyComponent(@NotNull NBT nbt) { + if (nbt instanceof NBTCompound compound) { + return deserializeComponent(compound); + } else { + //todo raw string + list + throw new UnsupportedOperationException("Unknown NBT type: " + nbt.getClass().getName()); + } + } + + private @NotNull Component deserializeComponent(@NotNull NBTCompound compound) { + ComponentBuilder builder; + var type = compound.getString("type"); + if (type != null) { + // If type is specified, use that + builder = switch (type) { + case "text" -> deserializeTextComponent(compound); + case "translatable" -> deserializeTranslatableComponent(compound); + case "score" -> deserializeScoreComponent(compound); + case "selector" -> deserializeSelectorComponent(compound); + case "keybind" -> deserializeKeybindComponent(compound); + case "nbt" -> deserializeNbtComponent(compound); + default -> throw new UnsupportedOperationException("Unknown component type: " + type); + }; + } else { + // Try to infer the type from the fields present. + if (compound.containsKey("text")) { + builder = deserializeTextComponent(compound); + } else if (compound.containsKey("translate")) { + builder = deserializeTranslatableComponent(compound); + } else if (compound.containsKey("score")) { + builder = deserializeScoreComponent(compound); + } else if (compound.containsKey("selector")) { + builder = deserializeSelectorComponent(compound); + } else if (compound.containsKey("keybind")) { + builder = deserializeKeybindComponent(compound); + } else if (compound.containsKey("nbt")) { + builder = deserializeNbtComponent(compound); + } else throw new UnsupportedOperationException("Unable to infer component type"); + } + + // Children + var extra = compound.getList("extra"); + Check.argCondition(extra != null && !extra.getSubtagType().equals(NBTType.TAG_Compound), + "Extra field must be a list of compounds"); + if (extra != null) { + var list = new ArrayList(); + for (var child : extra) list.add(deserializeAnyComponent(child)); + builder.append(list); + } + + // Formatting + var style = Style.style(); + var color = compound.getString("color"); + if (color != null) { + var hexColor = TextColor.fromHexString(color); + if (hexColor != null) { + style.color(hexColor); + } else { + var namedColor = NamedTextColor.NAMES.value(color); + if (namedColor != null) { + style.color(namedColor); + } else { + throw new UnsupportedOperationException("Unknown color: " + color); + } + } + } + @Subst("minecraft:default") var font = compound.getString("font"); + if (font != null) style.font(Key.key(font)); + var bold = compound.getByte("bold"); + if (bold != null) style.decoration(TextDecoration.BOLD, bold == 1 ? TextDecoration.State.TRUE : TextDecoration.State.FALSE); + var italic = compound.getByte("italic"); + if (italic != null) style.decoration(TextDecoration.ITALIC, italic == 1 ? TextDecoration.State.TRUE : TextDecoration.State.FALSE); + var underlined = compound.getByte("underlined"); + if (underlined != null) style.decoration(TextDecoration.UNDERLINED, underlined == 1 ? TextDecoration.State.TRUE : TextDecoration.State.FALSE); + var strikethrough = compound.getByte("strikethrough"); + if (strikethrough != null) style.decoration(TextDecoration.STRIKETHROUGH, strikethrough == 1 ? TextDecoration.State.TRUE : TextDecoration.State.FALSE); + var obfuscated = compound.getByte("obfuscated"); + if (obfuscated != null) style.decoration(TextDecoration.OBFUSCATED, obfuscated == 1 ? TextDecoration.State.TRUE : TextDecoration.State.FALSE); + builder.style(style.build()); + + // Interactivity + var insertion = compound.getString("insertion"); + if (insertion != null) builder.insertion(insertion); + var clickEvent = compound.getCompound("clickEvent"); + if (clickEvent != null) builder.clickEvent(deserializeClickEvent(clickEvent)); + var hoverEvent = compound.getCompound("hoverEvent"); + if (hoverEvent != null) builder.hoverEvent(deserializeHoverEvent(hoverEvent)); + + return builder.build(); + } + + private @NotNull ComponentBuilder deserializeTextComponent(@NotNull NBTCompound compound) { + var text = compound.getString("text"); + Check.notNull(text, "Text component must have a text field"); + return Component.text().content(text); + } + + private @NotNull ComponentBuilder deserializeTranslatableComponent(@NotNull NBTCompound compound) { + var key = compound.getString("translate"); + Check.notNull(key, "Translatable component must have a translate field"); + var builder = Component.translatable().key(key); + + var fallback = compound.getString("fallback"); + if (fallback != null) builder.fallback(fallback); + + NBTList args = compound.getList("with"); + Check.argCondition(args != null && !args.getSubtagType().equals(NBTType.TAG_Compound), + "Translatable component with field must be a list of compounds"); + if (args != null) { + var list = new ArrayList(); + for (var arg : args) list.add(deserializeComponent(arg)); + builder.arguments(list); + } + + return builder; + } + + private @NotNull ComponentBuilder deserializeScoreComponent(@NotNull NBTCompound compound) { + var scoreCompound = compound.getCompound("score"); + Check.notNull(scoreCompound, "Score component must have a score field"); + var name = scoreCompound.getString("name"); + Check.notNull(name, "Score component score field must have a name field"); + var objective = scoreCompound.getString("objective"); + Check.notNull(objective, "Score component score field must have an objective field"); + var builder = Component.score().name(name).objective(objective); + + var value = scoreCompound.getString("value"); + if (value != null) + //noinspection deprecation + builder.value(value); + + return builder; + } + + private @NotNull ComponentBuilder deserializeSelectorComponent(@NotNull NBTCompound compound) { + var selector = compound.getString("selector"); + Check.notNull(selector, "Selector component must have a selector field"); + var builder = Component.selector().pattern(selector); + + var separator = compound.get("separator"); + if (separator != null) builder.separator(deserializeAnyComponent(separator)); + + return builder; + } + + private @NotNull ComponentBuilder deserializeKeybindComponent(@NotNull NBTCompound compound) { + var keybind = compound.getString("keybind"); + Check.notNull(keybind, "Keybind component must have a keybind field"); + return Component.keybind().keybind(keybind); + } + + private @NotNull ComponentBuilder deserializeNbtComponent(@NotNull NBTCompound compound) { + throw new UnsupportedOperationException("NBTComponent is not implemented yet"); + } + + private @NotNull ClickEvent deserializeClickEvent(@NotNull NBTCompound compound) { + var actionName = compound.getString("action"); + Check.notNull(actionName, "Click event must have an action field"); + var action = ClickEvent.Action.NAMES.value(actionName); + Check.notNull(action, "Unknown click event action: " + actionName); + var value = compound.getString("value"); + Check.notNull(value, "Click event must have a value field"); + return ClickEvent.clickEvent(action, value); + } + + private @NotNull HoverEvent deserializeHoverEvent(@NotNull NBTCompound compound) { + var actionName = compound.getString("action"); + Check.notNull(actionName, "Hover event must have an action field"); + var contents = compound.getCompound("contents"); + Check.notNull(contents, "Hover event must have a contents field"); + + var action = HoverEvent.Action.NAMES.value(actionName); + if (action == HoverEvent.Action.SHOW_TEXT) { + return HoverEvent.showText(deserializeComponent(contents)); + } else if (action == HoverEvent.Action.SHOW_ITEM) { + @Subst("minecraft:stick") var id = contents.getString("id"); + Check.notNull(id, "Show item hover event must have an id field"); + var count = contents.getInt("count"); + var countInt = count == null ? 1 : count; + var tag = contents.getString("tag"); + var binaryTag = tag == null ? null : BinaryTagHolder.binaryTagHolder(tag); + return HoverEvent.showItem(Key.key(id), countInt, binaryTag); + } else if (action == HoverEvent.Action.SHOW_ENTITY) { + var name = contents.getCompound("name"); + var nameComponent = name == null ? null : deserializeComponent(name); + @Subst("minecraft:pig") var type = contents.getString("type"); + Check.notNull(type, "Show entity hover event must have a type field"); + var id = contents.getString("id"); + Check.notNull(id, "Show entity hover event must have an id field"); + return HoverEvent.showEntity(Key.key(type), UUID.fromString(id), nameComponent); + } else { + throw new UnsupportedOperationException("Unknown hover event action: " + actionName); + } + } + + // SERIALIZATION + + private @NotNull NBT serializeComponent(@NotNull Component component) { + MutableNBTCompound compound = new MutableNBTCompound(); + + // Base component types + if (component instanceof TextComponent text) { + compound.setString("type", "text"); + compound.setString("text", text.content()); + } else if (component instanceof TranslatableComponent translatable) { + compound.setString("type", "translatable"); + compound.setString("translate", translatable.key()); + var fallback = translatable.fallback(); + if (fallback != null) compound.setString("fallback", fallback); + var args = translatable.arguments(); + if (!args.isEmpty()) compound.set("with", serializeTranslationArgs(args)); + } else if (component instanceof ScoreComponent score) { + compound.setString("type", "score"); + var scoreCompound = new MutableNBTCompound(); + scoreCompound.setString("name", score.name()); + scoreCompound.setString("objective", score.objective()); + @SuppressWarnings("deprecation") var value = score.value(); + if (value != null) scoreCompound.setString("value", value); + compound.set("score", scoreCompound.toCompound()); + } else if (component instanceof SelectorComponent selector) { + compound.setString("type", "selector"); + compound.setString("selector", selector.pattern()); + var separator = selector.separator(); + if (separator != null) compound.set("separator", serializeComponent(separator)); + } else if (component instanceof KeybindComponent keybind) { + compound.setString("type", "keybind"); + compound.setString("keybind", keybind.keybind()); + } else if (component instanceof NBTComponent nbt) { + //todo + throw new UnsupportedOperationException("NBTComponent is not implemented yet"); + } else { + throw new UnsupportedOperationException("Unknown component type: " + component.getClass().getName()); + } + + // Children + if (!component.children().isEmpty()) { + var children = new ArrayList(); + for (var child : component.children()) + children.add(serializeComponent(child)); + compound.set("extra", new NBTList<>(NBTType.TAG_Compound, children)); + } + + // Formatting + var style = component.style(); + var color = style.color(); + if (color != null) { + if (color instanceof NamedTextColor named) { + compound.setString("color", named.toString()); + } else { + compound.setString("color", color.asHexString()); + } + } + var font = style.font(); + if (font != null) + compound.setString("font", font.toString()); + var bold = style.decoration(TextDecoration.BOLD); + if (bold != TextDecoration.State.NOT_SET) + setBool(compound, "bold", bold == TextDecoration.State.TRUE); + var italic = style.decoration(TextDecoration.ITALIC); + if (italic != TextDecoration.State.NOT_SET) + setBool(compound, "italic", italic == TextDecoration.State.TRUE); + var underlined = style.decoration(TextDecoration.UNDERLINED); + if (underlined != TextDecoration.State.NOT_SET) + setBool(compound, "underlined", underlined == TextDecoration.State.TRUE); + var strikethrough = style.decoration(TextDecoration.STRIKETHROUGH); + if (strikethrough != TextDecoration.State.NOT_SET) + setBool(compound, "strikethrough", strikethrough == TextDecoration.State.TRUE); + var obfuscated = style.decoration(TextDecoration.OBFUSCATED); + if (obfuscated != TextDecoration.State.NOT_SET) + setBool(compound, "obfuscated", obfuscated == TextDecoration.State.TRUE); + + // Interactivity + var insertion = component.insertion(); + if (insertion != null) compound.setString("insertion", insertion); + var clickEvent = component.clickEvent(); + if (clickEvent != null) compound.set("clickEvent", serializeClickEvent(clickEvent)); + var hoverEvent = component.hoverEvent(); + if (hoverEvent != null) compound.set("hoverEvent", serializeHoverEvent(hoverEvent)); + + return compound.toCompound(); + } + + private @NotNull NBT serializeTranslationArgs(@NotNull Collection args) { + var list = new ArrayList(); + for (var arg : args) + list.add(serializeComponent(arg.asComponent())); + return new NBTList<>(NBTType.TAG_Compound, list); + } + + private @NotNull NBT serializeClickEvent(@NotNull ClickEvent event) { + var compound = new MutableNBTCompound(); + compound.setString("action", event.action().toString()); + compound.setString("value", event.value()); + return compound.toCompound(); + } + + @SuppressWarnings("unchecked") + private @NotNull NBT serializeHoverEvent(@NotNull HoverEvent event) { + var compound = new MutableNBTCompound(); + + //todo surely there is a better way to do this? + compound.setString("action", event.action().toString()); + if (event.action() == HoverEvent.Action.SHOW_TEXT) { + var value = ((HoverEvent) event).value(); + compound.set("contents", serializeComponent(value)); + } else if (event.action() == HoverEvent.Action.SHOW_ITEM) { + var value = ((HoverEvent) event).value(); + + var itemCompound = new MutableNBTCompound(); + itemCompound.setString("id", value.item().asString()); + if (value.count() != 1) itemCompound.setInt("count", value.count()); + var tag = value.nbt(); + if (tag != null) itemCompound.setString("tag", tag.string()); + + compound.set("contents", itemCompound.toCompound()); + } else if (event.action() == HoverEvent.Action.SHOW_ENTITY) { + var value = ((HoverEvent) event).value(); + + var entityCompound = new MutableNBTCompound(); + var name = value.name(); + if (name != null) entityCompound.set("name", serializeComponent(name)); + entityCompound.setString("type", value.type().asString()); + entityCompound.setString("id", value.id().toString()); + + compound.set("contents", entityCompound.toCompound()); + } else { + throw new UnsupportedOperationException("Unknown hover event action: " + event.action()); + } + + return compound.toCompound(); + } + + private void setBool(@NotNull MutableNBTCompound compound, @NotNull String key, boolean value) { + compound.setByte(key, value ? (byte) 1 : 0); + } + + +} diff --git a/src/main/java/net/minestom/server/network/NetworkBufferTypes.java b/src/main/java/net/minestom/server/network/NetworkBufferTypes.java index 2d5a87d86..31c245436 100644 --- a/src/main/java/net/minestom/server/network/NetworkBufferTypes.java +++ b/src/main/java/net/minestom/server/network/NetworkBufferTypes.java @@ -1,7 +1,7 @@ package net.minestom.server.network; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.minestom.server.adventure.serializer.nbt.NbtComponentSerializer; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; @@ -293,13 +293,13 @@ final class NetworkBufferTypes { }); static final TypeImpl COMPONENT = new TypeImpl<>(Component.class, (buffer, value) -> { - final String json = GsonComponentSerializer.gson().serialize(value); - buffer.write(STRING, json); + final NBT nbt = NbtComponentSerializer.nbt().serialize(value); + buffer.write(NBT, nbt); return -1; }, buffer -> { - final String json = buffer.read(STRING); - return GsonComponentSerializer.gson().deserialize(json); + final NBT nbt = buffer.read(NBT); + return NbtComponentSerializer.nbt().deserialize(nbt); }); static final TypeImpl UUID = new TypeImpl<>(UUID.class, (buffer, value) -> {