diff --git a/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index b808c17a..d3d7d4a5 100644 --- a/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -668,6 +668,16 @@ public final class MinecraftReflection { return getMinecraftClass("network.chat.IChatBaseComponent$ChatSerializer", "network.chat.Component$Serializer", "IChatBaseComponent$ChatSerializer"); } + /** + * Retrieve the component style serializer class. + * + * @return The serializer class. + */ + public static Class getStyleSerializerClass() { + return getMinecraftClass("network.chat.ChatModifier$ChatModifierSerializer", "ChatModifier$ChatModifierSerializer"); + } + + /** * Retrieve the ServerPing class. * @@ -1018,6 +1028,15 @@ public final class MinecraftReflection { return getOptionalNMS("network.protocol.game.PacketPlayOutScoreboardTeam$b"); } + /** + * Retrieve the NMS component style class. + * + * @return The component style class. + */ + public static Class getComponentStyleClass() { + return getMinecraftClass("network.chat.ChatModifier", "ChatModifier"); + } + /** * Retrieve the Gson class used by Minecraft. * diff --git a/src/main/java/com/comphenix/protocol/wrappers/AdventureComponentConverter.java b/src/main/java/com/comphenix/protocol/wrappers/AdventureComponentConverter.java index d67c0cf2..a4d2a709 100644 --- a/src/main/java/com/comphenix/protocol/wrappers/AdventureComponentConverter.java +++ b/src/main/java/com/comphenix/protocol/wrappers/AdventureComponentConverter.java @@ -16,7 +16,10 @@ */ package com.comphenix.protocol.wrappers; +import com.comphenix.protocol.utility.MinecraftVersion; +import com.google.gson.JsonObject; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; /** @@ -25,7 +28,16 @@ import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; * Note: The Adventure API Component is not included in CraftBukkit, Bukkit or Spigot and but is present in PaperMC. */ public class AdventureComponentConverter { - + private static final GsonComponentSerializer SERIALIZER; + + static { + if (MinecraftVersion.NETHER_UPDATE.atOrAbove()) { + SERIALIZER = GsonComponentSerializer.gson(); + } else { + SERIALIZER = GsonComponentSerializer.colorDownsamplingGson(); + } + } + private AdventureComponentConverter() { } @@ -35,7 +47,7 @@ public class AdventureComponentConverter { * @return Component */ public static Component fromWrapper(WrappedChatComponent wrapper) { - return GsonComponentSerializer.gson().deserialize(wrapper.getJson()); + return SERIALIZER.deserialize(wrapper.getJson()); } /** @@ -44,11 +56,29 @@ public class AdventureComponentConverter { * @return ProtocolLib wrapper */ public static WrappedChatComponent fromComponent(Component component) { - return WrappedChatComponent.fromJson(GsonComponentSerializer.gson().serialize(component)); + return WrappedChatComponent.fromJson(SERIALIZER.serialize(component)); + } + + /** + * Converts a {@link WrappedComponentStyle} into a {@link Style} + * @param wrapper ProtocolLib wrapper + * @return Style + */ + public static Style fromWrapper(WrappedComponentStyle wrapper) { + return SERIALIZER.serializer().fromJson(wrapper.getJson(), Style.class); + } + + /** + * Converts a {@link Style} into a ProtocolLib wrapper + * @param style Style + * @return ProtocolLib wrapper + */ + public static WrappedComponentStyle fromStyle(Style style) { + return WrappedComponentStyle.fromJson((JsonObject) SERIALIZER.serializer().toJsonTree(style)); } public static Class getComponentClass() { - return Component.class; + return Component.class; } public static Component clone(Object component) { diff --git a/src/main/java/com/comphenix/protocol/wrappers/WrappedComponentStyle.java b/src/main/java/com/comphenix/protocol/wrappers/WrappedComponentStyle.java new file mode 100644 index 00000000..241af075 --- /dev/null +++ b/src/main/java/com/comphenix/protocol/wrappers/WrappedComponentStyle.java @@ -0,0 +1,63 @@ +package com.comphenix.protocol.wrappers; + +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.accessors.Accessors; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.utility.MinecraftVersion; +import com.comphenix.protocol.wrappers.codecs.WrappedCodec; +import com.comphenix.protocol.wrappers.codecs.WrappedDynamicOps; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * A wrapper around the component style NMS class. + * + * @author vytskalt + */ +public class WrappedComponentStyle extends AbstractWrapper { + private static final WrappedCodec CODEC; // 1.20.4+ + private static final Gson GSON; // Below 1.20.4 + + static { + if (MinecraftVersion.v1_20_4.atOrAbove()) { + FuzzyReflection fuzzySerializer = FuzzyReflection.fromClass(MinecraftReflection.getStyleSerializerClass(), true); + Object codec = Accessors.getFieldAccessor(fuzzySerializer.getFieldByType("CODEC", MinecraftReflection.getCodecClass())).get(null); + CODEC = WrappedCodec.fromHandle(codec); + GSON = null; + } else { + FuzzyReflection fuzzySerializer = FuzzyReflection.fromClass(MinecraftReflection.getChatSerializerClass(), true); + CODEC = null; + GSON = (Gson) Accessors.getFieldAccessor(fuzzySerializer.getFieldByType("gson", Gson.class)).get(null); + } + } + + public WrappedComponentStyle(Object handle) { + super(MinecraftReflection.getComponentStyleClass()); + setHandle(handle); + } + + public JsonObject getJson() { + if (CODEC != null) { + return (JsonObject) CODEC.encode(handle, WrappedDynamicOps.json(false)) + .getOrThrow(JsonParseException::new); + } else { + return (JsonObject) GSON.toJsonTree(handle); + } + } + + public static WrappedComponentStyle fromHandle(Object handle) { + return new WrappedComponentStyle(handle); + } + + public static WrappedComponentStyle fromJson(JsonObject json) { + Object handle; + if (CODEC != null) { + handle = CODEC.parse(json, WrappedDynamicOps.json(false)) + .getOrThrow(JsonParseException::new); + } else { + handle = GSON.fromJson(json, MinecraftReflection.getComponentStyleClass()); + } + return new WrappedComponentStyle(handle); + } +} diff --git a/src/test/java/com/comphenix/protocol/wrappers/WrappedComponentStyleTest.java b/src/test/java/com/comphenix/protocol/wrappers/WrappedComponentStyleTest.java new file mode 100644 index 00000000..483ddaf3 --- /dev/null +++ b/src/test/java/com/comphenix/protocol/wrappers/WrappedComponentStyleTest.java @@ -0,0 +1,40 @@ +package com.comphenix.protocol.wrappers; + +import com.comphenix.protocol.BukkitInitialization; +import com.google.gson.JsonObject; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextDecoration; +import net.minecraft.EnumChatFormat; +import net.minecraft.network.chat.ChatModifier; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class WrappedComponentStyleTest { + + @BeforeAll + public static void initializeBukkit() { + BukkitInitialization.initializeAll(); + } + + @Test + public void testComponentStyle() { + ChatModifier style = ChatModifier.a.b(EnumChatFormat.m).a(true); + WrappedComponentStyle wrapped = new WrappedComponentStyle(style); + JsonObject json = wrapped.getJson(); + assertEquals("{\"color\":\"red\",\"bold\":true}", json.toString()); + assertEquals(style, WrappedComponentStyle.fromJson(json).getHandle()); + } + + @Test + public void testStyleAdventureConversion() { + Style adventureStyle = Style.style(NamedTextColor.GREEN, TextDecoration.BOLD) + .clickEvent(ClickEvent.changePage(10)); + + WrappedComponentStyle wrapped = AdventureComponentConverter.fromStyle(adventureStyle); + assertEquals(adventureStyle, AdventureComponentConverter.fromWrapper(wrapped)); + } +}