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 components
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>
This commit is contained in:
Riley Park
2021-01-29 17:54:03 +01:00
parent b01c811c2f
commit 66779f5c86
103 changed files with 4975 additions and 392 deletions

View File

@@ -0,0 +1,450 @@
package io.papermc.paper.adventure;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.datafixers.util.Either;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.DynamicOps;
import com.mojang.serialization.JsonOps;
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.Map;
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.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.TranslationArgument;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.DataComponentValue;
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.plain.PlainTextComponentSerializer;
import net.minecraft.commands.arguments.selector.SelectorPattern;
import net.minecraft.core.UUIDUtil;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.Tag;
import net.minecraft.nbt.TagParser;
import net.minecraft.network.RegistryFriendlyByteBuf;
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.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.RegistryOps;
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.Codec.recursive;
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.kyori.adventure.text.TranslationArgument.bool;
import static net.kyori.adventure.text.TranslationArgument.component;
import static net.kyori.adventure.text.TranslationArgument.numeric;
@DefaultQualifier(NonNull.class)
public final class AdventureCodecs {
public static final Codec<Component> COMPONENT_CODEC = recursive("adventure Component", AdventureCodecs::createCodec);
public static final StreamCodec<RegistryFriendlyByteBuf, Component> STREAM_COMPONENT_CODEC = ByteBufCodecs.fromCodecWithRegistriesTrusted(COMPONENT_CODEC);
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),
componentCodec.lenientOptionalFieldOf("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 = isi.item.unwrapKey().orElseThrow().location().toString();
return HoverEvent.ShowItem.showItem(Key.key(typeKey), isi.count, PaperAdventure.asAdventure(isi.getItemStack().getComponentsPatch()));
}, si -> {
final Item itemType = BuiltInRegistries.ITEM.getValue(PaperAdventure.asVanilla(si.item()));
final Map<Key, DataComponentValue> dataComponentsMap = si.dataComponents();
final ItemStack stack = new ItemStack(BuiltInRegistries.ITEM.wrapAsHolder(itemType), si.count(), PaperAdventure.asVanilla(dataComponentsMap));
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", (component, registryOps, codec) -> DataResult.success(component));
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 component, final @Nullable RegistryOps<?> ops, final Codec<Component> componentCodec) {
try {
final CompoundTag tag = TagParser.parseTag(PlainTextComponentSerializer.plainText().serialize(component));
final DynamicOps<JsonElement> dynamicOps = ops != null ? ops.withParent(JsonOps.INSTANCE) : JsonOps.INSTANCE;
final DataResult<Component> entityNameResult = componentCodec.parse(dynamicOps, JsonParser.parseString(tag.getString("name")));
@Subst("key") final String keyString = tag.getString("type");
final UUID entityUUID = UUID.fromString(tag.getString("id"));
return entityNameResult.map(name -> HoverEvent.ShowEntity.showEntity(Key.key(keyString), entityUUID, name));
} catch (final Exception ex) {
return DataResult.error(() -> "Failed to parse tooltip: " + ex.getMessage());
}
}
static DataResult<HoverEvent.ShowItem> legacyDeserializeItem(final Component component, final @Nullable RegistryOps<?> ops, final Codec<Component> componentCodec) {
try {
final CompoundTag tag = TagParser.parseTag(PlainTextComponentSerializer.plainText().serialize(component));
final DynamicOps<Tag> dynamicOps = ops != null ? ops.withParent(NbtOps.INSTANCE) : NbtOps.INSTANCE;
final DataResult<ItemStack> stackResult = ItemStack.CODEC.parse(dynamicOps, tag);
return stackResult.map(stack -> {
@Subst("key:value") final String location = stack.getItemHolder().unwrapKey().orElseThrow().location().toString();
return HoverEvent.ShowItem.showItem(Key.key(location), stack.getCount(), PaperAdventure.asAdventure(stack.getComponentsPatch()));
});
} catch (final CommandSyntaxException ex) {
return DataResult.error(() -> "Failed to parse item tag: " + ex.getMessage());
}
}
@FunctionalInterface
interface LegacyDeserializer<T> {
DataResult<T> apply(Component component, @Nullable RegistryOps<?> ops, Codec<Component> componentCodec);
}
record HoverEventType<V>(Function<Codec<Component>, MapCodec<HoverEvent<V>>> codec, String id, Function<Codec<Component>, MapCodec<HoverEvent<V>>> legacyCodec) implements StringRepresentable {
HoverEventType(final Function<Codec<Component>, Codec<V>> contentCodec, final HoverEvent.Action<V> action, final String id, final LegacyDeserializer<V> legacyDeserializer) {
this(cc -> contentCodec.apply(cc).xmap(v -> HoverEvent.hoverEvent(action, v), HoverEvent::value).fieldOf("contents"),
id,
codec -> (new Codec<HoverEvent<V>>() {
public <D> DataResult<Pair<HoverEvent<V>, D>> decode(final DynamicOps<D> dynamicOps, final D object) {
return codec.decode(dynamicOps, object).flatMap(pair -> {
final DataResult<V> dataResult;
if (dynamicOps instanceof final RegistryOps<D> registryOps) {
dataResult = legacyDeserializer.apply(pair.getFirst(), registryOps, codec);
} else {
dataResult = legacyDeserializer.apply(pair.getFirst(), null, codec);
}
return dataResult.map(value -> Pair.of(HoverEvent.hoverEvent(action, value), pair.getSecond()));
});
}
public <D> DataResult<D> encode(final HoverEvent<V> hoverEvent, final DynamicOps<D> dynamicOps, final D object) {
return DataResult.error(() -> "Can't encode in legacy format");
}
}).fieldOf("value")
);
}
@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.withAlternative(
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()
);
public static final MapCodec<Style> STYLE_MAP_CODEC = mapCodec((instance) -> {
return instance.group(
TEXT_COLOR_CODEC.optionalFieldOf("color").forGetter(nullableGetter(Style::color)),
Codec.BOOL.optionalFieldOf("bold").forGetter(decorationGetter(TextDecoration.BOLD)),
Codec.BOOL.optionalFieldOf("italic").forGetter(decorationGetter(TextDecoration.ITALIC)),
Codec.BOOL.optionalFieldOf("underlined").forGetter(decorationGetter(TextDecoration.UNDERLINED)),
Codec.BOOL.optionalFieldOf("strikethrough").forGetter(decorationGetter(TextDecoration.STRIKETHROUGH)),
Codec.BOOL.optionalFieldOf("obfuscated").forGetter(decorationGetter(TextDecoration.OBFUSCATED)),
CLICK_EVENT_CODEC.optionalFieldOf("clickEvent").forGetter(nullableGetter(Style::clickEvent)),
HOVER_EVENT_CODEC.optionalFieldOf("hoverEvent").forGetter(nullableGetter(Style::hoverEvent)),
Codec.STRING.optionalFieldOf("insertion").forGetter(nullableGetter(Style::insertion)),
KEY_CODEC.optionalFieldOf("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.JAVA.validate(TranslatableContents::filterAllowedArguments);
static final Codec<TranslationArgument> ARG_CODEC = Codec.either(PRIMITIVE_ARG_CODEC, COMPONENT_CODEC).flatXmap((primitiveOrComponent) -> {
return primitiveOrComponent.map(o -> {
final TranslationArgument arg;
if (o instanceof String s) {
arg = component(text(s));
} else if (o instanceof Boolean bool) {
arg = bool(bool);
} else if (o instanceof Number num) {
arg = numeric(num);
} else {
return DataResult.error(() -> o + " is not a valid translation argument primitive");
}
return DataResult.success(arg);
}, component -> DataResult.success(component(component)));
}, translationArgument -> {
if (translationArgument.value() instanceof Number || translationArgument.value() instanceof Boolean) {
return DataResult.success(Either.left(translationArgument.value()));
}
final Component component = translationArgument.asComponent();
final @Nullable String collapsed = tryCollapseToString(component);
if (collapsed != null) {
return DataResult.success(Either.left(collapsed)); // attempt to collapse all text components to strings
}
return DataResult.success(Either.right(component));
});
static final MapCodec<TranslatableComponent> TRANSLATABLE_COMPONENT_MAP_CODEC = mapCodec((instance) -> {
return instance.group(
Codec.STRING.fieldOf("translate").forGetter(TranslatableComponent::key),
Codec.STRING.lenientOptionalFieldOf("fallback").forGetter(nullableGetter(TranslatableComponent::fallback)),
ARG_CODEC.listOf().optionalFieldOf("with").forGetter(c -> c.arguments().isEmpty() ? Optional.empty() : Optional.of(c.arguments()))
).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.name().map(SelectorPattern::pattern, Function.identity()), s.objective()),
s -> new ScoreContents(SelectorPattern.parse(s.name()).<Either<SelectorPattern, String>>map(Either::left).result().orElse(Either.right(s.name())), s.objective())
); // TODO we might want to ask adventure for a nice way we can avoid parsing and flattening the SelectorPattern on every conversion.
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),
COMPONENT_CODEC.optionalFieldOf("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.lenientOptionalFieldOf("interpret", false).forGetter(NBTComponent::interpret),
COMPONENT_CODEC.lenientOptionalFieldOf("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()),
ExtraCodecs.nonEmptyList(selfCodec.listOf()).optionalFieldOf("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);
});
}
public 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() {
}
}

View File

@@ -0,0 +1,88 @@
package io.papermc.paper.adventure;
import java.util.List;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.minecraft.network.chat.ComponentContents;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.network.chat.Style;
import net.minecraft.network.chat.contents.PlainTextContents;
import net.minecraft.util.FormattedCharSequence;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.jetbrains.annotations.Nullable;
public final class AdventureComponent implements net.minecraft.network.chat.Component {
final Component adventure;
private net.minecraft.network.chat.@MonotonicNonNull Component vanilla;
public AdventureComponent(final Component adventure) {
this.adventure = adventure;
}
public net.minecraft.network.chat.Component deepConverted() {
net.minecraft.network.chat.Component vanilla = this.vanilla;
if (vanilla == null) {
vanilla = PaperAdventure.WRAPPER_AWARE_SERIALIZER.serialize(this.adventure);
this.vanilla = vanilla;
}
return vanilla;
}
public net.minecraft.network.chat.@Nullable Component deepConvertedIfPresent() {
return this.vanilla;
}
@Override
public Style getStyle() {
return this.deepConverted().getStyle();
}
@Override
public ComponentContents getContents() {
if (this.adventure instanceof TextComponent) {
return PlainTextContents.create(((TextComponent) this.adventure).content());
} else {
return this.deepConverted().getContents();
}
}
@Override
public String getString() {
return PlainTextComponentSerializer.plainText().serialize(this.adventure);
}
@Override
public List<net.minecraft.network.chat.Component> getSiblings() {
return this.deepConverted().getSiblings();
}
@Override
public MutableComponent plainCopy() {
return this.deepConverted().plainCopy();
}
@Override
public MutableComponent copy() {
return this.deepConverted().copy();
}
@Override
public FormattedCharSequence getVisualOrderText() {
return this.deepConverted().getVisualOrderText();
}
public Component adventure$component() {
return this.adventure;
}
@Override
public int hashCode() {
return this.deepConverted().hashCode();
}
@Override
public boolean equals(final Object obj) {
return this.deepConverted().equals(obj);
}
}

View File

@@ -0,0 +1,85 @@
package io.papermc.paper.adventure;
import com.google.common.collect.Collections2;
import java.util.Set;
import java.util.function.Function;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.bossbar.BossBarImplementation;
import net.kyori.adventure.bossbar.BossBarViewer;
import net.kyori.adventure.text.Component;
import net.minecraft.network.protocol.game.ClientboundBossEventPacket;
import net.minecraft.server.level.ServerBossEvent;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.BossEvent;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.jetbrains.annotations.NotNull;
@SuppressWarnings("UnstableApiUsage")
public final class BossBarImplementationImpl implements BossBar.Listener, BossBarImplementation {
private final BossBar bar;
private ServerBossEvent vanilla;
public BossBarImplementationImpl(final BossBar bar) {
this.bar = bar;
}
public void playerShow(final CraftPlayer player) {
if (this.vanilla == null) {
this.vanilla = new ServerBossEvent(
PaperAdventure.asVanilla(this.bar.name()),
PaperAdventure.asVanilla(this.bar.color()),
PaperAdventure.asVanilla(this.bar.overlay())
);
this.vanilla.adventure = this.bar;
this.bar.addListener(this);
}
this.vanilla.addPlayer(player.getHandle());
}
public void playerHide(final CraftPlayer player) {
if (this.vanilla != null) {
this.vanilla.removePlayer(player.getHandle());
if (this.vanilla.getPlayers().isEmpty()) {
this.bar.removeListener(this);
this.vanilla = null;
}
}
}
@Override
public void bossBarNameChanged(final @NonNull BossBar bar, final @NonNull Component oldName, final @NonNull Component newName) {
this.maybeBroadcast(ClientboundBossEventPacket::createUpdateNamePacket);
}
@Override
public void bossBarProgressChanged(final @NonNull BossBar bar, final float oldProgress, final float newProgress) {
this.maybeBroadcast(ClientboundBossEventPacket::createUpdateProgressPacket);
}
@Override
public void bossBarColorChanged(final @NonNull BossBar bar, final BossBar.@NonNull Color oldColor, final BossBar.@NonNull Color newColor) {
this.maybeBroadcast(ClientboundBossEventPacket::createUpdateStylePacket);
}
@Override
public void bossBarOverlayChanged(final @NonNull BossBar bar, final BossBar.@NonNull Overlay oldOverlay, final BossBar.@NonNull Overlay newOverlay) {
this.maybeBroadcast(ClientboundBossEventPacket::createUpdateStylePacket);
}
@Override
public void bossBarFlagsChanged(final @NonNull BossBar bar, final @NonNull Set<BossBar.Flag> flagsAdded, final @NonNull Set<BossBar.Flag> flagsRemoved) {
this.maybeBroadcast(ClientboundBossEventPacket::createUpdatePropertiesPacket);
}
@Override
public @NotNull Iterable<? extends BossBarViewer> viewers() {
return this.vanilla == null ? Set.of() : Collections2.transform(this.vanilla.getPlayers(), ServerPlayer::getBukkitEntity);
}
private void maybeBroadcast(final Function<BossEvent, ClientboundBossEventPacket> fn) {
if (this.vanilla != null) {
this.vanilla.broadcast(fn);
}
}
}

View File

@@ -0,0 +1,376 @@
package io.papermc.paper.adventure;
import io.papermc.paper.chat.ChatRenderer;
import io.papermc.paper.event.player.AbstractChatEvent;
import io.papermc.paper.event.player.AsyncChatEvent;
import io.papermc.paper.event.player.ChatEvent;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.BitSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.audience.ForwardingAudience;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.minecraft.Optionull;
import net.minecraft.Util;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.ChatType;
import net.minecraft.network.chat.OutgoingChatMessage;
import net.minecraft.network.chat.PlayerChatMessage;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.craftbukkit.util.LazyPlayerSet;
import org.bukkit.craftbukkit.util.Waitable;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerChatEvent;
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.serializer.legacy.LegacyComponentSerializer.legacySection;
@DefaultQualifier(NonNull.class)
public final class ChatProcessor {
static final ResourceKey<ChatType> PAPER_RAW = ResourceKey.create(Registries.CHAT_TYPE, ResourceLocation.fromNamespaceAndPath(ResourceLocation.PAPER_NAMESPACE, "raw"));
static final String DEFAULT_LEGACY_FORMAT = "<%1$s> %2$s"; // copied from PlayerChatEvent/AsyncPlayerChatEvent
final MinecraftServer server;
final ServerPlayer player;
final PlayerChatMessage message;
final boolean async;
final String craftbukkit$originalMessage;
final Component paper$originalMessage;
final OutgoingChatMessage outgoing;
static final int MESSAGE_CHANGED = 1;
static final int FORMAT_CHANGED = 2;
static final int SENDER_CHANGED = 3; // Not used
private final BitSet flags = new BitSet(3);
public ChatProcessor(final MinecraftServer server, final ServerPlayer player, final PlayerChatMessage message, final boolean async) {
this.server = server;
this.player = player;
this.message = message;
this.async = async;
this.craftbukkit$originalMessage = message.unsignedContent() != null ? LegacyComponentSerializer.legacySection().serialize(PaperAdventure.asAdventure(message.unsignedContent())) : message.signedContent();
this.paper$originalMessage = PaperAdventure.asAdventure(this.message.decoratedContent());
this.outgoing = OutgoingChatMessage.create(this.message);
}
@SuppressWarnings("deprecated")
public void process() {
final boolean listenersOnAsyncEvent = canYouHearMe(AsyncPlayerChatEvent.getHandlerList());
final boolean listenersOnSyncEvent = canYouHearMe(PlayerChatEvent.getHandlerList());
if (listenersOnAsyncEvent || listenersOnSyncEvent) {
final CraftPlayer player = this.player.getBukkitEntity();
final AsyncPlayerChatEvent ae = new AsyncPlayerChatEvent(this.async, player, this.craftbukkit$originalMessage, new LazyPlayerSet(this.server));
this.post(ae);
if (listenersOnSyncEvent) {
final PlayerChatEvent se = new PlayerChatEvent(player, ae.getMessage(), ae.getFormat(), ae.getRecipients());
se.setCancelled(ae.isCancelled()); // propagate cancelled state
this.queueIfAsyncOrRunImmediately(new Waitable<Void>() {
@Override
protected Void evaluate() {
ChatProcessor.this.post(se);
return null;
}
});
this.readLegacyModifications(se.getMessage(), se.getFormat(), se.getPlayer());
this.processModern(
this.modernRenderer(se.getFormat()),
this.viewersFromLegacy(se.getRecipients()),
this.modernMessage(se.getMessage()),
se.getPlayer(),
se.isCancelled()
);
} else {
this.readLegacyModifications(ae.getMessage(), ae.getFormat(), ae.getPlayer());
this.processModern(
this.modernRenderer(ae.getFormat()),
this.viewersFromLegacy(ae.getRecipients()),
this.modernMessage(ae.getMessage()),
ae.getPlayer(),
ae.isCancelled()
);
}
} else {
this.processModern(
defaultRenderer(),
new LazyChatAudienceSet(this.server),
this.paper$originalMessage,
this.player.getBukkitEntity(),
false
);
}
}
private ChatRenderer modernRenderer(final String format) {
if (this.flags.get(FORMAT_CHANGED)) {
return legacyRenderer(format);
} else {
return defaultRenderer();
}
}
private Component modernMessage(final String legacyMessage) {
if (this.flags.get(MESSAGE_CHANGED)) {
return legacySection().deserialize(legacyMessage);
} else {
return this.paper$originalMessage;
}
}
private void readLegacyModifications(final String message, final String format, final Player playerSender) {
this.flags.set(MESSAGE_CHANGED, !message.equals(this.craftbukkit$originalMessage));
this.flags.set(FORMAT_CHANGED, !format.equals(DEFAULT_LEGACY_FORMAT));
this.flags.set(SENDER_CHANGED, playerSender != this.player.getBukkitEntity());
}
private void processModern(final ChatRenderer renderer, final Set<Audience> viewers, final Component message, final Player player, final boolean cancelled) {
final PlayerChatMessage.AdventureView signedMessage = this.message.adventureView();
final AsyncChatEvent ae = new AsyncChatEvent(this.async, player, viewers, renderer, message, this.paper$originalMessage, signedMessage);
ae.setCancelled(cancelled); // propagate cancelled state
this.post(ae);
final boolean listenersOnSyncEvent = canYouHearMe(ChatEvent.getHandlerList());
if (listenersOnSyncEvent) {
this.queueIfAsyncOrRunImmediately(new Waitable<Void>() {
@Override
protected Void evaluate() {
final ChatEvent se = new ChatEvent(player, ae.viewers(), ae.renderer(), ae.message(), ChatProcessor.this.paper$originalMessage/*, ae.usePreviewComponent()*/, signedMessage);
se.setCancelled(ae.isCancelled()); // propagate cancelled state
ChatProcessor.this.post(se);
ChatProcessor.this.readModernModifications(se, renderer);
ChatProcessor.this.complete(se);
return null;
}
});
} else {
this.readModernModifications(ae, renderer);
this.complete(ae);
}
}
private void readModernModifications(final AbstractChatEvent chatEvent, final ChatRenderer originalRenderer) {
this.flags.set(MESSAGE_CHANGED, !chatEvent.message().equals(this.paper$originalMessage));
if (originalRenderer != chatEvent.renderer()) { // don't set to false if it hasn't changed
this.flags.set(FORMAT_CHANGED, true);
}
}
private void complete(final AbstractChatEvent event) {
if (event.isCancelled()) {
return;
}
final CraftPlayer player = ((CraftPlayer) event.getPlayer());
final Component displayName = displayName(player);
final Component message = event.message();
final ChatRenderer renderer = event.renderer();
final Set<Audience> viewers = event.viewers();
final ResourceKey<ChatType> chatTypeKey = renderer instanceof ChatRenderer.Default ? ChatType.CHAT : PAPER_RAW;
final ChatType.Bound chatType = ChatType.bind(chatTypeKey, this.player.level().registryAccess(), PaperAdventure.asVanilla(displayName(player)));
OutgoingChat outgoingChat = viewers instanceof LazyChatAudienceSet lazyAudienceSet && lazyAudienceSet.isLazy() ? new ServerOutgoingChat() : new ViewersOutgoingChat();
if (this.flags.get(FORMAT_CHANGED)) {
if (renderer instanceof ChatRenderer.ViewerUnaware unaware) {
outgoingChat.sendFormatChangedViewerUnaware(player, PaperAdventure.asVanilla(unaware.render(player, displayName, message)), viewers, chatType);
} else {
outgoingChat.sendFormatChangedViewerAware(player, displayName, message, renderer, viewers, chatType);
}
} else if (this.flags.get(MESSAGE_CHANGED)) {
if (!(renderer instanceof ChatRenderer.ViewerUnaware unaware)) {
throw new IllegalStateException("BUG: This should be a ViewerUnaware renderer at this point");
}
final Component renderedComponent = chatTypeKey == ChatType.CHAT ? message : unaware.render(player, displayName, message);
outgoingChat.sendMessageChanged(player, PaperAdventure.asVanilla(renderedComponent), viewers, chatType);
} else {
outgoingChat.sendOriginal(player, viewers, chatType);
}
}
interface OutgoingChat {
default void sendFormatChangedViewerUnaware(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set<Audience> viewers, ChatType.Bound chatType) {
this.sendMessageChanged(player, renderedMessage, viewers, chatType);
}
void sendFormatChangedViewerAware(CraftPlayer player, Component displayName, Component message, ChatRenderer renderer, Set<Audience> viewers, ChatType.Bound chatType);
void sendMessageChanged(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set<Audience> viewers, ChatType.Bound chatType);
void sendOriginal(CraftPlayer player, Set<Audience> viewers, ChatType.Bound chatType);
}
final class ServerOutgoingChat implements OutgoingChat {
@Override
public void sendFormatChangedViewerAware(CraftPlayer player, Component displayName, Component message, ChatRenderer renderer, Set<Audience> viewers, ChatType.Bound chatType) {
ChatProcessor.this.server.getPlayerList().broadcastChatMessage(ChatProcessor.this.message, ChatProcessor.this.player, chatType, viewer -> PaperAdventure.asVanilla(renderer.render(player, displayName, message, viewer)));
}
@Override
public void sendMessageChanged(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set<Audience> viewers, ChatType.Bound chatType) {
ChatProcessor.this.server.getPlayerList().broadcastChatMessage(ChatProcessor.this.message.withUnsignedContent(renderedMessage), ChatProcessor.this.player, chatType);
}
@Override
public void sendOriginal(CraftPlayer player, Set<Audience> viewers, ChatType.Bound chatType) {
ChatProcessor.this.server.getPlayerList().broadcastChatMessage(ChatProcessor.this.message, ChatProcessor.this.player, chatType);
}
}
final class ViewersOutgoingChat implements OutgoingChat {
@Override
public void sendFormatChangedViewerAware(CraftPlayer player, Component displayName, Component message, ChatRenderer renderer, Set<Audience> viewers, ChatType.Bound chatType) {
this.broadcastToViewers(viewers, chatType, v -> PaperAdventure.asVanilla(renderer.render(player, displayName, message, v)));
}
@Override
public void sendMessageChanged(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set<Audience> viewers, ChatType.Bound chatType) {
this.broadcastToViewers(viewers, chatType, $ -> renderedMessage);
}
@Override
public void sendOriginal(CraftPlayer player, Set<Audience> viewers, ChatType.Bound chatType) {
this.broadcastToViewers(viewers, chatType, null);
}
private void broadcastToViewers(Collection<Audience> viewers, final ChatType.Bound chatType, final @Nullable Function<Audience, net.minecraft.network.chat.Component> msgFunction) {
for (Audience viewer : viewers) {
if (acceptsNative(viewer)) {
this.sendNative(viewer, chatType, msgFunction);
} else {
final net.minecraft.network.chat.@Nullable Component unsigned = Optionull.map(msgFunction, f -> f.apply(viewer));
final PlayerChatMessage msg = unsigned == null ? ChatProcessor.this.message : ChatProcessor.this.message.withUnsignedContent(unsigned);
viewer.sendMessage(msg.adventureView(), this.adventure(chatType));
}
}
}
private static final Map<String, net.kyori.adventure.chat.ChatType> BUILT_IN_CHAT_TYPES = Util.make(() -> {
final Map<String, net.kyori.adventure.chat.ChatType> map = new HashMap<>();
for (final Field declaredField : net.kyori.adventure.chat.ChatType.class.getDeclaredFields()) {
if (Modifier.isStatic(declaredField.getModifiers()) && declaredField.getType().equals(ChatType.class)) {
try {
final net.kyori.adventure.chat.ChatType type = (net.kyori.adventure.chat.ChatType) declaredField.get(null);
map.put(type.key().asString(), type);
} catch (final ReflectiveOperationException ignore) {
}
}
}
return map;
});
private net.kyori.adventure.chat.ChatType.Bound adventure(ChatType.Bound chatType) {
@Subst("key:value") final String stringKey = Objects.requireNonNull(
chatType.chatType().unwrapKey().orElseThrow().location(),
() -> "No key for '%s' in CHAT_TYPE registry.".formatted(chatType)
).toString();
net.kyori.adventure.chat.@Nullable ChatType adventure = BUILT_IN_CHAT_TYPES.get(stringKey);
if (adventure == null) {
adventure = net.kyori.adventure.chat.ChatType.chatType(Key.key(stringKey));
}
return adventure.bind(
PaperAdventure.asAdventure(chatType.name()),
chatType.targetName().map(PaperAdventure::asAdventure).orElse(null)
);
}
private static boolean acceptsNative(final Audience viewer) {
if (viewer instanceof Player || viewer instanceof ConsoleCommandSender) {
return true;
}
if (viewer instanceof ForwardingAudience.Single single) {
return acceptsNative(single.audience());
}
return false;
}
private void sendNative(final Audience viewer, final ChatType.Bound chatType, final @Nullable Function<Audience, net.minecraft.network.chat.Component> msgFunction) {
if (viewer instanceof ConsoleCommandSender) {
this.sendToServer(chatType, msgFunction);
} else if (viewer instanceof CraftPlayer craftPlayer) {
craftPlayer.getHandle().sendChatMessage(ChatProcessor.this.outgoing, ChatProcessor.this.player.shouldFilterMessageTo(craftPlayer.getHandle()), chatType, Optionull.map(msgFunction, f -> f.apply(viewer)));
} else if (viewer instanceof ForwardingAudience.Single single) {
this.sendNative(single.audience(), chatType, msgFunction);
} else {
throw new IllegalStateException("Should only be a Player or Console or ForwardingAudience.Single pointing to one!");
}
}
private void sendToServer(final ChatType.Bound chatType, final @Nullable Function<Audience, net.minecraft.network.chat.Component> msgFunction) {
final PlayerChatMessage toConsoleMessage = msgFunction == null ? ChatProcessor.this.message : ChatProcessor.this.message.withUnsignedContent(msgFunction.apply(ChatProcessor.this.server.console));
ChatProcessor.this.server.logChatMessage(toConsoleMessage.decoratedContent(), chatType, ChatProcessor.this.server.getPlayerList().verifyChatTrusted(toConsoleMessage) ? null : "Not Secure");
}
}
private Set<Audience> viewersFromLegacy(final Set<Player> recipients) {
if (recipients instanceof LazyPlayerSet lazyPlayerSet && lazyPlayerSet.isLazy()) {
return new LazyChatAudienceSet(this.server);
}
final HashSet<Audience> viewers = new HashSet<>(recipients);
viewers.add(this.server.console);
return viewers;
}
static String legacyDisplayName(final CraftPlayer player) {
return player.getDisplayName();
}
static Component displayName(final CraftPlayer player) {
return player.displayName();
}
private static ChatRenderer.Default defaultRenderer() {
return (ChatRenderer.Default) ChatRenderer.defaultRenderer();
}
private static ChatRenderer legacyRenderer(final String format) {
if (DEFAULT_LEGACY_FORMAT.equals(format)) {
return defaultRenderer();
}
return ChatRenderer.viewerUnaware((player, sourceDisplayName, message) -> legacySection().deserialize(legacyFormat(format, player, legacySection().serialize(message))));
}
static String legacyFormat(final String format, Player player, String message) {
return String.format(format, legacyDisplayName((CraftPlayer) player), message);
}
private void queueIfAsyncOrRunImmediately(final Waitable<Void> waitable) {
if (this.async) {
this.server.processQueue.add(waitable);
} else {
waitable.run();
}
try {
waitable.get();
} catch (final InterruptedException e) {
Thread.currentThread().interrupt(); // tag, you're it
} catch (final ExecutionException e) {
throw new RuntimeException("Exception processing chat", e.getCause());
}
}
private void post(final Event event) {
this.server.server.getPluginManager().callEvent(event);
}
static boolean canYouHearMe(final HandlerList handlers) {
return handlers.getRegisteredListeners().length > 0;
}
}

View File

@@ -0,0 +1,25 @@
package io.papermc.paper.adventure;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.minecraft.server.level.ServerPlayer;
import org.bukkit.ChatColor;
import org.bukkit.craftbukkit.entity.CraftPlayer;
public final class DisplayNames {
private DisplayNames() {
}
public static String getLegacy(final CraftPlayer player) {
return getLegacy(player.getHandle());
}
@SuppressWarnings("deprecation") // Valid suppress due to supporting legacy display name formatting
public static String getLegacy(final ServerPlayer player) {
final String legacy = player.displayName;
if (legacy != null) {
// thank you for being worse than wet socks, Bukkit
return LegacyComponentSerializer.legacySection().serialize(player.adventure$displayName) + ChatColor.getLastColors(player.displayName);
}
return LegacyComponentSerializer.legacySection().serialize(player.adventure$displayName);
}
}

View File

@@ -0,0 +1,55 @@
package io.papermc.paper.adventure;
import io.papermc.paper.event.player.AsyncChatCommandDecorateEvent;
import io.papermc.paper.event.player.AsyncChatDecorateEvent;
import java.util.concurrent.CompletableFuture;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.ChatDecorator;
import net.minecraft.network.chat.Component;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.DefaultQualifier;
@DefaultQualifier(NonNull.class)
public final class ImprovedChatDecorator implements ChatDecorator {
private final MinecraftServer server;
public ImprovedChatDecorator(final MinecraftServer server) {
this.server = server;
}
@Override
public CompletableFuture<Component> decorate(final @Nullable ServerPlayer sender, final Component message) {
return decorate(this.server, sender, null, message);
}
@Override
public CompletableFuture<Component> decorate(final @Nullable ServerPlayer sender, final @Nullable CommandSourceStack commandSourceStack, final Component message) {
return decorate(this.server, sender, commandSourceStack, message);
}
private static CompletableFuture<Component> decorate(final MinecraftServer server, final @Nullable ServerPlayer player, final @Nullable CommandSourceStack commandSourceStack, final Component originalMessage) {
return CompletableFuture.supplyAsync(() -> {
final net.kyori.adventure.text.Component initialResult = PaperAdventure.asAdventure(originalMessage);
final @Nullable CraftPlayer craftPlayer = player == null ? null : player.getBukkitEntity();
final AsyncChatDecorateEvent event;
if (commandSourceStack != null) {
// TODO more command decorate context
event = new AsyncChatCommandDecorateEvent(craftPlayer, initialResult);
} else {
event = new AsyncChatDecorateEvent(craftPlayer, initialResult);
}
if (event.callEvent()) {
return PaperAdventure.asVanilla(event.result());
}
return originalMessage;
}, server.chatExecutor);
}
}

View File

@@ -0,0 +1,26 @@
package io.papermc.paper.adventure;
import java.util.HashSet;
import java.util.Set;
import net.kyori.adventure.audience.Audience;
import net.minecraft.server.MinecraftServer;
import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.util.LazyHashSet;
import org.bukkit.craftbukkit.util.LazyPlayerSet;
import org.bukkit.entity.Player;
final class LazyChatAudienceSet extends LazyHashSet<Audience> {
private final MinecraftServer server;
public LazyChatAudienceSet(final MinecraftServer server) {
this.server = server;
}
@Override
protected Set<Audience> makeReference() {
final Set<Player> playerSet = LazyPlayerSet.makePlayerSet(this.server);
final HashSet<Audience> audiences = new HashSet<>(playerSet);
audiences.add(Bukkit.getConsoleSender());
return audiences;
}
}

View File

@@ -0,0 +1,505 @@
package io.papermc.paper.adventure;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.serialization.JavaOps;
import io.netty.util.AttributeKey;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
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;
import java.util.stream.StreamSupport;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.inventory.Book;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.nbt.api.BinaryTagHolder;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.TranslationArgument;
import net.kyori.adventure.text.event.DataComponentValue;
import net.kyori.adventure.text.event.DataComponentValueConverterRegistry;
import net.kyori.adventure.text.flattener.ComponentFlattener;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.serializer.ComponentSerializer;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.kyori.adventure.translation.GlobalTranslator;
import net.kyori.adventure.translation.TranslationRegistry;
import net.kyori.adventure.translation.Translator;
import net.kyori.adventure.util.Codec;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.locale.Language;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.Tag;
import net.minecraft.nbt.TagParser;
import net.minecraft.network.chat.ComponentUtils;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientboundSoundEntityPacket;
import net.minecraft.network.protocol.game.ClientboundSoundPacket;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.network.Filterable;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.BossEvent;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.WrittenBookContent;
import org.bukkit.command.CommandSender;
import org.bukkit.craftbukkit.CraftRegistry;
import org.bukkit.craftbukkit.command.VanillaCommandWrapper;
import org.bukkit.craftbukkit.entity.CraftEntity;
import org.intellij.lang.annotations.Subst;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static java.util.Objects.requireNonNull;
public final class PaperAdventure {
private static final Pattern LOCALIZATION_PATTERN = Pattern.compile("%(?:(\\d+)\\$)?s");
public static final ComponentFlattener FLATTENER = ComponentFlattener.basic().toBuilder()
.complexMapper(TranslatableComponent.class, (translatable, consumer) -> {
if (!Language.getInstance().has(translatable.key())) {
for (final Translator source : GlobalTranslator.translator().sources()) {
if (source instanceof TranslationRegistry registry && registry.contains(translatable.key())) {
consumer.accept(GlobalTranslator.render(translatable, Locale.US));
return;
}
}
}
final @Nullable String fallback = translatable.fallback();
final @NotNull String translated = Language.getInstance().getOrDefault(translatable.key(), fallback != null ? fallback : translatable.key());
final Matcher matcher = LOCALIZATION_PATTERN.matcher(translated);
final List<TranslationArgument> args = translatable.arguments();
int argPosition = 0;
int lastIdx = 0;
while (matcher.find()) {
// append prior
if (lastIdx < matcher.start()) {
consumer.accept(Component.text(translated.substring(lastIdx, matcher.start())));
}
lastIdx = matcher.end();
final @Nullable String argIdx = matcher.group(1);
// calculate argument position
if (argIdx != null) {
try {
final int idx = Integer.parseInt(argIdx) - 1;
if (idx < args.size()) {
consumer.accept(args.get(idx).asComponent());
}
} catch (final NumberFormatException ex) {
// ignore, drop the format placeholder
}
} else {
final int idx = argPosition++;
if (idx < args.size()) {
consumer.accept(args.get(idx).asComponent());
}
}
}
// append tail
if (lastIdx < translated.length()) {
consumer.accept(Component.text(translated.substring(lastIdx)));
}
})
.build();
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 Codec<Tag, String, CommandSyntaxException, RuntimeException> NBT_CODEC = new Codec<>() {
@Override
public @NotNull Tag decode(final @NotNull String encoded) throws CommandSyntaxException {
return new TagParser(new StringReader(encoded)).readValue();
}
@Override
public @NotNull String encode(final @NotNull Tag decoded) {
return decoded.toString();
}
};
public static final ComponentSerializer<Component, Component, net.minecraft.network.chat.Component> WRAPPER_AWARE_SERIALIZER = new WrapperAwareSerializer(() -> CraftRegistry.getMinecraftRegistry().createSerializationContext(JavaOps.INSTANCE));
private PaperAdventure() {
}
// Key
public static Key asAdventure(final ResourceLocation key) {
return Key.key(key.getNamespace(), key.getPath());
}
public static ResourceLocation asVanilla(final Key key) {
return ResourceLocation.fromNamespaceAndPath(key.namespace(), key.value());
}
public static <T> ResourceKey<T> asVanilla(
final ResourceKey<? extends net.minecraft.core.Registry<T>> registry,
final Key key
) {
return ResourceKey.create(registry, asVanilla(key));
}
public static Key asAdventureKey(final ResourceKey<?> key) {
return asAdventure(key.location());
}
public static @Nullable ResourceLocation asVanillaNullable(final Key key) {
if (key == null) {
return null;
}
return asVanilla(key);
}
public static Holder<SoundEvent> resolveSound(final Key key) {
ResourceLocation id = asVanilla(key);
Optional<Holder.Reference<SoundEvent>> vanilla = BuiltInRegistries.SOUND_EVENT.get(id);
if (vanilla.isPresent()) {
return vanilla.get();
}
// sound is not known so not in the registry but might be used by the client with a resource pack
return Holder.direct(SoundEvent.createVariableRangeEvent(id));
}
// Component
public static @NotNull Component asAdventure(@Nullable final net.minecraft.network.chat.Component component) {
return component == null ? Component.empty() : WRAPPER_AWARE_SERIALIZER.deserialize(component);
}
public static ArrayList<Component> asAdventure(final List<? extends net.minecraft.network.chat.Component> vanillas) {
final ArrayList<Component> adventures = new ArrayList<>(vanillas.size());
for (final net.minecraft.network.chat.Component vanilla : vanillas) {
adventures.add(asAdventure(vanilla));
}
return adventures;
}
public static ArrayList<Component> asAdventureFromJson(final List<String> jsonStrings) {
final ArrayList<Component> adventures = new ArrayList<>(jsonStrings.size());
for (final String json : jsonStrings) {
adventures.add(GsonComponentSerializer.gson().deserialize(json));
}
return adventures;
}
public static List<String> asJson(final List<? extends Component> adventures) {
final List<String> jsons = new ArrayList<>(adventures.size());
for (final Component component : adventures) {
jsons.add(GsonComponentSerializer.gson().serialize(component));
}
return jsons;
}
public static net.minecraft.network.chat.@NotNull Component asVanillaNullToEmpty(final @Nullable Component component) {
if (component == null) return net.minecraft.network.chat.CommonComponents.EMPTY;
return asVanilla(component);
}
@Contract("null -> null; !null -> !null")
public static net.minecraft.network.chat.Component asVanilla(final @Nullable Component component) {
if (component == null) return null;
if (true) return new AdventureComponent(component);
return WRAPPER_AWARE_SERIALIZER.serialize(component);
}
public static List<net.minecraft.network.chat.Component> asVanilla(final List<? extends Component> adventures) {
final List<net.minecraft.network.chat.Component> vanillas = new ArrayList<>(adventures.size());
for (final Component adventure : adventures) {
vanillas.add(asVanilla(adventure));
}
return vanillas;
}
public static String asJsonString(final Component component, final Locale locale) {
return GsonComponentSerializer.gson().serialize(translated(component, locale));
}
public static boolean hasAnyTranslations() {
return StreamSupport.stream(GlobalTranslator.translator().sources().spliterator(), false)
.anyMatch(t -> t.hasAnyTranslations().toBooleanOrElse(true));
}
private static final Map<Locale, com.mojang.serialization.Codec<Component>> LOCALIZED_CODECS = new ConcurrentHashMap<>();
public static com.mojang.serialization.Codec<Component> localizedCodec(final @Nullable Locale l) {
if (l == null) {
return AdventureCodecs.COMPONENT_CODEC;
}
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) {
return PlainTextComponentSerializer.plainText().serialize(translated(component, locale));
}
private static Component translated(final Component component, final Locale locale) {
//noinspection ConstantValue
return GlobalTranslator.render(
component,
// play it safe
locale != null
? locale
: Locale.US
);
}
public static Component resolveWithContext(final @NotNull Component component, final @Nullable CommandSender context, final @Nullable org.bukkit.entity.Entity scoreboardSubject, final boolean bypassPermissions) throws IOException {
final CommandSourceStack css = context != null ? VanillaCommandWrapper.getListener(context) : null;
Boolean previous = null;
if (css != null && bypassPermissions) {
previous = css.bypassSelectorPermissions;
css.bypassSelectorPermissions = true;
}
try {
return asAdventure(ComponentUtils.updateForEntity(css, asVanilla(component), scoreboardSubject == null ? null : ((CraftEntity) scoreboardSubject).getHandle(), 0));
} catch (final CommandSyntaxException e) {
throw new IOException(e);
} finally {
if (css != null && previous != null) {
css.bypassSelectorPermissions = previous;
}
}
}
// BossBar
public static BossEvent.BossBarColor asVanilla(final BossBar.Color color) {
return switch (color) {
case PINK -> BossEvent.BossBarColor.PINK;
case BLUE -> BossEvent.BossBarColor.BLUE;
case RED -> BossEvent.BossBarColor.RED;
case GREEN -> BossEvent.BossBarColor.GREEN;
case YELLOW -> BossEvent.BossBarColor.YELLOW;
case PURPLE -> BossEvent.BossBarColor.PURPLE;
case WHITE -> BossEvent.BossBarColor.WHITE;
};
}
public static BossBar.Color asAdventure(final BossEvent.BossBarColor color) {
return switch (color) {
case PINK -> BossBar.Color.PINK;
case BLUE -> BossBar.Color.BLUE;
case RED -> BossBar.Color.RED;
case GREEN -> BossBar.Color.GREEN;
case YELLOW -> BossBar.Color.YELLOW;
case PURPLE -> BossBar.Color.PURPLE;
case WHITE -> BossBar.Color.WHITE;
};
}
public static BossEvent.BossBarOverlay asVanilla(final BossBar.Overlay overlay) {
return switch (overlay) {
case PROGRESS -> BossEvent.BossBarOverlay.PROGRESS;
case NOTCHED_6 -> BossEvent.BossBarOverlay.NOTCHED_6;
case NOTCHED_10 -> BossEvent.BossBarOverlay.NOTCHED_10;
case NOTCHED_12 -> BossEvent.BossBarOverlay.NOTCHED_12;
case NOTCHED_20 -> BossEvent.BossBarOverlay.NOTCHED_20;
};
}
public static BossBar.Overlay asAdventure(final BossEvent.BossBarOverlay overlay) {
return switch (overlay) {
case PROGRESS -> BossBar.Overlay.PROGRESS;
case NOTCHED_6 -> BossBar.Overlay.NOTCHED_6;
case NOTCHED_10 -> BossBar.Overlay.NOTCHED_10;
case NOTCHED_12 -> BossBar.Overlay.NOTCHED_12;
case NOTCHED_20 -> BossBar.Overlay.NOTCHED_20;
};
}
public static void setFlag(final BossBar bar, final BossBar.Flag flag, final boolean value) {
if (value) {
bar.addFlag(flag);
} else {
bar.removeFlag(flag);
}
}
// Book
public static ItemStack asItemStack(final Book book, final Locale locale) {
final ItemStack item = new ItemStack(net.minecraft.world.item.Items.WRITTEN_BOOK, 1);
item.set(DataComponents.WRITTEN_BOOK_CONTENT, new WrittenBookContent(
Filterable.passThrough(validateField(asPlain(book.title(), locale), WrittenBookContent.TITLE_MAX_LENGTH, "title")),
asPlain(book.author(), locale),
0,
book.pages().stream().map(c -> Filterable.passThrough(PaperAdventure.asVanilla(c))).toList(), // TODO should we validate legnth?
false
));
return item;
}
private static String validateField(final String content, final int length, final String name) {
final int actual = content.length();
if (actual > length) {
throw new IllegalArgumentException("Field '" + name + "' has a maximum length of " + length + " but was passed '" + content + "', which was " + actual + " characters long.");
}
return content;
}
// Sounds
public static SoundSource asVanilla(final Sound.Source source) {
return switch (source) {
case MASTER -> SoundSource.MASTER;
case MUSIC -> SoundSource.MUSIC;
case RECORD -> SoundSource.RECORDS;
case WEATHER -> SoundSource.WEATHER;
case BLOCK -> SoundSource.BLOCKS;
case HOSTILE -> SoundSource.HOSTILE;
case NEUTRAL -> SoundSource.NEUTRAL;
case PLAYER -> SoundSource.PLAYERS;
case AMBIENT -> SoundSource.AMBIENT;
case VOICE -> SoundSource.VOICE;
};
}
public static @Nullable SoundSource asVanillaNullable(final Sound.@Nullable Source source) {
if (source == null) {
return null;
}
return asVanilla(source);
}
public static Packet<?> asSoundPacket(final Sound sound, final double x, final double y, final double z, final long seed, @Nullable BiConsumer<Packet<?>, Float> packetConsumer) {
final ResourceLocation name = asVanilla(sound.name());
final Optional<SoundEvent> soundEvent = BuiltInRegistries.SOUND_EVENT.getOptional(name);
final SoundSource source = asVanilla(sound.source());
final Holder<SoundEvent> soundEventHolder = soundEvent.map(BuiltInRegistries.SOUND_EVENT::wrapAsHolder).orElseGet(() -> Holder.direct(SoundEvent.createVariableRangeEvent(name)));
final Packet<?> packet = new ClientboundSoundPacket(soundEventHolder, source, x, y, z, sound.volume(), sound.pitch(), seed);
if (packetConsumer != null) {
packetConsumer.accept(packet, soundEventHolder.value().getRange(sound.volume()));
}
return packet;
}
public static Packet<?> asSoundPacket(final Sound sound, final Entity emitter, final long seed, @Nullable BiConsumer<Packet<?>, Float> packetConsumer) {
final ResourceLocation name = asVanilla(sound.name());
final Optional<SoundEvent> soundEvent = BuiltInRegistries.SOUND_EVENT.getOptional(name);
final SoundSource source = asVanilla(sound.source());
final Holder<SoundEvent> soundEventHolder = soundEvent.map(BuiltInRegistries.SOUND_EVENT::wrapAsHolder).orElseGet(() -> Holder.direct(SoundEvent.createVariableRangeEvent(name)));
final Packet<?> packet = new ClientboundSoundEntityPacket(soundEventHolder, source, emitter, sound.volume(), sound.pitch(), seed);
if (packetConsumer != null) {
packetConsumer.accept(packet, soundEventHolder.value().getRange(sound.volume()));
}
return packet;
}
// NBT
@SuppressWarnings({"rawtypes", "unchecked"})
public static Map<Key, ? extends DataComponentValue> asAdventure(
final DataComponentPatch patch
) {
if (patch.isEmpty()) {
return Collections.emptyMap();
}
final Map<Key, DataComponentValue> map = new HashMap<>();
for (final Map.Entry<DataComponentType<?>, Optional<?>> entry : patch.entrySet()) {
if (entry.getKey().isTransient()) continue;
@Subst("key:value") final String typeKey = requireNonNull(BuiltInRegistries.DATA_COMPONENT_TYPE.getKey(entry.getKey())).toString();
if (entry.getValue().isEmpty()) {
map.put(Key.key(typeKey), DataComponentValue.removed());
} else {
map.put(Key.key(typeKey), new DataComponentValueImpl(entry.getKey().codec(), entry.getValue().get()));
}
}
return map;
}
@SuppressWarnings({"rawtypes", "unchecked"})
public static DataComponentPatch asVanilla(final Map<? extends Key, ? extends DataComponentValue> map) {
if (map.isEmpty()) {
return DataComponentPatch.EMPTY;
}
final DataComponentPatch.Builder builder = DataComponentPatch.builder();
map.forEach((key, dataComponentValue) -> {
final DataComponentType<?> type = requireNonNull(BuiltInRegistries.DATA_COMPONENT_TYPE.getValue(asVanilla(key)));
if (dataComponentValue instanceof DataComponentValue.Removed) {
builder.remove(type);
return;
}
final DataComponentValueImpl<?> converted = DataComponentValueConverterRegistry.convert(DataComponentValueImpl.class, key, dataComponentValue);
builder.set((DataComponentType) type, (Object) converted.value());
});
return builder.build();
}
public record DataComponentValueImpl<T>(com.mojang.serialization.Codec<T> codec, T value) implements DataComponentValue.TagSerializable {
@Override
public @NotNull BinaryTagHolder asBinaryTag() {
return BinaryTagHolder.encode(this.codec.encodeStart(CraftRegistry.getMinecraftRegistry().createSerializationContext(NbtOps.INSTANCE), this.value).getOrThrow(IllegalArgumentException::new), NBT_CODEC);
}
}
public static @Nullable BinaryTagHolder asBinaryTagHolder(final @Nullable CompoundTag tag) {
if (tag == null) {
return null;
}
return BinaryTagHolder.encode(tag, NBT_CODEC);
}
// Colors
public static @NotNull TextColor asAdventure(final ChatFormatting formatting) {
final Integer color = formatting.getColor();
if (color == null) {
throw new IllegalArgumentException("Not a valid color");
}
return TextColor.color(color);
}
public static @Nullable ChatFormatting asVanilla(final TextColor color) {
return ChatFormatting.getByHexValue(color.value());
}
// Style
public static net.minecraft.network.chat.Style asVanilla(final Style style) {
final RegistryOps<Object> ops = CraftRegistry.getMinecraftRegistry().createSerializationContext(JavaOps.INSTANCE);
final Object encoded = AdventureCodecs.STYLE_MAP_CODEC.codec()
.encodeStart(ops, style).getOrThrow(IllegalStateException::new);
return net.minecraft.network.chat.Style.Serializer.CODEC
.parse(ops, encoded).getOrThrow(IllegalStateException::new);
}
public static Style asAdventure(final net.minecraft.network.chat.Style style) {
final RegistryOps<Object> ops = CraftRegistry.getMinecraftRegistry().createSerializationContext(JavaOps.INSTANCE);
final Object encoded = net.minecraft.network.chat.Style.Serializer.CODEC
.encodeStart(ops, style).getOrThrow(IllegalStateException::new);
return AdventureCodecs.STYLE_MAP_CODEC.codec()
.parse(ops, encoded).getOrThrow(IllegalStateException::new);
}
}

View File

@@ -0,0 +1,43 @@
package io.papermc.paper.adventure;
import com.google.common.base.Suppliers;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.JavaOps;
import java.util.function.Supplier;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.ComponentSerializer;
import net.minecraft.network.chat.ComponentSerialization;
import net.minecraft.resources.RegistryOps;
import org.bukkit.craftbukkit.CraftRegistry;
public final class WrapperAwareSerializer implements ComponentSerializer<Component, Component, net.minecraft.network.chat.Component> {
private final Supplier<RegistryOps<Object>> javaOps;
public WrapperAwareSerializer(final Supplier<RegistryOps<Object>> javaOps) {
this.javaOps = Suppliers.memoize(javaOps::get);
}
@Override
public Component deserialize(final net.minecraft.network.chat.Component input) {
if (input instanceof AdventureComponent) {
return ((AdventureComponent) input).adventure;
}
final RegistryOps<Object> ops = this.javaOps.get();
final Object obj = ComponentSerialization.CODEC.encodeStart(ops, input)
.getOrThrow(s -> new RuntimeException("Failed to encode Minecraft Component: " + input + "; " + s));
final Pair<Component, Object> converted = AdventureCodecs.COMPONENT_CODEC.decode(ops, obj)
.getOrThrow(s -> new RuntimeException("Failed to decode to adventure Component: " + obj + "; " + s));
return converted.getFirst();
}
@Override
public net.minecraft.network.chat.Component serialize(final Component component) {
final RegistryOps<Object> ops = this.javaOps.get();
final Object obj = AdventureCodecs.COMPONENT_CODEC.encodeStart(ops, component)
.getOrThrow(s -> new RuntimeException("Failed to encode adventure Component: " + component + "; " + s));
final Pair<net.minecraft.network.chat.Component, Object> converted = ComponentSerialization.CODEC.decode(ops, obj)
.getOrThrow(s -> new RuntimeException("Failed to decode to Minecraft Component: " + obj + "; " + s));
return converted.getFirst();
}
}

View File

@@ -0,0 +1,14 @@
package io.papermc.paper.adventure.providers;
import io.papermc.paper.adventure.BossBarImplementationImpl;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.bossbar.BossBarImplementation;
import org.jetbrains.annotations.NotNull;
@SuppressWarnings("UnstableApiUsage") // permitted provider
public class BossBarImplementationProvider implements BossBarImplementation.Provider {
@Override
public @NotNull BossBarImplementation create(final @NotNull BossBar bar) {
return new BossBarImplementationImpl(bar);
}
}

View File

@@ -0,0 +1,96 @@
package io.papermc.paper.adventure.providers;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.event.ClickCallback;
import net.kyori.adventure.text.event.ClickEvent;
import org.jetbrains.annotations.NotNull;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
@SuppressWarnings("UnstableApiUsage") // permitted provider
public class ClickCallbackProviderImpl implements ClickCallback.Provider {
public static final CallbackManager CALLBACK_MANAGER = new CallbackManager();
@Override
public @NotNull ClickEvent create(final @NotNull ClickCallback<Audience> callback, final ClickCallback.@NotNull Options options) {
return ClickEvent.runCommand("/paper:callback " + CALLBACK_MANAGER.addCallback(callback, options));
}
public static final class CallbackManager {
private final Map<UUID, StoredCallback> callbacks = new HashMap<>();
private final Queue<StoredCallback> queue = new ConcurrentLinkedQueue<>();
private CallbackManager() {
}
public UUID addCallback(final @NotNull ClickCallback<Audience> callback, final ClickCallback.@NotNull Options options) {
final UUID id = UUID.randomUUID();
this.queue.add(new StoredCallback(callback, options, id));
return id;
}
public void handleQueue(final int currentTick) {
// Evict expired entries
if (currentTick % 100 == 0) {
this.callbacks.values().removeIf(callback -> !callback.valid());
}
// Add entries from queue
StoredCallback callback;
while ((callback = this.queue.poll()) != null) {
this.callbacks.put(callback.id(), callback);
}
}
public void runCallback(final @NotNull Audience audience, final UUID id) {
final StoredCallback callback = this.callbacks.get(id);
if (callback != null && callback.valid()) { //TODO Message if expired/invalid?
callback.takeUse();
callback.callback.accept(audience);
}
}
}
private static final class StoredCallback {
private final long startedAt = System.nanoTime();
private final ClickCallback<Audience> callback;
private final long lifetime;
private final UUID id;
private int remainingUses;
private StoredCallback(final @NotNull ClickCallback<Audience> callback, final ClickCallback.@NotNull Options options, final UUID id) {
this.callback = callback;
this.lifetime = options.lifetime().toNanos();
this.remainingUses = options.uses();
this.id = id;
}
public void takeUse() {
if (this.remainingUses != ClickCallback.UNLIMITED_USES) {
this.remainingUses--;
}
}
public boolean hasRemainingUses() {
return this.remainingUses == ClickCallback.UNLIMITED_USES || this.remainingUses > 0;
}
public boolean expired() {
return System.nanoTime() - this.startedAt >= this.lifetime;
}
public boolean valid() {
return hasRemainingUses() && !expired();
}
public UUID id() {
return this.id;
}
}
}

View File

@@ -0,0 +1,20 @@
package io.papermc.paper.adventure.providers;
import io.papermc.paper.adventure.PaperAdventure;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
import net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider;
import org.jetbrains.annotations.NotNull;
import org.slf4j.LoggerFactory;
@SuppressWarnings("UnstableApiUsage")
public class ComponentLoggerProviderImpl implements ComponentLoggerProvider {
@Override
public @NotNull ComponentLogger logger(@NotNull LoggerHelper helper, @NotNull String name) {
return helper.delegating(LoggerFactory.getLogger(name), this::serialize);
}
private String serialize(final Component message) {
return PaperAdventure.asPlain(message, null);
}
}

View File

@@ -0,0 +1,82 @@
package io.papermc.paper.adventure.providers;
import com.google.gson.JsonElement;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.serialization.DynamicOps;
import com.mojang.serialization.JsonOps;
import io.papermc.paper.adventure.PaperAdventure;
import java.util.List;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.nbt.api.BinaryTagHolder;
import net.kyori.adventure.text.event.DataComponentValue;
import net.kyori.adventure.text.event.DataComponentValueConverterRegistry;
import net.kyori.adventure.text.serializer.gson.GsonDataComponentValue;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.RegistryOps;
import org.bukkit.craftbukkit.CraftRegistry;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.DefaultQualifier;
import static net.kyori.adventure.text.serializer.gson.GsonDataComponentValue.gsonDataComponentValue;
@DefaultQualifier(NonNull.class)
public class DataComponentValueConverterProviderImpl implements DataComponentValueConverterRegistry.Provider {
static final Key ID = Key.key("adventure", "platform/paper");
@Override
public Key id() {
return ID;
}
private static <T> RegistryOps<T> createOps(final DynamicOps<T> delegate) {
return CraftRegistry.getMinecraftRegistry().createSerializationContext(delegate);
}
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public Iterable<DataComponentValueConverterRegistry.Conversion<?, ?>> conversions() {
return List.of(
DataComponentValueConverterRegistry.Conversion.convert(
PaperAdventure.DataComponentValueImpl.class,
GsonDataComponentValue.class,
(key, dataComponentValue) -> gsonDataComponentValue((JsonElement) dataComponentValue.codec().encodeStart(createOps(JsonOps.INSTANCE), dataComponentValue.value()).getOrThrow())
),
DataComponentValueConverterRegistry.Conversion.convert(
GsonDataComponentValue.class,
PaperAdventure.DataComponentValueImpl.class,
(key, dataComponentValue) -> {
final @Nullable DataComponentType<?> type = BuiltInRegistries.DATA_COMPONENT_TYPE.getValue(PaperAdventure.asVanilla(key));
if (type == null) {
throw new IllegalArgumentException("Unknown data component type: " + key);
}
return new PaperAdventure.DataComponentValueImpl(type.codecOrThrow(), type.codecOrThrow().parse(createOps(JsonOps.INSTANCE), dataComponentValue.element()).getOrThrow(IllegalArgumentException::new));
}
),
DataComponentValueConverterRegistry.Conversion.convert(
PaperAdventure.DataComponentValueImpl.class,
DataComponentValue.TagSerializable.class,
(key, dataComponentValue) -> BinaryTagHolder.encode((Tag) dataComponentValue.codec().encodeStart(createOps(NbtOps.INSTANCE), dataComponentValue.value()).getOrThrow(), PaperAdventure.NBT_CODEC)
),
DataComponentValueConverterRegistry.Conversion.convert(
DataComponentValue.TagSerializable.class,
PaperAdventure.DataComponentValueImpl.class,
(key, tagSerializable) -> {
final @Nullable DataComponentType<?> type = BuiltInRegistries.DATA_COMPONENT_TYPE.getValue(PaperAdventure.asVanilla(key));
if (type == null) {
throw new IllegalArgumentException("Unknown data component type: " + key);
}
try {
return new PaperAdventure.DataComponentValueImpl(type.codecOrThrow(), type.codecOrThrow().parse(createOps(NbtOps.INSTANCE), tagSerializable.asBinaryTag().get(PaperAdventure.NBT_CODEC)).getOrThrow(IllegalArgumentException::new));
} catch (final CommandSyntaxException e) {
throw new IllegalArgumentException(e);
}
}
)
);
}
}

View File

@@ -0,0 +1,30 @@
package io.papermc.paper.adventure.providers;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer;
@SuppressWarnings("UnstableApiUsage") // permitted provider
public class GsonComponentSerializerProviderImpl implements GsonComponentSerializer.Provider {
@Override
public @NotNull GsonComponentSerializer gson() {
return GsonComponentSerializer.builder()
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.INSTANCE)
.build();
}
@Override
public @NotNull GsonComponentSerializer gsonLegacy() {
return GsonComponentSerializer.builder()
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.INSTANCE)
.downsampleColors()
.build();
}
@Override
public @NotNull Consumer<GsonComponentSerializer.Builder> builder() {
return builder -> builder.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.INSTANCE);
}
}

