From 6e3d7add287eaf62d0fbb3ebd8e4e6436cc3648f Mon Sep 17 00:00:00 2001 From: Jake Potrebic <jake.m.potrebic@gmail.com> Date: Mon, 11 Dec 2023 22:02:06 -0800 Subject: [PATCH] Use Codecs for adventure Component conversions & network serialization (#10014) * finish implementing all adventure components in codecs * add some initial tests * Add round trip tests for text and translatable components * Add more round trip test data (score component is failing) * Add more round trip test data * Fix SCORE_COMPONENT_MAP_CODEC * Improve test failure messages * Add failure cases * Add a couple more test data * Make use of AdventureCodecs * Update patches after rebase * Squash changes into adventure patch * Fix AT formatting * update comment --------- Co-authored-by: Jason Penilla <11360596+jpenilla@users.noreply.github.com> --- patches/server/Adventure.patch | 903 +++++++++++++++++- .../Player-Tab-List-and-Title-APIs.patch | 2 +- ...oleAppender-for-console-improvements.patch | 4 +- ...tial-work-on-native-Adventure-codecs.patch | 303 ------ 4 files changed, 878 insertions(+), 334 deletions(-) delete mode 100644 patches/server/initial-work-on-native-Adventure-codecs.patch diff --git a/patches/server/Adventure.patch b/patches/server/Adventure.patch index 0a0a4a52c2..15ddbb0164 100644 --- a/patches/server/Adventure.patch +++ b/patches/server/Adventure.patch @@ -3,9 +3,414 @@ From: Riley Park <rileysebastianpark@gmail.com> Date: Fri, 29 Jan 2021 17:54:03 +0100 Subject: [PATCH] Adventure +== AT == +public net.minecraft.network.chat.HoverEvent$ItemStackInfo item +public net.minecraft.network.chat.HoverEvent$ItemStackInfo count +public net.minecraft.network.chat.HoverEvent$ItemStackInfo tag +public net.minecraft.network.chat.contents.TranslatableContents filterAllowedArguments(Ljava/lang/Object;)Lcom/mojang/serialization/DataResult; + Co-authored-by: zml <zml@stellardrift.ca> Co-authored-by: Jake Potrebic <jake.m.potrebic@gmail.com> +diff --git a/src/main/java/io/papermc/paper/adventure/AdventureCodecs.java b/src/main/java/io/papermc/paper/adventure/AdventureCodecs.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/adventure/AdventureCodecs.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.adventure; ++ ++import com.mojang.brigadier.exceptions.CommandSyntaxException; ++import com.mojang.datafixers.util.Either; ++import com.mojang.serialization.Codec; ++import com.mojang.serialization.DataResult; ++import com.mojang.serialization.Encoder; ++import com.mojang.serialization.MapCodec; ++import com.mojang.serialization.codecs.RecordCodecBuilder; ++import java.io.IOException; ++import java.util.Collections; ++import java.util.List; ++import java.util.Optional; ++import java.util.UUID; ++import java.util.function.Consumer; ++import java.util.function.Function; ++import java.util.function.Predicate; ++import net.kyori.adventure.key.Key; ++import net.kyori.adventure.nbt.api.BinaryTagHolder; ++import net.kyori.adventure.text.BlockNBTComponent; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.EntityNBTComponent; ++import net.kyori.adventure.text.KeybindComponent; ++import net.kyori.adventure.text.NBTComponent; ++import net.kyori.adventure.text.NBTComponentBuilder; ++import net.kyori.adventure.text.ScoreComponent; ++import net.kyori.adventure.text.SelectorComponent; ++import net.kyori.adventure.text.StorageNBTComponent; ++import net.kyori.adventure.text.TextComponent; ++import net.kyori.adventure.text.TranslatableComponent; ++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.kyori.adventure.text.serializer.gson.GsonComponentSerializer; ++import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; ++import net.minecraft.core.UUIDUtil; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.TagParser; ++import net.minecraft.network.chat.ComponentSerialization; ++import net.minecraft.network.chat.contents.KeybindContents; ++import net.minecraft.network.chat.contents.ScoreContents; ++import net.minecraft.network.chat.contents.TranslatableContents; ++import net.minecraft.util.ExtraCodecs; ++import net.minecraft.util.StringRepresentable; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.item.ItemStack; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.intellij.lang.annotations.Subst; ++ ++import static com.mojang.serialization.codecs.RecordCodecBuilder.mapCodec; ++import static java.util.function.Function.identity; ++import static net.kyori.adventure.text.Component.text; ++import static net.minecraft.util.ExtraCodecs.recursive; ++import static net.minecraft.util.ExtraCodecs.strictOptionalField; ++ ++@DefaultQualifier(NonNull.class) ++public final class AdventureCodecs { ++ ++ public static final Codec<Component> COMPONENT_CODEC = recursive("adventure Component", AdventureCodecs::createCodec); ++ ++ static final Codec<TextColor> TEXT_COLOR_CODEC = Codec.STRING.comapFlatMap(s -> { ++ if (s.startsWith("#")) { ++ @Nullable TextColor value = TextColor.fromHexString(s); ++ return value != null ? DataResult.success(value) : DataResult.error(() -> "Cannot convert " + s + " to adventure TextColor"); ++ } else { ++ final @Nullable NamedTextColor value = NamedTextColor.NAMES.value(s); ++ return value != null ? DataResult.success(value) : DataResult.error(() -> "Cannot convert " + s + " to adventure NamedTextColor"); ++ } ++ }, textColor -> { ++ if (textColor instanceof NamedTextColor named) { ++ return NamedTextColor.NAMES.keyOrThrow(named); ++ } else { ++ return textColor.asHexString(); ++ } ++ }); ++ ++ static final Codec<Key> KEY_CODEC = Codec.STRING.comapFlatMap(s -> { ++ return Key.parseable(s) ? DataResult.success(Key.key(s)) : DataResult.error(() -> "Cannot convert " + s + " to adventure Key"); ++ }, Key::asString); ++ ++ static final Codec<ClickEvent.Action> CLICK_EVENT_ACTION_CODEC = Codec.STRING.comapFlatMap(s -> { ++ final ClickEvent.@Nullable Action value = ClickEvent.Action.NAMES.value(s); ++ return value != null ? DataResult.success(value) : DataResult.error(() -> "Cannot convert " + s + " to adventure ClickEvent$Action"); ++ }, ClickEvent.Action.NAMES::keyOrThrow); ++ static final Codec<ClickEvent> CLICK_EVENT_CODEC = RecordCodecBuilder.create((instance) -> { ++ return instance.group( ++ CLICK_EVENT_ACTION_CODEC.fieldOf("action").forGetter(ClickEvent::action), ++ Codec.STRING.fieldOf("value").forGetter(ClickEvent::value) ++ ).apply(instance, ClickEvent::clickEvent); ++ }); ++ ++ static Codec<HoverEvent.ShowEntity> showEntityCodec(final Codec<Component> componentCodec) { ++ return RecordCodecBuilder.create((instance) -> { ++ return instance.group( ++ KEY_CODEC.fieldOf("type").forGetter(HoverEvent.ShowEntity::type), ++ UUIDUtil.LENIENT_CODEC.fieldOf("id").forGetter(HoverEvent.ShowEntity::id), ++ strictOptionalField(componentCodec, "name").forGetter(he -> Optional.ofNullable(he.name())) ++ ).apply(instance, (key, uuid, component) -> { ++ return HoverEvent.ShowEntity.showEntity(key, uuid, component.orElse(null)); ++ }); ++ }); ++ } ++ ++ static Codec<HoverEvent.ShowItem> showItemCodec(final Codec<Component> componentCodec) { ++ return net.minecraft.network.chat.HoverEvent.ItemStackInfo.CODEC.xmap(isi -> { ++ @Subst("key") final String typeKey = BuiltInRegistries.ITEM.getKey(isi.item).toString(); ++ return HoverEvent.ShowItem.showItem(Key.key(typeKey), isi.count, PaperAdventure.asBinaryTagHolder(isi.tag.orElse(null))); ++ }, si -> { ++ final Item itemType = BuiltInRegistries.ITEM.get(PaperAdventure.asVanilla(si.item())); ++ final ItemStack stack; ++ try { ++ final @Nullable CompoundTag tag = si.nbt() != null ? si.nbt().get(PaperAdventure.NBT_CODEC) : null; ++ stack = new ItemStack(BuiltInRegistries.ITEM.wrapAsHolder(itemType), si.count(), Optional.ofNullable(tag)); ++ } catch (final IOException e) { ++ throw new RuntimeException(e); ++ } ++ return new net.minecraft.network.chat.HoverEvent.ItemStackInfo(stack); ++ }); ++ } ++ ++ static final HoverEventType<HoverEvent.ShowEntity> SHOW_ENTITY_HOVER_EVENT_TYPE = new HoverEventType<>(AdventureCodecs::showEntityCodec, HoverEvent.Action.SHOW_ENTITY, "show_entity", AdventureCodecs::legacyDeserializeEntity); ++ static final HoverEventType<HoverEvent.ShowItem> SHOW_ITEM_HOVER_EVENT_TYPE = new HoverEventType<>(AdventureCodecs::showItemCodec, HoverEvent.Action.SHOW_ITEM, "show_item", AdventureCodecs::legacyDeserializeItem); ++ static final HoverEventType<Component> SHOW_TEXT_HOVER_EVENT_TYPE = new HoverEventType<>(identity(), HoverEvent.Action.SHOW_TEXT, "show_text", DataResult::success); ++ static final Codec<HoverEventType<?>> HOVER_EVENT_TYPE_CODEC = StringRepresentable.fromValues(() -> new HoverEventType<?>[]{ SHOW_ENTITY_HOVER_EVENT_TYPE, SHOW_ITEM_HOVER_EVENT_TYPE, SHOW_TEXT_HOVER_EVENT_TYPE }); ++ ++ static DataResult<HoverEvent.ShowEntity> legacyDeserializeEntity(final Component text) { ++ try { ++ final CompoundTag tag = TagParser.parseTag(PlainTextComponentSerializer.plainText().serialize(text)); ++ final @Nullable Component entityName = GsonComponentSerializer.gson().deserializeOrNull(tag.getString("name")); ++ @Subst("key") final String keyString = tag.getString("type"); ++ final UUID entityUUID = UUID.fromString(tag.getString("id")); ++ return DataResult.success(HoverEvent.ShowEntity.showEntity(Key.key(keyString), entityUUID, entityName)); ++ } catch (final Exception ex) { ++ return DataResult.error(() -> "Failed to parse tooltip: " + ex.getMessage()); ++ } ++ } ++ ++ static DataResult<HoverEvent.ShowItem> legacyDeserializeItem(final Component text) { ++ try { ++ final ItemStack stack = ItemStack.of(TagParser.parseTag(PlainTextComponentSerializer.plainText().serialize(text))); ++ @Subst("key") final String keyString = BuiltInRegistries.ITEM.getKey(stack.getItem()).toString(); ++ return DataResult.success(HoverEvent.ShowItem.showItem(Key.key(keyString), stack.getCount(), stack.getTag() != null ? BinaryTagHolder.encode(stack.getTag(), PaperAdventure.NBT_CODEC) : null)); ++ } catch (final CommandSyntaxException | IOException ex) { ++ return DataResult.error(() -> "Failed to parse item tag: " + ex.getMessage()); ++ } ++ } ++ ++ record HoverEventType<V>(Function<Codec<Component>, Codec<HoverEvent<V>>> codec, String id, Function<Codec<Component>, Codec<HoverEvent<V>>> legacyCodec) implements StringRepresentable { ++ HoverEventType(final Function<Codec<Component>, Codec<V>> contentCodec, final HoverEvent.Action<V> action, final String id, final Function<Component, DataResult<V>> legacyDeserializer) { ++ this(cc -> contentCodec.apply(cc).xmap(v -> HoverEvent.hoverEvent(action, v), HoverEvent::value).fieldOf("contents").codec(), ++ id, ++ codec -> Codec.of( ++ Encoder.error("Can't encode in legacy format"), ++ codec.flatMap(legacyDeserializer).map(text -> HoverEvent.hoverEvent(action, text)) ++ ) ++ ); ++ } ++ @Override ++ public String getSerializedName() { ++ return this.id; ++ } ++ } ++ ++ private static final Function<HoverEvent<?>, HoverEventType<?>> GET_HOVER_EVENT_TYPE = he -> { ++ if (he.action() == HoverEvent.Action.SHOW_ENTITY) { ++ return SHOW_ENTITY_HOVER_EVENT_TYPE; ++ } else if (he.action() == HoverEvent.Action.SHOW_ITEM) { ++ return SHOW_ITEM_HOVER_EVENT_TYPE; ++ } else if (he.action() == HoverEvent.Action.SHOW_TEXT) { ++ return SHOW_TEXT_HOVER_EVENT_TYPE; ++ } else { ++ throw new IllegalStateException(); ++ } ++ }; ++ static final Codec<HoverEvent<?>> HOVER_EVENT_CODEC = Codec.either( ++ HOVER_EVENT_TYPE_CODEC.<HoverEvent<?>>dispatchMap("action", GET_HOVER_EVENT_TYPE, het -> het.codec.apply(COMPONENT_CODEC)).codec(), ++ HOVER_EVENT_TYPE_CODEC.<HoverEvent<?>>dispatchMap("action", GET_HOVER_EVENT_TYPE, het -> het.legacyCodec.apply(COMPONENT_CODEC)).codec() ++ ).xmap(either -> either.map(identity(), identity()), Either::left); ++ ++ public static final MapCodec<Style> STYLE_MAP_CODEC = mapCodec((instance) -> { ++ return instance.group( ++ strictOptionalField(TEXT_COLOR_CODEC, "color").forGetter(nullableGetter(Style::color)), ++ strictOptionalField(Codec.BOOL, "bold").forGetter(decorationGetter(TextDecoration.BOLD)), ++ strictOptionalField(Codec.BOOL, "italic").forGetter(decorationGetter(TextDecoration.ITALIC)), ++ strictOptionalField(Codec.BOOL, "underlined").forGetter(decorationGetter(TextDecoration.UNDERLINED)), ++ strictOptionalField(Codec.BOOL, "strikethrough").forGetter(decorationGetter(TextDecoration.STRIKETHROUGH)), ++ strictOptionalField(Codec.BOOL, "obfuscated").forGetter(decorationGetter(TextDecoration.OBFUSCATED)), ++ strictOptionalField(CLICK_EVENT_CODEC, "clickEvent").forGetter(nullableGetter(Style::clickEvent)), ++ strictOptionalField(HOVER_EVENT_CODEC, "hoverEvent").forGetter(nullableGetter(Style::hoverEvent)), ++ strictOptionalField(Codec.STRING, "insertion").forGetter(nullableGetter(Style::insertion)), ++ strictOptionalField(KEY_CODEC, "font").forGetter(nullableGetter(Style::font)) ++ ).apply(instance, (textColor, bold, italic, underlined, strikethrough, obfuscated, clickEvent, hoverEvent, insertion, font) -> { ++ return Style.style(builder -> { ++ textColor.ifPresent(builder::color); ++ bold.ifPresent(styleBooleanConsumer(builder, TextDecoration.BOLD)); ++ italic.ifPresent(styleBooleanConsumer(builder, TextDecoration.ITALIC)); ++ underlined.ifPresent(styleBooleanConsumer(builder, TextDecoration.UNDERLINED)); ++ strikethrough.ifPresent(styleBooleanConsumer(builder, TextDecoration.STRIKETHROUGH)); ++ obfuscated.ifPresent(styleBooleanConsumer(builder, TextDecoration.OBFUSCATED)); ++ clickEvent.ifPresent(builder::clickEvent); ++ hoverEvent.ifPresent(builder::hoverEvent); ++ insertion.ifPresent(builder::insertion); ++ font.ifPresent(builder::font); ++ }); ++ }); ++ }); ++ static Consumer<Boolean> styleBooleanConsumer(final Style.Builder builder, final TextDecoration decoration) { ++ return b -> builder.decoration(decoration, b); ++ } ++ ++ static Function<Style, Optional<Boolean>> decorationGetter(final TextDecoration decoration) { ++ return style -> Optional.ofNullable(style.decoration(decoration) == TextDecoration.State.NOT_SET ? null : style.decoration(decoration) == TextDecoration.State.TRUE); ++ } ++ ++ static <R, T> Function<R, Optional<T>> nullableGetter(final Function<R, @Nullable T> getter) { ++ return style -> Optional.ofNullable(getter.apply(style)); ++ } ++ ++ static final MapCodec<TextComponent> TEXT_COMPONENT_MAP_CODEC = mapCodec((instance) -> { ++ return instance.group(Codec.STRING.fieldOf("text").forGetter(TextComponent::content)).apply(instance, Component::text); ++ }); ++ static final Codec<Object> PRIMITIVE_ARG_CODEC = ExtraCodecs.validate(ExtraCodecs.JAVA, TranslatableContents::filterAllowedArguments); ++ static final Codec<Component> ARG_CODEC = Codec.either(PRIMITIVE_ARG_CODEC, COMPONENT_CODEC).xmap((primitiveOrComponent) -> { ++ // just toString all primitives (not 100% correct to vanilla spec) ++ // vanilla allows primitive translatable args, but adventure doesn't (in 4.14) ++ return primitiveOrComponent.map(o -> text(String.valueOf(o)), identity()); ++ }, Either::right); ++ static final MapCodec<TranslatableComponent> TRANSLATABLE_COMPONENT_MAP_CODEC = mapCodec((instance) -> { ++ return instance.group( ++ Codec.STRING.fieldOf("translate").forGetter(TranslatableComponent::key), ++ Codec.STRING.optionalFieldOf("fallback").forGetter(nullableGetter(TranslatableComponent::fallback)), ++ strictOptionalField(ARG_CODEC.listOf(), "with").forGetter(c -> c.args().isEmpty() ? Optional.empty() : Optional.of(c.args())) ++ ).apply(instance, (key, fallback, components) -> { ++ return Component.translatable(key, components.orElse(Collections.emptyList())).fallback(fallback.orElse(null)); ++ }); ++ }); ++ ++ static final MapCodec<KeybindComponent> KEYBIND_COMPONENT_MAP_CODEC = KeybindContents.CODEC.xmap(k -> Component.keybind(k.getName()), k -> new KeybindContents(k.keybind())); ++ static final MapCodec<ScoreComponent> SCORE_COMPONENT_INNER_MAP_CODEC = ScoreContents.INNER_CODEC.xmap(s -> Component.score(s.getName(), s.getObjective()), s -> new ScoreContents(s.name(), s.objective())); ++ static final MapCodec<ScoreComponent> SCORE_COMPONENT_MAP_CODEC = SCORE_COMPONENT_INNER_MAP_CODEC.fieldOf("score"); ++ static final MapCodec<SelectorComponent> SELECTOR_COMPONENT_MAP_CODEC = mapCodec((instance) -> { ++ return instance.group( ++ Codec.STRING.fieldOf("selector").forGetter(SelectorComponent::pattern), ++ strictOptionalField(COMPONENT_CODEC, "separator").forGetter(nullableGetter(SelectorComponent::separator)) ++ ).apply(instance, (selector, component) -> Component.selector(selector, component.orElse(null))); ++ }); ++ ++ interface NbtComponentDataSource { ++ NBTComponentBuilder<?, ?> builder(); ++ ++ DataSourceType<?> type(); ++ } ++ ++ record StorageDataSource(Key storage) implements NbtComponentDataSource { ++ @Override ++ public NBTComponentBuilder<?, ?> builder() { ++ return Component.storageNBT().storage(this.storage()); ++ } ++ ++ @Override ++ public DataSourceType<?> type() { ++ return STORAGE_DATA_SOURCE_TYPE; ++ } ++ } ++ ++ record BlockDataSource(String posPattern) implements NbtComponentDataSource { ++ @Override ++ public NBTComponentBuilder<?, ?> builder() { ++ return Component.blockNBT().pos(BlockNBTComponent.Pos.fromString(this.posPattern)); ++ } ++ ++ @Override ++ public DataSourceType<?> type() { ++ return BLOCK_DATA_SOURCE_TYPE; ++ } ++ } ++ ++ record EntityDataSource(String selectorPattern) implements NbtComponentDataSource { ++ @Override ++ public NBTComponentBuilder<?, ?> builder() { ++ return Component.entityNBT().selector(this.selectorPattern()); ++ } ++ ++ @Override ++ public DataSourceType<?> type() { ++ return ENTITY_DATA_SOURCE_TYPE; ++ } ++ } ++ ++ static final DataSourceType<StorageDataSource> STORAGE_DATA_SOURCE_TYPE = new DataSourceType<>(mapCodec((instance) -> instance.group(KEY_CODEC.fieldOf("storage").forGetter(StorageDataSource::storage)).apply(instance, StorageDataSource::new)), "storage"); ++ static final DataSourceType<BlockDataSource> BLOCK_DATA_SOURCE_TYPE = new DataSourceType<>(mapCodec((instance) -> instance.group(Codec.STRING.fieldOf("block").forGetter(BlockDataSource::posPattern)).apply(instance, BlockDataSource::new)), "block"); ++ static final DataSourceType<EntityDataSource> ENTITY_DATA_SOURCE_TYPE = new DataSourceType<>(mapCodec((instance) -> instance.group(Codec.STRING.fieldOf("entity").forGetter(EntityDataSource::selectorPattern)).apply(instance, EntityDataSource::new)), "entity"); ++ ++ static final MapCodec<NbtComponentDataSource> NBT_COMPONENT_DATA_SOURCE_CODEC = ComponentSerialization.createLegacyComponentMatcher(new DataSourceType<?>[]{ENTITY_DATA_SOURCE_TYPE, BLOCK_DATA_SOURCE_TYPE, STORAGE_DATA_SOURCE_TYPE}, DataSourceType::codec, NbtComponentDataSource::type, "source"); ++ ++ record DataSourceType<D extends NbtComponentDataSource>(MapCodec<D> codec, String id) implements StringRepresentable { ++ @Override ++ public String getSerializedName() { ++ return this.id(); ++ } ++ } ++ ++ static final MapCodec<NBTComponent<?, ?>> NBT_COMPONENT_MAP_CODEC = mapCodec((instance) -> { ++ return instance.group( ++ Codec.STRING.fieldOf("nbt").forGetter(NBTComponent::nbtPath), ++ Codec.BOOL.optionalFieldOf("interpret", false).forGetter(NBTComponent::interpret), ++ COMPONENT_CODEC.optionalFieldOf("separator").forGetter(nullableGetter(NBTComponent::separator)), ++ NBT_COMPONENT_DATA_SOURCE_CODEC.forGetter(nbtComponent -> { ++ if (nbtComponent instanceof final EntityNBTComponent entityNBTComponent) { ++ return new EntityDataSource(entityNBTComponent.selector()); ++ } else if (nbtComponent instanceof final BlockNBTComponent blockNBTComponent) { ++ return new BlockDataSource(blockNBTComponent.pos().asString()); ++ } else if (nbtComponent instanceof final StorageNBTComponent storageNBTComponent) { ++ return new StorageDataSource(storageNBTComponent.storage()); ++ } else { ++ throw new IllegalArgumentException(nbtComponent + " isn't a valid nbt component"); ++ } ++ }) ++ ).apply(instance, (nbtPath, interpret, separator, dataSource) -> { ++ return dataSource.builder().nbtPath(nbtPath).interpret(interpret).separator(separator.orElse(null)).build(); ++ }); ++ }); ++ ++ @SuppressWarnings("NonExtendableApiUsage") ++ record ComponentType<C extends Component>(MapCodec<C> codec, Predicate<Component> test, String id) implements StringRepresentable { ++ @Override ++ public String getSerializedName() { ++ return this.id; ++ } ++ } ++ ++ static final ComponentType<TextComponent> PLAIN = new ComponentType<>(TEXT_COMPONENT_MAP_CODEC, TextComponent.class::isInstance, "text"); ++ static final ComponentType<TranslatableComponent> TRANSLATABLE = new ComponentType<>(TRANSLATABLE_COMPONENT_MAP_CODEC, TranslatableComponent.class::isInstance, "translatable"); ++ static final ComponentType<KeybindComponent> KEYBIND = new ComponentType<>(KEYBIND_COMPONENT_MAP_CODEC, KeybindComponent.class::isInstance, "keybind"); ++ static final ComponentType<ScoreComponent> SCORE = new ComponentType<>(SCORE_COMPONENT_MAP_CODEC, ScoreComponent.class::isInstance, "score"); ++ static final ComponentType<SelectorComponent> SELECTOR = new ComponentType<>(SELECTOR_COMPONENT_MAP_CODEC, SelectorComponent.class::isInstance, "selector"); ++ static final ComponentType<NBTComponent<?, ?>> NBT = new ComponentType<>(NBT_COMPONENT_MAP_CODEC, NBTComponent.class::isInstance, "nbt"); ++ ++ static Codec<Component> createCodec(final Codec<Component> selfCodec) { ++ final ComponentType<?>[] types = new ComponentType<?>[]{PLAIN, TRANSLATABLE, KEYBIND, SCORE, SELECTOR, NBT}; ++ final MapCodec<Component> legacyCodec = ComponentSerialization.createLegacyComponentMatcher(types, ComponentType::codec, component -> { ++ for (final ComponentType<?> type : types) { ++ if (type.test().test(component)) { ++ return type; ++ } ++ } ++ throw new IllegalStateException("Unexpected component type " + component); ++ }, "type"); ++ ++ final Codec<Component> directCodec = RecordCodecBuilder.create((instance) -> { ++ return instance.group( ++ legacyCodec.forGetter(identity()), ++ strictOptionalField(ExtraCodecs.nonEmptyList(selfCodec.listOf()), "extra", List.of()).forGetter(Component::children), ++ STYLE_MAP_CODEC.forGetter(Component::style) ++ ).apply(instance, (component, children, style) -> { ++ return component.style(style).children(children); ++ }); ++ }); ++ ++ return Codec.either(Codec.either(Codec.STRING, ExtraCodecs.nonEmptyList(selfCodec.listOf())), directCodec).xmap((stringOrListOrComponent) -> { ++ return stringOrListOrComponent.map((stringOrList) -> stringOrList.map(Component::text, AdventureCodecs::createFromList), identity()); ++ }, (text) -> { ++ final @Nullable String string = tryCollapseToString(text); ++ return string != null ? Either.left(Either.left(string)) : Either.right(text); ++ }); ++ } ++ ++ static @Nullable String tryCollapseToString(final Component component) { ++ if (component instanceof final TextComponent textComponent) { ++ if (component.children().isEmpty() && component.style().isEmpty()) { ++ return textComponent.content(); ++ } ++ } ++ return null; ++ } ++ ++ static Component createFromList(final List<? extends Component> components) { ++ Component component = components.get(0); ++ for (int i = 1; i < components.size(); i++) { ++ component = component.append(components.get(i)); ++ } ++ return component; ++ } ++ ++ private AdventureCodecs() { ++ } ++} diff --git a/src/main/java/io/papermc/paper/adventure/AdventureComponent.java b/src/main/java/io/papermc/paper/adventure/AdventureComponent.java new file mode 100644 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 @@ -14,14 +419,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 @@ -0,0 +0,0 @@ +package io.papermc.paper.adventure; + -+import com.google.gson.JsonElement; -+import com.google.gson.JsonSerializationContext; -+import com.google.gson.JsonSerializer; -+import java.lang.reflect.Type; +import java.util.List; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; -+import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.minecraft.network.chat.ComponentContents; +import net.minecraft.network.chat.MutableComponent; @@ -94,13 +494,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + public Component adventure$component() { + return this.adventure; + } -+ -+ public static class Serializer implements JsonSerializer<AdventureComponent> { -+ @Override -+ public JsonElement serialize(final AdventureComponent src, final Type type, final JsonSerializationContext context) { -+ return GsonComponentSerializer.gson().serializer().toJsonTree(src.adventure, Component.class); -+ } -+ } +} diff --git a/src/main/java/io/papermc/paper/adventure/BossBarImplementationImpl.java b/src/main/java/io/papermc/paper/adventure/BossBarImplementationImpl.java new file mode 100644 @@ -840,7 +1233,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; ++import java.util.Map; +import java.util.Optional; ++import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; @@ -941,7 +1336,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + public static final AttributeKey<Locale> LOCALE_ATTRIBUTE = AttributeKey.valueOf("adventure:locale"); // init after FLATTENER because classloading triggered here might create a logger + @Deprecated + public static final PlainComponentSerializer PLAIN = PlainComponentSerializer.builder().flattener(FLATTENER).build(); -+ private static final Codec<CompoundTag, String, IOException, IOException> NBT_CODEC = new Codec<CompoundTag, String, IOException, IOException>() { ++ static final Codec<CompoundTag, String, IOException, IOException> NBT_CODEC = new Codec<CompoundTag, String, IOException, IOException>() { + @Override + public @NotNull CompoundTag decode(final @NotNull String encoded) throws IOException { + try { @@ -977,7 +1372,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + // Component + + public static Component asAdventure(final net.minecraft.network.chat.Component component) { -+ return component == null ? Component.empty() : GsonComponentSerializer.gson().serializer().fromJson(net.minecraft.network.chat.Component.Serializer.toJsonTree(component), Component.class); ++ return component == null ? Component.empty() : WRAPPER_AWARE_SERIALIZER.deserialize(component); + } + + public static ArrayList<Component> asAdventure(final List<net.minecraft.network.chat.Component> vanillas) { @@ -1007,7 +1402,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + public static net.minecraft.network.chat.Component asVanilla(final Component component) { + if (component == null) return null; + if (true) return new AdventureComponent(component); -+ return net.minecraft.network.chat.Component.Serializer.fromJson(GsonComponentSerializer.gson().serializer().toJsonTree(component)); ++ return WRAPPER_AWARE_SERIALIZER.serialize(component); + } + + public static List<net.minecraft.network.chat.Component> asVanilla(final List<Component> adventures) { @@ -1022,11 +1417,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + return GsonComponentSerializer.gson().serialize(translated(component, locale)); + } + -+ public static String asJsonString(final net.minecraft.network.chat.Component component, final Locale locale) { -+ if (component instanceof AdventureComponent) { -+ return asJsonString(((AdventureComponent) component).adventure, locale); -+ } -+ return net.minecraft.network.chat.Component.Serializer.toJson(component); ++ private static final Map<Locale, com.mojang.serialization.Codec<Component>> LOCALIZED_CODECS = new ConcurrentHashMap<>(); ++ ++ public static com.mojang.serialization.Codec<Component> localizedCodec(final Locale l) { ++ return LOCALIZED_CODECS.computeIfAbsent(l, locale -> AdventureCodecs.COMPONENT_CODEC.xmap( ++ component -> component, // decode ++ component -> translated(component, locale) // encode ++ )); + } + + public static String asPlain(final Component component, final Locale locale) { @@ -1230,9 +1627,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 @@ -0,0 +0,0 @@ +package io.papermc.paper.adventure; + ++import com.mojang.datafixers.util.Pair; ++import java.util.function.Function; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.ComponentSerializer; -+import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; ++import net.minecraft.nbt.NbtOps; ++import net.minecraft.nbt.Tag; ++import net.minecraft.network.chat.ComponentSerialization; + +final class WrapperAwareSerializer implements ComponentSerializer<Component, Component, net.minecraft.network.chat.Component> { + @Override @@ -1240,12 +1641,28 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + if (input instanceof AdventureComponent) { + return ((AdventureComponent) input).adventure; + } -+ return GsonComponentSerializer.gson().serializer().fromJson(net.minecraft.network.chat.Component.Serializer.toJsonTree(input), Component.class); ++ final Tag tag = ComponentSerialization.CODEC.encodeStart(NbtOps.INSTANCE, input) ++ .get().map(Function.identity(), partial -> { ++ throw new RuntimeException("Failed to encode Minecraft Component: " + input + "; " + partial.message()); ++ }); ++ final Pair<Component, Tag> converted = AdventureCodecs.COMPONENT_CODEC.decode(NbtOps.INSTANCE, tag) ++ .get().map(Function.identity(), partial -> { ++ throw new RuntimeException("Failed to decode to adventure Component: " + tag + "; " + partial.message()); ++ }); ++ return converted.getFirst(); + } + + @Override + public net.minecraft.network.chat.Component serialize(final Component component) { -+ return net.minecraft.network.chat.Component.Serializer.fromJson(GsonComponentSerializer.gson().serializer().toJsonTree(component)); ++ final Tag tag = AdventureCodecs.COMPONENT_CODEC.encodeStart(NbtOps.INSTANCE, component) ++ .get().map(Function.identity(), partial -> { ++ throw new RuntimeException("Failed to encode adventure Component: " + component + "; " + partial.message()); ++ }); ++ final Pair<net.minecraft.network.chat.Component, Tag> converted = ComponentSerialization.CODEC.decode(NbtOps.INSTANCE, tag) ++ .get().map(Function.identity(), partial -> { ++ throw new RuntimeException("Failed to decode to Minecraft Component: " + tag + "; " + partial.message()); ++ }); ++ return converted.getFirst(); + } +} diff --git a/src/main/java/io/papermc/paper/adventure/providers/BossBarImplementationProvider.java b/src/main/java/io/papermc/paper/adventure/providers/BossBarImplementationProvider.java @@ -1718,18 +2135,22 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 return (Component) this.readWithCodecTrusted(NbtOps.INSTANCE, ComponentSerialization.CODEC); } -+ // Paper start ++ // Paper start - adventure + public FriendlyByteBuf writeComponent(final net.kyori.adventure.text.Component component) { -+ // TODO this.adventure$locale -+ return this.writeWithCodec(NbtOps.INSTANCE, ComponentSerialization.CODEC, io.papermc.paper.adventure.PaperAdventure.asVanilla(component)); ++ return this.writeWithCodec(NbtOps.INSTANCE, io.papermc.paper.adventure.PaperAdventure.localizedCodec(this.adventure$locale), component); + } -+ // Paper end + public FriendlyByteBuf writeComponent(Component text) { ++ if (text instanceof io.papermc.paper.adventure.AdventureComponent adv) { ++ return this.writeComponent(adv.adventure$component()); ++ } ++ + // TODO this.adventure$locale return this.writeWithCodec(NbtOps.INSTANCE, ComponentSerialization.CODEC, text); ++ // Paper end - adventure } + public <T extends Enum<T>> T readEnum(Class<T> enumClass) { diff --git a/src/main/java/net/minecraft/network/PacketEncoder.java b/src/main/java/net/minecraft/network/PacketEncoder.java index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 --- a/src/main/java/net/minecraft/network/PacketEncoder.java @@ -1848,6 +2269,62 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 return Component.Serializer.serialize(ichatbasecomponent); } } +diff --git a/src/main/java/net/minecraft/network/chat/ComponentSerialization.java b/src/main/java/net/minecraft/network/chat/ComponentSerialization.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/network/chat/ComponentSerialization.java ++++ b/src/main/java/net/minecraft/network/chat/ComponentSerialization.java +@@ -0,0 +0,0 @@ public class ComponentSerialization { + Codec<Component> codec = RecordCodecBuilder.create((instance) -> { + return instance.group(mapCodec.forGetter(Component::getContents), ExtraCodecs.strictOptionalField(ExtraCodecs.nonEmptyList(selfCodec.listOf()), "extra", List.of()).forGetter(Component::getSiblings), Style.Serializer.MAP_CODEC.forGetter(Component::getStyle)).apply(instance, MutableComponent::new); + }); ++ // Paper start ++ final Codec<Component> origCodec = codec; ++ codec = new Codec<>() { ++ @Override ++ public <T> DataResult<com.mojang.datafixers.util.Pair<Component, T>> decode(final DynamicOps<T> ops, final T input) { ++ return origCodec.decode(ops, input); ++ } ++ ++ @Override ++ public <T> DataResult<T> encode(final Component input, final DynamicOps<T> ops, final T prefix) { ++ if (input instanceof io.papermc.paper.adventure.AdventureComponent adv) { ++ if (adv.deepConvertedIfPresent() != null) { ++ return origCodec.encode(java.util.Objects.requireNonNull(adv.deepConvertedIfPresent()), ops, prefix); ++ } else { ++ // return io.papermc.paper.adventure.PaperAdventure.localizedCodec(locale).encode(adv.adventure$component(), ops, prefix); // TODO ++ return io.papermc.paper.adventure.AdventureCodecs.COMPONENT_CODEC.encode(adv.adventure$component(), ops, prefix); ++ } ++ } ++ return origCodec.encode(input, ops, prefix); ++ } ++ ++ @Override ++ public String toString() { ++ return origCodec.toString() + "[AdventureComponentAware]"; ++ } ++ }; ++ // Paper end + return Codec.either(Codec.either(Codec.STRING, ExtraCodecs.nonEmptyList(selfCodec.listOf())), codec).xmap((either) -> { + return either.map((either2) -> { + return either2.map(Component::literal, ComponentSerialization::createFromList); +@@ -0,0 +0,0 @@ public class ComponentSerialization { + for(MapDecoder<? extends T> mapDecoder : this.codecs) { + DataResult<? extends T> dataResult = mapDecoder.decode(dynamicOps, mapLike); + if (dataResult.result().isPresent()) { +- return dataResult; ++ return (DataResult<T>) dataResult; // Paper - decomp fix + } + } + +@@ -0,0 +0,0 @@ public class ComponentSerialization { + } + + public <S> RecordBuilder<S> encode(T object, DynamicOps<S> dynamicOps, RecordBuilder<S> recordBuilder) { +- MapEncoder<T> mapEncoder = this.encoderGetter.apply(object); ++ MapEncoder<T> mapEncoder = (MapEncoder<T>) this.encoderGetter.apply(object); // Paper - decomp fix + return mapEncoder.encode(object, dynamicOps, recordBuilder); + } + diff --git a/src/main/java/net/minecraft/network/chat/ComponentUtils.java b/src/main/java/net/minecraft/network/chat/ComponentUtils.java index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 --- a/src/main/java/net/minecraft/network/chat/ComponentUtils.java @@ -5006,6 +5483,378 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + "translation_key": "%s" + } +} +diff --git a/src/test/java/io/papermc/paper/adventure/AdventureCodecsTest.java b/src/test/java/io/papermc/paper/adventure/AdventureCodecsTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/adventure/AdventureCodecsTest.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.adventure; ++ ++import com.mojang.datafixers.util.Pair; ++import com.mojang.serialization.Codec; ++import com.mojang.serialization.DataResult; ++import java.io.IOException; ++import java.util.List; ++import java.util.UUID; ++import java.util.function.Function; ++import net.kyori.adventure.key.Key; ++import net.kyori.adventure.nbt.api.BinaryTagHolder; ++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.Style; ++import net.kyori.adventure.text.format.TextColor; ++import net.kyori.adventure.text.format.TextDecoration; ++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.ResourceLocation; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.item.Items; ++import org.apache.commons.lang3.RandomStringUtils; ++import org.bukkit.support.AbstractTestingBase; ++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 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 io.papermc.paper.adventure.PaperAdventure.NBT_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.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; ++ ++class AdventureCodecsTest extends AbstractTestingBase { ++ ++ @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 ++ @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<Component> 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.setHoverName(net.minecraft.network.chat.Component.literal("NAME")); ++ final HoverEvent<HoverEvent.ShowItem> hoverEvent = HoverEvent.showItem(key("minecraft:pumpkin"), 3, BinaryTagHolder.encode(requireNonNull(stack.getTag()), NBT_CODEC)); ++ final Tag result = HOVER_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, hoverEvent).result().orElseThrow(); ++ final DataResult<Pair<net.minecraft.network.chat.HoverEvent, Tag>> 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(), BuiltInRegistries.ITEM.getKey(value.item).toString()); ++ assertEquals(stack.getTag(), value.tag.orElse(null)); ++ } ++ ++ @Test ++ void testShowEntityHoverEvent() { ++ UUID uuid = UUID.randomUUID(); ++ final HoverEvent<HoverEvent.ShowEntity> hoverEvent = showEntity(key("minecraft:wolf"), uuid, text("NAME")); ++ final Tag result = HOVER_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, hoverEvent).result().orElseThrow(); ++ final DataResult<Pair<net.minecraft.network.chat.HoverEvent, Tag>> 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<Pair<net.minecraft.network.chat.Style, Tag>> 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()); ++ } ++ ++ @ParameterizedTest ++ @MethodSource({"testStyles"}) ++ void testDirectRoundTripStyle(final Style style) { ++ testDirectRoundTrip(STYLE_MAP_CODEC.codec(), style); ++ } ++ ++ @ParameterizedTest ++ @MethodSource({"testStyles"}) ++ void testMinecraftRoundTripStyle(final Style style) { ++ testMinecraftRoundTrip(STYLE_MAP_CODEC.codec(), net.minecraft.network.chat.Style.Serializer.CODEC, style); ++ } ++ ++ @ParameterizedTest ++ @MethodSource({"testTexts", "testTranslatables", "testKeybinds", "testScores", ++ "testSelectors", "testBlockNbts", "testEntityNbts", "testStorageNbts"}) ++ void testDirectRoundTripComponent(final Component component) { ++ testDirectRoundTrip(COMPONENT_CODEC, component); ++ } ++ ++ @ParameterizedTest ++ @MethodSource({"testTexts", "testTranslatables", "testKeybinds", "testScores", ++ "testSelectors", "testBlockNbts", "testEntityNbts", "testStorageNbts"}) ++ void testMinecraftRoundTripComponent(final Component component) { ++ testMinecraftRoundTrip(COMPONENT_CODEC, ComponentSerialization.CODEC, component); ++ } ++ ++ @ParameterizedTest ++ @MethodSource({"invalidData"}) ++ void invalidThrows(final Tag input) { ++ assertThrows(RuntimeException.class, () -> { ++ require( ++ COMPONENT_CODEC.decode(NbtOps.INSTANCE, input), ++ msg -> "Failed to decode " + input + ": " + msg ++ ); ++ }); ++ } ++ ++ static <A> void testDirectRoundTrip(final Codec<A> codec, final A adventure) { ++ final Tag encoded = require( ++ codec.encodeStart(NbtOps.INSTANCE, adventure), ++ msg -> "Failed to encode " + adventure + ": " + msg ++ ); ++ final Pair<A, Tag> roundTripResult = require( ++ codec.decode(NbtOps.INSTANCE, encoded), ++ msg -> "Failed to decode " + encoded + ": " + msg ++ ); ++ assertEquals(adventure, roundTripResult.getFirst()); ++ } ++ ++ static <A, M> void testMinecraftRoundTrip(final Codec<A> adventureCodec, final Codec<M> minecraftCodec, final A adventure) { ++ final Tag encoded = require( ++ adventureCodec.encodeStart(NbtOps.INSTANCE, adventure), ++ msg -> "Failed to encode " + adventure + ": " + msg ++ ); ++ final M minecraftResult = require( ++ minecraftCodec.decode(NbtOps.INSTANCE, encoded), ++ msg -> "Failed to decode to Minecraft: " + encoded + "; " + msg ++ ).getFirst(); ++ final Tag minecraftReEncoded = require( ++ minecraftCodec.encodeStart(NbtOps.INSTANCE, minecraftResult), ++ msg -> "Failed to re-encode Minecraft: " + minecraftResult + "; " + msg ++ ); ++ final Pair<A, Tag> roundTripResult = require( ++ adventureCodec.decode(NbtOps.INSTANCE, minecraftReEncoded), ++ msg -> "Failed to decode " + minecraftReEncoded + ": " + msg ++ ); ++ assertEquals(adventure, roundTripResult.getFirst()); ++ } ++ ++ static <R> R require(final DataResult<R> result, final Function<String, String> errorMessage) { ++ return result.get().map(Function.identity(), r -> { ++ throw new RuntimeException(errorMessage.apply(r.message())); ++ }); ++ } ++ ++ static List<Tag> invalidData() { ++ return List.of( ++ IntTag.valueOf(-1), ++ ByteTag.ZERO, ++ new CompoundTag(), ++ new ListTag() ++ ); ++ } ++ ++ static List<Style> testStyles() { ++ return List.of( ++ Style.empty(), ++ style(color(0x0a1ab9)), ++ style(NamedTextColor.LIGHT_PURPLE), ++ style(TextDecoration.BOLD), ++ style(TextDecoration.BOLD.withState(false)), ++ style(TextDecoration.BOLD.withState(TextDecoration.State.NOT_SET)), ++ style() ++ .font(key("kyori", "kittens")) ++ .color(NamedTextColor.RED) ++ .decoration(TextDecoration.BOLD, true) ++ .clickEvent(openUrl("https://github.com")) ++ .build(), ++ style() ++ .hoverEvent(HoverEvent.showEntity(HoverEvent.ShowEntity.showEntity( ++ Key.key(Key.MINECRAFT_NAMESPACE, "pig"), ++ UUID.randomUUID(), ++ Component.text("Dolores", TextColor.color(0x0a1ab9)) ++ ))) ++ .build() ++ ); ++ } ++ ++ static List<Component> testTexts() { ++ return List.of( ++ Component.empty(), ++ text("Hello, world."), ++ text().content("c") ++ .color(NamedTextColor.GOLD) ++ .append(text("o", NamedTextColor.DARK_AQUA)) ++ .append(text("l", NamedTextColor.LIGHT_PURPLE)) ++ .append(text("o", NamedTextColor.DARK_PURPLE)) ++ .append(text("u", NamedTextColor.BLUE)) ++ .append(text("r", NamedTextColor.DARK_GREEN)) ++ .append(text("s", NamedTextColor.RED)) ++ .build(), ++ text().content("This is a test.") ++ .color(NamedTextColor.DARK_PURPLE) ++ .hoverEvent(HoverEvent.showText(text("A test."))) ++ .append(text(" ")) ++ .append(text("A what?", NamedTextColor.DARK_AQUA)) ++ .build(), ++ text().append(text("Hello")).build(), ++ miniMessage().deserialize("<rainbow>|||||||||||||||||||||||<bold>|||||||||||||</bold>|||||||||") ++ ); ++ } ++ ++ static List<Component> testTranslatables() { ++ final String key = "multiplayer.player.left"; ++ final UUID id = UUID.fromString("eb121687-8b1a-4944-bd4d-e0a818d9dfe2"); ++ final String name = "kashike"; ++ final String command = String.format("/msg %s ", name); ++ ++ return List.of( ++ translatable(key), ++ translatable() ++ .key("thisIsA") ++ .fallback("This is a test.") ++ .build(), ++ translatable( ++ key, ++ text().content(name) ++ .clickEvent(suggestCommand(command)) ++ .hoverEvent(showEntity(HoverEvent.ShowEntity.showEntity( ++ key("minecraft", "player"), ++ id, ++ text(name) ++ ))) ++ .build() ++ ).color(NamedTextColor.YELLOW) ++ ); ++ } ++ ++ static List<Component> testKeybinds() { ++ return List.of(keybind("key.jump")); ++ } ++ ++ static List<Component> testScores() { ++ final String name = "abc"; ++ final String objective = "def"; ++ ++ return List.of(score(name, objective)); ++ } ++ ++ static List<Component> testSelectors() { ++ final String selector = "@p"; ++ ++ return List.of( ++ selector(selector), ++ selector(selector, text(',')) ++ ); ++ } ++ ++ static List<Component> testBlockNbts() { ++ return List.of( ++ blockNBT().nbtPath("abc").localPos(1.23d, 2.0d, 3.89d).build(), ++ blockNBT().nbtPath("xyz").absoluteWorldPos(4, 5, 6).interpret(true).build(), ++ blockNBT().nbtPath("eeee").relativeWorldPos(7, 83, 900) ++ .separator(text(';')) ++ .build(), ++ blockNBT().nbtPath("qwert").worldPos( ++ BlockNBTComponent.WorldPos.Coordinate.absolute(12), ++ BlockNBTComponent.WorldPos.Coordinate.relative(3), ++ BlockNBTComponent.WorldPos.Coordinate.absolute(1200) ++ ).build() ++ ); ++ } ++ ++ static List<Component> testEntityNbts() { ++ return List.of( ++ entityNBT().nbtPath("abc").selector("test").build(), ++ entityNBT().nbtPath("abc").selector("test").separator(text(',')).build(), ++ entityNBT().nbtPath("abc").selector("test").interpret(true).build() ++ ); ++ } ++ ++ static List<Component> testStorageNbts() { ++ return List.of( ++ storageNBT().nbtPath("abc").storage(key("doom:apple")).build(), ++ storageNBT().nbtPath("abc").storage(key("doom:apple")).separator(text(", ")).build(), ++ storageNBT().nbtPath("abc").storage(key("doom:apple")).interpret(true).build() ++ ); ++ } ++} diff --git a/src/test/java/io/papermc/paper/adventure/ComponentServicesTest.java b/src/test/java/io/papermc/paper/adventure/ComponentServicesTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 diff --git a/patches/server/Player-Tab-List-and-Title-APIs.patch b/patches/server/Player-Tab-List-and-Title-APIs.patch index b7d7eeb653..9eb3a046b3 100644 --- a/patches/server/Player-Tab-List-and-Title-APIs.patch +++ b/patches/server/Player-Tab-List-and-Title-APIs.patch @@ -9,8 +9,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/net/minecraft/network/FriendlyByteBuf.java +++ b/src/main/java/net/minecraft/network/FriendlyByteBuf.java @@ -0,0 +0,0 @@ public class FriendlyByteBuf extends ByteBuf { - // TODO this.adventure$locale return this.writeWithCodec(NbtOps.INSTANCE, ComponentSerialization.CODEC, text); + // Paper end - adventure } + // Paper start - deprecated Tab List & Title APIs + @Deprecated diff --git a/patches/server/Use-TerminalConsoleAppender-for-console-improvements.patch b/patches/server/Use-TerminalConsoleAppender-for-console-improvements.patch index e51426c9b0..6aa56bb74e 100644 --- a/patches/server/Use-TerminalConsoleAppender-for-console-improvements.patch +++ b/patches/server/Use-TerminalConsoleAppender-for-console-improvements.patch @@ -221,10 +221,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 public static final AttributeKey<Locale> LOCALE_ATTRIBUTE = AttributeKey.valueOf("adventure:locale"); // init after FLATTENER because classloading triggered here might create a logger @Deprecated public static final PlainComponentSerializer PLAIN = PlainComponentSerializer.builder().flattener(FLATTENER).build(); -+ + public static final ANSIComponentSerializer ANSI_SERIALIZER = ANSIComponentSerializer.builder().flattener(FLATTENER).build(); -+ - private static final Codec<CompoundTag, String, IOException, IOException> NBT_CODEC = new Codec<CompoundTag, String, IOException, IOException>() { + static final Codec<CompoundTag, String, IOException, IOException> NBT_CODEC = new Codec<CompoundTag, String, IOException, IOException>() { @Override public @NotNull CompoundTag decode(final @NotNull String encoded) throws IOException { diff --git a/src/main/java/io/papermc/paper/adventure/providers/ComponentLoggerProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/ComponentLoggerProviderImpl.java diff --git a/patches/server/initial-work-on-native-Adventure-codecs.patch b/patches/server/initial-work-on-native-Adventure-codecs.patch deleted file mode 100644 index a8d5a6e3be..0000000000 --- a/patches/server/initial-work-on-native-Adventure-codecs.patch +++ /dev/null @@ -1,303 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic <jake.m.potrebic@gmail.com> -Date: Tue, 5 Dec 2023 16:47:40 -0800 -Subject: [PATCH] initial work on native Adventure codecs - -== AT == -public net.minecraft.network.chat.HoverEvent$ItemStackInfo item -public net.minecraft.network.chat.HoverEvent$ItemStackInfo count -public net.minecraft.network.chat.HoverEvent$ItemStackInfo tag -public net.minecraft.network.chat.contents.TranslatableContents filterAllowedArguments(Ljava/lang/Object;)Lcom/mojang/serialization/DataResult; - -diff --git a/src/main/java/io/papermc/paper/adventure/AdventureCodecs.java b/src/main/java/io/papermc/paper/adventure/AdventureCodecs.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/adventure/AdventureCodecs.java -@@ -0,0 +0,0 @@ -+package io.papermc.paper.adventure; -+ -+import com.mojang.datafixers.util.Either; -+import com.mojang.serialization.Codec; -+import com.mojang.serialization.DataResult; -+import com.mojang.serialization.MapCodec; -+import com.mojang.serialization.codecs.RecordCodecBuilder; -+import java.io.IOException; -+import java.util.Collections; -+import java.util.List; -+import java.util.Optional; -+import java.util.function.Consumer; -+import java.util.function.Function; -+import java.util.function.Predicate; -+import net.kyori.adventure.key.Key; -+import net.kyori.adventure.text.Component; -+import net.kyori.adventure.text.KeybindComponent; -+import net.kyori.adventure.text.ScoreComponent; -+import net.kyori.adventure.text.SelectorComponent; -+import net.kyori.adventure.text.TextComponent; -+import net.kyori.adventure.text.TranslatableComponent; -+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.minecraft.core.UUIDUtil; -+import net.minecraft.core.registries.BuiltInRegistries; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.network.chat.ComponentSerialization; -+import net.minecraft.network.chat.contents.KeybindContents; -+import net.minecraft.network.chat.contents.ScoreContents; -+import net.minecraft.network.chat.contents.TranslatableContents; -+import net.minecraft.util.ExtraCodecs; -+import net.minecraft.util.StringRepresentable; -+import net.minecraft.world.item.Item; -+import net.minecraft.world.item.ItemStack; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.checkerframework.framework.qual.DefaultQualifier; -+import org.intellij.lang.annotations.Subst; -+ -+import static net.kyori.adventure.text.Component.text; -+import static net.minecraft.util.ExtraCodecs.recursive; -+import static net.minecraft.util.ExtraCodecs.strictOptionalField; -+ -+@DefaultQualifier(NonNull.class) -+public final class AdventureCodecs { -+ -+ public static final Codec<Component> COMPONENT_CODEC = recursive("adventure Component", AdventureCodecs::createCodec); -+ -+ private static final Codec<TextColor> TEXT_COLOR_CODEC = Codec.STRING.comapFlatMap(s -> { -+ if (s.startsWith("#")) { -+ @Nullable TextColor value = TextColor.fromHexString(s); -+ return value != null ? DataResult.success(value) : DataResult.error(() -> "Cannot convert " + s + " to adventure TextColor"); -+ } else { -+ final @Nullable NamedTextColor value = NamedTextColor.NAMES.value(s); -+ return value != null ? DataResult.success(value) : DataResult.error(() -> "Cannot convert " + s + " to adventure NamedTextColor"); -+ } -+ }, textColor -> { -+ if (textColor instanceof NamedTextColor named) { -+ return NamedTextColor.NAMES.keyOrThrow(named); -+ } else { -+ return textColor.asHexString(); -+ } -+ }); -+ -+ private static final Codec<Key> KEY_CODEC = Codec.STRING.comapFlatMap(s -> { -+ return Key.parseable(s) ? DataResult.success(Key.key(s)) : DataResult.error(() -> "Cannot convert " + s + " to adventure Key"); -+ }, Key::asString); -+ -+ private static final Codec<ClickEvent.Action> CLICK_EVENT_ACTION_CODEC = Codec.STRING.comapFlatMap(s -> { -+ final ClickEvent.@Nullable Action value = ClickEvent.Action.NAMES.value(s); -+ return value != null ? DataResult.success(value) : DataResult.error(() -> "Cannot convert " + s + " to adventure ClickEvent$Action"); -+ }, ClickEvent.Action.NAMES::keyOrThrow); -+ private static final Codec<ClickEvent> CLICK_EVENT_CODEC = RecordCodecBuilder.create((instance) -> { -+ return instance.group( -+ CLICK_EVENT_ACTION_CODEC.fieldOf("action").forGetter(ClickEvent::action), -+ Codec.STRING.fieldOf("value").forGetter(ClickEvent::value) -+ ).apply(instance, ClickEvent::clickEvent); -+ }); -+ -+ private static Codec<HoverEvent.ShowEntity> showEntityCodec(final Codec<Component> componentCodec) { -+ return RecordCodecBuilder.create((instance) -> { -+ return instance.group( -+ KEY_CODEC.fieldOf("type").forGetter(HoverEvent.ShowEntity::type), -+ UUIDUtil.LENIENT_CODEC.fieldOf("id").forGetter(HoverEvent.ShowEntity::id), -+ strictOptionalField(componentCodec, "name").forGetter(he -> Optional.ofNullable(he.name())) -+ ).apply(instance, (key, uuid, component) -> { -+ return HoverEvent.ShowEntity.showEntity(key, uuid, component.orElse(null)); -+ }); -+ }); -+ } -+ -+ private static Codec<HoverEvent.ShowItem> showItemCodec(final Codec<Component> componentCodec) { -+ return net.minecraft.network.chat.HoverEvent.ItemStackInfo.CODEC.xmap(isi -> { -+ @Subst("key") final String typeKey = BuiltInRegistries.ITEM.getKey(isi.item).toString(); -+ return HoverEvent.ShowItem.showItem(Key.key(typeKey), isi.count, PaperAdventure.asBinaryTagHolder(isi.tag.orElse(null))); -+ }, si -> { -+ final Item itemType = BuiltInRegistries.ITEM.get(PaperAdventure.asVanilla(si.item())); -+ final ItemStack stack; -+ try { -+ final @Nullable CompoundTag tag = si.nbt() != null ? si.nbt().get(PaperAdventure.NBT_CODEC) : null; -+ stack = new ItemStack(BuiltInRegistries.ITEM.wrapAsHolder(itemType), si.count(), Optional.ofNullable(tag)); -+ } catch (IOException e) { -+ throw new RuntimeException(e); -+ } -+ return new net.minecraft.network.chat.HoverEvent.ItemStackInfo(stack); -+ }); -+ } -+ -+ // TODO legacies -+ private static final HoverEventType<HoverEvent.ShowEntity> SHOW_ENTITY_HOVER_EVENT_TYPE = new HoverEventType<>(AdventureCodecs::showEntityCodec, HoverEvent.Action.SHOW_ENTITY, "show_entity"); -+ private static final HoverEventType<HoverEvent.ShowItem> SHOW_ITEM_HOVER_EVENT_TYPE = new HoverEventType<>(AdventureCodecs::showItemCodec, HoverEvent.Action.SHOW_ITEM, "show_item"); -+ private static final HoverEventType<Component> SHOW_TEXT_HOVER_EVENT_TYPE = new HoverEventType<>(Function.identity(), HoverEvent.Action.SHOW_TEXT, "show_text"); -+ private static final Codec<HoverEventType<?>> HOVER_EVENT_TYPE_CODEC = StringRepresentable.fromValues(() -> new HoverEventType<?>[]{ SHOW_ENTITY_HOVER_EVENT_TYPE, SHOW_ITEM_HOVER_EVENT_TYPE, SHOW_TEXT_HOVER_EVENT_TYPE }); -+ -+ private record HoverEventType<V>(Function<Codec<Component>, Codec<HoverEvent<V>>> codec, String id) implements StringRepresentable { -+ private HoverEventType(final Function<Codec<Component>, Codec<V>> contentCodec, final HoverEvent.Action<V> action, final String id) { -+ this(cc -> contentCodec.apply(cc).xmap(v -> { -+ return HoverEvent.hoverEvent(action, v); -+ }, HoverEvent::value), id); -+ } -+ @Override -+ public String getSerializedName() { -+ return this.id; -+ } -+ } -+ -+ private static MapCodec<HoverEvent<?>> hoverEventMapCodec(final Codec<Component> componentCodec) { -+ return HOVER_EVENT_TYPE_CODEC.dispatchMap("action", he -> { -+ if (he.action() == HoverEvent.Action.SHOW_ENTITY) { -+ return SHOW_ENTITY_HOVER_EVENT_TYPE; -+ } else if (he.action() == HoverEvent.Action.SHOW_ITEM) { -+ return SHOW_ITEM_HOVER_EVENT_TYPE; -+ } else if (he.action() == HoverEvent.Action.SHOW_TEXT) { -+ return SHOW_TEXT_HOVER_EVENT_TYPE; -+ } else { -+ throw new IllegalStateException(); -+ } -+ }, het -> het.codec.apply(componentCodec)); -+ } -+ -+ public static MapCodec<Style> styleCodec(final Codec<Component> componentCodec) { -+ return RecordCodecBuilder.mapCodec((instance) -> { -+ return instance.group( -+ strictOptionalField(TEXT_COLOR_CODEC, "color").forGetter(nullableGetter(Style::color)), -+ strictOptionalField(Codec.BOOL, "bold").forGetter(decorationGetter(TextDecoration.BOLD)), -+ strictOptionalField(Codec.BOOL, "italic").forGetter(decorationGetter(TextDecoration.ITALIC)), -+ strictOptionalField(Codec.BOOL, "underlined").forGetter(decorationGetter(TextDecoration.UNDERLINED)), -+ strictOptionalField(Codec.BOOL, "strikethrough").forGetter(decorationGetter(TextDecoration.STRIKETHROUGH)), -+ strictOptionalField(Codec.BOOL, "obfuscated").forGetter(decorationGetter(TextDecoration.OBFUSCATED)), -+ strictOptionalField(CLICK_EVENT_CODEC, "clickEvent").forGetter(nullableGetter(Style::clickEvent)), -+ strictOptionalField(hoverEventMapCodec(componentCodec).codec(), "hoverEvent").forGetter(nullableGetter(Style::hoverEvent)), -+ strictOptionalField(Codec.STRING, "insertion").forGetter(nullableGetter(Style::insertion)), -+ strictOptionalField(KEY_CODEC, "font").forGetter(nullableGetter(Style::font)) -+ ).apply(instance, (textColor, bold, italic, underlined, strikethrough, obfuscated, clickEvent, hoverEvent, insertion, font) -> { -+ return Style.style(builder -> { -+ textColor.ifPresent(builder::color); -+ bold.ifPresent(styleBooleanConsumer(builder, TextDecoration.BOLD)); -+ italic.ifPresent(styleBooleanConsumer(builder, TextDecoration.ITALIC)); -+ underlined.ifPresent(styleBooleanConsumer(builder, TextDecoration.UNDERLINED)); -+ strikethrough.ifPresent(styleBooleanConsumer(builder, TextDecoration.STRIKETHROUGH)); -+ obfuscated.ifPresent(styleBooleanConsumer(builder, TextDecoration.OBFUSCATED)); -+ clickEvent.ifPresent(builder::clickEvent); -+ hoverEvent.ifPresent(builder::hoverEvent); -+ insertion.ifPresent(builder::insertion); -+ font.ifPresent(builder::font); -+ }); -+ }); -+ }); -+ } -+ private static Consumer<Boolean> styleBooleanConsumer(final Style.Builder builder, final TextDecoration decoration) { -+ return b -> builder.decoration(decoration, b); -+ } -+ -+ private static Function<Style, Optional<Boolean>> decorationGetter(final TextDecoration decoration) { -+ return style -> Optional.ofNullable(style.decoration(decoration) == TextDecoration.State.NOT_SET ? null : style.decoration(decoration) == TextDecoration.State.TRUE); -+ } -+ -+ private static <R, T> Function<R, Optional<T>> nullableGetter(final Function<R, @Nullable T> getter) { -+ return style -> Optional.ofNullable(getter.apply(style)); -+ } -+ -+ private static final MapCodec<TextComponent> TEXT_COMPONENT_MAP_CODEC = RecordCodecBuilder.mapCodec((instance) -> { -+ return instance.group(Codec.STRING.fieldOf("text").forGetter(TextComponent::content)).apply(instance, Component::text); -+ }); -+ private static final Codec<Object> PRIMITIVE_ARG_CODEC = ExtraCodecs.validate(ExtraCodecs.JAVA, TranslatableContents::filterAllowedArguments); -+ private static final Codec<Component> ARG_CODEC = Codec.either(PRIMITIVE_ARG_CODEC, COMPONENT_CODEC).xmap((primitiveOrComponent) -> { -+ return primitiveOrComponent.map(o -> text(String.valueOf(o)), Function.identity()); // just toString all primitives (not 100% correct to vanilla spec) -+ }, Either::right); -+ private static final MapCodec<TranslatableComponent> TRANSLATABLE_COMPONENT_MAP_CODEC = RecordCodecBuilder.mapCodec((instance) -> { -+ return instance.group( -+ Codec.STRING.fieldOf("translate").forGetter(TranslatableComponent::key), -+ Codec.STRING.optionalFieldOf("fallback").forGetter(nullableGetter(TranslatableComponent::fallback)), -+ strictOptionalField(ARG_CODEC.listOf(), "with").forGetter(c -> c.args().isEmpty() ? Optional.empty() : Optional.of(c.args())) -+ ).apply(instance, (key, fallback, components) -> { -+ return Component.translatable(key, components.orElse(Collections.emptyList())).fallback(fallback.orElse(null)); -+ }); -+ }); -+ -+ private static final MapCodec<KeybindComponent> KEYBIND_COMPONENT_MAP_CODEC = KeybindContents.CODEC.xmap(k -> Component.keybind(k.getName()), k -> new KeybindContents(k.keybind())); -+ private static final MapCodec<ScoreComponent> SCORE_COMPONENT_MAP_CODEC = ScoreContents.INNER_CODEC.xmap(s -> Component.score(s.getName(), s.getObjective()), s -> new ScoreContents(s.name(), s.objective())); -+ private static final MapCodec<SelectorComponent> SELECTOR_COMPONENT_MAP_CODEC = RecordCodecBuilder.mapCodec((instance) -> { -+ return instance.group( -+ Codec.STRING.fieldOf("selector").forGetter(SelectorComponent::pattern), -+ strictOptionalField(COMPONENT_CODEC, "separator").forGetter(nullableGetter(SelectorComponent::separator)) -+ ).apply(instance, (selector, component) -> Component.selector(selector, component.orElse(null))); -+ }); -+ -+ private record ComponentType<C extends Component>(MapCodec<C> codec, Predicate<Component> test, String id) implements StringRepresentable { -+ @Override -+ public String getSerializedName() { -+ return this.id; -+ } -+ } -+ -+ private static final ComponentType<TextComponent> PLAIN = new ComponentType<>(TEXT_COMPONENT_MAP_CODEC, TextComponent.class::isInstance, "text"); -+ private static final ComponentType<TranslatableComponent> TRANSLATABLE = new ComponentType<>(TRANSLATABLE_COMPONENT_MAP_CODEC, TranslatableComponent.class::isInstance, "translatable"); -+ private static final ComponentType<KeybindComponent> KEYBIND = new ComponentType<>(KEYBIND_COMPONENT_MAP_CODEC, KeybindComponent.class::isInstance, "keybind"); -+ private static final ComponentType<ScoreComponent> SCORE = new ComponentType<>(SCORE_COMPONENT_MAP_CODEC, ScoreComponent.class::isInstance, "score"); -+ private static final ComponentType<SelectorComponent> SELECTOR = new ComponentType<>(SELECTOR_COMPONENT_MAP_CODEC, SelectorComponent.class::isInstance, "selector"); -+ -+ private static Codec<Component> createCodec(final Codec<Component> selfCodec) { -+ final ComponentType<?>[] types = new ComponentType<?>[]{PLAIN, TRANSLATABLE, KEYBIND, SCORE, SELECTOR}; -+ final MapCodec<Component> legacyCodec = ComponentSerialization.createLegacyComponentMatcher(types, ComponentType::codec, component -> { -+ for (final ComponentType<?> type : types) { -+ if (type.test().test(component)) { -+ return type; -+ } -+ } -+ throw new IllegalStateException("Unexpected component type " + component); -+ }, "type"); -+ -+ final Codec<Component> directCodec = RecordCodecBuilder.create((instance) -> { -+ return instance.group( -+ legacyCodec.forGetter(Function.identity()), -+ strictOptionalField(ExtraCodecs.nonEmptyList(selfCodec.listOf()), "extra", List.of()).forGetter(Component::children), -+ styleCodec(selfCodec).forGetter(Component::style) -+ ).apply(instance, (component, children, style) -> { -+ return component.style(style).children(children); -+ }); -+ }); -+ -+ return Codec.either(Codec.either(Codec.STRING, ExtraCodecs.nonEmptyList(selfCodec.listOf())), directCodec).xmap((stringOrListOrComponent) -> { -+ return stringOrListOrComponent.map((stringOrList) -> stringOrList.map(Component::text, AdventureCodecs::createFromList), Function.identity()); -+ }, (text) -> { -+ final @Nullable String string = tryCollapseToString(text); -+ return string != null ? Either.left(Either.left(string)) : Either.right(text); -+ }); -+ } -+ -+ private static @Nullable String tryCollapseToString(final Component component) { -+ if (component instanceof final TextComponent textComponent) { -+ if (component.children().isEmpty() && component.style().isEmpty()) { -+ return textComponent.content(); -+ } -+ } -+ return null; -+ } -+ -+ private static Component createFromList(final List<Component> components) { -+ Component component = components.get(0); -+ for (int i = 1; i < components.size(); i++) { -+ component = component.append(components.get(i)); -+ } -+ return component; -+ } -+ -+ private AdventureCodecs() { -+ } -+} -diff --git a/src/main/java/io/papermc/paper/adventure/PaperAdventure.java b/src/main/java/io/papermc/paper/adventure/PaperAdventure.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/io/papermc/paper/adventure/PaperAdventure.java -+++ b/src/main/java/io/papermc/paper/adventure/PaperAdventure.java -@@ -0,0 +0,0 @@ public final class PaperAdventure { - - public static final ANSIComponentSerializer ANSI_SERIALIZER = ANSIComponentSerializer.builder().flattener(FLATTENER).build(); - -- private static final Codec<CompoundTag, String, IOException, IOException> NBT_CODEC = new Codec<CompoundTag, String, IOException, IOException>() { -+ public static final Codec<CompoundTag, String, IOException, IOException> NBT_CODEC = new Codec<CompoundTag, String, IOException, IOException>() { - @Override - public @NotNull CompoundTag decode(final @NotNull String encoded) throws IOException { - try {