mirror of
https://github.com/PaperMC/Paper.git
synced 2024-12-23 15:00:30 +01:00
d0476837a5
and fix tests
395 lines
22 KiB
Diff
395 lines
22 KiB
Diff
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..1a13bf3b79b2d11e4ce6cc46eff65fe81a05a1ad
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/adventure/AdventureCodecs.java
|
|
@@ -0,0 +1,365 @@
|
|
+package io.papermc.paper.adventure;
|
|
+
|
|
+import com.google.common.base.Suppliers;
|
|
+import com.mojang.datafixers.util.Either;
|
|
+import com.mojang.serialization.Codec;
|
|
+import com.mojang.serialization.DataResult;
|
|
+import com.mojang.serialization.DynamicOps;
|
|
+import com.mojang.serialization.MapCodec;
|
|
+import com.mojang.serialization.MapLike;
|
|
+import com.mojang.serialization.RecordBuilder;
|
|
+import com.mojang.serialization.codecs.RecordCodecBuilder;
|
|
+import java.io.IOException;
|
|
+import java.util.Collections;
|
|
+import java.util.List;
|
|
+import java.util.Locale;
|
|
+import java.util.Optional;
|
|
+import java.util.function.Consumer;
|
|
+import java.util.function.Function;
|
|
+import java.util.function.Supplier;
|
|
+import java.util.stream.Stream;
|
|
+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.kyori.adventure.translation.GlobalTranslator;
|
|
+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.strictOptionalField;
|
|
+
|
|
+@DefaultQualifier(NonNull.class)
|
|
+public final class AdventureCodecs {
|
|
+
|
|
+ private static final MapCodec<Component> COMPONENT_MAP_CODEC = new RecursiveMapCodec<>("adventure Component", c -> createCodec(c, false));
|
|
+ public static final Codec<Component> COMPONENT_CODEC = indirectCodec(COMPONENT_MAP_CODEC.codec());
|
|
+ private static final MapCodec<Component> RENDERING_COMPONENT_MAP_CODEC = new RecursiveMapCodec<>("rendering adventure Component", c -> createCodec(c, true));
|
|
+ public static final Codec<Component> RENDERING_COMPONENT_CODEC = indirectCodec(RENDERING_COMPONENT_MAP_CODEC.codec());
|
|
+
|
|
+ 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_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 Codec<Component> argCodec(final Codec<Component> componentCodec) {
|
|
+ return Codec.either(PRIMITIVE_ARG_CODEC, componentCodec).xmap((either) -> {
|
|
+ return either.map((object) -> {
|
|
+ if (object instanceof final Integer integer) {
|
|
+ return text(integer);
|
|
+ } else if (object instanceof final Long l) {
|
|
+ return text(l);
|
|
+ } else if (object instanceof final String s) {
|
|
+ return text(s);
|
|
+ } else if (object instanceof final Boolean bool) {
|
|
+ return text(bool);
|
|
+ } else if (object instanceof final Float f) {
|
|
+ return text(f);
|
|
+ } else if (object instanceof final Double d) {
|
|
+ return text(d);
|
|
+ } else if (object instanceof final Short s) {
|
|
+ return text(s);
|
|
+ } else {
|
|
+ throw new IllegalStateException();
|
|
+ }
|
|
+ }, (text) -> text);
|
|
+ }, Either::right);
|
|
+ }
|
|
+ private static MapCodec<TranslatableComponent> translatableComponentCodec(final Codec<Component> componentCodec) {
|
|
+ return RecordCodecBuilder.mapCodec((instance) -> {
|
|
+ return instance.group(
|
|
+ Codec.STRING.fieldOf("translate").forGetter(TranslatableComponent::key),
|
|
+ Codec.STRING.optionalFieldOf("fallback").forGetter(nullableGetter(TranslatableComponent::fallback)),
|
|
+ strictOptionalField(argCodec(componentCodec).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 MapCodec<TranslatableComponent> renderingTranslatableComponentCodec(final Codec<Component> componentCodec) {
|
|
+ return new MapCodec<>() {
|
|
+ @Override
|
|
+ public <T> Stream<T> keys(final DynamicOps<T> ops) {
|
|
+ return COMPONENT_MAP_CODEC.keys(ops);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public <T> DataResult<TranslatableComponent> decode(final DynamicOps<T> ops, final MapLike<T> input) {
|
|
+ return DataResult.error(() -> "Cannot decode using the rendering translatable component codec");
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public <T> RecordBuilder<T> encode(final TranslatableComponent input, final DynamicOps<T> ops, final RecordBuilder<T> prefix) {
|
|
+ final Component rendered = GlobalTranslator.render(input, Locale.US); // TODO get player's locale somehow
|
|
+ return COMPONENT_MAP_CODEC.encode(rendered, ops, prefix); // all render-able translatables should be gone, safe to use the non-rendering codec
|
|
+ }
|
|
+ };
|
|
+ }
|
|
+ private static final MapCodec<KeybindComponent> KEYBIND_COMPONENT_CODEC = KeybindContents.CODEC.xmap(k -> Component.keybind(k.getName()), k -> new KeybindContents(k.keybind()));
|
|
+ private static final MapCodec<ScoreComponent> SCORE_COMPONENT_CODEC = ScoreContents.INNER_CODEC.xmap(s -> Component.score(s.getName(), s.getObjective()), s -> new ScoreContents(s.name(), s.objective()));
|
|
+ private static MapCodec<SelectorComponent> selectorComponentCodec(final Codec<Component> componentCodec) {
|
|
+ return RecordCodecBuilder.mapCodec((instance) -> {
|
|
+ return instance.group(
|
|
+ Codec.STRING.fieldOf("selector").forGetter(SelectorComponent::pattern),
|
|
+ strictOptionalField(componentCodec, "separator").forGetter(nullableGetter(SelectorComponent::separator))
|
|
+ ).apply(instance, (selector, component) -> Component.selector(selector, component.orElse(null)));
|
|
+ });
|
|
+ }
|
|
+
|
|
+ private record ComponentType<C extends Component>(Function<Codec<Component>, MapCodec<C>> codec, String id) implements StringRepresentable {
|
|
+ @Override
|
|
+ public String getSerializedName() {
|
|
+ return this.id;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private static final ComponentType<TextComponent> PLAIN = new ComponentType<>($ -> TEXT_COMPONENT_CODEC, "text");
|
|
+ private static final ComponentType<TranslatableComponent> TRANSLATABLE = new ComponentType<>(AdventureCodecs::translatableComponentCodec, "translatable");
|
|
+ private static final ComponentType<TranslatableComponent> RENDERING_TRANSLATABLE = new ComponentType<>(AdventureCodecs::renderingTranslatableComponentCodec, "translatable");
|
|
+ private static final ComponentType<KeybindComponent> KEYBIND = new ComponentType<>($ -> KEYBIND_COMPONENT_CODEC, "keybind");
|
|
+ private static final ComponentType<ScoreComponent> SCORE = new ComponentType<>($ -> SCORE_COMPONENT_CODEC, "score");
|
|
+ private static final ComponentType<SelectorComponent> SELECTOR = new ComponentType<>(AdventureCodecs::selectorComponentCodec, "selector");
|
|
+
|
|
+ private static MapCodec<Component> createCodec(final Codec<Component> selfCodec, final boolean renderTranslatables) {
|
|
+ final ComponentType<?>[] types = new ComponentType<?>[]{PLAIN, TRANSLATABLE, KEYBIND, SCORE};
|
|
+ final MapCodec<Component> legacyCodec = ComponentSerialization.createLegacyComponentMatcher(types, ct -> ct.codec().apply(selfCodec), component -> {
|
|
+ if (component instanceof TextComponent) {
|
|
+ return PLAIN;
|
|
+ } else if (component instanceof TranslatableComponent) {
|
|
+ return renderTranslatables ? RENDERING_TRANSLATABLE : TRANSLATABLE;
|
|
+ } else if (component instanceof KeybindComponent) {
|
|
+ return KEYBIND;
|
|
+ } else if (component instanceof ScoreComponent) {
|
|
+ return SCORE;
|
|
+ } else if (component instanceof SelectorComponent) {
|
|
+ return SELECTOR;
|
|
+ } else {
|
|
+ throw new IllegalStateException();
|
|
+ }
|
|
+ }, "type");
|
|
+
|
|
+ return RecordCodecBuilder.mapCodec((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);
|
|
+ });
|
|
+ });
|
|
+ }
|
|
+
|
|
+ private static Codec<Component> indirectCodec(final Codec<Component> selfCodec) {
|
|
+ return Codec.either(Codec.either(Codec.STRING, ExtraCodecs.nonEmptyList(selfCodec.listOf())), selfCodec).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;
|
|
+ }
|
|
+
|
|
+ static class RecursiveMapCodec<T> extends MapCodec<T> {
|
|
+
|
|
+ private final String name;
|
|
+ private final Supplier<MapCodec<T>> wrapped;
|
|
+
|
|
+ RecursiveMapCodec(final String name, final Function<Codec<T>, MapCodec<T>> factory) {
|
|
+ this.name = name;
|
|
+ this.wrapped = Suppliers.memoize(() -> factory.apply(this.codec()));
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public <T1> Stream<T1> keys(final DynamicOps<T1> ops) {
|
|
+ return this.wrapped.get().keys(ops);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public <T1> DataResult<T> decode(final DynamicOps<T1> ops, final MapLike<T1> input) {
|
|
+ return this.wrapped.get().decode(ops, input);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public <T1> RecordBuilder<T1> encode(final T input, final DynamicOps<T1> ops, final RecordBuilder<T1> prefix) {
|
|
+ return this.wrapped.get().encode(input, ops, prefix);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public String toString() {
|
|
+ return "RecursiveMapCodec[" + this.name + "]";
|
|
+ }
|
|
+ }
|
|
+
|
|
+ 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 db54a9c32578defa02fa58dc694c96684a4885ac..4ded541e9f2f19ef244fed290da3983b319f2c39 100644
|
|
--- a/src/main/java/io/papermc/paper/adventure/PaperAdventure.java
|
|
+++ b/src/main/java/io/papermc/paper/adventure/PaperAdventure.java
|
|
@@ -111,7 +111,7 @@ 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 {
|