View File

@@ -0,0 +1,36 @@
package io.papermc.paper.adventure.providers;
import io.papermc.paper.adventure.PaperAdventure;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer;
@SuppressWarnings("UnstableApiUsage") // permitted provider
public class LegacyComponentSerializerProviderImpl implements LegacyComponentSerializer.Provider {
@Override
public @NotNull LegacyComponentSerializer legacyAmpersand() {
return LegacyComponentSerializer.builder()
.flattener(PaperAdventure.FLATTENER)
.character(LegacyComponentSerializer.AMPERSAND_CHAR)
.hexColors()
.useUnusualXRepeatedCharacterHexFormat()
.build();
}
@Override
public @NotNull LegacyComponentSerializer legacySection() {
return LegacyComponentSerializer.builder()
.flattener(PaperAdventure.FLATTENER)
.character(LegacyComponentSerializer.SECTION_CHAR)
.hexColors()
.useUnusualXRepeatedCharacterHexFormat()
.build();
}
@Override
public @NotNull Consumer<LegacyComponentSerializer.Builder> legacy() {
return builder -> builder.flattener(PaperAdventure.FLATTENER);
}
}

View File

@@ -0,0 +1,20 @@
package io.papermc.paper.adventure.providers;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer;
@SuppressWarnings("UnstableApiUsage") // permitted provider
public class MiniMessageProviderImpl implements MiniMessage.Provider {
@Override
public @NotNull MiniMessage miniMessage() {
return MiniMessage.builder().build();
}
@Override
public @NotNull Consumer<MiniMessage.Builder> builder() {
return builder -> {};
}
}

