diff --git a/settings.gradle.kts b/settings.gradle.kts index ebbd66710..392a42f7f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -48,6 +48,7 @@ sequenceOf( "text-serializer-legacy", "text-serializer-plain", "text-serializer-ansi", + "text-serializer-nbt" ).forEach { include("adventure-$it") project(":adventure-$it").projectDir = file(it) diff --git a/text-serializer-commons/src/main/java/net/kyori/adventure/text/serializer/commons/ComponentTreeConstants.java b/text-serializer-commons/src/main/java/net/kyori/adventure/text/serializer/commons/ComponentTreeConstants.java index fb1fb9fc5..b880568be 100644 --- a/text-serializer-commons/src/main/java/net/kyori/adventure/text/serializer/commons/ComponentTreeConstants.java +++ b/text-serializer-commons/src/main/java/net/kyori/adventure/text/serializer/commons/ComponentTreeConstants.java @@ -71,6 +71,8 @@ public final class ComponentTreeConstants { @Deprecated public static final String HOVER_EVENT_VALUE = "value"; @Deprecated + public static final String SHOW_TEXT_TEXT = "text"; + @Deprecated public static final String SHOW_ENTITY_TYPE = "type"; public static final String SHOW_ENTITY_ID = "id"; public static final String SHOW_ENTITY_UUID = "uuid"; diff --git a/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/ShowItemSerializer.java b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/ShowItemSerializer.java index 58fd90125..0025850ea 100644 --- a/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/ShowItemSerializer.java +++ b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/ShowItemSerializer.java @@ -149,7 +149,7 @@ public void write(final JsonWriter out, final HoverEvent.ShowItem value) throws out.name(SHOW_ITEM_COMPONENTS); out.beginObject(); for (final Map.Entry entry : value.dataComponentsAs(GsonDataComponentValue.class).entrySet()) { - final JsonElement el = entry.getValue().element();; + final JsonElement el = entry.getValue().element(); if (el instanceof JsonNull) { // removed out.name(DATA_COMPONENT_REMOVAL_PREFIX + entry.getKey().asString()); out.beginObject().endObject(); diff --git a/text-serializer-nbt/build.gradle.kts b/text-serializer-nbt/build.gradle.kts new file mode 100644 index 000000000..b82e13ce5 --- /dev/null +++ b/text-serializer-nbt/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("adventure.common-conventions") +} + +dependencies { + api(libs.option) + api(projects.adventureApi) + api(projects.adventureNbt) + compileOnlyApi(libs.autoService.annotations) + implementation(projects.adventureTextSerializerCommons) + annotationProcessor(libs.autoService) +} + +applyJarMetadata("net.kyori.adventure.text.serializer.nbt") diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/ClickEventSerializer.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/ClickEventSerializer.java new file mode 100644 index 000000000..36f05b6d0 --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/ClickEventSerializer.java @@ -0,0 +1,140 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import java.io.IOException; +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.BinaryTagTypes; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.nbt.IntBinaryTag; +import net.kyori.adventure.nbt.StringBinaryTag; +import net.kyori.adventure.nbt.api.BinaryTagHolder; +import net.kyori.adventure.text.event.ClickEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.CLICK_EVENT_ACTION; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.CLICK_EVENT_COMMAND; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.CLICK_EVENT_ID; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.CLICK_EVENT_PAGE; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.CLICK_EVENT_PAYLOAD; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.CLICK_EVENT_URL; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.CLICK_EVENT_VALUE; +import static net.kyori.adventure.text.serializer.nbt.NBTSerializerUtils.SNBT_CODEC; +import static net.kyori.adventure.text.serializer.nbt.NBTSerializerUtils.requiredTag; + +final class ClickEventSerializer { + + private ClickEventSerializer() { + } + + static @Nullable ClickEvent deserialize(final @NotNull CompoundBinaryTag compound, final boolean snakeCase) { + final StringBinaryTag actionTag = requiredTag(compound, CLICK_EVENT_ACTION, BinaryTagTypes.STRING); + final ClickEvent.Action action = ClickEvent.Action.NAMES.valueOrThrow(actionTag.value()); + + if (!action.readable()) { + return null; + } + + if (snakeCase) { + switch (action) { + case OPEN_URL: + final StringBinaryTag urlTag = requiredTag(compound, CLICK_EVENT_URL, BinaryTagTypes.STRING); + return ClickEvent.openUrl(urlTag.value()); + case RUN_COMMAND: + case SUGGEST_COMMAND: + final StringBinaryTag commandTag = requiredTag(compound, CLICK_EVENT_COMMAND, BinaryTagTypes.STRING); + final String command = commandTag.value(); + return action == ClickEvent.Action.RUN_COMMAND ? ClickEvent.runCommand(command) : ClickEvent.suggestCommand(command); + case CHANGE_PAGE: + final IntBinaryTag pageTag = requiredTag(compound, CLICK_EVENT_PAGE, BinaryTagTypes.INT); + return ClickEvent.changePage(pageTag.value()); + case COPY_TO_CLIPBOARD: + final StringBinaryTag valueTag = requiredTag(compound, CLICK_EVENT_VALUE, BinaryTagTypes.STRING); + return ClickEvent.copyToClipboard(valueTag.value()); + case CUSTOM: + try { + final StringBinaryTag clickEventIdTag = requiredTag(compound, CLICK_EVENT_ID, BinaryTagTypes.STRING); + final BinaryTag payloadTag = requiredTag(compound, CLICK_EVENT_PAYLOAD); + return ClickEvent.custom(KeySerializer.deserialize(clickEventIdTag), BinaryTagHolder.encode(payloadTag, SNBT_CODEC)); + } catch (final IOException exception) { + throw new RuntimeException("An error occurred while encoding payload tag", exception); + } + default: + // Never called, but needed for proper compilation + throw new IllegalArgumentException("Unknown click event action: " + action); + } + } else { + final StringBinaryTag valueTag = requiredTag(compound, CLICK_EVENT_VALUE, BinaryTagTypes.STRING); + return ClickEvent.clickEvent(action, valueTag.value()); + } + } + + static @Nullable CompoundBinaryTag serialize(final @NotNull ClickEvent event, final boolean snakeCase) { + final CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder() + .putString(CLICK_EVENT_ACTION, ClickEvent.Action.NAMES.keyOrThrow(event.action())); + + if (snakeCase) { + final ClickEvent.Action action = event.action(); + if (!action.readable()) { + return null; + } + + final ClickEvent.Payload payload = event.payload(); + if (payload instanceof ClickEvent.Payload.Text) { + final String payloadFieldName; + switch (action) { + case OPEN_URL: + payloadFieldName = CLICK_EVENT_URL; + break; + case RUN_COMMAND: + case SUGGEST_COMMAND: + payloadFieldName = CLICK_EVENT_COMMAND; + break; + case COPY_TO_CLIPBOARD: + payloadFieldName = CLICK_EVENT_VALUE; + break; + default: + // Never called, but needed for proper compilation + throw new IllegalArgumentException("Unknown click event action: " + action); + } + builder.putString(payloadFieldName, ((ClickEvent.Payload.Text) payload).value()); + } else if (payload instanceof ClickEvent.Payload.Custom) { + try { + final ClickEvent.Payload.Custom castPayload = (ClickEvent.Payload.Custom) payload; + builder.put(CLICK_EVENT_ID, KeySerializer.serialize(castPayload.key())); + builder.put(CLICK_EVENT_PAYLOAD, castPayload.nbt().get(SNBT_CODEC)); + } catch (final IOException exception) { + throw new RuntimeException("An error occurred while decoding a payload tag", exception); + } + } else if (payload instanceof ClickEvent.Payload.Int) { + builder.putInt(CLICK_EVENT_PAGE, ((ClickEvent.Payload.Int) payload).integer()); + } + } else { + builder.putString(CLICK_EVENT_VALUE, event.value()); + } + + return builder.build(); + } +} diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/HoverEventSerializer.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/HoverEventSerializer.java new file mode 100644 index 000000000..59f23b0ff --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/HoverEventSerializer.java @@ -0,0 +1,119 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.HoverEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.HOVER_EVENT_ACTION; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.HOVER_EVENT_CONTENTS; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.HOVER_EVENT_VALUE; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.SHOW_TEXT_TEXT; +import static net.kyori.adventure.text.serializer.nbt.NBTSerializerUtils.requiredTag; + +final class HoverEventSerializer { + + private HoverEventSerializer() { + } + + static @Nullable HoverEvent deserialize(final @NotNull CompoundBinaryTag compound, final boolean snakeCase, + final @NotNull NBTComponentSerializerImpl serializer) { + final String actionString = compound.getString(HOVER_EVENT_ACTION); + final HoverEvent.Action action = HoverEvent.Action.NAMES.valueOrThrow(actionString); + + if (!action.readable()) { + return null; + } + + if (action == HoverEvent.Action.SHOW_TEXT) { + BinaryTag textTag; + + if (snakeCase) { + textTag = compound.get(HOVER_EVENT_VALUE); + if (textTag == null) { + textTag = compound.get(SHOW_TEXT_TEXT); + } + + if (textTag == null) { + throw new IllegalStateException("Could not find a field containing text of the show_text hover event"); + } + } else { + textTag = requiredTag(compound, HOVER_EVENT_CONTENTS); + } + + return HoverEvent.showText(serializer.deserialize(textTag)); + } else if (action == HoverEvent.Action.SHOW_ITEM) { + final BinaryTag contentsTag = snakeCase ? compound : requiredTag(compound, HOVER_EVENT_CONTENTS); + return HoverEvent.showItem(ShowItemSerializer.deserialize(contentsTag, snakeCase, serializer)); + } else if (action == HoverEvent.Action.SHOW_ENTITY) { + final BinaryTag contentsTag = snakeCase ? compound : requiredTag(compound, HOVER_EVENT_CONTENTS); + return HoverEvent.showEntity(ShowEntitySerializer.deserialize(contentsTag, snakeCase, serializer)); + } else { + throw new IllegalArgumentException("Don't know how to deserialize a hoverEvent with action of " + actionString + " from a binary tag"); + } + } + + static @Nullable CompoundBinaryTag serialize(final @NotNull HoverEvent event, final boolean snakeCase, + final @NotNull NBTComponentSerializerImpl serializer) { + final HoverEvent.Action action = event.action(); + if (!action.readable()) { + return null; + } + + final BinaryTag contentsTag; + if (action == HoverEvent.Action.SHOW_TEXT) { + final BinaryTag serializedComponent = serializer.serialize((Component) event.value()); + if (snakeCase) { + final String textFieldName = serializer.options().value(NBTSerializerOptions.EMIT_SHOW_TEXT_HOVER_TEXT_FIELD) ? SHOW_TEXT_TEXT : HOVER_EVENT_VALUE; + contentsTag = CompoundBinaryTag.builder() + .put(textFieldName, serializedComponent) + .build(); + } else { + contentsTag = serializedComponent; + } + } else if (action == HoverEvent.Action.SHOW_ITEM) { + contentsTag = ShowItemSerializer.serialize((HoverEvent.ShowItem) event.value(), snakeCase, serializer); + } else if (action == HoverEvent.Action.SHOW_ENTITY) { + contentsTag = ShowEntitySerializer.serialize((HoverEvent.ShowEntity) event.value(), snakeCase, serializer); + } else { + throw new IllegalArgumentException("Don't know how to serialize " + event + " as a binary tag"); + } + + final CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder() + .putString(HOVER_EVENT_ACTION, HoverEvent.Action.NAMES.keyOrThrow(action)); + + if (snakeCase) { + final CompoundBinaryTag castContentsTag = (CompoundBinaryTag) contentsTag; + castContentsTag.forEach(entry -> builder.put(entry.getKey(), entry.getValue())); + } else { + builder.put(HOVER_EVENT_CONTENTS, contentsTag); + } + + return builder.build(); + } +} diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/KeySerializer.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/KeySerializer.java new file mode 100644 index 000000000..064d52cc5 --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/KeySerializer.java @@ -0,0 +1,42 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.StringBinaryTag; +import org.jetbrains.annotations.NotNull; + +final class KeySerializer { + + private KeySerializer() { + } + + static @NotNull Key deserialize(final @NotNull StringBinaryTag tag) { + return Key.key(tag.value()); + } + + static @NotNull StringBinaryTag serialize(final @NotNull Key key) { + return StringBinaryTag.stringBinaryTag(key.asString()); + } +} diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTComponentSerializer.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTComponentSerializer.java new file mode 100644 index 000000000..4ce379d2d --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTComponentSerializer.java @@ -0,0 +1,149 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import java.util.function.Consumer; +import net.kyori.adventure.builder.AbstractBuilder; +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.serializer.ComponentSerializer; +import net.kyori.adventure.util.PlatformAPI; +import net.kyori.option.OptionState; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * A NBT component serializer. + * + * @since 4.24.0 + * @sinceMinecraft 1.20.3 + */ +public interface NBTComponentSerializer extends ComponentSerializer { + /** + * Deserializes a {@linkplain Style style} from a {@linkplain BinaryTag binary tag}. + * + * @param tag the binary tag + * @return the style + * @since 4.24.0 + */ + @NotNull Style deserializeStyle(final @NotNull CompoundBinaryTag tag); + + /** + * Serializes a {@linkplain Style style} to a {@linkplain BinaryTag binary tag}. + * + * @param style the style + * @return the binary tag + * @since 4.24.0 + */ + @NotNull CompoundBinaryTag serializeStyle(final @NotNull Style style); + + /** + * Gets a component serializer for NBT serialization and deserialization. + * + * @return a NBT component serializer + * @since 4.24.0 + */ + static @NotNull NBTComponentSerializer nbt() { + return NBTComponentSerializerImpl.Instances.INSTANCE; + } + + /** + * Creates a new {@link NBTComponentSerializer.Builder}. + * + * @return a builder + * @since 4.24.0 + */ + static @NotNull Builder builder() { + return new NBTComponentSerializerImpl.BuilderImpl(); + } + + /** + * A builder for {@link NBTComponentSerializer}. + * + * @since 4.24.0 + */ + interface Builder extends AbstractBuilder { + /** + * Set the option state to apply on this serializer. + * + *

This controls how the serializer emits and interprets components.

+ * + * @param flags the flag set to use + * @return this builder + * @see NBTSerializerOptions + * @since 4.24.0 + */ + @NotNull Builder options(final @NotNull OptionState flags); + + /** + * Edit the active set of serializer options. + * + * @param optionEditor the consumer operating on the existing flag set + * @return this builder + * @see NBTSerializerOptions + * @since 4.24.0 + */ + @NotNull Builder editOptions(final @NotNull Consumer optionEditor); + + /** + * Builds the serializer. + * + * @return the built serializer + * @since 4.24.0 + */ + @Override + @NotNull NBTComponentSerializer build(); + } + + /** + * A {@link NBTComponentSerializer} service provider. + * + * @since 4.24.0 + */ + @ApiStatus.Internal + @PlatformAPI + interface Provider { + /** + * Provides a standard {@link NBTComponentSerializer}. + * + * @return a {@link NBTComponentSerializer} + * @since 4.24.0 + */ + @ApiStatus.Internal + @PlatformAPI + @NotNull NBTComponentSerializer nbt(); + + /** + * Completes the building process of {@link Builder}. + * + * @return a {@link Consumer} + * @since 4.24.0 + */ + @ApiStatus.Internal + @PlatformAPI + @NotNull Consumer builder(); + } +} diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTComponentSerializerImpl.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTComponentSerializerImpl.java new file mode 100644 index 000000000..0b72f5193 --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTComponentSerializerImpl.java @@ -0,0 +1,359 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.BinaryTagTypes; +import net.kyori.adventure.nbt.ByteBinaryTag; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.nbt.ListBinaryTag; +import net.kyori.adventure.nbt.StringBinaryTag; +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.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.format.Style; +import net.kyori.adventure.util.Services; +import net.kyori.option.OptionState; +import org.jetbrains.annotations.NotNull; + +import static java.util.Objects.requireNonNull; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.EXTRA; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.KEYBIND; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.NBT; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.NBT_BLOCK; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.NBT_ENTITY; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.NBT_INTERPRET; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.NBT_STORAGE; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.SCORE; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.SCORE_NAME; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.SCORE_OBJECTIVE; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.SELECTOR; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.SEPARATOR; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.TEXT; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.TRANSLATE; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.TRANSLATE_FALLBACK; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.TRANSLATE_WITH; +import static net.kyori.adventure.text.serializer.nbt.NBTSerializerUtils.asBoolean; +import static net.kyori.adventure.text.serializer.nbt.NBTSerializerUtils.optionalTag; +import static net.kyori.adventure.text.serializer.nbt.NBTSerializerUtils.requiredTag; + +final class NBTComponentSerializerImpl implements NBTComponentSerializer { + + private static final Optional SERVICE = Services.service(Provider.class); + private static final Consumer BUILDER = SERVICE + .map(Provider::builder) + .orElse(builder -> { + // NOOP + }); + + @Override + public @NotNull Style deserializeStyle(final @NotNull CompoundBinaryTag tag) { + return StyleSerializer.deserialize(tag, this); + } + + @Override + public @NotNull CompoundBinaryTag serializeStyle(final @NotNull Style style) { + final CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder(); + StyleSerializer.serialize(style, builder, this); + return builder.build(); + } + + // We cannot store these fields in NBTComponentSerializerImpl directly due to class initialisation issues. + static final class Instances { + static final NBTComponentSerializer INSTANCE = SERVICE + .map(Provider::nbt) + .orElseGet(() -> new NBTComponentSerializerImpl(NBTSerializerOptions.schema().emptyState())); + } + + private final OptionState options; + + NBTComponentSerializerImpl(final @NotNull OptionState options) { + this.options = requireNonNull(options, "options"); + if (options.schema() != NBTSerializerOptions.schema()) { + throw new IllegalArgumentException("The specified option state does not use the NBT serializer option schema"); + } + } + + @Override + public @NotNull Component deserialize(final @NotNull BinaryTag input) { + if (input instanceof StringBinaryTag) { + return Component.text(((StringBinaryTag) input).value()); + } else if (input instanceof ListBinaryTag) { + final ListBinaryTag castInput = ((ListBinaryTag) input).unwrapHeterogeneity(); + if (castInput.isEmpty()) { + throw new IllegalArgumentException("The list binary tag representing a component must not be empty"); + } + + Component rootTag = this.deserialize(castInput.get(0)); + for (int index = 1; index < castInput.size(); index++) { + rootTag = rootTag.append(this.deserialize(castInput.get(index))); + } + + return rootTag; + } else if (!(input instanceof CompoundBinaryTag)) { + throw new IllegalArgumentException("The input isn't a compound, string or list binary tag"); + } + + final CompoundBinaryTag compound = (CompoundBinaryTag) input; + final Style style = StyleSerializer.deserialize(compound, this); + + final ListBinaryTag extraTag = optionalTag(compound, EXTRA, BinaryTagTypes.LIST); + final List children; + + if (extraTag == null) { + children = Collections.emptyList(); + } else { + children = new ArrayList<>(); + extraTag.unwrapHeterogeneity().forEach(child -> children.add(this.deserialize(child))); + } + + if (compound.get(TEXT) != null) { + return Component.text() + .content(requiredTag(compound, TEXT, BinaryTagTypes.STRING).value()) + .style(style) + .append(children) + .build(); + } else if (compound.get(TRANSLATE) != null) { + final StringBinaryTag translateTag = requiredTag(compound, TRANSLATE, BinaryTagTypes.STRING); + final ListBinaryTag translateWithTag = compound.getList(TRANSLATE_WITH).unwrapHeterogeneity(); + final StringBinaryTag fallbackTag = optionalTag(compound, TRANSLATE_FALLBACK, BinaryTagTypes.STRING); + + final List arguments = new ArrayList<>(); + translateWithTag.forEach(argumentTag -> arguments.add(TranslationArgumentSerializer.deserialize(argumentTag, this))); + + return Component.translatable() + .key(translateTag.value()) + .fallback(fallbackTag == null ? null : fallbackTag.value()) + .arguments(arguments) + .style(style) + .append(children) + .build(); + } else if (compound.get(KEYBIND) != null) { + return Component.keybind() + .keybind(requiredTag(compound, KEYBIND, BinaryTagTypes.STRING).value()) + .style(style) + .append(children) + .build(); + } else if (compound.get(SCORE) != null) { + final CompoundBinaryTag scoreTag = requiredTag(compound, SCORE, BinaryTagTypes.COMPOUND); + return Component.score() + .name(requiredTag(scoreTag, SCORE_NAME, BinaryTagTypes.STRING).value()) + .objective(requiredTag(scoreTag, SCORE_OBJECTIVE, BinaryTagTypes.STRING).value()) + .style(style) + .append(children) + .build(); + } else if (compound.get(SELECTOR) != null) { + final StringBinaryTag selectorTag = requiredTag(compound, SELECTOR, BinaryTagTypes.STRING); + final BinaryTag separatorTag = compound.get(SEPARATOR); + return Component.selector() + .pattern(selectorTag.value()) + .separator(separatorTag == null ? null : this.deserialize(separatorTag)) + .style(style) + .append(children) + .build(); + } else if (compound.get(NBT) != null) { + final String nbtPath = requiredTag(compound, NBT, BinaryTagTypes.STRING).value(); + + final ByteBinaryTag interpretTag = optionalTag(compound, NBT_INTERPRET, BinaryTagTypes.BYTE); + final boolean interpret = interpretTag != null && asBoolean(interpretTag); + + final BinaryTag separatorTag = compound.get(SEPARATOR); + Component separator = null; + + if (separatorTag != null) { + separator = this.deserialize(separatorTag); + } + + final StringBinaryTag blockTag = optionalTag(compound, NBT_BLOCK, BinaryTagTypes.STRING); + final StringBinaryTag entityTag = optionalTag(compound, NBT_ENTITY, BinaryTagTypes.STRING); + final StringBinaryTag storageTag = optionalTag(compound, NBT_STORAGE, BinaryTagTypes.STRING); + + if (blockTag != null) { + return Component.blockNBT() + .nbtPath(nbtPath) + .interpret(interpret) + .separator(separator) + .pos(BlockNBTComponent.Pos.fromString(blockTag.value())) + .style(style) + .append(children) + .build(); + } else if (entityTag != null) { + return Component.entityNBT() + .nbtPath(nbtPath) + .interpret(interpret) + .separator(separator) + .selector(entityTag.value()) + .style(style) + .append(children) + .build(); + } else if (storageTag != null) { + return Component.storageNBT() + .nbtPath(nbtPath) + .interpret(interpret) + .separator(separator) + .storage(KeySerializer.deserialize(storageTag)) + .style(style) + .append(children) + .build(); + } else { + throw notSureHowToDeserialize(input); + } + } else { + throw notSureHowToDeserialize(input); + } + } + + @Override + public @NotNull BinaryTag serialize(final @NotNull Component component) { + if (component instanceof TextComponent && !component.hasStyling() && component.children().isEmpty()) { + return StringBinaryTag.stringBinaryTag(((TextComponent) component).content()); + } + + final CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder(); + + if (component instanceof TextComponent) { + builder.putString(TEXT, ((TextComponent) component).content()); + } else if (component instanceof TranslatableComponent) { + final TranslatableComponent translatable = (TranslatableComponent) component; + builder.putString(TRANSLATE, translatable.key()); + + final String fallback = translatable.fallback(); + if (fallback != null) { + builder.putString(TRANSLATE_FALLBACK, fallback); + } + + final List arguments = translatable.arguments(); + if (!arguments.isEmpty()) { + final ListBinaryTag.Builder translateWithTagBuilder = ListBinaryTag.heterogeneousListBinaryTag(); + arguments.forEach(argument -> translateWithTagBuilder.add(TranslationArgumentSerializer.serialize(argument, this))); + builder.put(TRANSLATE_WITH, translateWithTagBuilder.build().wrapHeterogeneity()); + } + } else if (component instanceof KeybindComponent) { + builder.putString(KEYBIND, ((KeybindComponent) component).keybind()); + } else if (component instanceof ScoreComponent) { + final ScoreComponent score = (ScoreComponent) component; + + final CompoundBinaryTag.Builder scoreTagBuilder = CompoundBinaryTag.builder() + .putString(SCORE_NAME, score.name()) + .putString(SCORE_OBJECTIVE, score.objective()); + + builder.put(SCORE, scoreTagBuilder.build()); + } else if (component instanceof SelectorComponent) { + final SelectorComponent selector = (SelectorComponent) component; + builder.putString(SELECTOR, selector.pattern()); + + final Component separator = selector.separator(); + if (separator != null) { + builder.put(SEPARATOR, this.serialize(separator)); + } + } else if (component instanceof NBTComponent) { + final NBTComponent nbt = (NBTComponent) component; + builder.putString(NBT, nbt.nbtPath()); + + if (nbt.interpret()) { + builder.putBoolean(NBT_INTERPRET, true); + } + + final Component separator = nbt.separator(); + if (separator != null) { + builder.put(SEPARATOR, this.serialize(separator)); + } + + if (nbt instanceof BlockNBTComponent) { + builder.putString(NBT_BLOCK, ((BlockNBTComponent) nbt).pos().asString()); + } else if (nbt instanceof EntityNBTComponent) { + builder.putString(NBT_ENTITY, ((EntityNBTComponent) nbt).selector()); + } else if (nbt instanceof StorageNBTComponent) { + builder.put(NBT_STORAGE, KeySerializer.serialize(((StorageNBTComponent) nbt).storage())); + } else { + throw notSureHowToSerialize(component); + } + } else { + throw notSureHowToSerialize(component); + } + + final List children = component.children(); + if (!children.isEmpty()) { + final ListBinaryTag.Builder extraTagBuilder = ListBinaryTag.heterogeneousListBinaryTag(); + children.forEach(child -> extraTagBuilder.add(this.serialize(child))); + builder.put(EXTRA, extraTagBuilder.build().wrapHeterogeneity()); + } + + StyleSerializer.serialize(component.style(), builder, this); + return builder.build(); + } + + @NotNull OptionState options() { + return this.options; + } + + private static @NotNull IllegalArgumentException notSureHowToDeserialize(final @NotNull BinaryTag tag) { + return new IllegalArgumentException("Don't know how to turn " + tag + " into a Component"); + } + + private static @NotNull IllegalArgumentException notSureHowToSerialize(final @NotNull Component component) { + return new IllegalArgumentException("Don't know how to serialize " + component + " as a Component"); + } + + static final class BuilderImpl implements NBTComponentSerializer.Builder { + + private OptionState flags = NBTSerializerOptions.schema().emptyState(); + + BuilderImpl() { + BUILDER.accept(this); // let service provider touch the builder before anybody else touches it + } + + @Override + public @NotNull Builder options(final @NotNull OptionState flags) { + this.flags = requireNonNull(flags, "flags"); + return this; + } + + @Override + public @NotNull Builder editOptions(final @NotNull Consumer optionEditor) { + final OptionState.Builder builder = NBTSerializerOptions.schema().stateBuilder().values(this.flags); + requireNonNull(optionEditor, "optionEditor").accept(builder); + this.flags = builder.build(); + return this; + } + + @Override + public @NotNull NBTComponentSerializer build() { + return new NBTComponentSerializerImpl(this.flags); + } + } +} diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTDataComponentValue.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTDataComponentValue.java new file mode 100644 index 000000000..2b9398cf1 --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTDataComponentValue.java @@ -0,0 +1,66 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.EndBinaryTag; +import net.kyori.adventure.text.event.DataComponentValue; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import static java.util.Objects.requireNonNull; + +/** + * An {@link DataComponentValue} implementation that holds a {@linkplain BinaryTag binary tag}. + * + *

This holder is exposed to allow conversions to/from NBT data holders.

+ * + * @since 4.24.0 + * @sinceMinecraft 1.20.3 + */ +@ApiStatus.NonExtendable +public interface NBTDataComponentValue extends DataComponentValue { + /** + * The contained element. + * + * @return the contained element + * @since 4.24.0 + */ + @NotNull BinaryTag binaryTag(); + + /** + * Create a box for item data that can be understood by the NBT serializer. + * + * @param data the item data to hold + * @return a newly created item data holder instance + * @since 4.24.0 + */ + static @NotNull NBTDataComponentValue nbtDataComponentValue(final @NotNull BinaryTag data) { + if (data instanceof EndBinaryTag) { + return NBTDataComponentValueImpl.RemovedNBTComponentValueImpl.INSTANCE; + } else { + return new NBTDataComponentValueImpl(requireNonNull(data, "data")); + } + } +} diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTDataComponentValueImpl.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTDataComponentValueImpl.java new file mode 100644 index 000000000..b842b1587 --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTDataComponentValueImpl.java @@ -0,0 +1,65 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import java.util.Objects; +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.EndBinaryTag; +import net.kyori.adventure.text.event.DataComponentValue; +import org.jetbrains.annotations.NotNull; + +class NBTDataComponentValueImpl implements NBTDataComponentValue { + + private final BinaryTag binaryTag; + + NBTDataComponentValueImpl(final @NotNull BinaryTag binaryTag) { + this.binaryTag = binaryTag; + } + + @Override + public @NotNull BinaryTag binaryTag() { + return this.binaryTag; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof NBTDataComponentValueImpl)) return false; + final NBTDataComponentValueImpl that = (NBTDataComponentValueImpl) o; + return Objects.equals(this.binaryTag, that.binaryTag); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.binaryTag); + } + + static final class RemovedNBTComponentValueImpl extends NBTDataComponentValueImpl implements DataComponentValue.Removed { + static final RemovedNBTComponentValueImpl INSTANCE = new RemovedNBTComponentValueImpl(); + + RemovedNBTComponentValueImpl() { + super(EndBinaryTag.endBinaryTag()); + } + } +} diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTSerializerOptions.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTSerializerOptions.java new file mode 100644 index 000000000..e6457edbf --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTSerializerOptions.java @@ -0,0 +1,270 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import net.kyori.option.Option; +import net.kyori.option.OptionSchema; +import net.kyori.option.OptionState; +import org.jetbrains.annotations.NotNull; + +/** + * Options that can apply to {@linkplain NBTComponentSerializer NBT component serializers}. + * + *

See serializer documentation for specific details on which flags are supported.

+ * + * @since 4.24.0 + * @sinceMinecraft 1.20.3 + */ +public final class NBTSerializerOptions { + + /** + * How to emit shadow colour data. + * + * @since 4.24.0 + */ + public static final Option SHADOW_COLOR_MODE; + + /** + * Control how hover event values should be emitted. + * + * @since 4.24.0 + */ + public static final Option EMIT_HOVER_EVENT_TYPE; + + /** + * Control how click event values should be emitted. + * + * @since 4.24.0 + */ + public static final Option EMIT_CLICK_EVENT_TYPE; + + /** + * Whether to emit the default hover event item stack quantity of {@code 1}. + * + *

When enabled, this matches Vanilla as of 1.20.5.

+ * + * @since 4.24.0 + */ + public static final Option EMIT_DEFAULT_ITEM_HOVER_QUANTITY; + + /** + * How to emit show item hovers in {@code hoverEvent} (camelCase) fields. + * + * @since 4.24.0 + */ + public static final Option SHOW_ITEM_HOVER_DATA_MODE; + + /** + * Whether to emit {@code text} field instead of {@code value} field in {@code show_item} + * hover events specified in {@code hover_event} (snake_case) fields. + * + * @since 4.24.0 + */ + public static final Option EMIT_SHOW_TEXT_HOVER_TEXT_FIELD; + + private static final OptionSchema SCHEMA; + private static final OptionState.Versioned BY_DATA_VERSION; + + private static final int VERSION_23W40A = 3679; // 1.20.3 snapshot, initial version with NBT component serialization + private static final int VERSION_24W09A = 3819; // 1.20.5 snapshot + private static final int VERSION_24W10A = 3821; // 1.20.5 snapshot + private static final int VERSION_24W44A = 4174; // 1.21.4 snapshot + private static final int VERSION_25W02A = 4298; // 1.21.5 snapshot + private static final int VERSION_25W03A = 4304; // 1.21.5 snapshot + + static { + final OptionSchema.Mutable schema = OptionSchema.emptySchema(); + SHADOW_COLOR_MODE = schema.enumOption(key("emit/shadow_color"), ShadowColorEmitMode.class, ShadowColorEmitMode.EMIT_INTEGER); + EMIT_HOVER_EVENT_TYPE = schema.enumOption(key("emit/hover_value_mode"), HoverEventValueMode.class, HoverEventValueMode.SNAKE_CASE); + EMIT_CLICK_EVENT_TYPE = schema.enumOption(key("emit/click_value_mode"), ClickEventValueMode.class, ClickEventValueMode.SNAKE_CASE); + EMIT_DEFAULT_ITEM_HOVER_QUANTITY = schema.booleanOption(key("emit/default_item_hover_quantity"), true); + SHOW_ITEM_HOVER_DATA_MODE = schema.enumOption(key("emit/show_item_hover_data"), ShowItemHoverDataMode.class, ShowItemHoverDataMode.EMIT_EITHER); + EMIT_SHOW_TEXT_HOVER_TEXT_FIELD = schema.booleanOption(key("emit/show_text_hover_text_field"), false); + SCHEMA = schema.frozenView(); + + BY_DATA_VERSION = SCHEMA.versionedStateBuilder() + .version( + VERSION_23W40A, + builder -> builder.value(SHADOW_COLOR_MODE, ShadowColorEmitMode.NONE) + .value(EMIT_HOVER_EVENT_TYPE, HoverEventValueMode.CAMEL_CASE) + .value(EMIT_CLICK_EVENT_TYPE, ClickEventValueMode.CAMEL_CASE) + .value(EMIT_DEFAULT_ITEM_HOVER_QUANTITY, false) + .value(SHOW_ITEM_HOVER_DATA_MODE, ShowItemHoverDataMode.EMIT_LEGACY_NBT) + .value(EMIT_SHOW_TEXT_HOVER_TEXT_FIELD, false) + ) + .version( + VERSION_24W09A, + builder -> builder.value(SHOW_ITEM_HOVER_DATA_MODE, ShowItemHoverDataMode.EMIT_DATA_COMPONENTS) + ) + .version( + VERSION_24W10A, + builder -> builder.value(EMIT_DEFAULT_ITEM_HOVER_QUANTITY, true) + ) + .version( + VERSION_24W44A, + builder -> builder.value(SHADOW_COLOR_MODE, ShadowColorEmitMode.EMIT_ARRAY) + ) + .version( + VERSION_25W02A, + builder -> builder.value(EMIT_HOVER_EVENT_TYPE, HoverEventValueMode.SNAKE_CASE) + .value(EMIT_CLICK_EVENT_TYPE, ClickEventValueMode.SNAKE_CASE) + .value(EMIT_SHOW_TEXT_HOVER_TEXT_FIELD, true) + ) + .version( + VERSION_25W03A, + builder -> builder.value(EMIT_SHOW_TEXT_HOVER_TEXT_FIELD, false) + ) + .build(); + } + + private NBTSerializerOptions() { + } + + private static String key(final String value) { + return "adventure:nbt/" + value; + } + + /** + * A schema of available options. + * + * @return the schema of known NBT serializer options + * @since 4.24.0 + */ + public static @NotNull OptionSchema schema() { + return SCHEMA; + } + + /** + * NBT serializer options delineated by world data version. + * + * @return the versioned option state + * @since 4.24.0 + */ + public static OptionState.@NotNull Versioned byDataVersion() { + return BY_DATA_VERSION; + } + + /** + * Configure how to emit hover event values. + * + * @since 4.24.0 + */ + public enum HoverEventValueMode { + /** + * Only emit the 1.21.5+ hover events using the {@code hover_event} field. + * + * @since 4.24.0 + */ + SNAKE_CASE, + /** + * Only emit the 1.16+ hover events using the {@code hoverEvent} field. + * + * @since 4.24.0 + */ + CAMEL_CASE, + /** + * Include both camel and snake case hover event fields, for maximum compatibility. + * + * @since 4.24.0 + */ + BOTH + } + + /** + * Configure how to emit click event values. + * + * @since 4.24.0 + */ + public enum ClickEventValueMode { + /** + * Only emit the 1.21.5+ click events using the {@code click_event} field. + * + * @since 4.24.0 + */ + SNAKE_CASE, + /** + * Only emit the pre-1.21.5 click events using the {@code clickEvent} field. + * + * @since 4.24.0 + */ + CAMEL_CASE, + /** + * Include both camel and snake case click event fields, for maximum compatibility. + * + * @since 4.24.0 + */ + BOTH, + } + + /** + * How text shadow colors should be emitted. + * + * @since 4.24.0 + * @sinceMinecraft 1.21.4 + */ + public enum ShadowColorEmitMode { + /** + * Do not emit shadow colours. + */ + NONE, + /** + * Emit as a single packed integer value containing, in order, ARGB bytes. + * + * @since 4.24.0 + */ + EMIT_INTEGER, + /** + * Emit a colour as 4-element float array of the RGBA components of the colour. + * + * @since 4.24.0 + */ + EMIT_ARRAY + } + + /** + * Configure how to emit show item hovers in {@code hoverEvent} (camelCase) fields. + * + * @since 4.24.0 + */ + public enum ShowItemHoverDataMode { + /** + * Only emit the pre-1.20.5 item NBT. + * + * @since 4.24.0 + */ + EMIT_LEGACY_NBT, + /** + * Only emit modern data components. + * + * @since 4.24.0 + */ + EMIT_DATA_COMPONENTS, + /** + * Emit whichever of legacy or modern data the item has. + * + * @since 4.24.0 + */ + EMIT_EITHER, + } +} diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTSerializerUtils.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTSerializerUtils.java new file mode 100644 index 000000000..68fb6cb0a --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTSerializerUtils.java @@ -0,0 +1,93 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import java.io.IOException; +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.BinaryTagType; +import net.kyori.adventure.nbt.ByteBinaryTag; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.nbt.NumberBinaryTag; +import net.kyori.adventure.nbt.TagStringIO; +import net.kyori.adventure.util.Codec; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +final class NBTSerializerUtils { + + static final TagStringIO SNBT_IO = TagStringIO.tagStringIO(); + static final Codec SNBT_CODEC = Codec.codec(SNBT_IO::asTag, SNBT_IO::asString); + + private NBTSerializerUtils() { + } + + static @NotNull BinaryTag requiredTag(final @NotNull CompoundBinaryTag compound, final @NotNull String name) { + final BinaryTag tag = compound.get(name); + if (tag == null) { + throw noSuchField(name); + } + return tag; + } + + static @NotNull B requiredTag(final @NotNull CompoundBinaryTag compound, + final @NotNull String name, final @NotNull BinaryTagType tagType) { + final B tag = optionalTag(compound, name, tagType); + if (tag == null) { + throw noSuchField(name); + } + return tag; + } + + static @Nullable B optionalTag(final @NotNull CompoundBinaryTag compound, + final @NotNull String name, final @NotNull BinaryTagType tagType) { + final BinaryTag tag = compound.get(name); + if (tag == null) { + return null; + } + + final BinaryTagType actualTagType = tag.type(); + if (actualTagType != tagType) { + throw new IllegalArgumentException( + "A type of the tag is different than expected." + + " Expected: " + tagType.getClass().getSimpleName() + + " Actual: " + actualTagType.getClass().getSimpleName() + ); + } + + return (B) tag; + } + + static boolean asBoolean(final @NotNull NumberBinaryTag tag) { + // != 0 might look weird, but it is what vanilla does + return tag.byteValue() != 0; + } + + static @NotNull ByteBinaryTag asTag(final boolean value) { + return value ? ByteBinaryTag.ONE : ByteBinaryTag.ZERO; + } + + private static @NotNull IllegalArgumentException noSuchField(final @NotNull String name) { + return new IllegalArgumentException("The specified compound tag does not contain a field with name of \"" + name + "\""); + } +} diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/ShadowColorSerializer.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/ShadowColorSerializer.java new file mode 100644 index 000000000..1049ac0bb --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/ShadowColorSerializer.java @@ -0,0 +1,84 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.BinaryTagTypes; +import net.kyori.adventure.nbt.FloatBinaryTag; +import net.kyori.adventure.nbt.IntBinaryTag; +import net.kyori.adventure.nbt.ListBinaryTag; +import net.kyori.adventure.text.format.ShadowColor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +final class ShadowColorSerializer { + + private ShadowColorSerializer() { + } + + static @NotNull ShadowColor deserialize(final @NotNull BinaryTag tag) { + if (tag instanceof IntBinaryTag) { + final IntBinaryTag castTag = (IntBinaryTag) tag; + return ShadowColor.shadowColor(castTag.value()); + } else if (tag instanceof ListBinaryTag) { + final ListBinaryTag castTag = (ListBinaryTag) tag; + return ShadowColor.shadowColor( + shadowColorComponent(castTag, 0), + shadowColorComponent(castTag, 1), + shadowColorComponent(castTag, 2), + shadowColorComponent(castTag, 3) + ); + } else { + throw new IllegalArgumentException("The binary tag representing the shadow color is of an invalid type"); + } + } + + static @Nullable BinaryTag serialize(final @NotNull ShadowColor color, final @NotNull NBTComponentSerializerImpl serializer) { + final NBTSerializerOptions.ShadowColorEmitMode emitMode = serializer.options().value(NBTSerializerOptions.SHADOW_COLOR_MODE); + switch (emitMode) { + case NONE: + return null; + case EMIT_INTEGER: + return IntBinaryTag.intBinaryTag(color.value()); + case EMIT_ARRAY: + final ListBinaryTag.Builder builder = ListBinaryTag.builder(BinaryTagTypes.FLOAT); + addShadowColorComponent(builder, color.red()); + addShadowColorComponent(builder, color.green()); + addShadowColorComponent(builder, color.blue()); + addShadowColorComponent(builder, color.alpha()); + return builder.build(); + default: + // Never called, but needed for proper compilation + throw new IllegalArgumentException("Unknown shadow color emit mode: " + emitMode); + } + } + + private static int shadowColorComponent(final @NotNull ListBinaryTag tag, final int index) { + return (int) (tag.getFloat(index) * 0xff); + } + + private static void addShadowColorComponent(final ListBinaryTag.@NotNull Builder builder, final int element) { + builder.add(FloatBinaryTag.floatBinaryTag((float) element / 0xff)); + } +} diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/ShowEntitySerializer.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/ShowEntitySerializer.java new file mode 100644 index 000000000..dbbcacad3 --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/ShowEntitySerializer.java @@ -0,0 +1,108 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import java.io.IOException; +import java.util.UUID; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.BinaryTagTypes; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.HoverEvent; +import org.jetbrains.annotations.NotNull; + +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.SHOW_ENTITY_ID; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.SHOW_ENTITY_NAME; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.SHOW_ENTITY_TYPE; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.SHOW_ENTITY_UUID; +import static net.kyori.adventure.text.serializer.nbt.NBTSerializerUtils.SNBT_IO; +import static net.kyori.adventure.text.serializer.nbt.NBTSerializerUtils.requiredTag; + +final class ShowEntitySerializer { + + private ShowEntitySerializer() { + } + + static HoverEvent.@NotNull ShowEntity deserialize(final @NotNull BinaryTag tag, final boolean snakeCase, + final @NotNull NBTComponentSerializerImpl serializer) { + try { + return deserializeModern((CompoundBinaryTag) tag, snakeCase, serializer); + } catch (final Exception exception) { + if (snakeCase) { + throw notSureHowToDeserialize(tag); + } else { + return deserializeLegacy(tag, serializer); + } + } + } + + static @NotNull CompoundBinaryTag serialize(final HoverEvent.@NotNull ShowEntity showEntity, final boolean snakeCase, + final @NotNull NBTComponentSerializerImpl serializer) { + final CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder() + .put(snakeCase ? SHOW_ENTITY_ID : SHOW_ENTITY_TYPE, KeySerializer.serialize(showEntity.type())) + .put(snakeCase ? SHOW_ENTITY_UUID : SHOW_ENTITY_ID, UUIDSerializer.serialize(showEntity.id())); + + final Component entityName = showEntity.name(); + if (entityName != null) { + builder.put(SHOW_ENTITY_NAME, serializer.serialize(entityName)); + } + + return builder.build(); + } + + private static HoverEvent.@NotNull ShowEntity deserializeModern(final @NotNull CompoundBinaryTag compound, final boolean snakeCase, + final @NotNull NBTComponentSerializerImpl serializer) { + final Key entityType = KeySerializer.deserialize(requiredTag(compound, snakeCase ? SHOW_ENTITY_ID : SHOW_ENTITY_TYPE, BinaryTagTypes.STRING)); + final BinaryTag entityIdTag = requiredTag(compound, snakeCase ? SHOW_ENTITY_UUID : SHOW_ENTITY_ID); + final BinaryTag entityNameTag = compound.get(SHOW_ENTITY_NAME); + + final UUID entityId = UUIDSerializer.deserialize(entityIdTag); + if (entityNameTag == null) { + return HoverEvent.ShowEntity.showEntity(entityType, entityId); + } else { + return HoverEvent.ShowEntity.showEntity(entityType, entityId, serializer.deserialize(entityNameTag)); + } + } + + private static HoverEvent.@NotNull ShowEntity deserializeLegacy(final @NotNull BinaryTag tag, final @NotNull NBTComponentSerializerImpl serializer) { + try { + final Component component = serializer.deserialize(tag); + if (!(component instanceof TextComponent)) { + throw notSureHowToDeserialize(tag); + } + + final String content = ((TextComponent) component).content(); + final CompoundBinaryTag compound = SNBT_IO.asCompound(content); + return deserializeModern(compound, false, serializer); + } catch (final IOException exception) { + throw notSureHowToDeserialize(tag); + } + } + + private static @NotNull IllegalArgumentException notSureHowToDeserialize(final @NotNull BinaryTag tag) { + return new IllegalArgumentException("Don't know how to turn " + tag + " into a show entity hover event data"); + } +} diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/ShowItemSerializer.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/ShowItemSerializer.java new file mode 100644 index 000000000..ba61a24bc --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/ShowItemSerializer.java @@ -0,0 +1,182 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.BinaryTagTypes; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.nbt.EndBinaryTag; +import net.kyori.adventure.nbt.IntBinaryTag; +import net.kyori.adventure.nbt.StringBinaryTag; +import net.kyori.adventure.nbt.api.BinaryTagHolder; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.DataComponentValue; +import net.kyori.adventure.text.event.HoverEvent; +import org.jetbrains.annotations.NotNull; + +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.SHOW_ITEM_COMPONENTS; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.SHOW_ITEM_COUNT; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.SHOW_ITEM_ID; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.SHOW_ITEM_TAG; +import static net.kyori.adventure.text.serializer.nbt.NBTSerializerUtils.SNBT_CODEC; +import static net.kyori.adventure.text.serializer.nbt.NBTSerializerUtils.SNBT_IO; +import static net.kyori.adventure.text.serializer.nbt.NBTSerializerUtils.optionalTag; +import static net.kyori.adventure.text.serializer.nbt.NBTSerializerUtils.requiredTag; + +final class ShowItemSerializer { + + private static final String DATA_COMPONENT_REMOVAL_PREFIX = "!"; + private static final String LEGACY_ITEM_COUNT = "Count"; + + private static final int DEFAULT_ITEM_QUANTITY = 1; + + private ShowItemSerializer() { + } + + static HoverEvent.@NotNull ShowItem deserialize(final @NotNull BinaryTag tag, final boolean snakeCase, + final @NotNull NBTComponentSerializerImpl serializer) { + try { + return deserializeModern(tag, snakeCase); + } catch (final Exception exception) { + if (snakeCase) { + throw notSureHowToDeserialize(tag); + } else { + return deserializeLegacy(tag, serializer); + } + } + } + + static @NotNull CompoundBinaryTag serialize(final HoverEvent.@NotNull ShowItem showItem, final boolean snakeCase, + final @NotNull NBTComponentSerializerImpl serializer) { + final CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder() + .put(SHOW_ITEM_ID, KeySerializer.serialize(showItem.item())); + + final int count = showItem.count(); + if (count != DEFAULT_ITEM_QUANTITY || serializer.options().value(NBTSerializerOptions.EMIT_DEFAULT_ITEM_HOVER_QUANTITY)) { + builder.putInt(SHOW_ITEM_COUNT, count); + } + + final NBTSerializerOptions.ShowItemHoverDataMode dataMode = serializer.options().value(NBTSerializerOptions.SHOW_ITEM_HOVER_DATA_MODE); + if ((snakeCase || dataMode != NBTSerializerOptions.ShowItemHoverDataMode.EMIT_LEGACY_NBT) && !showItem.dataComponents().isEmpty()) { + final CompoundBinaryTag.Builder componentsTagBuilder = CompoundBinaryTag.builder(); + final Map components = showItem.dataComponentsAs(NBTDataComponentValue.class); + + for (final Map.Entry entry : components.entrySet()) { + final BinaryTag value = entry.getValue().binaryTag(); + + String key = entry.getKey().asString(); + if (value instanceof EndBinaryTag) { // removed + key = DATA_COMPONENT_REMOVAL_PREFIX + key; + } + + componentsTagBuilder.put(key, value); + } + + builder.put(SHOW_ITEM_COMPONENTS, componentsTagBuilder.build()); + } else if (!snakeCase && dataMode != NBTSerializerOptions.ShowItemHoverDataMode.EMIT_DATA_COMPONENTS) { + final BinaryTagHolder nbt = showItem.nbt(); + if (nbt != null) { + builder.putString(SHOW_ITEM_TAG, nbt.string()); + } + } + + return builder.build(); + } + + private static HoverEvent.@NotNull ShowItem deserializeModern(final @NotNull BinaryTag tag, final boolean snakeCase) { + if (tag instanceof StringBinaryTag && !snakeCase) { + final StringBinaryTag castTag = (StringBinaryTag) tag; + return HoverEvent.ShowItem.showItem(KeySerializer.deserialize(castTag), DEFAULT_ITEM_QUANTITY); + } else if (!(tag instanceof CompoundBinaryTag)) { + if (snakeCase) { + throw new IllegalArgumentException("The specified binary tag isn't a compound tag"); + } else { + throw new IllegalArgumentException("The specified binary tag isn't either a string tag or compound tag"); + } + } + + final CompoundBinaryTag compound = (CompoundBinaryTag) tag; + + final Key itemId = KeySerializer.deserialize(requiredTag(compound, SHOW_ITEM_ID, BinaryTagTypes.STRING)); + final IntBinaryTag countTag = optionalTag(compound, SHOW_ITEM_COUNT, BinaryTagTypes.INT); + final int itemCount = countTag == null ? DEFAULT_ITEM_QUANTITY : countTag.value(); + + final CompoundBinaryTag componentsTag = optionalTag(compound, SHOW_ITEM_COMPONENTS, BinaryTagTypes.COMPOUND); + final StringBinaryTag nbtTag = optionalTag(compound, SHOW_ITEM_TAG, BinaryTagTypes.STRING); + + if (componentsTag == null) { + if (snakeCase || nbtTag == null) { + return HoverEvent.ShowItem.showItem(itemId, itemCount); + } + return HoverEvent.ShowItem.showItem(itemId, itemCount, BinaryTagHolder.binaryTagHolder(nbtTag.value())); + } else { + final Map componentValues = new HashMap<>(); + + for (final String string : componentsTag.keySet()) { + final boolean removed = string.startsWith(DATA_COMPONENT_REMOVAL_PREFIX); + + final BinaryTag valueTag = componentsTag.get(string); + if (valueTag == null) continue; + + final String key = removed ? string.substring(1) : string; + componentValues.put(Key.key(key), removed ? DataComponentValue.removed() : NBTDataComponentValue.nbtDataComponentValue(valueTag)); + } + + return HoverEvent.ShowItem.showItem(itemId, itemCount, componentValues); + } + } + + private static HoverEvent.@NotNull ShowItem deserializeLegacy(final @NotNull BinaryTag tag, final @NotNull NBTComponentSerializerImpl serializer) { + try { + final Component component = serializer.deserialize(tag); + if (!(component instanceof TextComponent)) { + throw notSureHowToDeserialize(tag); + } + + final String content = ((TextComponent) component).content(); + final CompoundBinaryTag compound = SNBT_IO.asCompound(content); + + final Key key = KeySerializer.deserialize(requiredTag(compound, SHOW_ITEM_ID, BinaryTagTypes.STRING)); + final byte count = requiredTag(compound, LEGACY_ITEM_COUNT, BinaryTagTypes.BYTE).value(); + + final CompoundBinaryTag nbtTag = optionalTag(compound, SHOW_ITEM_TAG, BinaryTagTypes.COMPOUND); + if (nbtTag == null) { + return HoverEvent.ShowItem.showItem(key, count); + } else { + return HoverEvent.ShowItem.showItem(key, count, BinaryTagHolder.encode(nbtTag, SNBT_CODEC)); + } + } catch (final IOException exception) { + throw notSureHowToDeserialize(tag); + } + } + + private static @NotNull IllegalArgumentException notSureHowToDeserialize(final @NotNull BinaryTag tag) { + return new IllegalArgumentException("Don't know how to turn " + tag + " into a show item hover event data"); + } +} diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/StyleSerializer.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/StyleSerializer.java new file mode 100644 index 000000000..e495476b3 --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/StyleSerializer.java @@ -0,0 +1,189 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.BinaryTagTypes; +import net.kyori.adventure.nbt.ByteBinaryTag; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.nbt.StringBinaryTag; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.ShadowColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.option.OptionState; +import org.jetbrains.annotations.NotNull; + +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.CLICK_EVENT_CAMEL; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.CLICK_EVENT_SNAKE; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.COLOR; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.FONT; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.HOVER_EVENT_CAMEL; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.HOVER_EVENT_SNAKE; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.INSERTION; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.SHADOW_COLOR; +import static net.kyori.adventure.text.serializer.nbt.NBTSerializerUtils.optionalTag; + +final class StyleSerializer { + + private StyleSerializer() { + } + + static @NotNull Style deserialize(final @NotNull CompoundBinaryTag compound, final @NotNull NBTComponentSerializerImpl serializer) { + final Style.Builder styleBuilder = Style.style(); + + final StringBinaryTag colorTag = optionalTag(compound, COLOR, BinaryTagTypes.STRING); + if (colorTag != null) { + styleBuilder.color(TextColorSerializer.deserialize(colorTag)); + } + + for (final TextDecoration decoration : TextDecoration.values()) { + final String name = TextDecoration.NAMES.keyOrThrow(decoration); + final ByteBinaryTag decorationTag = optionalTag(compound, name, BinaryTagTypes.BYTE); + if (decorationTag == null) continue; + styleBuilder.decoration(decoration, NBTSerializerUtils.asBoolean(decorationTag)); + } + + final StringBinaryTag fontTag = optionalTag(compound, FONT, BinaryTagTypes.STRING); + if (fontTag != null) { + styleBuilder.font(KeySerializer.deserialize(fontTag)); + } + + final StringBinaryTag insertionTag = optionalTag(compound, INSERTION, BinaryTagTypes.STRING); + if (insertionTag != null) { + styleBuilder.insertion(insertionTag.value()); + } + + CompoundBinaryTag clickEventTag = optionalTag(compound, CLICK_EVENT_SNAKE, BinaryTagTypes.COMPOUND); + if (clickEventTag == null) { + clickEventTag = optionalTag(compound, CLICK_EVENT_CAMEL, BinaryTagTypes.COMPOUND); + if (clickEventTag != null) { + styleBuilder.clickEvent(ClickEventSerializer.deserialize(clickEventTag, false)); + } + } else { + styleBuilder.clickEvent(ClickEventSerializer.deserialize(clickEventTag, true)); + } + + CompoundBinaryTag hoverEventTag = optionalTag(compound, HOVER_EVENT_SNAKE, BinaryTagTypes.COMPOUND); + if (hoverEventTag == null) { + hoverEventTag = optionalTag(compound, HOVER_EVENT_CAMEL, BinaryTagTypes.COMPOUND); + if (hoverEventTag != null) { + styleBuilder.hoverEvent(HoverEventSerializer.deserialize(hoverEventTag, false, serializer)); + } + } else { + styleBuilder.hoverEvent(HoverEventSerializer.deserialize(hoverEventTag, true, serializer)); + } + + final BinaryTag shadowColorTag = compound.get(SHADOW_COLOR); + if (shadowColorTag != null) { + styleBuilder.shadowColor(ShadowColorSerializer.deserialize(shadowColorTag)); + } + + return styleBuilder.build(); + } + + static void serialize(final @NotNull Style style, final CompoundBinaryTag.@NotNull Builder builder, + final @NotNull NBTComponentSerializerImpl serializer) { + final OptionState flags = serializer.options(); + + final TextColor color = style.color(); + if (color != null) { + builder.put(COLOR, TextColorSerializer.serialize(color)); + } + + final ShadowColor shadowColor = style.shadowColor(); + if (shadowColor != null) { + final BinaryTag shadowColorTag = ShadowColorSerializer.serialize(shadowColor, serializer); + if (shadowColorTag != null) { + builder.put(SHADOW_COLOR, shadowColorTag); + } + } + + for (final TextDecoration decoration : TextDecoration.values()) { + final TextDecoration.State state = style.decoration(decoration); + if (state == TextDecoration.State.NOT_SET) continue; + final String name = TextDecoration.NAMES.keyOrThrow(decoration); + builder.putBoolean(name, state == TextDecoration.State.TRUE); + } + + final Key font = style.font(); + if (font != null) { + builder.put(FONT, KeySerializer.serialize(font)); + } + + final String insertion = style.insertion(); + if (insertion != null) { + builder.putString(INSERTION, insertion); + } + + final ClickEvent clickEvent = style.clickEvent(); + if (clickEvent != null) { + final NBTSerializerOptions.ClickEventValueMode clickEventValueMode = flags.value(NBTSerializerOptions.EMIT_CLICK_EVENT_TYPE); + + final boolean emitBothClickEvents = clickEventValueMode == NBTSerializerOptions.ClickEventValueMode.BOTH; + final boolean emitSnakeCaseClickEvent = clickEventValueMode == NBTSerializerOptions.ClickEventValueMode.SNAKE_CASE; + final boolean emitCamelCaseClickEvent = clickEventValueMode == NBTSerializerOptions.ClickEventValueMode.CAMEL_CASE; + + if (emitBothClickEvents || emitSnakeCaseClickEvent) { + final BinaryTag clickEventTag = ClickEventSerializer.serialize(clickEvent, true); + if (clickEventTag != null) { + builder.put(CLICK_EVENT_SNAKE, clickEventTag); + } + } + + if (emitBothClickEvents || emitCamelCaseClickEvent) { + final BinaryTag clickEventTag = ClickEventSerializer.serialize(clickEvent, false); + if (clickEventTag != null) { + builder.put(CLICK_EVENT_CAMEL, clickEventTag); + } + } + } + + final HoverEvent hoverEvent = style.hoverEvent(); + if (hoverEvent != null) { + final NBTSerializerOptions.HoverEventValueMode hoverEventValueMode = flags.value(NBTSerializerOptions.EMIT_HOVER_EVENT_TYPE); + + final boolean emitBothHoverEvents = hoverEventValueMode == NBTSerializerOptions.HoverEventValueMode.BOTH; + final boolean emitSnakeCaseHoverEvent = hoverEventValueMode == NBTSerializerOptions.HoverEventValueMode.SNAKE_CASE; + final boolean emitCamelCaseHoverEvent = hoverEventValueMode == NBTSerializerOptions.HoverEventValueMode.CAMEL_CASE; + + if (emitBothHoverEvents || emitSnakeCaseHoverEvent) { + final BinaryTag hoverEventTag = HoverEventSerializer.serialize(hoverEvent, true, serializer); + if (hoverEventTag != null) { + builder.put(HOVER_EVENT_SNAKE, hoverEventTag); + } + } + + if (emitBothHoverEvents || emitCamelCaseHoverEvent) { + final BinaryTag hoverEventTag = HoverEventSerializer.serialize(hoverEvent, false, serializer); + if (hoverEventTag != null) { + builder.put(HOVER_EVENT_CAMEL, hoverEventTag); + } + } + } + } +} diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/TextColorSerializer.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/TextColorSerializer.java new file mode 100644 index 000000000..8921cbd4b --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/TextColorSerializer.java @@ -0,0 +1,60 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import java.util.Locale; +import net.kyori.adventure.nbt.StringBinaryTag; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import org.jetbrains.annotations.NotNull; + +final class TextColorSerializer { + + private TextColorSerializer() { + } + + static @NotNull TextColor deserialize(final @NotNull StringBinaryTag tag) { + final String value = tag.value(); + if (value.startsWith(TextColor.HEX_PREFIX)) { + final TextColor color = TextColor.fromHexString(value); + if (color == null) { + throw new IllegalArgumentException("Invalid hex text color: " + value); + } + return color; + } else { + return NamedTextColor.NAMES.valueOrThrow(value); + } + } + + static @NotNull StringBinaryTag serialize(final @NotNull TextColor color) { + final String value = color instanceof NamedTextColor + ? NamedTextColor.NAMES.keyOrThrow((NamedTextColor) color) + : asUpperCaseHexString(color); + return StringBinaryTag.stringBinaryTag(value); + } + + private static String asUpperCaseHexString(final TextColor color) { + return String.format(Locale.ROOT, "%c%06X", TextColor.HEX_CHARACTER, color.value()); // to be consistent with vanilla + } +} diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/TranslationArgumentSerializer.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/TranslationArgumentSerializer.java new file mode 100644 index 000000000..124ab4191 --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/TranslationArgumentSerializer.java @@ -0,0 +1,75 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.ByteBinaryTag; +import net.kyori.adventure.nbt.DoubleBinaryTag; +import net.kyori.adventure.nbt.FloatBinaryTag; +import net.kyori.adventure.nbt.IntBinaryTag; +import net.kyori.adventure.nbt.LongBinaryTag; +import net.kyori.adventure.nbt.NumberBinaryTag; +import net.kyori.adventure.nbt.ShortBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TranslationArgument; +import org.jetbrains.annotations.NotNull; + +final class TranslationArgumentSerializer { + + private TranslationArgumentSerializer() { + } + + static @NotNull TranslationArgument deserialize(final @NotNull BinaryTag tag, final @NotNull NBTComponentSerializerImpl serializer) { + /* Serialized booleans are not deserialized as booleans because Minecraft also does that - NbtOps serializes + booleans as byte tags and there is no way to distinguish the original type during deserialization.*/ + if (tag instanceof NumberBinaryTag) { + return TranslationArgument.numeric(((NumberBinaryTag) tag).numberValue()); + } else { + return TranslationArgument.component(serializer.deserialize(tag)); + } + } + + static @NotNull BinaryTag serialize(final @NotNull TranslationArgument argument, final @NotNull NBTComponentSerializerImpl serializer) { + final Object value = argument.value(); + if (value instanceof Boolean) { + return NBTSerializerUtils.asTag((boolean) value); + } else if (value instanceof Byte) { + return ByteBinaryTag.byteBinaryTag((byte) value); + } else if (value instanceof Short) { + return ShortBinaryTag.shortBinaryTag((short) value); + } else if (value instanceof Integer) { + return IntBinaryTag.intBinaryTag((int) value); + } else if (value instanceof Long) { + return LongBinaryTag.longBinaryTag((long) value); + } else if (value instanceof Float) { + return FloatBinaryTag.floatBinaryTag((float) value); + } else if (value instanceof Number) { + return DoubleBinaryTag.doubleBinaryTag(((Number) value).doubleValue()); + } else if (value instanceof Component) { + return serializer.serialize((Component) value); + } else { + throw new IllegalArgumentException("Don't know how to serialize the specified translation argument value: " + value); + } + } +} diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/UUIDSerializer.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/UUIDSerializer.java new file mode 100644 index 000000000..974cc4f3b --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/UUIDSerializer.java @@ -0,0 +1,85 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import java.util.UUID; +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.IntArrayBinaryTag; +import net.kyori.adventure.nbt.ListBinaryTag; +import net.kyori.adventure.nbt.StringBinaryTag; +import org.jetbrains.annotations.NotNull; + +final class UUIDSerializer { + + private static final long LONG_HALF = 0xffffffffL; + + private UUIDSerializer() { + } + + static @NotNull UUID deserialize(final @NotNull BinaryTag tag) { + if (tag instanceof StringBinaryTag) { + return UUID.fromString(((StringBinaryTag) tag).value()); + } else if (tag instanceof IntArrayBinaryTag) { + return createUUIDFromArray(((IntArrayBinaryTag) tag).value()); + } else if (tag instanceof ListBinaryTag) { + final ListBinaryTag castTag = (ListBinaryTag) tag; + final int[] array = new int[castTag.size()]; + + for (int index = 0; index < array.length; index++) { + array[index] = castTag.getInt(index); + } + + return createUUIDFromArray(array); + } else { + throw new IllegalArgumentException("Don't know how to deserialize an UUID from the specified binary tag: " + tag.getClass().getSimpleName()); + } + } + + static @NotNull BinaryTag serialize(final @NotNull UUID uuid) { + final long mostSignificantBits = uuid.getMostSignificantBits(); + final long leastSignificantBits = uuid.getLeastSignificantBits(); + return IntArrayBinaryTag.intArrayBinaryTag( + mostSignificantBits(mostSignificantBits), leastSignificantBits(mostSignificantBits), + mostSignificantBits(leastSignificantBits), leastSignificantBits(leastSignificantBits) + ); + } + + private static @NotNull UUID createUUIDFromArray(final int @NotNull [] array) { + final long mostSignificantBits = binaryConcat(array[0], array[1]); + final long leastSignificantBits = binaryConcat(array[2], array[3]); + return new UUID(mostSignificantBits, leastSignificantBits); + } + + private static long binaryConcat(final int mostSignificantBits, final int leastSignificantBits) { + return ((long) mostSignificantBits << Integer.SIZE) | ((long) leastSignificantBits & LONG_HALF); + } + + private static int mostSignificantBits(final long value) { + return (int) (value >> Integer.SIZE); + } + + private static int leastSignificantBits(final long value) { + return (int) (value & LONG_HALF); + } +} diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/impl/NBTDataComponentValueConverterProvider.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/impl/NBTDataComponentValueConverterProvider.java new file mode 100644 index 000000000..00f24c94b --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/impl/NBTDataComponentValueConverterProvider.java @@ -0,0 +1,65 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt.impl; + +import com.google.auto.service.AutoService; +import java.util.Collections; +import net.kyori.adventure.Adventure; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.EndBinaryTag; +import net.kyori.adventure.text.event.DataComponentValue; +import net.kyori.adventure.text.event.DataComponentValueConverterRegistry; +import net.kyori.adventure.text.serializer.nbt.NBTDataComponentValue; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * A provider for NBT component serializer's implementations of data component value converters. + * + *

This is public SPI, not API.

+ * + * @since 4.24.0 + */ +@AutoService(DataComponentValueConverterRegistry.Provider.class) +@ApiStatus.Internal +public final class NBTDataComponentValueConverterProvider implements DataComponentValueConverterRegistry.Provider { + + private static final Key ID = Key.key(Adventure.NAMESPACE, "serializer/nbt"); + + @Override + public @NotNull Key id() { + return ID; + } + + @Override + public @NotNull Iterable> conversions() { + return Collections.singletonList( + DataComponentValueConverterRegistry.Conversion.convert( + DataComponentValue.Removed.class, + NBTDataComponentValue.class, + (key, removed) -> NBTDataComponentValue.nbtDataComponentValue(EndBinaryTag.endBinaryTag()) + ) + ); + } +} diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/impl/package-info.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/impl/package-info.java new file mode 100644 index 000000000..d29533daf --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/impl/package-info.java @@ -0,0 +1,33 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +/** + * Internal classes for the NBT component serializer. + * + * @since 4.24.0 + * @sinceMinecraft 1.20.3 + */ +@ApiStatus.Internal +package net.kyori.adventure.text.serializer.nbt.impl; + +import org.jetbrains.annotations.ApiStatus; diff --git a/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/package-info.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/package-info.java new file mode 100644 index 000000000..75a191b4e --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/package-info.java @@ -0,0 +1,30 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +/** + * NBT-based component serialization and deserialization. + * + * @since 4.24.0 + * @sinceMinecraft 1.20.3 + */ +package net.kyori.adventure.text.serializer.nbt; diff --git a/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/BlockNBTComponentTest.java b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/BlockNBTComponentTest.java new file mode 100644 index 000000000..17fa37256 --- /dev/null +++ b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/BlockNBTComponentTest.java @@ -0,0 +1,120 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.text.BlockNBTComponent; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.commons.ComponentTreeConstants; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.testComponent; + +final class BlockNBTComponentTest { + @Test + void testLocal() { + final String nbtPath = "abc"; + + final double left = 1.23D; + final double up = 2.0D; + final double forwards = 3.89D; + + testComponent( + Component.blockNBT() + .nbtPath(nbtPath) + .localPos(left, up, forwards) + .build(), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.NBT, nbtPath) + .putString(ComponentTreeConstants.NBT_BLOCK, "^" + left + " ^" + up + " ^" + forwards) + .build() + ); + } + + @Test + void testAbsoluteWorld() { + final String nbtPath = "xyz"; + + final int x = 4; + final int y = 5; + final int z = 6; + + testComponent( + Component.blockNBT() + .nbtPath(nbtPath) + .absoluteWorldPos(x, y, z) + .interpret(true) + .build(), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.NBT, nbtPath) + .putBoolean(ComponentTreeConstants.NBT_INTERPRET, true) + .putString(ComponentTreeConstants.NBT_BLOCK, x + " " + y + " " + z) + .build() + ); + } + + @Test + void testRelativeWorld() { + final String nbtPath = "eeee"; + + final int x = 7; + final int y = 83; + final int z = 900; + + testComponent( + Component.blockNBT() + .nbtPath(nbtPath) + .relativeWorldPos(x, y, z) + .build(), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.NBT, nbtPath) + .putString(ComponentTreeConstants.NBT_BLOCK, "~" + x + " ~" + y + " ~" + z) + .build() + ); + } + + @Test + void testMixedAbsoluteAndRelative() { + final String nbtPath = "qwert"; + + final int x = 12; + final int y = 3; + final int z = 1200; + + testComponent( + Component.blockNBT() + .nbtPath(nbtPath) + .worldPos( + BlockNBTComponent.WorldPos.Coordinate.absolute(12), + BlockNBTComponent.WorldPos.Coordinate.relative(3), + BlockNBTComponent.WorldPos.Coordinate.absolute(1200) + ) + .build(), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.NBT, nbtPath) + .putString(ComponentTreeConstants.NBT_BLOCK, x + " ~" + y + " " + z) + .build() + ); + } +} diff --git a/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/EntityNBTComponentTest.java b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/EntityNBTComponentTest.java new file mode 100644 index 000000000..0af07bc87 --- /dev/null +++ b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/EntityNBTComponentTest.java @@ -0,0 +1,69 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.commons.ComponentTreeConstants; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.testComponent; + +final class EntityNBTComponentTest { + @Test + void testWithoutInterpret() { + final String nbtPath = "abc"; + final String selector = "test"; + + testComponent( + Component.entityNBT() + .nbtPath(nbtPath) + .selector(selector) + .build(), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.NBT, nbtPath) + .putString(ComponentTreeConstants.NBT_ENTITY, selector) + .build() + ); + } + + @Test + void testWithInterpret() { + final String nbtPath = "abc"; + final String selector = "test"; + + testComponent( + Component.entityNBT() + .nbtPath(nbtPath) + .selector(selector) + .interpret(true) + .build(), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.NBT, nbtPath) + .putBoolean(ComponentTreeConstants.NBT_INTERPRET, true) + .putString(ComponentTreeConstants.NBT_ENTITY, selector) + .build() + ); + } +} diff --git a/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/KeybindComponentTest.java b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/KeybindComponentTest.java new file mode 100644 index 000000000..8f37179e8 --- /dev/null +++ b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/KeybindComponentTest.java @@ -0,0 +1,44 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.commons.ComponentTreeConstants; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.testComponent; + +final class KeybindComponentTest { + @Test + void test() { + final String keybind = "key.jump"; + testComponent( + Component.keybind(keybind), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.KEYBIND, keybind) + .build() + ); + } +} diff --git a/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/ListComponentDeserializationTest.java b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/ListComponentDeserializationTest.java new file mode 100644 index 000000000..e179d2e45 --- /dev/null +++ b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/ListComponentDeserializationTest.java @@ -0,0 +1,150 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import net.kyori.adventure.nbt.BinaryTagTypes; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.nbt.ListBinaryTag; +import net.kyori.adventure.nbt.StringBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.serializer.commons.ComponentTreeConstants; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.deserializeComponent; +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.name; +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class ListComponentDeserializationTest { + @Test + void testStringListDeserialization() { + assertEquals( + Component.text() + .content("a") + .append(Component.text("b")) + .append(Component.text("c")) + .build(), + deserializeComponent( + ListBinaryTag.builder(BinaryTagTypes.STRING) + .add(StringBinaryTag.stringBinaryTag("a")) + .add(StringBinaryTag.stringBinaryTag("b")) + .add(StringBinaryTag.stringBinaryTag("c")) + .build() + ) + ); + } + + @Test + void testCompoundListDeserialization() { + assertEquals( + Component.text() + .content("x") + .color(NamedTextColor.RED) + .append(Component.translatable("message.disconnection", Style.style(TextDecoration.BOLD))) + .append(Component.text("z", Style.style(TextDecoration.ITALIC.withState(false), NamedTextColor.DARK_AQUA))) + .append( + Component.text() + .content("qwerty") + .color(NamedTextColor.BLACK) + .append(Component.text("abc")) + .build() + ) + .build(), + deserializeComponent( + ListBinaryTag.builder(BinaryTagTypes.COMPOUND) + .add( + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, "x") + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.RED)) + .put( + ComponentTreeConstants.EXTRA, + ListBinaryTag.builder(BinaryTagTypes.COMPOUND) + .add( + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TRANSLATE, "message.disconnection") + .putBoolean(name(TextDecoration.BOLD), true) + .build() + ) + .build() + ) + .build() + ) + .add( + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, "z") + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.DARK_AQUA)) + .putBoolean(name(TextDecoration.ITALIC), false) + .build() + ) + .add( + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, "qwerty") + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.BLACK)) + .put( + ComponentTreeConstants.EXTRA, + ListBinaryTag.builder(BinaryTagTypes.STRING) + .add(StringBinaryTag.stringBinaryTag("abc")) + .build() + ) + .build() + ) + .build() + ) + ); + } + + @Test + void testHeterogeneousListDeserialization() { + assertEquals( + Component.text() + .content("a") + .color(NamedTextColor.RED) + .append(Component.empty()) + .append(Component.text("b", NamedTextColor.YELLOW)) + .append(Component.text("qwerty")) + .build(), + deserializeComponent( + ListBinaryTag.heterogeneousListBinaryTag() + .add( + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, "a") + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.RED)) + .build() + ) + .add(StringBinaryTag.stringBinaryTag("")) + .add( + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, "b") + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.YELLOW)) + .build() + ) + .add(StringBinaryTag.stringBinaryTag("qwerty")) + .build() + .wrapHeterogeneity() + ) + ); + } +} diff --git a/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/ScoreComponentTest.java b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/ScoreComponentTest.java new file mode 100644 index 000000000..e77c4940a --- /dev/null +++ b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/ScoreComponentTest.java @@ -0,0 +1,71 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.commons.ComponentTreeConstants; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.deserializeComponent; +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.testComponent; +import static org.junit.jupiter.api.Assertions.assertThrows; + +final class ScoreComponentTest { + @Test + void test() { + final String name = "abc"; + final String objective = "def"; + + testComponent( + Component.score(name, objective), + CompoundBinaryTag.builder() + .put( + ComponentTreeConstants.SCORE, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.SCORE_NAME, name) + .putString(ComponentTreeConstants.SCORE_OBJECTIVE, objective) + .build() + ) + .build() + ); + } + + @Test + void testWithoutObjective() { + assertThrows( + IllegalArgumentException.class, + () -> deserializeComponent( + CompoundBinaryTag.builder() + .put( + ComponentTreeConstants.SCORE, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.SCORE_NAME, "qwerty") + .build() + ) + .build() + ) + ); + } +} diff --git a/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/SelectorComponentTest.java b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/SelectorComponentTest.java new file mode 100644 index 000000000..8e23e5282 --- /dev/null +++ b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/SelectorComponentTest.java @@ -0,0 +1,59 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.commons.ComponentTreeConstants; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.serializeComponent; +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.testComponent; + +final class SelectorComponentTest { + @Test + void test() { + final String pattern = "@p"; + testComponent( + Component.selector(pattern), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.SELECTOR, pattern) + .build() + ); + } + + @Test + void testSeparator() { + final String pattern = "@r"; + final Component separator = Component.text(","); + + testComponent( + Component.selector(pattern, separator), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.SELECTOR, pattern) + .put(ComponentTreeConstants.SEPARATOR, serializeComponent(separator)) + .build() + ); + } +} diff --git a/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/SerializerTests.java b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/SerializerTests.java new file mode 100644 index 000000000..c7a10f369 --- /dev/null +++ b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/SerializerTests.java @@ -0,0 +1,92 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextDecoration; +import org.jetbrains.annotations.NotNull; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class SerializerTests { + + private static final NBTComponentSerializer DEFAULT_SERIALIZER = NBTComponentSerializer.nbt(); + + private SerializerTests() { + } + + static void testComponent(final @NotNull Component component, final @NotNull BinaryTag tag) { + testComponent(DEFAULT_SERIALIZER, component, tag); + } + + static void testComponent(final @NotNull NBTComponentSerializer serializer, + final @NotNull Component component, final @NotNull BinaryTag tag) { + assertEquals(tag, serializer.serialize(component)); + assertEquals(component, serializer.deserialize(tag)); + } + + static void testStyle(final @NotNull Style style, final @NotNull CompoundBinaryTag tag) { + testStyle(DEFAULT_SERIALIZER, style, tag); + } + + static void testStyle(final @NotNull NBTComponentSerializer serializer, + final @NotNull Style style, final @NotNull CompoundBinaryTag tag) { + assertEquals(tag, serializer.serializeStyle(style)); + assertEquals(style, serializer.deserializeStyle(tag)); + } + + static @NotNull Component deserializeComponent(final @NotNull BinaryTag tag) { + return DEFAULT_SERIALIZER.deserialize(tag); + } + + static @NotNull BinaryTag serializeComponent(final @NotNull Component component) { + return DEFAULT_SERIALIZER.serialize(component); + } + + static @NotNull Style deserializeStyle(final @NotNull CompoundBinaryTag tag) { + return DEFAULT_SERIALIZER.deserializeStyle(tag); + } + + static @NotNull String name(final @NotNull TextDecoration decoration) { + return TextDecoration.NAMES.keyOrThrow(decoration); + } + + static @NotNull String name(final @NotNull NamedTextColor decoration) { + return NamedTextColor.NAMES.keyOrThrow(decoration); + } + + static @NotNull String name(final ClickEvent.@NotNull Action action) { + return ClickEvent.Action.NAMES.keyOrThrow(action); + } + + static @NotNull String name(final HoverEvent.@NotNull Action action) { + return HoverEvent.Action.NAMES.keyOrThrow(action); + } +} diff --git a/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/ShowEntityTest.java b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/ShowEntityTest.java new file mode 100644 index 000000000..60ec6bc28 --- /dev/null +++ b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/ShowEntityTest.java @@ -0,0 +1,169 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import java.io.IOException; +import java.util.UUID; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.nbt.IntArrayBinaryTag; +import net.kyori.adventure.text.Component; +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.serializer.commons.ComponentTreeConstants; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.text.serializer.nbt.NBTSerializerUtils.SNBT_IO; +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.deserializeStyle; +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.name; +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.serializeComponent; +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.testStyle; +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class ShowEntityTest { + @Test + void testWithoutName() { + final UUID uuid = UUID.fromString("c04d19f7-9854-4122-93ab-ad7d4e1af8bc"); + testStyle( + Style.style() + .hoverEvent(HoverEvent.showEntity(Key.key("zombie"), uuid)) + .build(), + CompoundBinaryTag.builder() + .put( + ComponentTreeConstants.HOVER_EVENT_SNAKE, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.HOVER_EVENT_ACTION, name(HoverEvent.Action.SHOW_ENTITY)) + .putString(ComponentTreeConstants.SHOW_ENTITY_ID, "minecraft:zombie") + .put( + ComponentTreeConstants.SHOW_ENTITY_UUID, + IntArrayBinaryTag.intArrayBinaryTag(-1068688905, -1739308766, -1817465475, 1310390460) + ) + .build() + ) + .build() + ); + } + + @Test + void testWithName() { + final String entityId = "minecraft:spider"; + final UUID uuid = UUID.randomUUID(); + final String entityName = "Adventure spider"; + + testStyle( + Style.style() + .hoverEvent(HoverEvent.showEntity(Key.key(entityId), uuid, Component.text(entityName, NamedTextColor.RED))) + .build(), + CompoundBinaryTag.builder() + .put( + ComponentTreeConstants.HOVER_EVENT_SNAKE, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.HOVER_EVENT_ACTION, name(HoverEvent.Action.SHOW_ENTITY)) + .putString(ComponentTreeConstants.SHOW_ENTITY_ID, entityId) + .put( + ComponentTreeConstants.SHOW_ENTITY_UUID, + IntArrayBinaryTag.intArrayBinaryTag( + (int) (uuid.getMostSignificantBits() >> Integer.SIZE), + (int) uuid.getMostSignificantBits(), + (int) (uuid.getLeastSignificantBits() >> Integer.SIZE), + (int) uuid.getLeastSignificantBits() + ) + ) + .put( + ComponentTreeConstants.SHOW_ENTITY_NAME, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, entityName) + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.RED)) + .build() + ) + .build() + ) + .build() + ); + } + + @Test + void testLegacyWithoutName() throws IOException { + final String entityType = "minecraft:blaze"; + final UUID uuid = UUID.randomUUID(); + + final CompoundBinaryTag contentsTag = CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.SHOW_ENTITY_TYPE, entityType) + .putString(ComponentTreeConstants.SHOW_ENTITY_ID, uuid.toString()) + .build(); + + assertEquals( + Style.style() + .hoverEvent(HoverEvent.showEntity(Key.key(entityType), uuid)) + .build(), + deserializeStyle( + CompoundBinaryTag.builder() + .put( + ComponentTreeConstants.HOVER_EVENT_CAMEL, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.HOVER_EVENT_ACTION, name(HoverEvent.Action.SHOW_ENTITY)) + .put( + ComponentTreeConstants.HOVER_EVENT_CONTENTS, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, SNBT_IO.asString(contentsTag)) + .build() + ) + .build() + ) + .build() + ) + ); + } + + @Test + void testLegacyWithName() throws IOException { + final String entityType = "minecraft:chicken"; + final UUID uuid = UUID.fromString("a8aa3054-ca11-41bd-ac7e-95967816a135"); + final Component entityName = Component.text("Lava chicken", NamedTextColor.DARK_RED); + + final CompoundBinaryTag contentsTag = CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.SHOW_ENTITY_TYPE, entityType) + .putString(ComponentTreeConstants.SHOW_ENTITY_ID, uuid.toString()) + .put(ComponentTreeConstants.SHOW_ENTITY_NAME, serializeComponent(entityName)) + .build(); + + assertEquals( + Style.style() + .hoverEvent(HoverEvent.showEntity(Key.key(entityType), uuid, entityName)) + .build(), + deserializeStyle( + CompoundBinaryTag.builder() + .put( + ComponentTreeConstants.HOVER_EVENT_CAMEL, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.HOVER_EVENT_ACTION, name(HoverEvent.Action.SHOW_ENTITY)) + .putString(ComponentTreeConstants.HOVER_EVENT_CONTENTS, SNBT_IO.asString(contentsTag)) + .build() + ) + .build() + ) + ); + } +} diff --git a/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/ShowItemTest.java b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/ShowItemTest.java new file mode 100644 index 000000000..feb9f5946 --- /dev/null +++ b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/ShowItemTest.java @@ -0,0 +1,247 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import java.io.IOException; +import java.util.Collections; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.nbt.EndBinaryTag; +import net.kyori.adventure.nbt.TagStringIO; +import net.kyori.adventure.nbt.api.BinaryTagHolder; +import net.kyori.adventure.text.event.DataComponentValue; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.serializer.commons.ComponentTreeConstants; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.text.serializer.nbt.NBTSerializerUtils.SNBT_CODEC; +import static net.kyori.adventure.text.serializer.nbt.NBTSerializerUtils.SNBT_IO; +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.deserializeStyle; +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.name; +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.testStyle; +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class ShowItemTest { + + private static final String LEGACY_COUNT = "Count"; + + @Test + void testWithPopulatedTag() throws IOException { + final String item = "minecraft:diamond"; + final int count = 2; + + testStyle( + NBTComponentSerializer.builder() + .editOptions(builder -> { + builder.value( + NBTSerializerOptions.SHOW_ITEM_HOVER_DATA_MODE, + NBTSerializerOptions.ShowItemHoverDataMode.EMIT_EITHER + ); + builder.value( + NBTSerializerOptions.EMIT_HOVER_EVENT_TYPE, + NBTSerializerOptions.HoverEventValueMode.CAMEL_CASE + ); + }) + .build(), + Style.style() + .hoverEvent(HoverEvent.showItem( + Key.key(item), count, + BinaryTagHolder.binaryTagHolder(TagStringIO.tagStringIO().asString( + CompoundBinaryTag.builder() + .put("display", CompoundBinaryTag.builder() + .putString("Name", "A test!") + .build()) + .build() + )) + )) + .build(), + CompoundBinaryTag.builder() + .put( + ComponentTreeConstants.HOVER_EVENT_CAMEL, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.HOVER_EVENT_ACTION, name(HoverEvent.Action.SHOW_ITEM)) + .put( + ComponentTreeConstants.HOVER_EVENT_CONTENTS, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.SHOW_ITEM_ID, item) + .putInt(ComponentTreeConstants.SHOW_ITEM_COUNT, count) + .putString(ComponentTreeConstants.SHOW_ITEM_TAG, "{display:{Name:\"A test!\"}}") + .build() + ) + .build() + ) + .build() + ); + } + + @Test + void testWithoutAdditionalData() { + final String item = "minecraft:diamond"; + final int count = 2; + + testStyle( + Style.style() + .hoverEvent(HoverEvent.showItem(Key.key(item), count, Collections.emptyMap())) + .build(), + CompoundBinaryTag.builder() + .put( + ComponentTreeConstants.HOVER_EVENT_SNAKE, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.HOVER_EVENT_ACTION, name(HoverEvent.Action.SHOW_ITEM)) + .putString(ComponentTreeConstants.SHOW_ITEM_ID, item) + .putInt(ComponentTreeConstants.SHOW_ITEM_COUNT, count) + .build() + ) + .build() + ); + } + + @Test + void testWithCountOfOne() { + final String item = "minecraft:diamond"; + final int count = 1; + + testStyle( + Style.style() + .hoverEvent(HoverEvent.showItem(Key.key(item), count)) + .build(), + CompoundBinaryTag.builder() + .put( + ComponentTreeConstants.HOVER_EVENT_SNAKE, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.HOVER_EVENT_ACTION, name(HoverEvent.Action.SHOW_ITEM)) + .putString(ComponentTreeConstants.SHOW_ITEM_ID, item) + .putInt(ComponentTreeConstants.SHOW_ITEM_COUNT, count) + .build() + ) + .build() + ); + } + + @Test + void testWithRemovedComponent() { + final String item = "minecraft:diamond"; + final int count = 2; + final String component = "minecraft:damage"; + + testStyle( + Style.style() + .hoverEvent( + HoverEvent.showItem( + Key.key(item), count, + Collections.singletonMap(Key.key(component), DataComponentValue.removed()) + ) + ) + .build(), + CompoundBinaryTag.builder() + .put( + ComponentTreeConstants.HOVER_EVENT_SNAKE, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.HOVER_EVENT_ACTION, name(HoverEvent.Action.SHOW_ITEM)) + .putString(ComponentTreeConstants.SHOW_ITEM_ID, item) + .putInt(ComponentTreeConstants.SHOW_ITEM_COUNT, count) + .put( + ComponentTreeConstants.SHOW_ITEM_COMPONENTS, + CompoundBinaryTag.builder() + .put("!" + component, EndBinaryTag.endBinaryTag()) + .build() + ) + .build() + ) + .build() + ); + } + + @Test + void testLegacyWithoutTag() throws IOException { + final String item = "minecraft:diamond"; + final byte count = 3; + + final CompoundBinaryTag itemData = CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.SHOW_ITEM_ID, item) + .putByte(LEGACY_COUNT, count) + .build(); + + assertEquals( + Style.style() + .hoverEvent(HoverEvent.showItem(Key.key(item), count)) + .build(), + deserializeStyle( + CompoundBinaryTag.builder() + .put( + ComponentTreeConstants.HOVER_EVENT_CAMEL, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.HOVER_EVENT_ACTION, name(HoverEvent.Action.SHOW_ITEM)) + .putString(ComponentTreeConstants.HOVER_EVENT_CONTENTS, SNBT_IO.asString(itemData)) + .build() + ) + .build() + ) + ); + } + + @Test + void testLegacyWithTag() throws IOException { + final String item = "minecraft:diamond"; + final byte count = 1; + + final CompoundBinaryTag itemTag = CompoundBinaryTag.builder() + .put( + "display", + CompoundBinaryTag.builder() + .putString("Name", "Legacy test!") + .build() + ) + .build(); + + final CompoundBinaryTag itemData = CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.SHOW_ITEM_ID, item) + .putByte(LEGACY_COUNT, count) + .put(ComponentTreeConstants.SHOW_ITEM_TAG, itemTag) + .build(); + + assertEquals( + Style.style() + .hoverEvent(HoverEvent.showItem(Key.key(item), count, BinaryTagHolder.encode(itemTag, SNBT_CODEC))) + .build(), + deserializeStyle( + CompoundBinaryTag.builder() + .put( + ComponentTreeConstants.HOVER_EVENT_CAMEL, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.HOVER_EVENT_ACTION, name(HoverEvent.Action.SHOW_ITEM)) + .put( + ComponentTreeConstants.HOVER_EVENT_CONTENTS, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, SNBT_IO.asString(itemData)) + .build() + ) + .build() + ) + .build() + ) + ); + } +} diff --git a/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/StorageNBTComponentTest.java b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/StorageNBTComponentTest.java new file mode 100644 index 000000000..7b91908be --- /dev/null +++ b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/StorageNBTComponentTest.java @@ -0,0 +1,70 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.commons.ComponentTreeConstants; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.testComponent; + +final class StorageNBTComponentTest { + @Test + void testWithoutInterpret() { + final String nbtPath = "abc"; + final String storage = "doom:apple"; + + testComponent( + Component.storageNBT() + .nbtPath(nbtPath) + .storage(Key.key(storage)) + .build(), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.NBT, nbtPath) + .putString(ComponentTreeConstants.NBT_STORAGE, storage) + .build() + ); + } + + @Test + void testWithInterpret() { + final String nbtPath = "abc"; + final String storage = "doom:apple"; + + testComponent( + Component.storageNBT() + .nbtPath(nbtPath) + .storage(Key.key(storage)) + .interpret(true) + .build(), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.NBT, nbtPath) + .putBoolean(ComponentTreeConstants.NBT_INTERPRET, true) + .putString(ComponentTreeConstants.NBT_STORAGE, storage) + .build() + ); + } +} diff --git a/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/StyleTest.java b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/StyleTest.java new file mode 100644 index 000000000..9a6fd817a --- /dev/null +++ b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/StyleTest.java @@ -0,0 +1,218 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import java.util.UUID; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.BinaryTagTypes; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.nbt.EndBinaryTag; +import net.kyori.adventure.nbt.FloatBinaryTag; +import net.kyori.adventure.nbt.IntArrayBinaryTag; +import net.kyori.adventure.nbt.ListBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.ShadowColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.serializer.commons.ComponentTreeConstants; +import net.kyori.adventure.util.TriState; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.deserializeStyle; +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.name; +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.testStyle; +import static org.junit.jupiter.api.Assertions.assertThrows; + +final class StyleTest { + @Test + void testEmpty() { + testStyle(Style.empty(), CompoundBinaryTag.empty()); + } + + @Test + void testHexColor() { + testStyle( + Style.style(TextColor.color(0x0a1ab9)), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.COLOR, "#0A1AB9") + .build() + ); + } + + @Test + void testNamedColor() { + testStyle( + Style.style(NamedTextColor.LIGHT_PURPLE), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.LIGHT_PURPLE)) + .build() + ); + } + + @Test + void testDecoration() { + testStyle( + Style.style(TextDecoration.BOLD), + CompoundBinaryTag.builder() + .putBoolean(name(TextDecoration.BOLD), true) + .build() + ); + + testStyle( + Style.style(TextDecoration.BOLD.withState(false)), + CompoundBinaryTag.builder() + .putBoolean(name(TextDecoration.BOLD), false) + .build() + ); + + testStyle( + Style.style(TextDecoration.BOLD.withState(TriState.NOT_SET)), + CompoundBinaryTag.empty() + ); + + assertThrows( + IllegalArgumentException.class, + () -> deserializeStyle( + CompoundBinaryTag.builder() + .put(name(TextDecoration.BOLD), EndBinaryTag.endBinaryTag()) + .build() + ) + ); + } + + @Test + void testShadowColorInt() { + final int shadowColorValue = 0xCCFF0022; + testStyle( + Style.style(ShadowColor.shadowColor(shadowColorValue)), + CompoundBinaryTag.builder() + .putInt(ComponentTreeConstants.SHADOW_COLOR, shadowColorValue) + .build() + ); + } + + @Test + void testShadowColorFloats() { + testStyle( + NBTComponentSerializer.builder() + .editOptions(builder -> builder.value(NBTSerializerOptions.SHADOW_COLOR_MODE, NBTSerializerOptions.ShadowColorEmitMode.EMIT_ARRAY)) + .build(), + Style.style(ShadowColor.shadowColor(0x80, 0x40, 0xcc, 0xff)), + CompoundBinaryTag.builder() + .put( + ComponentTreeConstants.SHADOW_COLOR, + ListBinaryTag.builder(BinaryTagTypes.FLOAT) + .add(FloatBinaryTag.floatBinaryTag(0.5019608f)) + .add(FloatBinaryTag.floatBinaryTag(0.2509804f)) + .add(FloatBinaryTag.floatBinaryTag(0.8f)) + .add(FloatBinaryTag.floatBinaryTag(1f)) + .build() + ) + .build() + ); + } + + @Test + void testInsertion() { + final String insertion = "honk"; + testStyle( + Style.style() + .insertion(insertion) + .build(), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.INSERTION, insertion) + .build() + ); + } + + @Test + void testMixedFontColorDecorationClickEvent() { + final String clickEventUrl = "https://github.com"; + testStyle( + Style.style() + .font(Key.key("kyori", "kittens")) + .color(NamedTextColor.RED) + .decoration(TextDecoration.BOLD, true) + .clickEvent(ClickEvent.openUrl(clickEventUrl)) + .build(), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.FONT, "kyori:kittens") + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.RED)) + .putBoolean(name(TextDecoration.BOLD), true) + .put( + ComponentTreeConstants.CLICK_EVENT_SNAKE, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.CLICK_EVENT_ACTION, name(ClickEvent.Action.OPEN_URL)) + .putString(ComponentTreeConstants.CLICK_EVENT_URL, clickEventUrl) + .build() + ) + .build() + ); + } + + @Test + void testShowEntityHoverEvent() { + final UUID showEntityUUID = UUID.randomUUID(); + final String showEntityName = "Dolores"; + + testStyle( + Style.style() + .hoverEvent(HoverEvent.showEntity( + Key.key(Key.MINECRAFT_NAMESPACE, "pig"), + showEntityUUID, + Component.text(showEntityName, TextColor.color(0x0a1ab9)) + )) + .build(), + CompoundBinaryTag.builder() + .put( + ComponentTreeConstants.HOVER_EVENT_SNAKE, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.HOVER_EVENT_ACTION, name(HoverEvent.Action.SHOW_ENTITY)) + .putString(ComponentTreeConstants.SHOW_ENTITY_ID, "minecraft:pig") + .put( + ComponentTreeConstants.SHOW_ENTITY_UUID, + IntArrayBinaryTag.intArrayBinaryTag( + (int) (showEntityUUID.getMostSignificantBits() >> 32), + (int) (showEntityUUID.getMostSignificantBits() & 0xffffffffL), + (int) (showEntityUUID.getLeastSignificantBits() >> 32), + (int) (showEntityUUID.getLeastSignificantBits() & 0xffffffffL) + ) + ) + .put( + ComponentTreeConstants.SHOW_ENTITY_NAME, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, showEntityName) + .putString(ComponentTreeConstants.COLOR, "#0A1AB9") + .build() + ) + .build() + ) + .build() + ); + } +} diff --git a/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/TextComponentTest.java b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/TextComponentTest.java new file mode 100644 index 000000000..68598a86e --- /dev/null +++ b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/TextComponentTest.java @@ -0,0 +1,128 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.nbt.ListBinaryTag; +import net.kyori.adventure.nbt.StringBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.serializer.commons.ComponentTreeConstants; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.name; +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.testComponent; + +final class TextComponentTest { + @Test + void testSimple() { + testComponent( + Component.text("Hello, world."), + StringBinaryTag.stringBinaryTag("Hello, world.") + ); + } + + @Test + void testComplex1() { + testComponent( + Component.text().content("c") + .color(NamedTextColor.GOLD) + .append(Component.text("o", NamedTextColor.DARK_AQUA)) + .append(Component.text("l", NamedTextColor.LIGHT_PURPLE)) + .append(Component.text("o", NamedTextColor.DARK_PURPLE)) + .append(Component.text("u", NamedTextColor.BLUE)) + .append(Component.text("r", NamedTextColor.DARK_GREEN)) + .append(Component.text("s", NamedTextColor.RED)) + .build(), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, "c") + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.GOLD)) + .put( + ComponentTreeConstants.EXTRA, + ListBinaryTag.builder() + .add(CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, "o") + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.DARK_AQUA)) + .build()) + .add(CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, "l") + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.LIGHT_PURPLE)) + .build()) + .add(CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, "o") + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.DARK_PURPLE)) + .build()) + .add(CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, "u") + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.BLUE)) + .build()) + .add(CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, "r") + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.DARK_GREEN)) + .build()) + .add(CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, "s") + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.RED)) + .build()) + .build() + ) + .build() + ); + } + + @Test + void testComplex2() { + testComponent( + Component.text().content("This is a test.") + .color(NamedTextColor.DARK_PURPLE) + .hoverEvent(HoverEvent.showText(Component.text("A test."))) + .append(Component.text(" ")) + .append(Component.text("A what?", NamedTextColor.DARK_AQUA)) + .build(), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, "This is a test.") + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.DARK_PURPLE)) + .put( + ComponentTreeConstants.HOVER_EVENT_SNAKE, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.HOVER_EVENT_ACTION, "show_text") + .putString(ComponentTreeConstants.HOVER_EVENT_VALUE, "A test.") + .build() + ) + .put( + ComponentTreeConstants.EXTRA, + ListBinaryTag.heterogeneousListBinaryTag() + .add(StringBinaryTag.stringBinaryTag(" ")) + .add(CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, "A what?") + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.DARK_AQUA)) + .build()) + .build() + .wrapHeterogeneity() + ) + .build() + ); + } +} diff --git a/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/TranslatableComponentTest.java b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/TranslatableComponentTest.java new file mode 100644 index 000000000..d94e64004 --- /dev/null +++ b/text-serializer-nbt/src/test/java/net/kyori/adventure/text/serializer/nbt/TranslatableComponentTest.java @@ -0,0 +1,124 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.serializer.nbt; + +import java.util.UUID; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.nbt.IntArrayBinaryTag; +import net.kyori.adventure.nbt.ListBinaryTag; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.serializer.commons.ComponentTreeConstants; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.name; +import static net.kyori.adventure.text.serializer.nbt.SerializerTests.testComponent; + +final class TranslatableComponentTest { + @Test + void testNoArgs() { + final String translationKey = "multiplayer.player.left"; + testComponent( + Component.translatable(translationKey), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TRANSLATE, translationKey) + .build() + ); + } + + @Test + void testFallback() { + final String translationKey = "thisIsA"; + final String fallback = "This is a test."; + + testComponent( + Component.translatable() + .key(translationKey) + .fallback(fallback) + .build(), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TRANSLATE, translationKey) + .putString(ComponentTreeConstants.TRANSLATE_FALLBACK, fallback) + .build() + ); + } + + @Test + void testSingleArgWithEvents() { + final String translationKey = "translatable.message"; + + final UUID id = UUID.fromString("86365c36-e272-4d32-8ab8-d4fee19f6231"); + final String name = "Codestech"; + final String command = String.format("/msg %s ", name); + final String showEntityId = "minecraft:player"; + + testComponent( + Component.translatable() + .key(translationKey) + .color(NamedTextColor.YELLOW) + .arguments(Component.text() + .content(name) + .clickEvent(ClickEvent.suggestCommand(command)) + .hoverEvent(HoverEvent.showEntity(Key.key(showEntityId), id, Component.text(name))) + .build()) + .build(), + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TRANSLATE, translationKey) + .putString(ComponentTreeConstants.COLOR, name(NamedTextColor.YELLOW)) + .put( + ComponentTreeConstants.TRANSLATE_WITH, + ListBinaryTag.builder() + .add( + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.TEXT, name) + .put( + ComponentTreeConstants.CLICK_EVENT_SNAKE, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.CLICK_EVENT_ACTION, name(ClickEvent.Action.SUGGEST_COMMAND)) + .putString(ComponentTreeConstants.CLICK_EVENT_COMMAND, command) + .build() + ) + .put( + ComponentTreeConstants.HOVER_EVENT_SNAKE, + CompoundBinaryTag.builder() + .putString(ComponentTreeConstants.HOVER_EVENT_ACTION, name(HoverEvent.Action.SHOW_ENTITY)) + .putString(ComponentTreeConstants.SHOW_ENTITY_ID, showEntityId) + .put( + ComponentTreeConstants.SHOW_ENTITY_UUID, + IntArrayBinaryTag.intArrayBinaryTag(-2043257802, -495825614, -1967598338, -509648335) + ) + .putString(ComponentTreeConstants.SHOW_ENTITY_NAME, name) + .build() + ) + .build() + ) + .build() + ) + .build() + ); + } +}