package io.papermc.paper.adventure; import com.mojang.datafixers.util.Pair; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.DynamicOps; import com.mojang.serialization.JavaOps; import com.mojang.serialization.JsonOps; import io.papermc.paper.util.MethodParameterSource; import java.io.IOException; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.List; import java.util.UUID; import java.util.function.Function; import java.util.stream.Stream; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.BlockNBTComponent; import net.kyori.adventure.text.Component; 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.ShadowColor; import net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextDecoration; import net.minecraft.core.component.DataComponents; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.ByteTag; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.IntTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.Tag; import net.minecraft.network.chat.ComponentSerialization; import net.minecraft.resources.RegistryOps; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import org.apache.commons.lang3.RandomStringUtils; import org.bukkit.support.RegistryHelper; import org.bukkit.support.environment.VanillaFeature; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import org.junitpioneer.jupiter.cartesian.CartesianTest; import static io.papermc.paper.adventure.AdventureCodecs.CLICK_EVENT_CODEC; import static io.papermc.paper.adventure.AdventureCodecs.COMPONENT_CODEC; import static io.papermc.paper.adventure.AdventureCodecs.HOVER_EVENT_CODEC; import static io.papermc.paper.adventure.AdventureCodecs.KEY_CODEC; import static io.papermc.paper.adventure.AdventureCodecs.STYLE_MAP_CODEC; import static io.papermc.paper.adventure.AdventureCodecs.TEXT_COLOR_CODEC; import static java.util.Objects.requireNonNull; import static net.kyori.adventure.key.Key.key; import static net.kyori.adventure.text.Component.blockNBT; import static net.kyori.adventure.text.Component.entityNBT; import static net.kyori.adventure.text.Component.keybind; import static net.kyori.adventure.text.Component.score; import static net.kyori.adventure.text.Component.selector; import static net.kyori.adventure.text.Component.storageNBT; import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.Component.translatable; import static net.kyori.adventure.text.TranslationArgument.numeric; import static net.kyori.adventure.text.event.ClickEvent.openUrl; import static net.kyori.adventure.text.event.ClickEvent.suggestCommand; import static net.kyori.adventure.text.event.HoverEvent.showEntity; import static net.kyori.adventure.text.format.Style.style; import static net.kyori.adventure.text.format.TextColor.color; import static net.kyori.adventure.text.minimessage.MiniMessage.miniMessage; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @VanillaFeature class AdventureCodecsTest { static final String PARAMETERIZED_NAME = "[{index}] {displayName}: {arguments}"; @Test void testTextColor() { final TextColor color = color(0x1d38df); final Tag result = TEXT_COLOR_CODEC.encodeStart(NbtOps.INSTANCE, color).result().orElseThrow(); assertEquals(color.asHexString(), result.getAsString()); final net.minecraft.network.chat.TextColor nms = net.minecraft.network.chat.TextColor.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst(); assertEquals(color.value(), nms.getValue()); } @Test void testNamedTextColor() { final NamedTextColor color = NamedTextColor.BLUE; final Tag result = TEXT_COLOR_CODEC.encodeStart(NbtOps.INSTANCE, color).result().orElseThrow(); assertEquals(NamedTextColor.NAMES.keyOrThrow(color), result.getAsString()); final net.minecraft.network.chat.TextColor nms = net.minecraft.network.chat.TextColor.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst(); assertEquals(color.value(), nms.getValue()); } @Test void testKey() { final Key key = key("hello", "there"); final Tag result = KEY_CODEC.encodeStart(NbtOps.INSTANCE, key).result().orElseThrow(); assertEquals(key.asString(), result.getAsString()); final ResourceLocation location = ResourceLocation.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst(); assertEquals(key.asString(), location.toString()); } @ParameterizedTest(name = PARAMETERIZED_NAME) @EnumSource(value = ClickEvent.Action.class, mode = EnumSource.Mode.EXCLUDE, names = {"OPEN_FILE"}) void testClickEvent(final ClickEvent.Action action) { final ClickEvent event = ClickEvent.clickEvent(action, RandomStringUtils.randomAlphanumeric(20)); final Tag result = CLICK_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, event).result().orElseThrow(); final net.minecraft.network.chat.ClickEvent nms = net.minecraft.network.chat.ClickEvent.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst(); assertEquals(event.action().toString(), nms.getAction().getSerializedName()); assertEquals(event.value(), nms.getValue()); } @Test void testShowTextHoverEvent() { final HoverEvent hoverEvent = HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, text("hello")); final Tag result = HOVER_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, hoverEvent).result().orElseThrow(); final net.minecraft.network.chat.HoverEvent nms = net.minecraft.network.chat.HoverEvent.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst(); assertEquals(hoverEvent.action().toString(), nms.getAction().getSerializedName()); assertNotNull(nms.getValue(net.minecraft.network.chat.HoverEvent.Action.SHOW_TEXT)); } @Test void testShowItemHoverEvent() throws IOException { final ItemStack stack = new ItemStack(Items.PUMPKIN, 3); stack.set(DataComponents.CUSTOM_NAME, net.minecraft.network.chat.Component.literal("NAME")); final HoverEvent hoverEvent = HoverEvent.showItem(key("minecraft:pumpkin"), 3, PaperAdventure.asAdventure(stack.getComponentsPatch())); final Tag result = HOVER_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, hoverEvent).result().orElseThrow(); final DataResult> dataResult = net.minecraft.network.chat.HoverEvent.CODEC.decode(NbtOps.INSTANCE, result); assertTrue(dataResult.result().isPresent(), () -> dataResult + " result is not present"); final net.minecraft.network.chat.HoverEvent nms = dataResult.result().orElseThrow().getFirst(); assertEquals(hoverEvent.action().toString(), nms.getAction().getSerializedName()); final net.minecraft.network.chat.HoverEvent.ItemStackInfo value = nms.getValue(net.minecraft.network.chat.HoverEvent.Action.SHOW_ITEM); assertNotNull(value); assertEquals(hoverEvent.value().count(), value.count); assertEquals(hoverEvent.value().item().asString(), value.item.unwrapKey().orElseThrow().location().toString()); assertEquals(stack.getComponentsPatch(), value.components); } @Test void testShowEntityHoverEvent() { UUID uuid = UUID.randomUUID(); final HoverEvent hoverEvent = showEntity(key("minecraft:wolf"), uuid, text("NAME")); final Tag result = HOVER_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, hoverEvent).result().orElseThrow(); final DataResult> dataResult = net.minecraft.network.chat.HoverEvent.CODEC.decode(NbtOps.INSTANCE, result); assertTrue(dataResult.result().isPresent(), () -> dataResult + " result is not present"); final net.minecraft.network.chat.HoverEvent nms = dataResult.result().orElseThrow().getFirst(); assertEquals(hoverEvent.action().toString(), nms.getAction().getSerializedName()); final net.minecraft.network.chat.HoverEvent.EntityTooltipInfo value = nms.getValue(net.minecraft.network.chat.HoverEvent.Action.SHOW_ENTITY); assertNotNull(value); assertEquals(hoverEvent.value().type().asString(), BuiltInRegistries.ENTITY_TYPE.getKey(value.type).toString()); assertEquals(hoverEvent.value().id(), value.id); assertEquals("NAME", value.name.orElseThrow().getString()); } @Test void testSimpleStyle() { final Style style = style().decorate(TextDecoration.BOLD).color(NamedTextColor.RED).build(); final Tag result = STYLE_MAP_CODEC.codec().encodeStart(NbtOps.INSTANCE, style).result().orElseThrow(); final DataResult> dataResult = net.minecraft.network.chat.Style.Serializer.CODEC.decode(NbtOps.INSTANCE, result); assertTrue(dataResult.result().isPresent(), () -> dataResult + " result is not present"); final net.minecraft.network.chat.Style nms = dataResult.result().get().getFirst(); assertTrue(nms.isBold()); assertEquals(requireNonNull(style.color()).value(), requireNonNull(nms.getColor()).getValue()); } @CartesianTest(name = PARAMETERIZED_NAME) void testDirectRoundTripStyle( @MethodParameterSource("dynamicOps") final DynamicOps dynamicOps, @MethodParameterSource("testStyles") final Style style ) { testDirectRoundTrip(dynamicOps, STYLE_MAP_CODEC.codec(), style); } @CartesianTest(name = PARAMETERIZED_NAME) void testMinecraftRoundTripStyle( @MethodParameterSource("dynamicOps") final DynamicOps dynamicOps, @MethodParameterSource("testStyles") final Style style ) { testMinecraftRoundTrip(dynamicOps, STYLE_MAP_CODEC.codec(), net.minecraft.network.chat.Style.Serializer.CODEC, style); } @CartesianTest(name = PARAMETERIZED_NAME) void testDirectRoundTripComponent( @MethodParameterSource("dynamicOps") final DynamicOps dynamicOps, @TestComponents final Component component ) { testDirectRoundTrip(dynamicOps, COMPONENT_CODEC, component); } @CartesianTest(name = PARAMETERIZED_NAME) void testMinecraftRoundTripComponent( @MethodParameterSource("dynamicOps") final DynamicOps dynamicOps, @TestComponents final Component component ) { testMinecraftRoundTrip(dynamicOps, COMPONENT_CODEC, ComponentSerialization.CODEC, component); } static List> dynamicOps() { return Stream.of( NbtOps.INSTANCE, JavaOps.INSTANCE, JsonOps.INSTANCE ) .map(ops -> RegistryHelper.getRegistry().createSerializationContext(ops)) .toList(); } @ParameterizedTest(name = PARAMETERIZED_NAME) @MethodSource({"invalidData"}) void invalidThrows(final Tag input) { assertThrows(RuntimeException.class, () -> { require( COMPONENT_CODEC.decode(NbtOps.INSTANCE, input), msg -> "Failed to decode " + input + ": " + msg ); }); } static void testDirectRoundTrip(final DynamicOps ops, final Codec codec, final A adventure) { final O encoded = require( codec.encodeStart(ops, adventure), msg -> "Failed to encode " + adventure + ": " + msg ); final Pair roundTripResult = require( codec.decode(ops, encoded), msg -> "Failed to decode " + encoded + ": " + msg ); assertEquals(adventure, roundTripResult.getFirst()); } static void testMinecraftRoundTrip(final DynamicOps ops, final Codec adventureCodec, final Codec minecraftCodec, final A adventure) { final O encoded = require( adventureCodec.encodeStart(ops, adventure), msg -> "Failed to encode " + adventure + ": " + msg ); final M minecraftResult = require( minecraftCodec.decode(ops, encoded), msg -> "Failed to decode to Minecraft: " + encoded + "; " + msg ).getFirst(); final O minecraftReEncoded = require( minecraftCodec.encodeStart(ops, minecraftResult), msg -> "Failed to re-encode Minecraft: " + minecraftResult + "; " + msg ); final Pair roundTripResult = require( adventureCodec.decode(ops, minecraftReEncoded), msg -> "Failed to decode " + minecraftReEncoded + ": " + msg ); assertEquals(adventure, roundTripResult.getFirst()); } static R require(final DataResult result, final Function errorMessage) { return result.getOrThrow(s -> new RuntimeException(errorMessage.apply(s))); } static List invalidData() { return List.of( IntTag.valueOf(-1), ByteTag.ZERO, new CompoundTag(), new ListTag() ); } static List