View File

@@ -0,0 +1,91 @@
package io.papermc.paper.adventure.providers;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import java.io.IOException;
import java.util.UUID;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.nbt.api.BinaryTagHolder;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.serializer.json.LegacyHoverEventSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.kyori.adventure.util.Codec;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag;
import net.minecraft.nbt.TagParser;
import org.intellij.lang.annotations.Subst;
final class NBTLegacyHoverEventSerializer implements LegacyHoverEventSerializer {
public static final NBTLegacyHoverEventSerializer INSTANCE = new NBTLegacyHoverEventSerializer();
private static final Codec<CompoundTag, String, CommandSyntaxException, RuntimeException> SNBT_CODEC = Codec.codec(TagParser::parseTag, Tag::toString);
static final String ITEM_TYPE = "id";
static final String ITEM_COUNT = "Count";
static final String ITEM_TAG = "tag";
static final String ENTITY_NAME = "name";
static final String ENTITY_TYPE = "type";
static final String ENTITY_ID = "id";
NBTLegacyHoverEventSerializer() {
}
@Override
public HoverEvent.ShowItem deserializeShowItem(final Component input) throws IOException {
final String raw = PlainTextComponentSerializer.plainText().serialize(input);
try {
final CompoundTag contents = SNBT_CODEC.decode(raw);
final CompoundTag tag = contents.getCompound(ITEM_TAG);
@Subst("key") final String keyString = contents.getString(ITEM_TYPE);
return HoverEvent.ShowItem.showItem(
Key.key(keyString),
contents.contains(ITEM_COUNT) ? contents.getByte(ITEM_COUNT) : 1,
tag.isEmpty() ? null : BinaryTagHolder.encode(tag, SNBT_CODEC)
);
} catch (final CommandSyntaxException ex) {
throw new IOException(ex);
}
}
@Override
public HoverEvent.ShowEntity deserializeShowEntity(final Component input, final Codec.Decoder<Component, String, ? extends RuntimeException> componentCodec) throws IOException {
final String raw = PlainTextComponentSerializer.plainText().serialize(input);
try {
final CompoundTag contents = SNBT_CODEC.decode(raw);
@Subst("key") final String keyString = contents.getString(ENTITY_TYPE);
return HoverEvent.ShowEntity.showEntity(
Key.key(keyString),
UUID.fromString(contents.getString(ENTITY_ID)),
componentCodec.decode(contents.getString(ENTITY_NAME))
);
} catch (final CommandSyntaxException ex) {
throw new IOException(ex);
}
}
@Override
public Component serializeShowItem(final HoverEvent.ShowItem input) throws IOException {
final CompoundTag tag = new CompoundTag();
tag.putString(ITEM_TYPE, input.item().asString());
tag.putByte(ITEM_COUNT, (byte) input.count());
if (input.nbt() != null) {
try {
tag.put(ITEM_TAG, input.nbt().get(SNBT_CODEC));
} catch (final CommandSyntaxException ex) {
throw new IOException(ex);
}
}
return Component.text(SNBT_CODEC.encode(tag));
}
@Override
public Component serializeShowEntity(final HoverEvent.ShowEntity input, final Codec.Encoder<Component, String, ? extends RuntimeException> componentCodec) {
final CompoundTag tag = new CompoundTag();
tag.putString(ENTITY_ID, input.id().toString());
tag.putString(ENTITY_TYPE, input.type().asString());
if (input.name() != null) {
tag.putString(ENTITY_NAME, componentCodec.encode(input.name()));
}
return Component.text(SNBT_CODEC.encode(tag));
}
}

View File

@@ -0,0 +1,23 @@
package io.papermc.paper.adventure.providers;
import io.papermc.paper.adventure.PaperAdventure;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer;
@SuppressWarnings("UnstableApiUsage") // permitted provider
public class PlainTextComponentSerializerProviderImpl implements PlainTextComponentSerializer.Provider {
@Override
public @NotNull PlainTextComponentSerializer plainTextSimple() {
return PlainTextComponentSerializer.builder()
.flattener(PaperAdventure.FLATTENER)
.build();
}
@Override
public @NotNull Consumer<PlainTextComponentSerializer.Builder> plainText() {
return builder -> builder.flattener(PaperAdventure.FLATTENER);
}
}