diff --git a/nbt/src/main/java/net/kyori/adventure/nbt/TagStringWriter.java b/nbt/src/main/java/net/kyori/adventure/nbt/TagStringWriter.java index 287cf507da..83810ef167 100644 --- a/nbt/src/main/java/net/kyori/adventure/nbt/TagStringWriter.java +++ b/nbt/src/main/java/net/kyori/adventure/nbt/TagStringWriter.java @@ -25,7 +25,9 @@ import java.io.IOException; import java.io.Writer; -import java.util.Map; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * An emitter for the SNBT format. @@ -80,10 +82,17 @@ public TagStringWriter writeTag(final BinaryTag tag) throws IOException { private TagStringWriter writeCompound(final CompoundBinaryTag tag) throws IOException { this.beginCompound(); - for (final Map.Entry entry : tag) { - this.key(entry.getKey()); - this.writeTag(entry.getValue()); + + final List keys = new ArrayList<>(tag.keySet()); + Collections.sort(keys); + + for (final String key : keys) { + final BinaryTag value = tag.get(key); + if (value == null) continue; + this.key(key); + this.writeTag(value); } + this.endCompound(); return this; } diff --git a/nbt/src/test/resources/bigtest.snbt b/nbt/src/test/resources/bigtest.snbt index ec5e3b4a2d..ce40e15049 100644 --- a/nbt/src/test/resources/bigtest.snbt +++ b/nbt/src/test/resources/bigtest.snbt @@ -1,11 +1,8 @@ { - shortTest: 32767s, - longTest: 9223372036854775807L, - byteTest: 127b, "byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))": [B; 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B, 0B, 62B, 34B, 16B, 8B, 10B, 22B, 44B, 76B, 18B, 70B, 32B, 4B, 86B, 78B, 80B, 92B, 14B, 46B, 88B, 40B, 2B, 74B, 56B, 48B, 50B, 62B, 84B, 16B, 58B, 10B, 72B, 44B, 26B, 18B, 20B, 32B, 54B, 86B, 28B, 80B, 42B, 14B, 96B, 88B, 90B, 2B, 24B, 56B, 98B, 50B, 12B, 84B, 66B, 58B, 60B, 72B, 94B, 26B, 68B, 20B, 82B, 54B, 36B, 28B, 30B, 42B, 64B, 96B, 38B, 90B, 52B, 24B, 6B, 98B, 0B, 12B, 34B, 66B, 8B, 60B, 22B, 94B, 76B, 68B, 70B, 82B, 4B, 36B, 78B, 30B, 92B, 64B, 46B, 38B, 40B, 52B, 74B, 6B, 48B], - "listTest (long)": [11L, 12L, 13L, 14L, 15L], - floatTest: 0.49823147f, + byteTest: 127b, doubleTest: 0.4931287132182315d, + floatTest: 0.49823147f, intTest: 2147483647, "listTest (compound)": [ { @@ -17,6 +14,8 @@ name: "Compound tag #1" } ], + "listTest (long)": [11L, 12L, 13L, 14L, 15L], + longTest: 9223372036854775807L, "nested compound test": { egg: { name: "Eggbert", @@ -27,5 +26,6 @@ value: 0.75f } }, + shortTest: 32767s, stringTest: "HELLO WORLD THIS IS A TEST STRING ÅÄÖ!" } diff --git a/settings.gradle.kts b/settings.gradle.kts index d3452a6f15..7ee02508a0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -51,6 +51,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 68bbe4b0b3..05508bf1dc 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 @@ -86,6 +86,8 @@ public final class ComponentTreeConstants { @ApiStatus.Obsolete public static final String HOVER_EVENT_VALUE = "value"; @ApiStatus.Obsolete + public static final String SHOW_TEXT_TEXT = "text"; + @ApiStatus.Obsolete 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 3418ef7476..db9e731c58 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 @@ -147,7 +147,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 0000000000..b82e13ce5b --- /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 0000000000..347e39df85 --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/ClickEventSerializer.java @@ -0,0 +1,167 @@ +/* + * 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.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 static final String FALLBACK_URL_PROTOCOL = "https://"; + + 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) { + return switch (action) { + case ClickEvent.Action.OpenUrl ignored -> + ClickEvent.openUrl(requiredTag(compound, CLICK_EVENT_URL, BinaryTagTypes.STRING).value()); + case ClickEvent.Action.RunCommand ignored -> + ClickEvent.runCommand(requiredTag(compound, CLICK_EVENT_COMMAND, BinaryTagTypes.STRING).value()); + case ClickEvent.Action.SuggestCommand ignored -> + ClickEvent.suggestCommand(requiredTag(compound, CLICK_EVENT_COMMAND, BinaryTagTypes.STRING).value()); + case ClickEvent.Action.ChangePage ignored -> + ClickEvent.changePage(requiredTag(compound, CLICK_EVENT_PAGE, BinaryTagTypes.INT).value()); + case ClickEvent.Action.CopyToClipboard ignored -> + ClickEvent.copyToClipboard(requiredTag(compound, CLICK_EVENT_VALUE, BinaryTagTypes.STRING).value()); + case ClickEvent.Action.Custom ignored -> { + try { + final StringBinaryTag clickEventIdTag = requiredTag(compound, CLICK_EVENT_ID, BinaryTagTypes.STRING); + final BinaryTag payloadTag = requiredTag(compound, CLICK_EVENT_PAYLOAD); + yield 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); + } + } + // Non-readable actions are filtered out above. + case ClickEvent.Action.OpenFile ignored -> null; + case ClickEvent.Action.ShowDialog ignored -> null; + }; + } else { + final String value = requiredTag(compound, CLICK_EVENT_VALUE, BinaryTagTypes.STRING).value(); + if (action instanceof ClickEvent.Action.TextCarrier) { + @SuppressWarnings("unchecked") + final ClickEvent.Action textAction = (ClickEvent.Action) action; + return ClickEvent.clickEvent(textAction, ClickEvent.Payload.string(value)); + } else if (action instanceof ClickEvent.Action.ChangePage) { + return ClickEvent.changePage(Integer.parseInt(value)); + } + return null; + } + } + + static @Nullable CompoundBinaryTag serialize(final @NotNull ClickEvent event, final boolean snakeCase, + final @NotNull NBTComponentSerializerImpl serializer) { + final ClickEvent.Action action = event.action(); + if (!action.readable()) { + return null; + } + + final CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder() + .putString(CLICK_EVENT_ACTION, ClickEvent.Action.NAMES.keyOrThrow(action)); + + final boolean emitHttps = serializer.options().value(NBTSerializerOptions.EMIT_CLICK_URL_HTTPS); + + if (snakeCase) { + final ClickEvent.Payload payload = event.payload(); + switch (payload) { + case ClickEvent.Payload.Text text -> { + final String payloadFieldName = switch (action) { + case ClickEvent.Action.OpenUrl ignored -> CLICK_EVENT_URL; + case ClickEvent.Action.RunCommand ignored -> CLICK_EVENT_COMMAND; + case ClickEvent.Action.SuggestCommand ignored -> CLICK_EVENT_COMMAND; + case ClickEvent.Action.CopyToClipboard ignored -> CLICK_EVENT_VALUE; + default -> throw new IllegalArgumentException("Unexpected text-payload click event action: " + action); + }; + builder.putString(payloadFieldName, textPayloadValue(action, text.value(), emitHttps)); + } + case ClickEvent.Payload.Int intPayload -> builder.putInt(CLICK_EVENT_PAGE, intPayload.integer()); + case ClickEvent.Payload.Custom customPayload -> { + try { + builder.put(CLICK_EVENT_ID, KeySerializer.serialize(customPayload.key())); + final BinaryTagHolder nbt = customPayload.nbt(); + if (nbt != null) { + builder.put(CLICK_EVENT_PAYLOAD, nbt.get(SNBT_CODEC)); + } + } catch (final IOException exception) { + throw new RuntimeException("An error occurred while decoding a payload tag", exception); + } + } + case ClickEvent.Payload.Dialog ignored -> { + } + } + } else { + final ClickEvent.Payload payload = event.payload(); + final String value = switch (payload) { + case ClickEvent.Payload.Text text -> textPayloadValue(action, text.value(), emitHttps); + case ClickEvent.Payload.Int intPayload -> String.valueOf(intPayload.integer()); + case ClickEvent.Payload.Custom ignored -> null; + case ClickEvent.Payload.Dialog ignored -> null; + }; + if (value == null) { + return null; + } + builder.putString(CLICK_EVENT_VALUE, value); + } + + return builder.build(); + } + + private static @NotNull String textPayloadValue(final ClickEvent.@NotNull Action action, final @NotNull String value, final boolean emitHttps) { + if (emitHttps && action == ClickEvent.Action.OPEN_URL && !hasUrlScheme(value)) { + return FALLBACK_URL_PROTOCOL + value; + } + return value; + } + + @SuppressWarnings("HttpUrlsUsage") + private static boolean hasUrlScheme(final @NotNull String url) { + return url.startsWith("http://") || url.startsWith("https://"); + } +} 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 0000000000..59f23b0fff --- /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 0000000000..064d52cc5d --- /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/NBTAggregateCollector.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTAggregateCollector.java new file mode 100644 index 0000000000..cedec95d0f --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTAggregateCollector.java @@ -0,0 +1,193 @@ +/* + * 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.Arrays; +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.ByteArrayBinaryTag; +import net.kyori.adventure.nbt.ByteBinaryTag; +import net.kyori.adventure.nbt.IntArrayBinaryTag; +import net.kyori.adventure.nbt.IntBinaryTag; +import net.kyori.adventure.nbt.ListBinaryTag; +import net.kyori.adventure.nbt.LongArrayBinaryTag; +import net.kyori.adventure.nbt.LongBinaryTag; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/*sealed*/ interface NBTAggregateCollector { + + void add(final @NotNull BinaryTag tag); + + @NotNull BinaryTag collect(); + + static @NotNull NBTAggregateCollector create(final @NotNull NBTComponentSerializerImpl serializer) { + return serializer.options().value(NBTSerializerOptions.EMIT_OPTIMIZED_LISTS) ? new Initial() : new ListCollector(); + } + + final class Initial implements NBTAggregateCollector { + + private @Nullable NBTAggregateCollector delegate = null; + + private Initial() { + } + + @Override + public void add(final @NotNull BinaryTag tag) { + if (this.delegate != null) { + this.delegate.add(tag); + } else if (tag instanceof ByteBinaryTag) { + this.delegate = new ByteArrayCollector(((ByteBinaryTag) tag).value()); + } else if (tag instanceof IntBinaryTag) { + this.delegate = new IntArrayCollector(((IntBinaryTag) tag).value()); + } else if (tag instanceof LongBinaryTag) { + this.delegate = new LongArrayCollector(((LongBinaryTag) tag).value()); + } else { + this.delegate = new ListCollector(tag); + } + } + + @Override + public @NotNull BinaryTag collect() { + return this.delegate == null ? ListBinaryTag.empty() : this.delegate.collect(); + } + } + + final class ListCollector implements NBTAggregateCollector { + + private final ListBinaryTag.Builder builder = ListBinaryTag.heterogeneousListBinaryTag(); + + private ListCollector() { + } + + private ListCollector(final @NotNull BinaryTag firstElement) { + this.add(firstElement); + } + + @Override + public void add(final @NotNull BinaryTag tag) { + this.builder.add(tag); + } + + @Override + public @NotNull BinaryTag collect() { + return this.builder.build().wrapHeterogeneity(); + } + } + + final class ByteArrayCollector implements NBTAggregateCollector { + + private byte @NotNull [] array; + private @Nullable NBTAggregateCollector delegate = null; + + private ByteArrayCollector(final byte firstElement) { + this.array = new byte[]{firstElement}; + } + + @Override + public void add(final @NotNull BinaryTag tag) { + if (this.delegate != null) { + this.delegate.add(tag); + } else if (tag instanceof ByteBinaryTag) { + final int index = this.array.length; + this.array = Arrays.copyOf(this.array, index + 1); + this.array[index] = ((ByteBinaryTag) tag).value(); + } else { + this.delegate = new ListCollector(); + for (final byte element : this.array) { + this.delegate.add(ByteBinaryTag.byteBinaryTag(element)); + } + this.delegate.add(tag); + } + } + + @Override + public @NotNull BinaryTag collect() { + return this.delegate == null ? ByteArrayBinaryTag.byteArrayBinaryTag(this.array) : this.delegate.collect(); + } + } + + final class IntArrayCollector implements NBTAggregateCollector { + + private int @NotNull [] array; + private @Nullable NBTAggregateCollector delegate = null; + + private IntArrayCollector(final int firstElement) { + this.array = new int[]{firstElement}; + } + + @Override + public void add(final @NotNull BinaryTag tag) { + if (this.delegate != null) { + this.delegate.add(tag); + } else if (tag instanceof IntBinaryTag) { + final int index = this.array.length; + this.array = Arrays.copyOf(this.array, index + 1); + this.array[index] = ((IntBinaryTag) tag).value(); + } else { + this.delegate = new ListCollector(); + for (final int element : this.array) { + this.delegate.add(IntBinaryTag.intBinaryTag(element)); + } + this.delegate.add(tag); + } + } + + @Override + public @NotNull BinaryTag collect() { + return this.delegate == null ? IntArrayBinaryTag.intArrayBinaryTag(this.array) : this.delegate.collect(); + } + } + + final class LongArrayCollector implements NBTAggregateCollector { + + private long @NotNull [] array; + private @Nullable NBTAggregateCollector delegate = null; + + private LongArrayCollector(final long firstElement) { + this.array = new long[]{firstElement}; + } + + @Override + public void add(final @NotNull BinaryTag tag) { + if (this.delegate != null) { + this.delegate.add(tag); + } else if (tag instanceof LongBinaryTag) { + final int index = this.array.length; + this.array = Arrays.copyOf(this.array, index + 1); + this.array[index] = ((LongBinaryTag) tag).value(); + } else { + this.delegate = new ListCollector(); + for (final long element : this.array) { + this.delegate.add(LongBinaryTag.longBinaryTag(element)); + } + this.delegate.add(tag); + } + } + + @Override + public @NotNull BinaryTag collect() { + return this.delegate == null ? LongArrayBinaryTag.longArrayBinaryTag(this.array) : this.delegate.collect(); + } + } +} 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 0000000000..f91c4a30ac --- /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.25.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.25.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.25.0 + */ + @NotNull CompoundBinaryTag serializeStyle(final @NotNull Style style); + + /** + * Gets a component serializer for NBT serialization and deserialization. + * + * @return a NBT component serializer + * @since 4.25.0 + */ + static @NotNull NBTComponentSerializer nbt() { + return NBTComponentSerializerImpl.Instances.INSTANCE; + } + + /** + * Creates a new {@link NBTComponentSerializer.Builder}. + * + * @return a builder + * @since 4.25.0 + */ + static @NotNull Builder builder() { + return new NBTComponentSerializerImpl.BuilderImpl(); + } + + /** + * A builder for {@link NBTComponentSerializer}. + * + * @since 4.25.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.25.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.25.0 + */ + @NotNull Builder editOptions(final @NotNull Consumer optionEditor); + + /** + * Builds the serializer. + * + * @return the built serializer + * @since 4.25.0 + */ + @Override + @NotNull NBTComponentSerializer build(); + } + + /** + * A {@link NBTComponentSerializer} service provider. + * + * @since 4.25.0 + */ + @ApiStatus.Internal + @PlatformAPI + interface Provider { + /** + * Provides a standard {@link NBTComponentSerializer}. + * + * @return a {@link NBTComponentSerializer} + * @since 4.25.0 + */ + @ApiStatus.Internal + @PlatformAPI + @NotNull NBTComponentSerializer nbt(); + + /** + * Completes the building process of {@link Builder}. + * + * @return a {@link Consumer} + * @since 4.25.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 0000000000..f2b2835aed --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTComponentSerializerImpl.java @@ -0,0 +1,378 @@ +/* + * 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.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.ObjectComponent; +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_PLAIN; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.NBT_STORAGE; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.OBJECT_PLAYER; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.OBJECT_SPRITE; +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.forEach; +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 BinaryTag extraTag = compound.get(EXTRA); + final List children = new ArrayList<>(); + + if (extraTag != null) { + forEach(extraTag, 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 BinaryTag translateWithTag = compound.get(TRANSLATE_WITH); + final StringBinaryTag fallbackTag = optionalTag(compound, TRANSLATE_FALLBACK, BinaryTagTypes.STRING); + + final List arguments = new ArrayList<>(); + if (translateWithTag != null) { + forEach(translateWithTag, 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 ByteBinaryTag plainTag = optionalTag(compound, NBT_PLAIN, BinaryTagTypes.BYTE); + final boolean plain = plainTag != null && asBoolean(plainTag); + + 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) + .plain(plain) + .separator(separator) + .pos(BlockNBTComponent.Pos.fromString(blockTag.value())) + .style(style) + .append(children) + .build(); + } else if (entityTag != null) { + return Component.entityNBT() + .nbtPath(nbtPath) + .interpret(interpret) + .plain(plain) + .separator(separator) + .selector(entityTag.value()) + .style(style) + .append(children) + .build(); + } else if (storageTag != null) { + return Component.storageNBT() + .nbtPath(nbtPath) + .interpret(interpret) + .plain(plain) + .separator(separator) + .storage(KeySerializer.deserialize(storageTag)) + .style(style) + .append(children) + .build(); + } else { + throw notSureHowToDeserialize(input); + } + } else if (compound.get(OBJECT_SPRITE) != null || compound.get(OBJECT_PLAYER) != null) { + return ObjectComponentSerializer.deserialize(compound, this) + .style(style) + .children(children); + } 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 NBTAggregateCollector translateWithTagBuilder = NBTAggregateCollector.create(this); + arguments.forEach(argument -> translateWithTagBuilder.add(TranslationArgumentSerializer.serialize(argument, this))); + builder.put(TRANSLATE_WITH, translateWithTagBuilder.collect()); + } + } 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); + } + + if (nbt.plain()) { + builder.putBoolean(NBT_PLAIN, 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 if (component instanceof ObjectComponent) { + ObjectComponentSerializer.serialize((ObjectComponent) component, builder, this); + } else { + throw notSureHowToSerialize(component); + } + + final List children = component.children(); + if (!children.isEmpty()) { + final NBTAggregateCollector extraTagBuilder = NBTAggregateCollector.create(this); + children.forEach(child -> extraTagBuilder.add(this.serialize(child))); + builder.put(EXTRA, extraTagBuilder.collect()); + } + + 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 0000000000..350b7864c2 --- /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.25.0 + * @sinceMinecraft 1.20.3 + */ +@ApiStatus.NonExtendable +public interface NBTDataComponentValue extends DataComponentValue { + /** + * The contained element. + * + * @return the contained element + * @since 4.25.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.25.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 0000000000..b842b1587f --- /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 0000000000..c2aeb78373 --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTSerializerOptions.java @@ -0,0 +1,273 @@ +/* + * 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.25.0 + * @sinceMinecraft 1.20.3 + */ +public final class NBTSerializerOptions { + + /** + * Whether to emit shadow colour data. + * + * @since 4.25.0 + * @sinceMinecraft 1.21.4 + */ + public static final Option EMIT_SHADOW_COLOR; + + /** + * Control how hover event values should be emitted. + * + * @since 4.25.0 + */ + public static final Option EMIT_HOVER_EVENT_TYPE; + + /** + * Control how click event values should be emitted. + * + * @since 4.25.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.25.0 + */ + public static final Option EMIT_DEFAULT_ITEM_HOVER_QUANTITY; + + /** + * How to emit show item hovers in {@code hoverEvent} (camelCase) fields. + * + * @since 4.25.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.25.0 + */ + public static final Option EMIT_SHOW_TEXT_HOVER_TEXT_FIELD; + + /** + * Whether to emit array binary tags instead of list binary tags when it's possible. + * + * @since 4.25.0 + */ + public static final Option EMIT_OPTIMIZED_LISTS; + + /** + * Whether to prepend {@code https://} to click event URLs that are missing a scheme. + * + *

As of Minecraft: Java Edition 1.21.5 an {@code open_url} click event url + * will fail to parse if it does not have a {@code http://} or {@code https://} scheme.

+ * + * @since 4.25.0 + * @sinceMinecraft 1.21.5 + */ + public static final Option EMIT_CLICK_URL_HTTPS; + + 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 + private static final int VERSION_25W04A = 4308; // 1.21.5 snapshot + + static { + final OptionSchema.Mutable schema = OptionSchema.emptySchema(); + EMIT_SHADOW_COLOR = schema.booleanOption(key("emit/shadow_color"), true); + 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); + EMIT_OPTIMIZED_LISTS = schema.booleanOption(key("emit/optimized_lists"), false); + EMIT_CLICK_URL_HTTPS = schema.booleanOption(key("emit/click_url_https"), false); + SCHEMA = schema.frozenView(); + + BY_DATA_VERSION = SCHEMA.versionedStateBuilder() + .version( + VERSION_23W40A, + builder -> builder.value(EMIT_SHADOW_COLOR, false) + .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) + .value(EMIT_OPTIMIZED_LISTS, true) + ) + .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(EMIT_SHADOW_COLOR, true) + ) + .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) + .value(EMIT_CLICK_URL_HTTPS, true) + ) + .version( + VERSION_25W03A, + builder -> builder.value(EMIT_SHOW_TEXT_HOVER_TEXT_FIELD, false) + ) + .version( + VERSION_25W04A, + builder -> builder.value(EMIT_OPTIMIZED_LISTS, 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.25.0 + */ + public static @NotNull OptionSchema schema() { + return SCHEMA; + } + + /** + * NBT serializer options delineated by world data version. + * + * @return the versioned option state + * @since 4.25.0 + */ + public static OptionState.@NotNull Versioned byDataVersion() { + return BY_DATA_VERSION; + } + + /** + * Configure how to emit hover event values. + * + * @since 4.25.0 + */ + public enum HoverEventValueMode { + /** + * Only emit the 1.21.5+ hover events using the {@code hover_event} field. + * + * @since 4.25.0 + */ + SNAKE_CASE, + /** + * Only emit the 1.16+ hover events using the {@code hoverEvent} field. + * + * @since 4.25.0 + */ + CAMEL_CASE, + /** + * Include both camel and snake case hover event fields, for maximum compatibility. + * + * @since 4.25.0 + */ + BOTH + } + + /** + * Configure how to emit click event values. + * + * @since 4.25.0 + */ + public enum ClickEventValueMode { + /** + * Only emit the 1.21.5+ click events using the {@code click_event} field. + * + * @since 4.25.0 + */ + SNAKE_CASE, + /** + * Only emit the pre-1.21.5 click events using the {@code clickEvent} field. + * + * @since 4.25.0 + */ + CAMEL_CASE, + /** + * Include both camel and snake case click event fields, for maximum compatibility. + * + * @since 4.25.0 + */ + BOTH, + } + + /** + * Configure how to emit show item hovers in {@code hoverEvent} (camelCase) fields. + * + * @since 4.25.0 + */ + public enum ShowItemHoverDataMode { + /** + * Only emit the pre-1.20.5 item NBT. + * + * @since 4.25.0 + */ + EMIT_LEGACY_NBT, + /** + * Only emit modern data components. + * + * @since 4.25.0 + */ + EMIT_DATA_COMPONENTS, + /** + * Emit whichever of legacy or modern data the item has. + * + * @since 4.25.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 0000000000..e22368be30 --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/NBTSerializerUtils.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 java.io.IOException; +import java.util.function.Consumer; +import net.kyori.adventure.nbt.BinaryTag; +import net.kyori.adventure.nbt.BinaryTagType; +import net.kyori.adventure.nbt.ByteArrayBinaryTag; +import net.kyori.adventure.nbt.ByteBinaryTag; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.nbt.IntArrayBinaryTag; +import net.kyori.adventure.nbt.IntBinaryTag; +import net.kyori.adventure.nbt.ListBinaryTag; +import net.kyori.adventure.nbt.LongArrayBinaryTag; +import net.kyori.adventure.nbt.LongBinaryTag; +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; + } + + static void forEach(final @NotNull BinaryTag tag, final @NotNull Consumer action) { + if (tag instanceof ListBinaryTag) { + ((ListBinaryTag) tag).unwrapHeterogeneity().forEach(action); + } else if (tag instanceof ByteArrayBinaryTag) { + for (final byte value : ((ByteArrayBinaryTag) tag).value()) { + action.accept(ByteBinaryTag.byteBinaryTag(value)); + } + } else if (tag instanceof IntArrayBinaryTag) { + for (final int value : ((IntArrayBinaryTag) tag).value()) { + action.accept(IntBinaryTag.intBinaryTag(value)); + } + } else if (tag instanceof LongArrayBinaryTag) { + for (final long value : ((LongArrayBinaryTag) tag).value()) { + action.accept(LongBinaryTag.longBinaryTag(value)); + } + } else { + throw new IllegalArgumentException("The specified tag (" + tag + ") does not represent an aggregate"); + } + } + + 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/ObjectComponentSerializer.java b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/ObjectComponentSerializer.java new file mode 100644 index 0000000000..1e2ec23c70 --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/ObjectComponentSerializer.java @@ -0,0 +1,184 @@ +/* + * 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.List; +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.ByteBinaryTag; +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.ObjectComponent; +import net.kyori.adventure.text.object.ObjectContents; +import net.kyori.adventure.text.object.PlayerHeadObjectContents; +import net.kyori.adventure.text.object.SpriteObjectContents; +import org.jetbrains.annotations.NotNull; + +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.OBJECT_ATLAS; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.OBJECT_FALLBACK; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.OBJECT_HAT; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.OBJECT_PLAYER; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.OBJECT_PLAYER_ID; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.OBJECT_PLAYER_NAME; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.OBJECT_PLAYER_PROPERTIES; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.OBJECT_PLAYER_TEXTURE; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.OBJECT_SPRITE; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.PROFILE_PROPERTY_NAME; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.PROFILE_PROPERTY_SIGNATURE; +import static net.kyori.adventure.text.serializer.commons.ComponentTreeConstants.PROFILE_PROPERTY_VALUE; +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 ObjectComponentSerializer { + + private ObjectComponentSerializer() { + } + + static @NotNull ObjectComponent deserialize(final @NotNull CompoundBinaryTag compound, + final @NotNull NBTComponentSerializerImpl serializer) { + final ObjectContents contents; + final StringBinaryTag spriteTag = optionalTag(compound, OBJECT_SPRITE, BinaryTagTypes.STRING); + if (spriteTag != null) { + final StringBinaryTag atlasTag = optionalTag(compound, OBJECT_ATLAS, BinaryTagTypes.STRING); + contents = ObjectContents.sprite( + atlasTag == null ? SpriteObjectContents.DEFAULT_ATLAS : KeySerializer.deserialize(atlasTag), + KeySerializer.deserialize(spriteTag) + ); + } else if (compound.get(OBJECT_PLAYER) != null) { + final PlayerHeadObjectContents.Builder playerHead = ObjectContents.playerHead(); + + final BinaryTag playerTag = requiredTag(compound, OBJECT_PLAYER); + if (playerTag instanceof StringBinaryTag) { + playerHead.name(((StringBinaryTag) playerTag).value()); + } else if (playerTag instanceof CompoundBinaryTag) { + final CompoundBinaryTag playerCompound = (CompoundBinaryTag) playerTag; + + final StringBinaryTag nameTag = optionalTag(playerCompound, OBJECT_PLAYER_NAME, BinaryTagTypes.STRING); + if (nameTag != null) playerHead.name(nameTag.value()); + + final BinaryTag idTag = playerCompound.get(OBJECT_PLAYER_ID); + if (idTag != null) playerHead.id(UUIDSerializer.deserialize(idTag)); + + final BinaryTag propertiesTag = playerCompound.get(OBJECT_PLAYER_PROPERTIES); + if (propertiesTag instanceof ListBinaryTag) { + for (final BinaryTag propertyTag : (ListBinaryTag) propertiesTag) { + if (propertyTag instanceof CompoundBinaryTag) { + playerHead.profileProperty(deserializeProperty((CompoundBinaryTag) propertyTag)); + } + } + } + + final StringBinaryTag textureTag = optionalTag(playerCompound, OBJECT_PLAYER_TEXTURE, BinaryTagTypes.STRING); + if (textureTag != null) playerHead.texture(KeySerializer.deserialize(textureTag)); + } else { + throw new IllegalArgumentException("The " + OBJECT_PLAYER + " field must be either a string or a compound tag"); + } + + final ByteBinaryTag hatTag = optionalTag(compound, OBJECT_HAT, BinaryTagTypes.BYTE); + if (hatTag != null) playerHead.hat(asBoolean(hatTag)); + + contents = playerHead.build(); + } else { + throw new IllegalArgumentException("Unable to determine object component contents: neither " + OBJECT_SPRITE + " nor " + OBJECT_PLAYER + " field is present"); + } + + final ObjectComponent.Builder builder = Component.object().contents(contents); + + final BinaryTag fallbackTag = compound.get(OBJECT_FALLBACK); + if (fallbackTag != null) { + builder.fallback(serializer.deserialize(fallbackTag)); + } + + return builder.build(); + } + + static void serialize(final @NotNull ObjectComponent component, + final @NotNull CompoundBinaryTag.Builder builder, + final @NotNull NBTComponentSerializerImpl serializer) { + final Component fallback = component.fallback(); + if (fallback != null) { + builder.put(OBJECT_FALLBACK, serializer.serialize(fallback)); + } + + final ObjectContents contents = component.contents(); + if (contents instanceof SpriteObjectContents) { + final SpriteObjectContents spriteContents = (SpriteObjectContents) contents; + if (!spriteContents.atlas().equals(SpriteObjectContents.DEFAULT_ATLAS)) { + builder.put(OBJECT_ATLAS, KeySerializer.serialize(spriteContents.atlas())); + } + builder.put(OBJECT_SPRITE, KeySerializer.serialize(spriteContents.sprite())); + } else if (contents instanceof PlayerHeadObjectContents) { + final PlayerHeadObjectContents playerHead = (PlayerHeadObjectContents) contents; + + if (playerHead.hat() != PlayerHeadObjectContents.DEFAULT_HAT) { + builder.putBoolean(OBJECT_HAT, playerHead.hat()); + } + + final String playerName = playerHead.name(); + final UUID playerId = playerHead.id(); + final List properties = playerHead.profileProperties(); + final Key texture = playerHead.texture(); + + if (playerName != null && playerId == null && properties.isEmpty() && texture == null) { + builder.putString(OBJECT_PLAYER, playerName); + } else { + final CompoundBinaryTag.Builder playerBuilder = CompoundBinaryTag.builder(); + if (playerName != null) playerBuilder.putString(OBJECT_PLAYER_NAME, playerName); + if (playerId != null) playerBuilder.put(OBJECT_PLAYER_ID, UUIDSerializer.serialize(playerId)); + if (!properties.isEmpty()) { + final ListBinaryTag.Builder propertiesBuilder = ListBinaryTag.builder(BinaryTagTypes.COMPOUND); + for (final PlayerHeadObjectContents.ProfileProperty property : properties) { + propertiesBuilder.add(serializeProperty(property)); + } + playerBuilder.put(OBJECT_PLAYER_PROPERTIES, propertiesBuilder.build()); + } + if (texture != null) playerBuilder.put(OBJECT_PLAYER_TEXTURE, KeySerializer.serialize(texture)); + builder.put(OBJECT_PLAYER, playerBuilder.build()); + } + } else { + throw new IllegalArgumentException("Don't know how to serialize object contents: " + contents); + } + } + + private static PlayerHeadObjectContents.@NotNull ProfileProperty deserializeProperty(final @NotNull CompoundBinaryTag compound) { + final String name = requiredTag(compound, PROFILE_PROPERTY_NAME, BinaryTagTypes.STRING).value(); + final String value = requiredTag(compound, PROFILE_PROPERTY_VALUE, BinaryTagTypes.STRING).value(); + final StringBinaryTag signatureTag = optionalTag(compound, PROFILE_PROPERTY_SIGNATURE, BinaryTagTypes.STRING); + return PlayerHeadObjectContents.property(name, value, signatureTag == null ? null : signatureTag.value()); + } + + private static @NotNull CompoundBinaryTag serializeProperty(final PlayerHeadObjectContents.@NotNull ProfileProperty property) { + final CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder() + .putString(PROFILE_PROPERTY_NAME, property.name()) + .putString(PROFILE_PROPERTY_VALUE, property.value()); + final String signature = property.signature(); + if (signature != null) builder.putString(PROFILE_PROPERTY_SIGNATURE, signature); + return builder.build(); + } +} 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 0000000000..02d5e2f9c8 --- /dev/null +++ b/text-serializer-nbt/src/main/java/net/kyori/adventure/text/serializer/nbt/ShadowColorSerializer.java @@ -0,0 +1,62 @@ +/* + * 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.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) { + return serializer.options().value(NBTSerializerOptions.EMIT_SHADOW_COLOR) ? IntBinaryTag.intBinaryTag(color.value()) : null; + } + + private static int shadowColorComponent(final @NotNull ListBinaryTag tag, final int index) { + return (int) (tag.getFloat(index) * 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 0000000000..dbbcacad31 --- /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 0000000000..ba61a24bc4 --- /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 0000000000..2045dd9f06 --- /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, serializer); + if (clickEventTag != null) { + builder.put(CLICK_EVENT_SNAKE, clickEventTag); + } + } + + if (emitBothClickEvents || emitCamelCaseClickEvent) { + final BinaryTag clickEventTag = ClickEventSerializer.serialize(clickEvent, false, serializer); + 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 0000000000..8921cbd4bb --- /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 0000000000..124ab41910 --- /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 0000000000..974cc4f3b5 --- /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 0000000000..36f5aa92f6 --- /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.25.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 0000000000..5f29119a3f --- /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.25.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 0000000000..ac638fb885 --- /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.25.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 0000000000..17fa37256d --- /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 0000000000..0af07bc878 --- /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 0000000000..8f37179e82 --- /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 0000000000..e179d2e45a --- /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 0000000000..e77c4940a0 --- /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 0000000000..8e23e5282b --- /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 0000000000..e496791de8 --- /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 0000000000..60ec6bc283 --- /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 0000000000..feb9f59460 --- /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 0000000000..7b91908bed --- /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 0000000000..fef9ea30cf --- /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.assertEquals; +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 testShadowColorFloatList() { + assertEquals( + Style.style(ShadowColor.shadowColor(0x80, 0x40, 0xcc, 0xff)), + deserializeStyle( + 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 0000000000..68598a86e1 --- /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 0000000000..d94e640046 --- /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() + ); + } +}