From aef6870dd610ddcdca09d3d254212cd6e1f509b9 Mon Sep 17 00:00:00 2001 From: zml Date: Sun, 17 Mar 2024 20:19:55 -0700 Subject: [PATCH 1/8] fix(text-serializer-gson): Include show item quantity always this matches new Vanilla behavior. --- .../text/serializer/gson/SerializerFactory.java | 2 +- .../text/serializer/gson/ShowItemSerializer.java | 12 ++++++++---- .../text/serializer/json/JSONOptions.java | 14 ++++++++++++++ .../serializer/json/ShowItemSerializerTest.java | 1 + 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/SerializerFactory.java b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/SerializerFactory.java index 784c8febc..7f8e66c93 100644 --- a/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/SerializerFactory.java +++ b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/SerializerFactory.java @@ -80,7 +80,7 @@ public TypeAdapter create(final Gson gson, final TypeToken type) { } else if (HOVER_ACTION_TYPE.isAssignableFrom(rawType)) { return (TypeAdapter) HoverEventActionSerializer.INSTANCE; } else if (SHOW_ITEM_TYPE.isAssignableFrom(rawType)) { - return (TypeAdapter) ShowItemSerializer.create(gson); + return (TypeAdapter) ShowItemSerializer.create(gson, this.features); } else if (SHOW_ENTITY_TYPE.isAssignableFrom(rawType)) { return (TypeAdapter) ShowEntitySerializer.create(gson); } else if (COLOR_WRAPPER_TYPE.isAssignableFrom(rawType)) { 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 7a37e960e..06b0cb3c1 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 @@ -33,6 +33,8 @@ import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.api.BinaryTagHolder; import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.serializer.json.JSONOptions; +import net.kyori.option.OptionState; import org.jetbrains.annotations.Nullable; import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.SHOW_ITEM_COUNT; @@ -40,14 +42,16 @@ import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.SHOW_ITEM_TAG; final class ShowItemSerializer extends TypeAdapter { - static TypeAdapter create(final Gson gson) { - return new ShowItemSerializer(gson).nullSafe(); + static TypeAdapter create(final Gson gson, final OptionState opt) { + return new ShowItemSerializer(gson, opt.value(JSONOptions.EMIT_DEFAULT_ITEM_HOVER_QUANTITY)).nullSafe(); } private final Gson gson; + private final boolean emitDefaultQuantity; - private ShowItemSerializer(final Gson gson) { + private ShowItemSerializer(final Gson gson, final boolean emitDefaultQuantity) { this.gson = gson; + this.emitDefaultQuantity = emitDefaultQuantity; } @Override @@ -96,7 +100,7 @@ public void write(final JsonWriter out, final HoverEvent.ShowItem value) throws this.gson.toJson(value.item(), SerializerFactory.KEY_TYPE, out); final int count = value.count(); - if (count != 1) { + if (count != 1 || this.emitDefaultQuantity) { out.name(SHOW_ITEM_COUNT); out.value(count); } diff --git a/text-serializer-json/src/main/java/net/kyori/adventure/text/serializer/json/JSONOptions.java b/text-serializer-json/src/main/java/net/kyori/adventure/text/serializer/json/JSONOptions.java index 0653d195c..185d012ce 100644 --- a/text-serializer-json/src/main/java/net/kyori/adventure/text/serializer/json/JSONOptions.java +++ b/text-serializer-json/src/main/java/net/kyori/adventure/text/serializer/json/JSONOptions.java @@ -41,6 +41,7 @@ private JSONOptions() { private static final int VERSION_INITIAL = 0; private static final int VERSION_1_16 = 2526; // 20w16a private static final int VERSION_1_20_3 = 3679; // 23w40a + private static final int VERSION_1_20_5 = 3819; // 24w09a /** * Whether to emit RGB text. @@ -82,6 +83,14 @@ private JSONOptions() { * @since 4.15.0 */ public static final Option VALIDATE_STRICT_EVENTS = Option.booleanOption(key("validate/strict_events"), true); + /** + * 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.17.0 + */ + public static final Option EMIT_DEFAULT_ITEM_HOVER_QUANTITY = Option.booleanOption(key("emit/default_item_hover_quantity"), true); /** * Versioned by world data version. @@ -93,6 +102,7 @@ private JSONOptions() { .value(EMIT_RGB, false) .value(EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, false) .value(VALIDATE_STRICT_EVENTS, false) + .value(EMIT_DEFAULT_ITEM_HOVER_QUANTITY, false) ) .version( VERSION_1_16, @@ -105,6 +115,10 @@ private JSONOptions() { .value(EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, true) .value(VALIDATE_STRICT_EVENTS, true) ) + .version( + VERSION_1_20_5, + b -> b.value(EMIT_DEFAULT_ITEM_HOVER_QUANTITY, true) + ) .build(); /** diff --git a/text-serializer-json/src/testFixtures/java/net/kyori/adventure/text/serializer/json/ShowItemSerializerTest.java b/text-serializer-json/src/testFixtures/java/net/kyori/adventure/text/serializer/json/ShowItemSerializerTest.java index 44d9a33dc..53e471880 100644 --- a/text-serializer-json/src/testFixtures/java/net/kyori/adventure/text/serializer/json/ShowItemSerializerTest.java +++ b/text-serializer-json/src/testFixtures/java/net/kyori/adventure/text/serializer/json/ShowItemSerializerTest.java @@ -107,6 +107,7 @@ void testDeserializeWithCountOfOne() throws IOException { hover.addProperty(JSONComponentConstants.HOVER_EVENT_ACTION, name(HoverEvent.Action.SHOW_ITEM)); hover.add(JSONComponentConstants.HOVER_EVENT_CONTENTS, object(contents -> { contents.addProperty(JSONComponentConstants.SHOW_ITEM_ID, "minecraft:diamond"); + contents.addProperty(JSONComponentConstants.SHOW_ITEM_COUNT, 1); contents.addProperty(JSONComponentConstants.SHOW_ITEM_TAG, "{display:{Name:\"A test!\"}}"); })); })); From 0d79ad7dacb6a801eb307102e8570a475f348906 Mon Sep 17 00:00:00 2001 From: zml Date: Sun, 14 Apr 2024 18:05:28 -0700 Subject: [PATCH 2/8] feat(api): item data components these still need serializer integration in order to be useful we should support working with both platform types and serializer types of data what is the destiny of BinaryTagHolder? --- .../adventure/text/event/HoverEvent.java | 113 ++++++++++-- .../adventure/text/event/ItemDataHolder.java | 39 ++++ .../flattener/ComponentFlattenerImpl.java | 98 ++-------- .../adventure/util/InheritanceAwareMap.java | 174 ++++++++++++++++++ .../util/InheritanceAwareMapImpl.java | 162 ++++++++++++++++ .../adventure/text/event/HoverEventTest.java | 5 +- 6 files changed, 492 insertions(+), 99 deletions(-) create mode 100644 api/src/main/java/net/kyori/adventure/text/event/ItemDataHolder.java create mode 100644 api/src/main/java/net/kyori/adventure/util/InheritanceAwareMap.java create mode 100644 api/src/main/java/net/kyori/adventure/util/InheritanceAwareMapImpl.java diff --git a/api/src/main/java/net/kyori/adventure/text/event/HoverEvent.java b/api/src/main/java/net/kyori/adventure/text/event/HoverEvent.java index 02c572db7..b70b8c5f4 100644 --- a/api/src/main/java/net/kyori/adventure/text/event/HoverEvent.java +++ b/api/src/main/java/net/kyori/adventure/text/event/HoverEvent.java @@ -23,6 +23,9 @@ */ package net.kyori.adventure.text.event; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.function.UnaryOperator; @@ -87,7 +90,7 @@ public final class HoverEvent implements Examinable, HoverEventSource, Sty * @since 4.0.0 */ public static @NotNull HoverEvent showItem(final @NotNull Key item, final @Range(from = 0, to = Integer.MAX_VALUE) int count) { - return showItem(item, count, null); + return showItem(item, count, Collections.emptyMap()); } /** @@ -99,7 +102,7 @@ public final class HoverEvent implements Examinable, HoverEventSource, Sty * @since 4.6.0 */ public static @NotNull HoverEvent showItem(final @NotNull Keyed item, final @Range(from = 0, to = Integer.MAX_VALUE) int count) { - return showItem(item, count, null); + return showItem(item, count, Collections.emptyMap()); } /** @@ -111,8 +114,9 @@ public final class HoverEvent implements Examinable, HoverEventSource, Sty * @return a hover event * @since 4.0.0 */ + @Deprecated public static @NotNull HoverEvent showItem(final @NotNull Key item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @Nullable BinaryTagHolder nbt) { - return showItem(ShowItem.of(item, count, nbt)); + return showItem(ShowItem.showItem(item, count, nbt)); } /** @@ -123,9 +127,24 @@ public final class HoverEvent implements Examinable, HoverEventSource, Sty * @param nbt the nbt * @return a hover event * @since 4.6.0 + * @deprecated since Minecraft 1.20.5 and replaced with data components, not scheduled for removal */ + @Deprecated public static @NotNull HoverEvent showItem(final @NotNull Keyed item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @Nullable BinaryTagHolder nbt) { - return showItem(ShowItem.of(item, count, nbt)); + return showItem(ShowItem.showItem(item, count, nbt)); + } + + /** + * Creates a hover event that shows an item on hover. + * + * @param item the item + * @param count the count + * @param dataComponents the data components + * @return a hover event + * @since 4.17.0 + */ + public static @NotNull HoverEvent showItem(final @NotNull Keyed item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @NotNull Map dataComponents) { + return showItem(ShowItem.showItem(item, count, dataComponents)); } /** @@ -216,7 +235,7 @@ public final class HoverEvent implements Examinable, HoverEventSource, Sty * @param value the achievement value * @return a hover event * @since 4.14.0 - * @deprecated Removed in Vanilla 1.12, but we keep it for backwards compat + * @deprecated Removed in Vanilla 1.12, but we keep it for backwards compatibility */ @Deprecated public static @NotNull HoverEvent showAchievement(final @NotNull String value) { @@ -344,6 +363,7 @@ public static final class ShowItem implements Examinable { private final Key item; private final int count; private final @Nullable BinaryTagHolder nbt; + private final Map dataComponents; /** * Creates. @@ -354,7 +374,7 @@ public static final class ShowItem implements Examinable { * @since 4.14.0 */ public static @NotNull ShowItem showItem(final @NotNull Key item, final @Range(from = 0, to = Integer.MAX_VALUE) int count) { - return showItem(item, count, null); + return showItem(item, count, Collections.emptyMap()); } /** @@ -369,7 +389,7 @@ public static final class ShowItem implements Examinable { @Deprecated @ApiStatus.ScheduledForRemoval(inVersion = "5.0.0") public static @NotNull ShowItem of(final @NotNull Key item, final @Range(from = 0, to = Integer.MAX_VALUE) int count) { - return of(item, count, null); + return showItem(item, count, Collections.emptyMap()); } /** @@ -381,7 +401,7 @@ public static final class ShowItem implements Examinable { * @since 4.14.0 */ public static @NotNull ShowItem showItem(final @NotNull Keyed item, final @Range(from = 0, to = Integer.MAX_VALUE) int count) { - return showItem(item, count, null); + return showItem(item, count, Collections.emptyMap()); } /** @@ -407,9 +427,11 @@ public static final class ShowItem implements Examinable { * @param nbt the nbt * @return a {@code ShowItem} * @since 4.14.0 + * @deprecated since Minecraft 1.20.5 and replaced with data components, not scheduled for removal */ + @Deprecated public static @NotNull ShowItem showItem(final @NotNull Key item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @Nullable BinaryTagHolder nbt) { - return new ShowItem(requireNonNull(item, "item"), count, nbt); + return new ShowItem(requireNonNull(item, "item"), count, nbt, Collections.emptyMap()); } /** @@ -425,7 +447,7 @@ public static final class ShowItem implements Examinable { @Deprecated @ApiStatus.ScheduledForRemoval(inVersion = "5.0.0") public static @NotNull ShowItem of(final @NotNull Key item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @Nullable BinaryTagHolder nbt) { - return new ShowItem(requireNonNull(item, "item"), count, nbt); + return new ShowItem(requireNonNull(item, "item"), count, nbt, Collections.emptyMap()); } /** @@ -436,9 +458,11 @@ public static final class ShowItem implements Examinable { * @param nbt the nbt * @return a {@code ShowItem} * @since 4.14.0 + * @deprecated since Minecraft 1.20.5 and replaced with data components, not scheduled for removal */ + @Deprecated public static @NotNull ShowItem showItem(final @NotNull Keyed item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @Nullable BinaryTagHolder nbt) { - return new ShowItem(requireNonNull(item, "item").key(), count, nbt); + return new ShowItem(requireNonNull(item, "item").key(), count, nbt, Collections.emptyMap()); } /** @@ -454,13 +478,28 @@ public static final class ShowItem implements Examinable { @Deprecated @ApiStatus.ScheduledForRemoval(inVersion = "5.0.0") public static @NotNull ShowItem of(final @NotNull Keyed item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @Nullable BinaryTagHolder nbt) { - return new ShowItem(requireNonNull(item, "item").key(), count, nbt); + return new ShowItem(requireNonNull(item, "item").key(), count, nbt, Collections.emptyMap()); } - private ShowItem(final @NotNull Key item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @Nullable BinaryTagHolder nbt) { + /** + * Creates. + * + * @param item the item + * @param count the count + * @param dataComponents the data components + * @return a {@code ShowItem} + * @since 4.17.0 + * @sinceMinecraft 1.20.5 + */ + public static @NotNull ShowItem showItem(final @NotNull Keyed item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @NotNull Map dataComponents) { + return new ShowItem(requireNonNull(item, "item").key(), count, null, dataComponents); + } + + private ShowItem(final @NotNull Key item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @Nullable BinaryTagHolder nbt, final @NotNull Map dataComponents) { this.item = item; this.count = count; this.nbt = nbt; + this.dataComponents = Collections.unmodifiableMap(new HashMap<>(dataComponents)); } /** @@ -482,7 +521,7 @@ private ShowItem(final @NotNull Key item, final @Range(from = 0, to = Integer.MA */ public @NotNull ShowItem item(final @NotNull Key item) { if (requireNonNull(item, "item").equals(this.item)) return this; - return new ShowItem(item, this.count, this.nbt); + return new ShowItem(item, this.count, this.nbt, this.dataComponents); } /** @@ -504,15 +543,19 @@ private ShowItem(final @NotNull Key item, final @Range(from = 0, to = Integer.MA */ public @NotNull ShowItem count(final @Range(from = 0, to = Integer.MAX_VALUE) int count) { if (count == this.count) return this; - return new ShowItem(this.item, count, this.nbt); + return new ShowItem(this.item, count, this.nbt, this.dataComponents); } /** * Gets the nbt. * + *

If there are data components on this item, it will never have NBT data.

+ * * @return the nbt * @since 4.0.0 + * @deprecated since Minecraft 1.20.5 and replaced with data components, not scheduled for removal */ + @Deprecated public @Nullable BinaryTagHolder nbt() { return this.nbt; } @@ -520,13 +563,45 @@ private ShowItem(final @NotNull Key item, final @Range(from = 0, to = Integer.MA /** * Sets the nbt. * + *

This will clear any modern data components set on the item.

+ * * @param nbt the nbt * @return a {@code ShowItem} * @since 4.0.0 + * @deprecated since Minecraft 1.20.5 and replaced with data components, not scheduled for removal */ + @Deprecated public @NotNull ShowItem nbt(final @Nullable BinaryTagHolder nbt) { if (Objects.equals(nbt, this.nbt)) return this; - return new ShowItem(this.item, this.count, nbt); + return new ShowItem(this.item, this.count, nbt, Collections.emptyMap()); + } + + /** + * Get the data components used for this item. + * + *

If there is NBT data on this item, it will never have any data components set.

+ * + * @return an unmodifiable map of data components + * @since 4.17.0 + * @sinceMinecraft 1.20.5 + */ + public @NotNull Map dataComponents() { + return this.dataComponents; + } + + /** + * Set the data components used on this item. + * + *

This will clear any legacy nbt-format data on the item.

+ * + * @param holder the new data components to set + * @return a show item data object that has the provided components + * @since 4.17.0 + * @sinceMinecraft 1.20.5 + */ + public @NotNull ShowItem dataComponents(final @NotNull Map holder) { + if (Objects.equals(this.dataComponents, holder)) return this; + return new ShowItem(this.item, this.count, null, holder.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(new HashMap<>(holder))); } @Override @@ -534,7 +609,7 @@ public boolean equals(final @Nullable Object other) { if (this == other) return true; if (other == null || this.getClass() != other.getClass()) return false; final ShowItem that = (ShowItem) other; - return this.item.equals(that.item) && this.count == that.count && Objects.equals(this.nbt, that.nbt); + return this.item.equals(that.item) && this.count == that.count && Objects.equals(this.nbt, that.nbt) && Objects.equals(this.dataComponents, that.dataComponents); } @Override @@ -542,6 +617,7 @@ public int hashCode() { int result = this.item.hashCode(); result = (31 * result) + Integer.hashCode(this.count); result = (31 * result) + Objects.hashCode(this.nbt); + result = (31 * result) + Objects.hashCode(this.dataComponents); return result; } @@ -550,7 +626,8 @@ public int hashCode() { return Stream.of( ExaminableProperty.of("item", this.item), ExaminableProperty.of("count", this.count), - ExaminableProperty.of("nbt", this.nbt) + ExaminableProperty.of("nbt", this.nbt), + ExaminableProperty.of("dataComponents", this.dataComponents) ); } diff --git a/api/src/main/java/net/kyori/adventure/text/event/ItemDataHolder.java b/api/src/main/java/net/kyori/adventure/text/event/ItemDataHolder.java new file mode 100644 index 000000000..79a8b1131 --- /dev/null +++ b/api/src/main/java/net/kyori/adventure/text/event/ItemDataHolder.java @@ -0,0 +1,39 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2024 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.event; + +import net.kyori.examination.Examinable; + +/** + * A holder for the value of an item's data component. + * + *

The exact value is platform-specific. Serializers may provide their + * own implementations as well, and any logic to serialize or deserialize + * should be done per-serializer.

+ * + * @since 4.17.0 + * @sinceMinecraft 1.20.5 + */ +public interface ItemDataHolder extends Examinable { +} diff --git a/api/src/main/java/net/kyori/adventure/text/flattener/ComponentFlattenerImpl.java b/api/src/main/java/net/kyori/adventure/text/flattener/ComponentFlattenerImpl.java index 92e25b3a3..aadbf7eed 100644 --- a/api/src/main/java/net/kyori/adventure/text/flattener/ComponentFlattenerImpl.java +++ b/api/src/main/java/net/kyori/adventure/text/flattener/ComponentFlattenerImpl.java @@ -23,11 +23,6 @@ */ package net.kyori.adventure.text.flattener; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; @@ -38,6 +33,7 @@ import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.util.InheritanceAwareMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -66,14 +62,11 @@ final class ComponentFlattenerImpl implements ComponentFlattener { private static final int MAX_DEPTH = 512; - private final Map, Function> flatteners; - private final Map, BiConsumer>> complexFlatteners; - private final ConcurrentMap, Handler> propagatedFlatteners = new ConcurrentHashMap<>(); + private final InheritanceAwareMap flatteners; private final Function unknownHandler; - ComponentFlattenerImpl(final Map, Function> flatteners, final Map, BiConsumer>> complexFlatteners, final @Nullable Function unknownHandler) { - this.flatteners = Collections.unmodifiableMap(new HashMap<>(flatteners)); - this.complexFlatteners = Collections.unmodifiableMap(new HashMap<>(complexFlatteners)); + ComponentFlattenerImpl(final InheritanceAwareMap flatteners, final @Nullable Function unknownHandler) { + this.flatteners = flatteners; this.unknownHandler = unknownHandler; } @@ -96,7 +89,7 @@ private void flatten0(final @NotNull Component input, final @NotNull FlattenerLi listener.pushStyle(inputStyle); try { if (flattener != null) { - flattener.handle(input, listener, depth + 1); + flattener.handle(this, input, listener, depth + 1); } if (!input.children().isEmpty() && listener.shouldContinue()) { @@ -109,34 +102,11 @@ private void flatten0(final @NotNull Component input, final @NotNull FlattenerLi } } - @SuppressWarnings("unchecked") private @Nullable Handler flattener(final T test) { - final Handler flattener = this.propagatedFlatteners.computeIfAbsent(test.getClass(), key -> { - // direct flatteners (just return strings) - final @Nullable Function value = (Function) this.flatteners.get(key); - if (value != null) return (component, listener, depth) -> listener.component(value.apply(component)); - - for (final Map.Entry, Function> entry : this.flatteners.entrySet()) { - if (entry.getKey().isAssignableFrom(key)) { - return (component, listener, depth) -> listener.component(((Function) entry.getValue()).apply(component)); - } - } - - // complex flatteners (these provide extra components) - final @Nullable BiConsumer> complexValue = (BiConsumer>) this.complexFlatteners.get(key); - if (complexValue != null) return (component, listener, depth) -> complexValue.accept(component, c -> this.flatten0(c, listener, depth)); - - for (final Map.Entry, BiConsumer>> entry : this.complexFlatteners.entrySet()) { - if (entry.getKey().isAssignableFrom(key)) { - return (component, listener, depth) -> ((BiConsumer>) entry.getValue()).accept(component, c -> this.flatten0(c, listener, depth)); - } - } + final Handler flattener = this.flatteners.get(test.getClass()); - return Handler.NONE; - }); - - if (flattener == Handler.NONE) { - return this.unknownHandler == null ? null : (component, listener, depth) -> listener.component(this.unknownHandler.apply(component)); + if (flattener == null && this.unknownHandler != null) { + return (self, component, listener, depth) -> listener.component(this.unknownHandler.apply(component)); } else { return flattener; } @@ -144,77 +114,47 @@ private void flatten0(final @NotNull Component input, final @NotNull FlattenerLi @Override public ComponentFlattener.@NotNull Builder toBuilder() { - return new BuilderImpl(this.flatteners, this.complexFlatteners, this.unknownHandler); + return new BuilderImpl(this.flatteners, this.unknownHandler); } // A function that allows nesting other flatten operations @FunctionalInterface interface Handler { - Handler NONE = (input, listener, depth) -> {}; - - void handle(final Component input, final FlattenerListener listener, final int depth); + void handle(final ComponentFlattenerImpl self, final Component input, final FlattenerListener listener, final int depth); } static final class BuilderImpl implements Builder { - private final Map, Function> flatteners; - private final Map, BiConsumer>> complexFlatteners; + private final InheritanceAwareMap.Builder flatteners; private @Nullable Function unknownHandler; BuilderImpl() { - this.flatteners = new HashMap<>(); - this.complexFlatteners = new HashMap<>(); + this.flatteners = InheritanceAwareMap.builder().strict(true); } - BuilderImpl(final Map, Function> flatteners, final Map, BiConsumer>> complexFlatteners, final @Nullable Function unknownHandler) { - this.flatteners = new HashMap<>(flatteners); - this.complexFlatteners = new HashMap<>(complexFlatteners); + BuilderImpl(final InheritanceAwareMap flatteners, final @Nullable Function unknownHandler) { + this.flatteners = InheritanceAwareMap.builder(flatteners).strict(true); this.unknownHandler = unknownHandler; } @Override public @NotNull ComponentFlattener build() { - return new ComponentFlattenerImpl(this.flatteners, this.complexFlatteners, this.unknownHandler); + return new ComponentFlattenerImpl(this.flatteners.build(), this.unknownHandler); } @Override + @SuppressWarnings("unchecked") public ComponentFlattener.@NotNull Builder mapper(final @NotNull Class type, final @NotNull Function converter) { - this.validateNoneInHierarchy(requireNonNull(type, "type")); - this.flatteners.put( - type, - requireNonNull(converter, "converter") - ); - this.complexFlatteners.remove(type); + this.flatteners.put(type, (self, component, listener, depth) -> listener.component(converter.apply((T) component))); return this; } @Override + @SuppressWarnings("unchecked") public ComponentFlattener.@NotNull Builder complexMapper(final @NotNull Class type, final @NotNull BiConsumer> converter) { - this.validateNoneInHierarchy(requireNonNull(type, "type")); - this.complexFlatteners.put( - type, - requireNonNull(converter, "converter") - ); - this.flatteners.remove(type); + this.flatteners.put(type, (self, component, listener, depth) -> converter.accept((T) component, c -> self.flatten0(c, listener, depth))); return this; } - private void validateNoneInHierarchy(final Class beingRegistered) { - for (final Class clazz : this.flatteners.keySet()) { - testHierarchy(clazz, beingRegistered); - } - - for (final Class clazz : this.complexFlatteners.keySet()) { - testHierarchy(clazz, beingRegistered); - } - } - - private static void testHierarchy(final Class existing, final Class beingRegistered) { - if (!existing.equals(beingRegistered) && (existing.isAssignableFrom(beingRegistered) || beingRegistered.isAssignableFrom(existing))) { - throw new IllegalArgumentException("Conflict detected between already registered type " + existing - + " and newly registered type " + beingRegistered + "! Types in a component flattener must not share a common hierarchy!"); - } - } - @Override public ComponentFlattener.@NotNull Builder unknownMapper(final @Nullable Function converter) { this.unknownHandler = converter; diff --git a/api/src/main/java/net/kyori/adventure/util/InheritanceAwareMap.java b/api/src/main/java/net/kyori/adventure/util/InheritanceAwareMap.java new file mode 100644 index 000000000..8bf0cc1da --- /dev/null +++ b/api/src/main/java/net/kyori/adventure/util/InheritanceAwareMap.java @@ -0,0 +1,174 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2024 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.util; + +import net.kyori.adventure.builder.AbstractBuilder; +import org.jetbrains.annotations.CheckReturnValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A map type that will traverse class hierarchy to find a value for a key. + * + *

These maps are null-hostile, so both keys and values must not be null.

+ * + *

There is a concept of strict mode, where map values have to be strictly non-ambiguous. + * When this enabled (by default it is not), a value will not be added if any subtypes or supertypes are already registered to the map.

+ * + *

Inheritance aware maps are always immutable, so any mutation operations will apply any changes to a new, modified instance.

+ * + * @param the base class type + * @param the value type + * @since 4.17.0 + */ +public interface InheritanceAwareMap { + /** + * Get an empty inheritance aware map. + * + * @param class type upper bound + * @param value type + * @return the map + * @since 4.17.0 + */ + @SuppressWarnings("unchecked") + static @NotNull InheritanceAwareMap empty() { + return InheritanceAwareMapImpl.EMPTY; + } + + /** + * Create a new builder for an inheritance aware map. + * + * @param class type upper bound + * @param value type + * @return a new builder + * @since 4.17.0 + */ + static InheritanceAwareMap.@NotNull Builder builder() { + return new InheritanceAwareMapImpl.BuilderImpl<>(); + } + + /** + * Create a new builder for an inheritance aware map. + * + * @param class type upper bound + * @param value type + * @param existing the existing map to populate the builder with + * @return a new builder + * @since 4.17.0 + */ + static InheritanceAwareMap.@NotNull Builder builder(final InheritanceAwareMap existing) { + return new InheritanceAwareMapImpl.BuilderImpl() + .putAll(existing); + } + + /** + * Check whether this map contains a value (direct or computed) for the provided class. + * + * @param clazz the class type to check + * @return whether such a value is present + * @since 4.17.0 + */ + boolean containsKey(final @NotNull Class clazz); + + /** + * Get the applicable value for the provided class. + * + *

This can be either a direct or inherited value.

+ * + * @param clazz the class type + * @return the value, if any is available + * @since 4.17.0 + */ + @Nullable V get(final @NotNull Class clazz); + + /** + * Get an updated inheritance aware map with the provided key changed. + * + * @param clazz the class type + * @param value the value to update to + * @return the updated map + * @since 4.17.0 + */ + @CheckReturnValue + @NotNull InheritanceAwareMap with(final @NotNull Class clazz, final @NotNull V value); + + /** + * Get an updated inheritance aware map with the provided key removed. + * + * @param clazz the class type to remove a direct value for + * @return the updated map + * @since 4.17.0 + */ + @CheckReturnValue + @NotNull InheritanceAwareMap without(final @NotNull Class clazz); + + /** + * A builder for inheritance-aware maps. + * + * @param the class type + * @param the value type + * @since 4.17.0 + */ + interface Builder extends AbstractBuilder> { + /** + * Set strict mode for this builder. + * + *

If this builder has values from when it was not in strict mode, all previous values will be re-validated for any hierarchy ambiguities.

+ * + * @param strict whether to enable strict mode. + * @return this builder + * @since 4.17.0 + */ + @NotNull Builder strict(final boolean strict); + + /** + * Put another value in this map. + * + * @param clazz the class type + * @param value the value for the provided type and any subtypes + * @return this builder + * @since 4.17.0 + */ + @NotNull Builder put(final @NotNull Class clazz, final @NotNull V value); + + /** + * Remove a value in this map. + * + * @param clazz the class type + * @return this builder + * @since 4.17.0 + */ + @NotNull Builder remove(final @NotNull Class clazz); + + /** + * Put values from an existing inheritance-aware map into this map. + * + * @param map the existing map + * @return this builder + * @since 4.17.0 + */ + @NotNull Builder putAll(final @NotNull InheritanceAwareMap map); + } + +} diff --git a/api/src/main/java/net/kyori/adventure/util/InheritanceAwareMapImpl.java b/api/src/main/java/net/kyori/adventure/util/InheritanceAwareMapImpl.java new file mode 100644 index 000000000..e114532f0 --- /dev/null +++ b/api/src/main/java/net/kyori/adventure/util/InheritanceAwareMapImpl.java @@ -0,0 +1,162 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2024 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.util; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static java.util.Objects.requireNonNull; + +final class InheritanceAwareMapImpl implements InheritanceAwareMap { + private static final Object NONE = new Object(); // null sentinel for CHM + @SuppressWarnings({"rawtypes", "unchecked"}) + static final InheritanceAwareMapImpl EMPTY = new InheritanceAwareMapImpl(false, Collections.emptyMap()); + + private final Map, V> declaredValues; + private final boolean strict; + private transient final ConcurrentMap, Object> cache = new ConcurrentHashMap<>(); + + InheritanceAwareMapImpl(final boolean strict, final Map, V> declaredValues) { + this.strict = strict; + this.declaredValues = declaredValues; + } + + @Override + public boolean containsKey(final @NotNull Class clazz) { + return this.get(clazz) != null; + } + + @Override + @SuppressWarnings("unchecked") + public @Nullable V get(final @NotNull Class clazz) { + final Object ret = this.cache.computeIfAbsent(clazz, c -> { + final @Nullable V value = this.declaredValues.get(c); + if (value != null) return value; + + for (final Map.Entry, V> entry : this.declaredValues.entrySet()) { + if (entry.getKey().isAssignableFrom(c)) { + return entry.getValue(); + } + } + + return NONE; + }); + + return ret == NONE ? null : (V) ret; + } + + @Override + public @NotNull InheritanceAwareMap with(final @NotNull Class clazz, final @NotNull V value) { + if (Objects.equals(this.declaredValues.get(clazz), value)) return this; + if (this.strict) validateNoneInHierarchy(clazz, this.declaredValues); + + final Map, V> newValues = new LinkedHashMap<>(this.declaredValues); + newValues.put(clazz, value); + return new InheritanceAwareMapImpl<>(this.strict, Collections.unmodifiableMap(newValues)); + } + + @Override + public @NotNull InheritanceAwareMap without(final @NotNull Class clazz) { + if (!this.declaredValues.containsKey(clazz)) return this; + + final Map, V> newValues = new LinkedHashMap<>(this.declaredValues); + newValues.remove(clazz); + return new InheritanceAwareMapImpl<>(this.strict, Collections.unmodifiableMap(newValues)); + } + + static final class BuilderImpl implements Builder { + private boolean strict; + private final Map, V> values = new LinkedHashMap<>(); + + @Override + public @NotNull InheritanceAwareMap build() { + return new InheritanceAwareMapImpl<>(this.strict, Collections.unmodifiableMap(new LinkedHashMap<>(this.values))); + } + + @Override + public @NotNull Builder strict(final boolean strict) { + if (strict && !this.strict) { // re-validate contents + for (final Class clazz : this.values.keySet()) { + validateNoneInHierarchy(clazz, this.values); + } + } + this.strict = strict; + return this; + } + + @Override + public @NotNull Builder put(final @NotNull Class clazz, final @NotNull V value) { + if (this.strict) validateNoneInHierarchy(clazz, this.values); + this.values.put( + requireNonNull(clazz, "clazz"), + requireNonNull(value, "value") + ); + return this; + } + + @Override + public @NotNull Builder remove(final @NotNull Class clazz) { + this.values.remove(requireNonNull(clazz, "clazz")); + return this; + } + + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public @NotNull Builder putAll(final @NotNull InheritanceAwareMap map) { + final InheritanceAwareMapImpl impl = (InheritanceAwareMapImpl) map; + if (this.strict) { + if (!this.values.isEmpty() || !impl.strict) { // validate all + for (final Map.Entry, V> entry : impl.declaredValues.entrySet()) { + validateNoneInHierarchy(entry.getKey(), this.values); + this.values.put((Class) entry.getKey(), entry.getValue()); + } + return this; + } + } + + // otherwise (simpler) + this.values.putAll((Map) impl.declaredValues); + return this; + } + } + + private static void validateNoneInHierarchy(final Class beingRegistered, final Map, ?> entries) { + for (final Class clazz : entries.keySet()) { + testHierarchy(clazz, beingRegistered); + } + } + + private static void testHierarchy(final Class existing, final Class beingRegistered) { + if (!existing.equals(beingRegistered) && (existing.isAssignableFrom(beingRegistered) || beingRegistered.isAssignableFrom(existing))) { + throw new IllegalArgumentException("Conflict detected between already registered type " + existing + + " and newly registered type " + beingRegistered + "! Types in a strict inheritance-aware map must not share a common hierarchy!"); + } + } +} diff --git a/api/src/test/java/net/kyori/adventure/text/event/HoverEventTest.java b/api/src/test/java/net/kyori/adventure/text/event/HoverEventTest.java index e5d429932..19f69b8b8 100644 --- a/api/src/test/java/net/kyori/adventure/text/event/HoverEventTest.java +++ b/api/src/test/java/net/kyori/adventure/text/event/HoverEventTest.java @@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.testing.EqualsTester; +import java.util.Collections; import java.util.UUID; import java.util.function.UnaryOperator; import net.kyori.adventure.key.Key; @@ -113,8 +114,8 @@ void testEquality() { HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.empty()) ) .addEqualityGroup( - HoverEvent.showItem(HoverEvent.ShowItem.showItem(Key.key("air"), 1, null)), - HoverEvent.hoverEvent(HoverEvent.Action.SHOW_ITEM, HoverEvent.ShowItem.showItem(Key.key("air"), 1, null)) + HoverEvent.showItem(HoverEvent.ShowItem.showItem(Key.key("air"), 1, Collections.emptyMap())), + HoverEvent.hoverEvent(HoverEvent.Action.SHOW_ITEM, HoverEvent.ShowItem.showItem(Key.key("air"), 1, Collections.emptyMap())) ) .addEqualityGroup( HoverEvent.showEntity(HoverEvent.ShowEntity.showEntity(Key.key("cat"), entity)), From 8b97c5cfb82f5112b7a591ecec47694f86850416 Mon Sep 17 00:00:00 2001 From: zml Date: Fri, 19 Apr 2024 19:10:25 -0700 Subject: [PATCH 3/8] feat(api): sketch out more data component handling this should be a system that can function, just needs polishing and testing --- .../adventure/nbt/api/BinaryTagHolder.java | 8 +- .../text/event/DataComponentValue.java | 80 ++++++ .../DataComponentValueConversionImpl.java | 90 ++++++ .../DataComponentValueConverterRegistry.java | 259 ++++++++++++++++++ .../adventure/text/event/HoverEvent.java | 34 ++- ...ava => RemovedDataComponentValueImpl.java} | 15 +- .../minimessage/tag/standard/HoverTag.java | 50 +++- .../gson/GsonDataComponentValue.java | 65 +++++ .../gson/GsonDataComponentValueImpl.java | 80 ++++++ .../serializer/gson/ShowItemSerializer.java | 48 +++- .../json/JSONComponentConstants.java | 3 +- .../json/ShowItemSerializerTest.java | 3 +- 12 files changed, 706 insertions(+), 29 deletions(-) create mode 100644 api/src/main/java/net/kyori/adventure/text/event/DataComponentValue.java create mode 100644 api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConversionImpl.java create mode 100644 api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java rename api/src/main/java/net/kyori/adventure/text/event/{ItemDataHolder.java => RemovedDataComponentValueImpl.java} (75%) create mode 100644 text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/GsonDataComponentValue.java create mode 100644 text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/GsonDataComponentValueImpl.java diff --git a/api/src/main/java/net/kyori/adventure/nbt/api/BinaryTagHolder.java b/api/src/main/java/net/kyori/adventure/nbt/api/BinaryTagHolder.java index 030247c2d..4922c888a 100644 --- a/api/src/main/java/net/kyori/adventure/nbt/api/BinaryTagHolder.java +++ b/api/src/main/java/net/kyori/adventure/nbt/api/BinaryTagHolder.java @@ -23,6 +23,7 @@ */ package net.kyori.adventure.nbt.api; +import net.kyori.adventure.text.event.DataComponentValue; import net.kyori.adventure.util.Codec; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -37,7 +38,7 @@ * * @since 4.0.0 */ -public interface BinaryTagHolder { +public interface BinaryTagHolder extends DataComponentValue.TagSerializable { /** * Encodes {@code nbt} using {@code codec}. * @@ -86,6 +87,11 @@ public interface BinaryTagHolder { */ @NotNull String string(); + @Override + default @NotNull BinaryTagHolder asBinaryTag() { + return this; + } + /** * Gets the held value as a binary tag. * diff --git a/api/src/main/java/net/kyori/adventure/text/event/DataComponentValue.java b/api/src/main/java/net/kyori/adventure/text/event/DataComponentValue.java new file mode 100644 index 000000000..911ea6096 --- /dev/null +++ b/api/src/main/java/net/kyori/adventure/text/event/DataComponentValue.java @@ -0,0 +1,80 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2024 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.event; + +import net.kyori.adventure.nbt.api.BinaryTagHolder; +import net.kyori.examination.Examinable; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * A holder for the value of an item's data component. + * + *

The exact value is platform-specific. Serializers may provide their + * own implementations as well, and any logic to serialize or deserialize + * should be done per-serializer.

+ * + * @since 4.17.0 + * @sinceMinecraft 1.20.5 + */ +public interface DataComponentValue extends Examinable { + /** + * Get a marker value to indicate that a data component's value should be removed. + * + * @return the removed holder + * @since 4.17.0 + * @sinceMinecraft 1.20.5 + */ + static DataComponentValue.@NotNull Removed removed() { + return RemovedDataComponentValueImpl.REMOVED; + } + + /** + * Represent an {@link DataComponentValue} that can be represented as a binary tag. + * + * @since 4.17.0 + * @sinceMinecraft 1.20.5 + */ + interface TagSerializable extends DataComponentValue { + + /** + * Convert this value into a binary tag value. + * + * @return the binary tag value + * @since 4.17.0 + * @sinceMinecraft 1.20.5 + */ + @NotNull BinaryTagHolder asBinaryTag(); + } + + /** + * Only valid in a patch-style usage, indicating that the data component with a certain key should be removed. + * + * @since 4.17.0 + * @sinceMinecraft 1.20.5 + */ + @ApiStatus.NonExtendable + interface Removed extends DataComponentValue { + } +} diff --git a/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConversionImpl.java b/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConversionImpl.java new file mode 100644 index 000000000..2469ea01c --- /dev/null +++ b/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConversionImpl.java @@ -0,0 +1,90 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2024 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.event; + +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.stream.Stream; +import net.kyori.adventure.internal.Internals; +import net.kyori.adventure.key.Key; +import net.kyori.examination.ExaminableProperty; +import org.jetbrains.annotations.NotNull; + +import static java.util.Objects.requireNonNull; + +final class DataComponentValueConversionImpl implements DataComponentValueConverterRegistry.Conversion { + private final Class source; + private final Class destination; + private final BiFunction conversion; + + DataComponentValueConversionImpl(final @NotNull Class source, final @NotNull Class destination, final @NotNull BiFunction conversion) { + this.source = source; + this.destination = destination; + this.conversion = conversion; + } + + @Override + public @NotNull Class source() { + return this.source; + } + + @Override + public @NotNull Class destination() { + return this.destination; + } + + @Override + public @NotNull O convert(final @NotNull Key key, final @NotNull I input) { + return this.conversion.apply(requireNonNull(key, "key"), requireNonNull(input, "input")); + } + + @Override + public @NotNull Stream examinableProperties() { + return Stream.of( + ExaminableProperty.of("source", this.source), + ExaminableProperty.of("destination", this.destination), + ExaminableProperty.of("conversion", this.conversion) + ); + } + + @Override + public String toString() { + return Internals.toString(this); + } + + @Override + public boolean equals(final Object other) { + if (this == other) return true; + if (other == null || getClass() != other.getClass()) return false; + final DataComponentValueConversionImpl that = (DataComponentValueConversionImpl) other; + return Objects.equals(this.source, that.source) + && Objects.equals(this.destination, that.destination) + && Objects.equals(this.conversion, that.conversion); + } + + @Override + public int hashCode() { + return Objects.hash(this.source, this.destination, this.conversion); + } +} diff --git a/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java b/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java new file mode 100644 index 000000000..298c3a430 --- /dev/null +++ b/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java @@ -0,0 +1,259 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2024 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.event; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.BiFunction; +import net.kyori.adventure.key.Key; +import net.kyori.examination.Examinable; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static java.util.Objects.requireNonNull; + +/** + * A registry for conversions between different data component value holder classes. + * + *

Conversions are discovered by {@link ServiceLoader} lookup of implementations of the {@link Provider} interface (using the loading thread's context classloader).

+ * + * @since 4.17.0 + */ +public final class DataComponentValueConverterRegistry { + private static final Set PROVIDERS; + + static { + final ServiceLoader providerLoader = ServiceLoader.load(Provider.class); + final Set providers = new HashSet<>(); + for (final Iterator it = providerLoader.iterator(); it.hasNext();) { + try { + providers.add(it.next()); + } catch (final ServiceConfigurationError ex) { + throw new RuntimeException("Failed to load data holder service provider: " + ex); + } + } + PROVIDERS = Collections.unmodifiableSet(providers); + } + + private DataComponentValueConverterRegistry() { + } + + /** + * Try to convert the data component value {@code in} to the provided output type. + * + * @param target the target type + * @param key the key this value is for + * @param in the input value + * @param the output type + * @return a value of target type + * @since 4.17.0 + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static @NotNull O convert(final @NotNull Class target, final @NotNull Key key, final @NotNull DataComponentValue in) { + if (target.isInstance(in)) { + return target.cast(in); + } + + final @Nullable Conversion converter = ConversionCache.converter(in.getClass(), target); + if (converter == null) { + throw new IllegalArgumentException("There is no data holder converter registered to convert from a " + in.getClass() + " instance to a " + target + " (on field " + key + ")"); + } + + return (O) ((Conversion) converter).convert(key, in); + } + + /** + * A provider for data component value converters. + * + * @since 4.17.0 + */ + interface Provider { + /** + * An identifier for this provider. + * + * @return the provider id + * @since 4.17.0 + */ + @NotNull Key id(); + + /** + * Return conversions available from this provider. + * + *

Conversions may only be queried once at application initialization, so changes to the result of this method may not have any effect.

+ * + * @return the conversions available + * @since 4.17.0 + */ + @NotNull Iterable> conversions(); + } + + /** + * A single conversion that may be provided by a provider. + * + * @param input type + * @param output type + * @since 4.17.0 + */ + @ApiStatus.NonExtendable + interface Conversion extends Examinable { + /** + * Create a new conversion. + * + * @param src the source type + * @param dst the destination type + * @param op the conversion operation + * @param the input type + * @param the output type + * @return a conversion object + * @since 4.17.0 + */ + static @NotNull Conversion convert(final @NotNull Class src, final @NotNull Class dst, final @NotNull BiFunction op) { + return new DataComponentValueConversionImpl<>( + requireNonNull(src, "src"), + requireNonNull(dst, "dst"), + requireNonNull(op, "op") + ); + } + + /** + * The source type. + * + * @return the source type + * @since 4.17.0 + */ + @Contract(pure = true) + @NotNull Class source(); + + /** + * The destination type. + * + * @return the destination type + * @since 4.17.0 + */ + @Contract(pure = true) + @NotNull Class destination(); + + /** + * Perform the actual conversion. + * + * @param key the key used for the data holder + * @param input the source type + * @return a data holder of the destination type + * @since 4.17.0 + */ + @NotNull O convert(final @NotNull Key key, final @NotNull I input); + } + + static final class ConversionCache { + // input -> output -> conversion + private static final ConcurrentMap, ConcurrentMap, RegisteredConversion>> CACHE = new ConcurrentHashMap<>(); + private static final Map, Set> CONVERSIONS = collectConversions(); + + private static Map, Set> collectConversions() { + final Map, Set> collected = new ConcurrentHashMap<>(); + for (final Provider provider : PROVIDERS) { + final @NotNull Key id = requireNonNull(provider.id(), () -> "ID of provider " + provider + " is null"); + for (final Conversion conv : provider.conversions()) { + collected.computeIfAbsent(conv.source(), $ -> ConcurrentHashMap.newKeySet()).add(new RegisteredConversion(id, conv)); + } + } + + for (final Map.Entry, Set> entry : collected.entrySet()) { + entry.setValue(Collections.unmodifiableSet(entry.getValue())); + } + + return new ConcurrentHashMap<>(collected); + } + + static RegisteredConversion compute(final Class src, final Class dst) { + final Deque> sourceTypes = new ArrayDeque<>(); + sourceTypes.add(src); + // walk up the source type hierarchy to find an option + for (Class sourcePtr; (sourcePtr = sourceTypes.poll()) != null;) { + final Set conversions = CONVERSIONS.get(sourcePtr); + if (conversions != null) { + // if we have values for the source type, evaluate each to find the one that matches either the exact destination type, or the nearest subtype + RegisteredConversion nearest = null; + for (final RegisteredConversion potential : conversions) { + final Class potentialDst = potential.conversion.destination(); + + if (dst.equals(potentialDst)) return potential; // exact match + if (!dst.isAssignableFrom(potentialDst)) continue; // out of hierarchy + + // if we are up the hierarchy + if (nearest == null || potentialDst.isAssignableFrom(nearest.conversion.destination())) { + nearest = potential; + } + } + + if (nearest != null) return nearest; // we found a match + } + + addSupertypes(sourcePtr, sourceTypes); + } + + return RegisteredConversion.NONE; + } + + private static void addSupertypes(final Class clazz, final Deque> queue) { + if (clazz.getSuperclass() != null) { + queue.add(clazz.getSuperclass()); + } + + queue.addAll(Arrays.asList(clazz.getInterfaces())); + } + + @SuppressWarnings("unchecked") + static @Nullable Conversion converter(final Class src, final Class dst) { + final RegisteredConversion result = CACHE.computeIfAbsent(src, $ -> new ConcurrentHashMap<>()).computeIfAbsent(dst, $$ -> compute(src, dst)); + if (result == RegisteredConversion.NONE) return null; + + return (Conversion) result.conversion; + } + } + + static final class RegisteredConversion { + static final RegisteredConversion NONE = new RegisteredConversion(null, null); + + final Key provider; + final Conversion conversion; + + RegisteredConversion(final Key provider, final Conversion conversion) { + this.provider = provider; + this.conversion = conversion; + } + } +} diff --git a/api/src/main/java/net/kyori/adventure/text/event/HoverEvent.java b/api/src/main/java/net/kyori/adventure/text/event/HoverEvent.java index b70b8c5f4..4466da8c5 100644 --- a/api/src/main/java/net/kyori/adventure/text/event/HoverEvent.java +++ b/api/src/main/java/net/kyori/adventure/text/event/HoverEvent.java @@ -143,7 +143,7 @@ public final class HoverEvent implements Examinable, HoverEventSource, Sty * @return a hover event * @since 4.17.0 */ - public static @NotNull HoverEvent showItem(final @NotNull Keyed item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @NotNull Map dataComponents) { + public static @NotNull HoverEvent showItem(final @NotNull Keyed item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @NotNull Map dataComponents) { return showItem(ShowItem.showItem(item, count, dataComponents)); } @@ -363,7 +363,7 @@ public static final class ShowItem implements Examinable { private final Key item; private final int count; private final @Nullable BinaryTagHolder nbt; - private final Map dataComponents; + private final Map dataComponents; /** * Creates. @@ -491,11 +491,11 @@ public static final class ShowItem implements Examinable { * @since 4.17.0 * @sinceMinecraft 1.20.5 */ - public static @NotNull ShowItem showItem(final @NotNull Keyed item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @NotNull Map dataComponents) { + public static @NotNull ShowItem showItem(final @NotNull Keyed item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @NotNull Map dataComponents) { return new ShowItem(requireNonNull(item, "item").key(), count, null, dataComponents); } - private ShowItem(final @NotNull Key item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @Nullable BinaryTagHolder nbt, final @NotNull Map dataComponents) { + private ShowItem(final @NotNull Key item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @Nullable BinaryTagHolder nbt, final @NotNull Map dataComponents) { this.item = item; this.count = count; this.nbt = nbt; @@ -585,7 +585,7 @@ private ShowItem(final @NotNull Key item, final @Range(from = 0, to = Integer.MA * @since 4.17.0 * @sinceMinecraft 1.20.5 */ - public @NotNull Map dataComponents() { + public @NotNull Map dataComponents() { return this.dataComponents; } @@ -599,11 +599,33 @@ private ShowItem(final @NotNull Key item, final @Range(from = 0, to = Integer.MA * @since 4.17.0 * @sinceMinecraft 1.20.5 */ - public @NotNull ShowItem dataComponents(final @NotNull Map holder) { + public @NotNull ShowItem dataComponents(final @NotNull Map holder) { if (Objects.equals(this.dataComponents, holder)) return this; return new ShowItem(this.item, this.count, null, holder.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(new HashMap<>(holder))); } + /** + * Return an unmodifiable map of data components coerced to the target type. + * + *

If there is no converter registered with the {@link DataComponentValueConverterRegistry} for the conversion of a value, a {@link IllegalArgumentException} will be thrown.

+ * + * @param targetType the expected target type + * @param the new data component value type + * @return the unmodifiable map + * @since 4.17.0 + */ + public @NotNull Map dataComponentsConvertedTo(final @NotNull Class targetType) { + if (this.dataComponents.isEmpty()) { + return Collections.emptyMap(); + } else { + final Map results = new HashMap<>(this.dataComponents.size()); + for (final Map.Entry entry : this.dataComponents.entrySet()) { + results.put(entry.getKey(), DataComponentValueConverterRegistry.convert(targetType, entry.getKey(), entry.getValue())); + } + return Collections.unmodifiableMap(results); + } + } + @Override public boolean equals(final @Nullable Object other) { if (this == other) return true; diff --git a/api/src/main/java/net/kyori/adventure/text/event/ItemDataHolder.java b/api/src/main/java/net/kyori/adventure/text/event/RemovedDataComponentValueImpl.java similarity index 75% rename from api/src/main/java/net/kyori/adventure/text/event/ItemDataHolder.java rename to api/src/main/java/net/kyori/adventure/text/event/RemovedDataComponentValueImpl.java index 79a8b1131..440bec0cb 100644 --- a/api/src/main/java/net/kyori/adventure/text/event/ItemDataHolder.java +++ b/api/src/main/java/net/kyori/adventure/text/event/RemovedDataComponentValueImpl.java @@ -23,17 +23,6 @@ */ package net.kyori.adventure.text.event; -import net.kyori.examination.Examinable; - -/** - * A holder for the value of an item's data component. - * - *

The exact value is platform-specific. Serializers may provide their - * own implementations as well, and any logic to serialize or deserialize - * should be done per-serializer.

- * - * @since 4.17.0 - * @sinceMinecraft 1.20.5 - */ -public interface ItemDataHolder extends Examinable { +enum RemovedDataComponentValueImpl implements DataComponentValue.Removed { + REMOVED } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/HoverTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/HoverTag.java index afa7e4073..098137bf1 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/HoverTag.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/HoverTag.java @@ -23,11 +23,14 @@ */ package net.kyori.adventure.text.minimessage.tag.standard; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; import net.kyori.adventure.key.InvalidKeyException; import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.api.BinaryTagHolder; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.DataComponentValue; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.minimessage.Context; @@ -123,10 +126,27 @@ private ShowItem() { @Override public HoverEvent.@NotNull ShowItem parse(final @NotNull ArgumentQueue args, final @NotNull Context ctx) throws ParsingException { try { + @SuppressWarnings("PatternValidation") final Key key = Key.key(args.popOr("Show item hover needs at least an item ID").value()); final int count = args.hasNext() ? args.pop().asInt().orElseThrow(() -> ctx.newException("The count argument was not a valid integer")) : 1; if (args.hasNext()) { - return HoverEvent.ShowItem.showItem(key, count, BinaryTagHolder.binaryTagHolder(args.pop().value())); + // Compatibility with legacy versions: + // if the value starts with a '{' we assume it's SNBT, and parse it as such to create a legacy holder + // otherwise, we'll parse argument pairs as a map of ResourceLocation -> SNBT value + final String value = args.peek().value(); + if (value.startsWith("{")) { + args.pop(); + return legacyShowItem(key, count, value); + } + + final Map datas = new HashMap<>(); + while (args.hasNext()) { + @SuppressWarnings("PatternValidation") + final Key dataKey = Key.key(args.pop().value()); + final String dataVal = args.popOr("a value was expected for key " + dataKey).value(); + datas.put(dataKey, BinaryTagHolder.binaryTagHolder(dataVal)); + } + return HoverEvent.ShowItem.showItem(key, count, datas); } else { return HoverEvent.ShowItem.showItem(key, count); } @@ -135,18 +155,40 @@ private ShowItem() { } } + @SuppressWarnings("deprecation") + private static HoverEvent.@NotNull ShowItem legacyShowItem(final Key id, final int count, final String value) { + return HoverEvent.ShowItem.showItem(id, count, BinaryTagHolder.binaryTagHolder(value)); + } + @Override public void emit(final HoverEvent.ShowItem event, final TokenEmitter emit) { emit.argument(compactAsString(event.item())); - if (event.count() != 1 || event.nbt() != null) { + if (event.count() != 1 || hasLegacy(event) || !event.dataComponents().isEmpty()) { emit.argument(Integer.toString(event.count())); - if (event.nbt() != null) { - emit.argument(event.nbt().string()); + if (hasLegacy(event)) { + emitLegacyHover(event, emit); + } else { + for (final Map.Entry entry : event.dataComponentsConvertedTo(DataComponentValue.TagSerializable.class).entrySet()) { + emit.argument(entry.getKey().asMinimalString()); + emit.argument(entry.getValue().asBinaryTag().string()); + } } } } + + @SuppressWarnings("deprecation") + static boolean hasLegacy(final HoverEvent.ShowItem event) { + return event.nbt() != null; + } + + @SuppressWarnings("deprecation") + static void emitLegacyHover(final HoverEvent.ShowItem event, final TokenEmitter emit) { + if (event.nbt() != null) { + emit.argument(event.nbt().string()); + } + } } static final class ShowEntity implements ActionHandler { diff --git a/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/GsonDataComponentValue.java b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/GsonDataComponentValue.java new file mode 100644 index 000000000..000d6efb8 --- /dev/null +++ b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/GsonDataComponentValue.java @@ -0,0 +1,65 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2024 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.gson; + +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +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 JsonElement. + * + *

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

+ * + * @since 4.17.0 + */ +@ApiStatus.NonExtendable +public interface GsonDataComponentValue extends DataComponentValue { + /** + * Create a box for item data that can be understood by the gson serializer. + * + * @param data the item data to hold + * @return a newly created item data holder instance + * @since 4.17.0 + */ + static GsonDataComponentValue gsonDatacomponentValue(final @NotNull JsonElement data) { + if (data instanceof JsonNull) { + return GsonDataComponentValueImpl.RemovedGsonComponentValueImpl.INSTANCE; + } else { + return new GsonDataComponentValueImpl(requireNonNull(data, "data")); + } + } + + /** + * The contained element, intended for read-only use. + * + * @return a copy of the contained element + * @since 4.17.0 + */ + @NotNull JsonElement element(); +} diff --git a/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/GsonDataComponentValueImpl.java b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/GsonDataComponentValueImpl.java new file mode 100644 index 000000000..67266d3ab --- /dev/null +++ b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/GsonDataComponentValueImpl.java @@ -0,0 +1,80 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2024 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.gson; + +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import java.util.Objects; +import java.util.stream.Stream; +import net.kyori.adventure.internal.Internals; +import net.kyori.adventure.text.event.DataComponentValue; +import net.kyori.examination.ExaminableProperty; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +class GsonDataComponentValueImpl implements GsonDataComponentValue { + private final JsonElement element; + + GsonDataComponentValueImpl(final @NotNull JsonElement element) { + this.element = element; + } + + @Override + public @NotNull JsonElement element() { + return this.element; + } + + @Override + public @NotNull Stream examinableProperties() { + return Stream.of( + ExaminableProperty.of("element", this.element) + ); + } + + @Override + public String toString() { + return Internals.toString(this); + } + + @Override + public boolean equals(final @Nullable Object other) { + if (this == other) return true; + if (other == null || getClass() != other.getClass()) return false; + final GsonDataComponentValueImpl that = (GsonDataComponentValueImpl) other; + return Objects.equals(this.element, that.element); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.element); + } + + static final class RemovedGsonComponentValueImpl extends GsonDataComponentValueImpl implements DataComponentValue.Removed { + static final RemovedGsonComponentValueImpl INSTANCE = new RemovedGsonComponentValueImpl(); + + private RemovedGsonComponentValueImpl() { + super(JsonNull.INSTANCE); + } + } +} 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 06b0cb3c1..9b32c67bc 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 @@ -24,19 +24,25 @@ package net.kyori.adventure.text.serializer.gson; import com.google.gson.Gson; +import com.google.gson.JsonElement; import com.google.gson.JsonParseException; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.api.BinaryTagHolder; +import net.kyori.adventure.text.event.DataComponentValue; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.serializer.json.JSONOptions; import net.kyori.option.OptionState; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.SHOW_ITEM_COMPONENTS; import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.SHOW_ITEM_COUNT; import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.SHOW_ITEM_ID; import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.SHOW_ITEM_TAG; @@ -55,12 +61,14 @@ private ShowItemSerializer(final Gson gson, final boolean emitDefaultQuantity) { } @Override + @SuppressWarnings("deprecation") public HoverEvent.ShowItem read(final JsonReader in) throws IOException { in.beginObject(); Key key = null; int count = 1; @Nullable BinaryTagHolder nbt = null; + @Nullable Map dataComponents = null; while (in.hasNext()) { final String fieldName = in.nextName(); @@ -79,6 +87,17 @@ public HoverEvent.ShowItem read(final JsonReader in) throws IOException { } else { throw new JsonParseException("Expected " + SHOW_ITEM_TAG + " to be a string"); } + } else if (fieldName.equals(SHOW_ITEM_COMPONENTS)) { + in.beginObject(); + while (in.peek() != JsonToken.END_OBJECT) { + final Key id = Key.key(in.nextName()); + final JsonElement tree = this.gson.fromJson(in, JsonElement.class); + if (dataComponents == null) { + dataComponents = new HashMap<>(); + } + dataComponents.put(id, GsonDataComponentValue.gsonDatacomponentValue(tree)); + } + in.endObject(); } else { in.skipValue(); } @@ -89,7 +108,14 @@ public HoverEvent.ShowItem read(final JsonReader in) throws IOException { } in.endObject(); - return HoverEvent.ShowItem.showItem(key, count, nbt); + if (dataComponents != null) { + if (nbt != null) { + // todo: strict + } + return HoverEvent.ShowItem.showItem(key, count, dataComponents); + } else { + return HoverEvent.ShowItem.showItem(key, count, nbt); + } } @Override @@ -105,12 +131,28 @@ public void write(final JsonWriter out, final HoverEvent.ShowItem value) throws out.value(count); } + final @NotNull Map dataComponents = value.dataComponents(); + if (!dataComponents.isEmpty()) { + out.name(SHOW_ITEM_COMPONENTS); + out.beginObject(); + for (final Map.Entry entry : value.dataComponentsConvertedTo(GsonDataComponentValue.class).entrySet()) { + out.name(entry.getKey().asMinimalString()); + this.gson.toJson(entry.getValue().element(), out); + } + out.endObject(); + } else { + writeLegacy(out, value); + } + + out.endObject(); + } + + @SuppressWarnings("deprecation") + private static void writeLegacy(final JsonWriter out, final HoverEvent.ShowItem value) throws IOException { final @Nullable BinaryTagHolder nbt = value.nbt(); if (nbt != null) { out.name(SHOW_ITEM_TAG); out.value(nbt.string()); } - - out.endObject(); } } diff --git a/text-serializer-json/src/main/java/net/kyori/adventure/text/serializer/json/JSONComponentConstants.java b/text-serializer-json/src/main/java/net/kyori/adventure/text/serializer/json/JSONComponentConstants.java index fc3d33ab5..a353820a6 100644 --- a/text-serializer-json/src/main/java/net/kyori/adventure/text/serializer/json/JSONComponentConstants.java +++ b/text-serializer-json/src/main/java/net/kyori/adventure/text/serializer/json/JSONComponentConstants.java @@ -64,7 +64,8 @@ public final class JSONComponentConstants { public static final String SHOW_ENTITY_NAME = "name"; public static final String SHOW_ITEM_ID = "id"; public static final String SHOW_ITEM_COUNT = "count"; - public static final String SHOW_ITEM_TAG = "tag"; + public static final @Deprecated String SHOW_ITEM_TAG = "tag"; + public static final String SHOW_ITEM_COMPONENTS = "components"; private JSONComponentConstants() { throw new IllegalStateException("Cannot instantiate"); diff --git a/text-serializer-json/src/testFixtures/java/net/kyori/adventure/text/serializer/json/ShowItemSerializerTest.java b/text-serializer-json/src/testFixtures/java/net/kyori/adventure/text/serializer/json/ShowItemSerializerTest.java index 53e471880..3b4d4bd56 100644 --- a/text-serializer-json/src/testFixtures/java/net/kyori/adventure/text/serializer/json/ShowItemSerializerTest.java +++ b/text-serializer-json/src/testFixtures/java/net/kyori/adventure/text/serializer/json/ShowItemSerializerTest.java @@ -24,6 +24,7 @@ package net.kyori.adventure.text.serializer.json; 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.StringBinaryTag; @@ -70,7 +71,7 @@ void testDeserializeWithNullTag() { HoverEvent.showItem( Key.key("minecraft", "diamond"), 2, - null + Collections.emptyMap() ) ).build(), json -> { From 9bf6b797f63e0b9819099f77fb6daaae887cbbaa Mon Sep 17 00:00:00 2001 From: zml Date: Tue, 23 Apr 2024 19:04:00 -0700 Subject: [PATCH 4/8] fix checkstyle, use Services abstraction for service loading --- .../DataComponentValueConverterRegistry.java | 23 ++----- .../net/kyori/adventure/util/Services.java | 31 ++++++++++ ...onDataComponentValueConverterProvider.java | 61 +++++++++++++++++++ .../serializer/gson/impl/package-info.java | 7 +++ ...taComponentValueConverterRegistry$Provider | 1 + 5 files changed, 104 insertions(+), 19 deletions(-) create mode 100644 text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/impl/GsonDataComponentValueConverterProvider.java create mode 100644 text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/impl/package-info.java create mode 100644 text-serializer-gson/src/main/resources/META-INF/services/net.kyori.adventure.text.event.DataComponentValueConverterRegistry$Provider diff --git a/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java b/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java index 298c3a430..cdae7d36a 100644 --- a/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java +++ b/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java @@ -27,16 +27,14 @@ import java.util.Arrays; import java.util.Collections; import java.util.Deque; -import java.util.HashSet; -import java.util.Iterator; import java.util.Map; -import java.util.ServiceConfigurationError; import java.util.ServiceLoader; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.BiFunction; import net.kyori.adventure.key.Key; +import net.kyori.adventure.util.Services; import net.kyori.examination.Examinable; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; @@ -53,20 +51,7 @@ * @since 4.17.0 */ public final class DataComponentValueConverterRegistry { - private static final Set PROVIDERS; - - static { - final ServiceLoader providerLoader = ServiceLoader.load(Provider.class); - final Set providers = new HashSet<>(); - for (final Iterator it = providerLoader.iterator(); it.hasNext();) { - try { - providers.add(it.next()); - } catch (final ServiceConfigurationError ex) { - throw new RuntimeException("Failed to load data holder service provider: " + ex); - } - } - PROVIDERS = Collections.unmodifiableSet(providers); - } + private static final Set PROVIDERS = Services.services(Provider.class); private DataComponentValueConverterRegistry() { } @@ -100,7 +85,7 @@ private DataComponentValueConverterRegistry() { * * @since 4.17.0 */ - interface Provider { + public interface Provider { /** * An identifier for this provider. * @@ -128,7 +113,7 @@ interface Provider { * @since 4.17.0 */ @ApiStatus.NonExtendable - interface Conversion extends Examinable { + public interface Conversion extends Examinable { /** * Create a new conversion. * diff --git a/api/src/main/java/net/kyori/adventure/util/Services.java b/api/src/main/java/net/kyori/adventure/util/Services.java index bb89f6f8b..3e1ff0e21 100644 --- a/api/src/main/java/net/kyori/adventure/util/Services.java +++ b/api/src/main/java/net/kyori/adventure/util/Services.java @@ -23,9 +23,13 @@ */ package net.kyori.adventure.util; +import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.Optional; +import java.util.ServiceConfigurationError; import java.util.ServiceLoader; +import java.util.Set; import net.kyori.adventure.internal.properties.AdventureProperties; import org.jetbrains.annotations.NotNull; @@ -121,4 +125,31 @@ public interface Fallback { return Optional.ofNullable(firstFallback); } + + /** + * Locates all providers for a certain service and initializes them. + * + * @param clazz the service interface + * @param

the service interface type + * @return an unmodifiable set of all known providers of the service + * @since 4.17.0 + */ + public static

Set

services(final Class clazz) { + final ServiceLoader loader = Services0.loader(clazz); + final Set

providers = new HashSet<>(); + for (final Iterator it = loader.iterator(); it.hasNext();) { + final P instance; + try { + instance = it.next(); + } catch (final ServiceConfigurationError ex) { + if (SERVICE_LOAD_FAILURES_ARE_FATAL) { + throw new IllegalStateException("Encountered an exception loading a provider for " + clazz + ": ", ex); + } else { + continue; + } + } + providers.add(instance); + } + return Collections.unmodifiableSet(providers); + } } diff --git a/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/impl/GsonDataComponentValueConverterProvider.java b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/impl/GsonDataComponentValueConverterProvider.java new file mode 100644 index 000000000..78ecb9cfb --- /dev/null +++ b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/impl/GsonDataComponentValueConverterProvider.java @@ -0,0 +1,61 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2024 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.gson.impl; + +import com.google.gson.JsonNull; +import java.util.Collections; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.event.DataComponentValue; +import net.kyori.adventure.text.event.DataComponentValueConverterRegistry; +import net.kyori.adventure.text.serializer.gson.GsonDataComponentValue; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * A provider for Gson's implementations of data component value converters. + * + *

This is public SPI, not API.

+ * + * @since 4.17.0 + */ +@ApiStatus.Internal +public final class GsonDataComponentValueConverterProvider implements DataComponentValueConverterRegistry.Provider { + private static final Key ID = Key.key("adventure", "serializer/gson"); + + @Override + public @NotNull Key id() { + return ID; + } + + @Override + public @NotNull Iterable> conversions() { + return Collections.singletonList( + DataComponentValueConverterRegistry.Conversion.convert( + DataComponentValue.Removed.class, + GsonDataComponentValue.class, + (k, removed) -> GsonDataComponentValue.gsonDatacomponentValue(JsonNull.INSTANCE) + ) + ); + } +} diff --git a/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/impl/package-info.java b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/impl/package-info.java new file mode 100644 index 000000000..b0363ba42 --- /dev/null +++ b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/impl/package-info.java @@ -0,0 +1,7 @@ +/** + * Internal classes for the Gson serializer. + */ +@ApiStatus.Internal +package net.kyori.adventure.text.serializer.gson.impl; + +import org.jetbrains.annotations.ApiStatus; diff --git a/text-serializer-gson/src/main/resources/META-INF/services/net.kyori.adventure.text.event.DataComponentValueConverterRegistry$Provider b/text-serializer-gson/src/main/resources/META-INF/services/net.kyori.adventure.text.event.DataComponentValueConverterRegistry$Provider new file mode 100644 index 000000000..79698ac09 --- /dev/null +++ b/text-serializer-gson/src/main/resources/META-INF/services/net.kyori.adventure.text.event.DataComponentValueConverterRegistry$Provider @@ -0,0 +1 @@ +net.kyori.adventure.text.serializer.gson.impl.GsonDataComponentValueConverterProvider From b48ced8d20e3cb080cd60d221548f24e63c2e8a6 Mon Sep 17 00:00:00 2001 From: zml Date: Tue, 23 Apr 2024 20:12:11 -0700 Subject: [PATCH 5/8] tidy up, add test coverage --- annotation-processors/build.gradle.kts | 2 +- api/build.gradle.kts | 2 + .../DataComponentValueConverterRegistry.java | 13 +- .../adventure/text/event/HoverEvent.java | 2 +- ...taComponentValueConverterRegistryTest.java | 159 ++++++++++++++++++ gradle/libs.versions.toml | 5 +- .../minimessage/tag/standard/HoverTag.java | 2 +- text-serializer-gson/build.gradle.kts | 2 + .../serializer/gson/ShowItemSerializer.java | 35 ++-- ...onDataComponentValueConverterProvider.java | 2 + .../JSONComponentSerializerProviderImpl.java | 5 +- ...taComponentValueConverterRegistry$Provider | 1 - ...izer.json.JSONComponentSerializer$Provider | 1 - .../text/serializer/json/JSONOptions.java | 36 ++++ .../json/ShowItemSerializerTest.java | 9 + 15 files changed, 246 insertions(+), 30 deletions(-) create mode 100644 api/src/test/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistryTest.java rename text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/{ => impl}/JSONComponentSerializerProviderImpl.java (90%) delete mode 100644 text-serializer-gson/src/main/resources/META-INF/services/net.kyori.adventure.text.event.DataComponentValueConverterRegistry$Provider delete mode 100644 text-serializer-gson/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.json.JSONComponentSerializer$Provider diff --git a/annotation-processors/build.gradle.kts b/annotation-processors/build.gradle.kts index 00605ba6b..ff45d3a34 100644 --- a/annotation-processors/build.gradle.kts +++ b/annotation-processors/build.gradle.kts @@ -3,7 +3,7 @@ plugins { } dependencies { - annotationProcessor(libs.autoService.processor) + annotationProcessor(libs.autoService) compileOnlyApi(libs.autoService.annotations) api(libs.jetbrainsAnnotations) } diff --git a/api/build.gradle.kts b/api/build.gradle.kts index b6eeac9a0..23be56c7b 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -16,6 +16,8 @@ dependencies { compileOnlyApi(libs.jetbrainsAnnotations) testImplementation(libs.guava) annotationProcessor(projects.adventureAnnotationProcessors) + testCompileOnly(libs.autoService.annotations) + testAnnotationProcessor(libs.autoService) } applyJarMetadata("net.kyori.adventure") diff --git a/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java b/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java index cdae7d36a..0372e9626 100644 --- a/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java +++ b/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java @@ -72,12 +72,16 @@ private DataComponentValueConverterRegistry() { return target.cast(in); } - final @Nullable Conversion converter = ConversionCache.converter(in.getClass(), target); + final @Nullable RegisteredConversion converter = ConversionCache.converter(in.getClass(), target); if (converter == null) { throw new IllegalArgumentException("There is no data holder converter registered to convert from a " + in.getClass() + " instance to a " + target + " (on field " + key + ")"); } - return (O) ((Conversion) converter).convert(key, in); + try { + return (O) ((Conversion) converter.conversion).convert(key, in); + } catch (final Exception ex) { + throw new IllegalStateException("Failed to convert data component value of type " + in.getClass() + " to type " + target + " due to an error in a converter provided by " + converter.provider.asString() + "!", ex); + } } /** @@ -221,12 +225,11 @@ private static void addSupertypes(final Class clazz, final Deque> qu queue.addAll(Arrays.asList(clazz.getInterfaces())); } - @SuppressWarnings("unchecked") - static @Nullable Conversion converter(final Class src, final Class dst) { + static @Nullable RegisteredConversion converter(final Class src, final Class dst) { final RegisteredConversion result = CACHE.computeIfAbsent(src, $ -> new ConcurrentHashMap<>()).computeIfAbsent(dst, $$ -> compute(src, dst)); if (result == RegisteredConversion.NONE) return null; - return (Conversion) result.conversion; + return result; } } diff --git a/api/src/main/java/net/kyori/adventure/text/event/HoverEvent.java b/api/src/main/java/net/kyori/adventure/text/event/HoverEvent.java index 4466da8c5..7a980896e 100644 --- a/api/src/main/java/net/kyori/adventure/text/event/HoverEvent.java +++ b/api/src/main/java/net/kyori/adventure/text/event/HoverEvent.java @@ -614,7 +614,7 @@ private ShowItem(final @NotNull Key item, final @Range(from = 0, to = Integer.MA * @return the unmodifiable map * @since 4.17.0 */ - public @NotNull Map dataComponentsConvertedTo(final @NotNull Class targetType) { + public @NotNull Map dataComponentsAs(final @NotNull Class targetType) { if (this.dataComponents.isEmpty()) { return Collections.emptyMap(); } else { diff --git a/api/src/test/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistryTest.java b/api/src/test/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistryTest.java new file mode 100644 index 000000000..dbb8b399f --- /dev/null +++ b/api/src/test/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistryTest.java @@ -0,0 +1,159 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2024 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.event; + +import com.google.auto.service.AutoService; +import java.util.Arrays; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.api.BinaryTagHolder; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.key.Key.key; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DataComponentValueConverterRegistryTest { + @Test + void testKnownSourceToUnknownDest() { + final IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> { + DataComponentValueConverterRegistry.convert(Unregistered.class, key("test"), new DirectValue(3)); + }); + + assertTrue(iae.getMessage().contains("There is no data holder converter registered")); + } + + @Test + void testUnknownSourceToKnownDest() { + final IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> { + DataComponentValueConverterRegistry.convert(DirectValue.class, key("test"), new Unregistered()); + }); + + assertTrue(iae.getMessage().contains("There is no data holder converter registered")); + + } + + @Test + void testFailedConversionBlamesProvider() { + final IllegalStateException ise = assertThrows(IllegalStateException.class, () -> { + DataComponentValueConverterRegistry.convert(Failing.class, key("test"), BinaryTagHolder.binaryTagHolder("{}")); + }); + + assertTrue(ise.getMessage().contains(TestConverterProvider.ID.asString())); + } + + @Test + void testExactToExact() { + final DirectValue input = new DirectValue(new Object()); + final ItfValueImpl result = DataComponentValueConverterRegistry.convert(ItfValueImpl.class, key("test"), input); + + assertEquals(input.value, result.value); + } + + @Test + void testSubtypeToExact() { + final ItfValue input = new ItfValueImpl(new Object()); + final DirectValue result = DataComponentValueConverterRegistry.convert(DirectValue.class, key("test"), input); + + assertEquals(input.value(), result.value); + } + + @Test + void testSubtypeToSupertype() { + final ItfValue input = new ItfValueImpl(new Object()); + final DataComponentValue result = DataComponentValueConverterRegistry.convert(Intermediate.class, key("test"), input); + + assertInstanceOf(DirectValue.class, result); + assertEquals(input.value(), ((DirectValue) result).value); + } + + @Test + void testExactToSupertype() { + final DirectValue input = new DirectValue(new Object()); + final ItfValue result = DataComponentValueConverterRegistry.convert(ItfValue.class, key("test"), input); + + assertEquals(input.value, result.value()); + } + + @AutoService(DataComponentValueConverterRegistry.Provider.class) + public static final class TestConverterProvider implements DataComponentValueConverterRegistry.Provider { + static final Key ID = key("adventure", "test/converter_registry"); + + @Override + public @NotNull Key id() { + return ID; + } + + @Override + public @NotNull Iterable> conversions() { + // gah j8 + return Arrays.asList( + DataComponentValueConverterRegistry.Conversion.convert(DirectValue.class, ItfValueImpl.class, (key, dir) -> new ItfValueImpl(dir.value)), + DataComponentValueConverterRegistry.Conversion.convert(ItfValue.class, DirectValue.class, (key, itf) -> new DirectValue(itf.value())), + DataComponentValueConverterRegistry.Conversion.convert(BinaryTagHolder.class, Failing.class, (key, itf) -> { + throw new RuntimeException("hah!"); + }) + ); + } + } + + static final class Unregistered implements DataComponentValue { + // i shall not be converted + } + + static final class Failing implements DataComponentValue { + // this is a marker interface to trigger a failure + } + + interface Intermediate extends DataComponentValue { + + } + + static final class DirectValue implements Intermediate { + final Object value; + + DirectValue(final Object value) { + this.value = value; + } + } + + interface ItfValue extends DataComponentValue { + Object value(); + } + + static final class ItfValueImpl implements ItfValue { + final Object value; + + ItfValueImpl(final Object value) { + this.value = value; + } + + @Override + public Object value() { + return this.value; + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 32c3c7bf8..b7303f64d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ version = "1.0" [versions] +autoService = "1.1.1" checkstyle = "10.15.0" errorprone = "2.26.1" examination = "1.3.0" @@ -15,6 +16,8 @@ truth = "1.4.2" [libraries] # shared +autoService = { module = "com.google.auto.service:auto-service", version.ref = "autoService" } +autoService-annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoService" } examination-api = { module = "net.kyori:examination-api", version.ref = "examination" } examination-string = { module = "net.kyori:examination-string", version.ref = "examination" } option = { module = "net.kyori:option", version = "1.0.0" } @@ -55,8 +58,6 @@ truth-java8 = { module = "com.google.truth.extensions:truth-java8-extension", ve contractValidator = "ca.stellardrift:contract-validator:1.0.1" errorprone = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone" } stylecheck = "ca.stellardrift:stylecheck:0.2.1" -autoService-annotations = "com.google.auto.service:auto-service-annotations:1.1.1" -autoService-processor = "com.google.auto.service:auto-service:1.1.1" build-errorpronePlugin = "net.ltgt.gradle:gradle-errorprone-plugin:3.1.0" build-indra = { module = "net.kyori:indra-common", version.ref = "indra" } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/HoverTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/HoverTag.java index 098137bf1..fa2ece7af 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/HoverTag.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/HoverTag.java @@ -170,7 +170,7 @@ public void emit(final HoverEvent.ShowItem event, final TokenEmitter emit) { if (hasLegacy(event)) { emitLegacyHover(event, emit); } else { - for (final Map.Entry entry : event.dataComponentsConvertedTo(DataComponentValue.TagSerializable.class).entrySet()) { + for (final Map.Entry entry : event.dataComponentsAs(DataComponentValue.TagSerializable.class).entrySet()) { emit.argument(entry.getKey().asMinimalString()); emit.argument(entry.getValue().asBinaryTag().string()); } diff --git a/text-serializer-gson/build.gradle.kts b/text-serializer-gson/build.gradle.kts index 5d6c1935e..610d9e1c3 100644 --- a/text-serializer-gson/build.gradle.kts +++ b/text-serializer-gson/build.gradle.kts @@ -5,6 +5,8 @@ plugins { dependencies { api(libs.gson) + compileOnlyApi(libs.autoService.annotations) + annotationProcessor(libs.autoService) } applyJarMetadata("net.kyori.adventure.text.serializer.gson") 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 9b32c67bc..e42196d1f 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 @@ -45,19 +45,23 @@ import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.SHOW_ITEM_COMPONENTS; import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.SHOW_ITEM_COUNT; import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.SHOW_ITEM_ID; -import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.SHOW_ITEM_TAG; final class ShowItemSerializer extends TypeAdapter { - static TypeAdapter create(final Gson gson, final OptionState opt) { - return new ShowItemSerializer(gson, opt.value(JSONOptions.EMIT_DEFAULT_ITEM_HOVER_QUANTITY)).nullSafe(); - } + @SuppressWarnings("deprecation") + private static final String LEGACY_SHOW_ITEM_TAG = net.kyori.adventure.text.serializer.json.JSONComponentConstants.SHOW_ITEM_TAG; private final Gson gson; private final boolean emitDefaultQuantity; + private final JSONOptions.ShowItemHoverDataMode itemDataMode; + + static TypeAdapter create(final Gson gson, final OptionState opt) { + return new ShowItemSerializer(gson, opt.value(JSONOptions.EMIT_DEFAULT_ITEM_HOVER_QUANTITY), opt.value(JSONOptions.SHOW_ITEM_HOVER_DATA_MODE)).nullSafe(); + } - private ShowItemSerializer(final Gson gson, final boolean emitDefaultQuantity) { + private ShowItemSerializer(final Gson gson, final boolean emitDefaultQuantity, final JSONOptions.ShowItemHoverDataMode itemDataMode) { this.gson = gson; this.emitDefaultQuantity = emitDefaultQuantity; + this.itemDataMode = itemDataMode; } @Override @@ -76,7 +80,7 @@ public HoverEvent.ShowItem read(final JsonReader in) throws IOException { key = this.gson.fromJson(in, SerializerFactory.KEY_TYPE); } else if (fieldName.equals(SHOW_ITEM_COUNT)) { count = in.nextInt(); - } else if (fieldName.equals(SHOW_ITEM_TAG)) { + } else if (fieldName.equals(LEGACY_SHOW_ITEM_TAG)) { final JsonToken token = in.peek(); if (token == JsonToken.STRING || token == JsonToken.NUMBER) { nbt = BinaryTagHolder.binaryTagHolder(in.nextString()); @@ -85,7 +89,7 @@ public HoverEvent.ShowItem read(final JsonReader in) throws IOException { } else if (token == JsonToken.NULL) { in.nextNull(); } else { - throw new JsonParseException("Expected " + SHOW_ITEM_TAG + " to be a string"); + throw new JsonParseException("Expected " + LEGACY_SHOW_ITEM_TAG + " to be a string"); } } else if (fieldName.equals(SHOW_ITEM_COMPONENTS)) { in.beginObject(); @@ -109,9 +113,6 @@ public HoverEvent.ShowItem read(final JsonReader in) throws IOException { in.endObject(); if (dataComponents != null) { - if (nbt != null) { - // todo: strict - } return HoverEvent.ShowItem.showItem(key, count, dataComponents); } else { return HoverEvent.ShowItem.showItem(key, count, nbt); @@ -132,26 +133,26 @@ public void write(final JsonWriter out, final HoverEvent.ShowItem value) throws } final @NotNull Map dataComponents = value.dataComponents(); - if (!dataComponents.isEmpty()) { + if (!dataComponents.isEmpty() && this.itemDataMode != JSONOptions.ShowItemHoverDataMode.EMIT_LEGACY_NBT) { out.name(SHOW_ITEM_COMPONENTS); out.beginObject(); - for (final Map.Entry entry : value.dataComponentsConvertedTo(GsonDataComponentValue.class).entrySet()) { - out.name(entry.getKey().asMinimalString()); + for (final Map.Entry entry : value.dataComponentsAs(GsonDataComponentValue.class).entrySet()) { + out.name(entry.getKey().asString()); this.gson.toJson(entry.getValue().element(), out); } out.endObject(); - } else { - writeLegacy(out, value); + } else if (this.itemDataMode != JSONOptions.ShowItemHoverDataMode.EMIT_DATA_COMPONENTS) { + maybeWriteLegacy(out, value); } out.endObject(); } @SuppressWarnings("deprecation") - private static void writeLegacy(final JsonWriter out, final HoverEvent.ShowItem value) throws IOException { + private static void maybeWriteLegacy(final JsonWriter out, final HoverEvent.ShowItem value) throws IOException { final @Nullable BinaryTagHolder nbt = value.nbt(); if (nbt != null) { - out.name(SHOW_ITEM_TAG); + out.name(LEGACY_SHOW_ITEM_TAG); out.value(nbt.string()); } } diff --git a/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/impl/GsonDataComponentValueConverterProvider.java b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/impl/GsonDataComponentValueConverterProvider.java index 78ecb9cfb..1cc28d6ae 100644 --- a/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/impl/GsonDataComponentValueConverterProvider.java +++ b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/impl/GsonDataComponentValueConverterProvider.java @@ -23,6 +23,7 @@ */ package net.kyori.adventure.text.serializer.gson.impl; +import com.google.auto.service.AutoService; import com.google.gson.JsonNull; import java.util.Collections; import net.kyori.adventure.key.Key; @@ -39,6 +40,7 @@ * * @since 4.17.0 */ +@AutoService(DataComponentValueConverterRegistry.Provider.class) @ApiStatus.Internal public final class GsonDataComponentValueConverterProvider implements DataComponentValueConverterRegistry.Provider { private static final Key ID = Key.key("adventure", "serializer/gson"); diff --git a/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/JSONComponentSerializerProviderImpl.java b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/impl/JSONComponentSerializerProviderImpl.java similarity index 90% rename from text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/JSONComponentSerializerProviderImpl.java rename to text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/impl/JSONComponentSerializerProviderImpl.java index 1087fe2e5..b9c11615b 100644 --- a/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/JSONComponentSerializerProviderImpl.java +++ b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/impl/JSONComponentSerializerProviderImpl.java @@ -21,9 +21,11 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -package net.kyori.adventure.text.serializer.gson; +package net.kyori.adventure.text.serializer.gson.impl; +import com.google.auto.service.AutoService; import java.util.function.Supplier; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.json.JSONComponentSerializer; import net.kyori.adventure.util.Services; import org.jetbrains.annotations.ApiStatus; @@ -35,6 +37,7 @@ * @since 4.14.0 */ @ApiStatus.Internal +@AutoService(JSONComponentSerializer.Provider.class) public final class JSONComponentSerializerProviderImpl implements JSONComponentSerializer.Provider, Services.Fallback { @Override public @NotNull JSONComponentSerializer instance() { diff --git a/text-serializer-gson/src/main/resources/META-INF/services/net.kyori.adventure.text.event.DataComponentValueConverterRegistry$Provider b/text-serializer-gson/src/main/resources/META-INF/services/net.kyori.adventure.text.event.DataComponentValueConverterRegistry$Provider deleted file mode 100644 index 79698ac09..000000000 --- a/text-serializer-gson/src/main/resources/META-INF/services/net.kyori.adventure.text.event.DataComponentValueConverterRegistry$Provider +++ /dev/null @@ -1 +0,0 @@ -net.kyori.adventure.text.serializer.gson.impl.GsonDataComponentValueConverterProvider diff --git a/text-serializer-gson/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.json.JSONComponentSerializer$Provider b/text-serializer-gson/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.json.JSONComponentSerializer$Provider deleted file mode 100644 index e018fe801..000000000 --- a/text-serializer-gson/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.json.JSONComponentSerializer$Provider +++ /dev/null @@ -1 +0,0 @@ -net.kyori.adventure.text.serializer.gson.JSONComponentSerializerProviderImpl diff --git a/text-serializer-json/src/main/java/net/kyori/adventure/text/serializer/json/JSONOptions.java b/text-serializer-json/src/main/java/net/kyori/adventure/text/serializer/json/JSONOptions.java index 185d012ce..05c1e1581 100644 --- a/text-serializer-json/src/main/java/net/kyori/adventure/text/serializer/json/JSONOptions.java +++ b/text-serializer-json/src/main/java/net/kyori/adventure/text/serializer/json/JSONOptions.java @@ -92,6 +92,13 @@ private JSONOptions() { */ public static final Option EMIT_DEFAULT_ITEM_HOVER_QUANTITY = Option.booleanOption(key("emit/default_item_hover_quantity"), true); + /** + * How to emit the item data on {@code show_item} hover events. + * + * @since 4.17.0 + */ + public static final Option SHOW_ITEM_HOVER_DATA_MODE = Option.enumOption(key("emit/show_item_hover_data"), ShowItemHoverDataMode.class, ShowItemHoverDataMode.EMIT_EITHER); + /** * Versioned by world data version. */ @@ -103,6 +110,7 @@ private JSONOptions() { .value(EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, false) .value(VALIDATE_STRICT_EVENTS, false) .value(EMIT_DEFAULT_ITEM_HOVER_QUANTITY, false) + .value(SHOW_ITEM_HOVER_DATA_MODE, ShowItemHoverDataMode.EMIT_LEGACY_NBT) ) .version( VERSION_1_16, @@ -118,6 +126,7 @@ private JSONOptions() { .version( VERSION_1_20_5, b -> b.value(EMIT_DEFAULT_ITEM_HOVER_QUANTITY, true) + .value(SHOW_ITEM_HOVER_DATA_MODE, ShowItemHoverDataMode.EMIT_DATA_COMPONENTS) ) .build(); @@ -131,6 +140,7 @@ private JSONOptions() { .value(EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, false) .value(EMIT_COMPACT_TEXT_COMPONENT, false) .value(VALIDATE_STRICT_EVENTS, false) + .value(SHOW_ITEM_HOVER_DATA_MODE, ShowItemHoverDataMode.EMIT_EITHER) .build(); private static String key(final String value) { @@ -184,4 +194,30 @@ public enum HoverEventValueMode { */ BOTH, } + + /** + * Configure how to emit show_item hovers. + * + * @since 4.17.0 + */ + public enum ShowItemHoverDataMode { + /** + * Only emit the pre-1.20.5 item nbt. + * + * @since 4.17.0 + */ + EMIT_LEGACY_NBT, + /** + * Only emit modern data components. + * + * @since 4.17.0 + */ + EMIT_DATA_COMPONENTS, + /** + * Emit whichever of legacy or modern data the item has. + * + * @since 4.17.0 + */ + EMIT_EITHER, + } } diff --git a/text-serializer-json/src/testFixtures/java/net/kyori/adventure/text/serializer/json/ShowItemSerializerTest.java b/text-serializer-json/src/testFixtures/java/net/kyori/adventure/text/serializer/json/ShowItemSerializerTest.java index 3b4d4bd56..2a1286655 100644 --- a/text-serializer-json/src/testFixtures/java/net/kyori/adventure/text/serializer/json/ShowItemSerializerTest.java +++ b/text-serializer-json/src/testFixtures/java/net/kyori/adventure/text/serializer/json/ShowItemSerializerTest.java @@ -37,7 +37,12 @@ final class ShowItemSerializerTest extends SerializerTest { @Test void testDeserializeWithPopulatedTag() throws IOException { + final JSONComponentSerializer serializer = JSONComponentSerializer.builder() + .editOptions(opts -> opts.value(JSONOptions.SHOW_ITEM_HOVER_DATA_MODE, JSONOptions.ShowItemHoverDataMode.EMIT_EITHER)) + .build(); + this.testObject( + serializer, Component.text().hoverEvent( HoverEvent.showItem( Key.key("minecraft", "diamond"), @@ -89,7 +94,11 @@ void testDeserializeWithNullTag() { @Test void testDeserializeWithCountOfOne() throws IOException { + final JSONComponentSerializer serializer = JSONComponentSerializer.builder() + .editOptions(opts -> opts.value(JSONOptions.SHOW_ITEM_HOVER_DATA_MODE, JSONOptions.ShowItemHoverDataMode.EMIT_EITHER)) + .build(); this.testObject( + serializer, Component.text().hoverEvent( HoverEvent.showItem( Key.key("minecraft", "diamond"), From bf2c77bcde152dcd6481fcc596e642e2dffeb9f0 Mon Sep 17 00:00:00 2001 From: zml Date: Tue, 23 Apr 2024 20:35:21 -0700 Subject: [PATCH 6/8] feat(serializer-configurate4): Update configurate serializer for data components --- .../ConfigurateComponentSerializerImpl.java | 1 + .../ConfigurateDataComponentValue.java | 54 +++++++++++++++ ...urateDataComponentValueTypeSerializer.java | 52 ++++++++++++++ .../HoverEventShowItemSerializer.java | 27 ++++++-- ...shottingConfigurateDataComponentValue.java | 68 +++++++++++++++++++ 5 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateDataComponentValue.java create mode 100644 serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateDataComponentValueTypeSerializer.java create mode 100644 serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/SnapshottingConfigurateDataComponentValue.java diff --git a/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateComponentSerializerImpl.java b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateComponentSerializerImpl.java index 0dafd26a8..052154dfc 100644 --- a/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateComponentSerializerImpl.java +++ b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateComponentSerializerImpl.java @@ -107,6 +107,7 @@ private ConfigurateComponentSerializerImpl(final @NotNull Builder builder) { .registerExact(new IndexSerializer<>(TypeToken.get(TextDecoration.class), TextDecoration.NAMES)) .registerExact(HoverEvent.ShowEntity.class, HoverEventShowEntitySerializer.INSTANCE) .registerExact(HoverEvent.ShowItem.class, HoverEventShowItemSerializer.INSTANCE) + .register(ConfigurateDataComponentValue.class, ConfigurateDataComponentValueTypeSerializer.INSTANCE) .build(); } diff --git a/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateDataComponentValue.java b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateDataComponentValue.java new file mode 100644 index 000000000..ced228b45 --- /dev/null +++ b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateDataComponentValue.java @@ -0,0 +1,54 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2024 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.serializer.configurate4; + +import net.kyori.adventure.text.event.DataComponentValue; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.configurate.ConfigurationNode; + +/** + * A data component value that can integrate with configuration nodes. + * + * @since 4.17.0 + */ +public interface ConfigurateDataComponentValue extends DataComponentValue { + /** + * Create a data component value capturing the value of an existing node. + * + * @param existing the existing node + * @return the captured value + * @since 4.17.0 + */ + static @NotNull ConfigurateDataComponentValue capturingDataComponentValue(final @NotNull ConfigurationNode existing) { + return SnapshottingConfigurateDataComponentValue.create(existing); + } + + /** + * Apply the contained value to the supplied node. + * + * @param node the node to apply this value to + * @since 4.17.0 + */ + void applyTo(final @NotNull ConfigurationNode node); +} diff --git a/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateDataComponentValueTypeSerializer.java b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateDataComponentValueTypeSerializer.java new file mode 100644 index 000000000..89d3b9c27 --- /dev/null +++ b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateDataComponentValueTypeSerializer.java @@ -0,0 +1,52 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2024 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.serializer.configurate4; + +import java.lang.reflect.Type; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.serialize.TypeSerializer; + +final class ConfigurateDataComponentValueTypeSerializer implements TypeSerializer { + static final TypeSerializer INSTANCE = new ConfigurateDataComponentValueTypeSerializer(); + + private ConfigurateDataComponentValueTypeSerializer() { + } + + @Override + public ConfigurateDataComponentValue deserialize(final Type type, final ConfigurationNode node) throws SerializationException { + return ConfigurateDataComponentValue.capturingDataComponentValue(node); + } + + @Override + public void serialize(final Type type, final @Nullable ConfigurateDataComponentValue obj, final ConfigurationNode node) throws SerializationException { + if (obj == null) { + node.set(null); + return; + } + + obj.applyTo(node); + } +} diff --git a/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/HoverEventShowItemSerializer.java b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/HoverEventShowItemSerializer.java index 0dd1b5d98..4b43b7086 100644 --- a/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/HoverEventShowItemSerializer.java +++ b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/HoverEventShowItemSerializer.java @@ -23,7 +23,10 @@ */ package net.kyori.adventure.serializer.configurate4; +import io.leangen.geantyref.TypeToken; import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.api.BinaryTagHolder; import net.kyori.adventure.text.event.HoverEvent; @@ -36,9 +39,13 @@ final class HoverEventShowItemSerializer implements TypeSerializer { static final HoverEventShowItemSerializer INSTANCE = new HoverEventShowItemSerializer(); + private static final TypeToken> COMPONENT_MAP_TYPE = new TypeToken>() { + }; + static final String ID = "id"; static final String COUNT = "count"; static final String TAG = "tag"; + static final String COMPONENTS = "components"; private HoverEventShowItemSerializer() { } @@ -50,9 +57,16 @@ public HoverEvent.ShowItem deserialize(final @NotNull Type type, final @NotNull throw new SerializationException("An id is required to deserialize the show_item hover event"); } final int count = value.node(COUNT).getInt(1); - final String tag = value.node(TAG).getString(); + final ConfigurationNode components = value.node(COMPONENTS); + if (!components.virtual()) { + final Map componentsMap = components.require(COMPONENT_MAP_TYPE); - return HoverEvent.ShowItem.showItem(id, count, tag == null ? null : BinaryTagHolder.binaryTagHolder(tag)); + return HoverEvent.ShowItem.showItem(id, count, new HashMap<>(componentsMap)); + } else { + // legacy (pre-1.20.5) + final String tag = value.node(TAG).getString(); + return HoverEvent.ShowItem.showItem(id, count, tag == null ? null : BinaryTagHolder.binaryTagHolder(tag)); + } } @Override @@ -65,10 +79,15 @@ public void serialize(final @NotNull Type type, final HoverEvent.@Nullable ShowI value.node(ID).set(Key.class, obj.item()); value.node(COUNT).set(obj.count()); - if (obj.nbt() == null) { + if (!obj.dataComponents().isEmpty()) { value.node(TAG).set(null); - } else { + value.node(COMPONENTS).set(COMPONENT_MAP_TYPE, obj.dataComponentsAs(ConfigurateDataComponentValue.class)); + } else if (obj.nbt() != null) { + // legacy (pre-1.20.5) + value.node(COMPONENTS).set(null); value.node(TAG).set(obj.nbt().string()); + } else { + value.node(COMPONENTS).set(null); } } } diff --git a/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/SnapshottingConfigurateDataComponentValue.java b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/SnapshottingConfigurateDataComponentValue.java new file mode 100644 index 000000000..3739e6fd9 --- /dev/null +++ b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/SnapshottingConfigurateDataComponentValue.java @@ -0,0 +1,68 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2024 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.serializer.configurate4; + +import java.util.stream.Stream; +import net.kyori.examination.ExaminableProperty; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.configurate.AttributedConfigurationNode; +import org.spongepowered.configurate.BasicConfigurationNode; +import org.spongepowered.configurate.CommentedConfigurationNode; +import org.spongepowered.configurate.ConfigurationNode; + +final class SnapshottingConfigurateDataComponentValue implements ConfigurateDataComponentValue { + private final ConfigurationNode ownedNode; + + // capture the value of an existing node without exposing any mutable state + static @NotNull SnapshottingConfigurateDataComponentValue create(final ConfigurationNode existing) { + final ConfigurationNode owned; + if (existing instanceof AttributedConfigurationNode) { + owned = AttributedConfigurationNode.root(((AttributedConfigurationNode) existing).tagName(), existing.options()); + } else if (existing instanceof CommentedConfigurationNode) { + owned = CommentedConfigurationNode.root(existing.options()); + } else { + owned = BasicConfigurationNode.root(existing.options()); + } + + owned.from(existing); + + return new SnapshottingConfigurateDataComponentValue(owned); + } + + private SnapshottingConfigurateDataComponentValue(final ConfigurationNode owned) { + this.ownedNode = owned; + } + + @Override + public void applyTo(final @NotNull ConfigurationNode node) { + node.from(this.ownedNode); + } + + @Override + public @NotNull Stream examinableProperties() { + return Stream.of( + ExaminableProperty.of("ownedNode", this.ownedNode) + ); + } +} From bd255ccd53ccd6e04286aa71daae3ea05ea9ba78 Mon Sep 17 00:00:00 2001 From: zml Date: Tue, 23 Apr 2024 21:13:46 -0700 Subject: [PATCH 7/8] minor fixes from self-review --- .../text/event/DataComponentValueConverterRegistry.java | 2 +- .../java/net/kyori/adventure/text/event/HoverEvent.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java b/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java index 0372e9626..0bfaf9cae 100644 --- a/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java +++ b/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java @@ -46,7 +46,7 @@ /** * A registry for conversions between different data component value holder classes. * - *

Conversions are discovered by {@link ServiceLoader} lookup of implementations of the {@link Provider} interface (using the loading thread's context classloader).

+ *

Conversions are discovered by {@link ServiceLoader} lookup of implementations of the {@link Provider} interface (on the classloader which loaded Adventure).

* * @since 4.17.0 */ diff --git a/api/src/main/java/net/kyori/adventure/text/event/HoverEvent.java b/api/src/main/java/net/kyori/adventure/text/event/HoverEvent.java index 7a980896e..e18291355 100644 --- a/api/src/main/java/net/kyori/adventure/text/event/HoverEvent.java +++ b/api/src/main/java/net/kyori/adventure/text/event/HoverEvent.java @@ -143,7 +143,7 @@ public final class HoverEvent implements Examinable, HoverEventSource, Sty * @return a hover event * @since 4.17.0 */ - public static @NotNull HoverEvent showItem(final @NotNull Keyed item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @NotNull Map dataComponents) { + public static @NotNull HoverEvent showItem(final @NotNull Keyed item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @NotNull Map dataComponents) { return showItem(ShowItem.showItem(item, count, dataComponents)); } @@ -491,11 +491,11 @@ public static final class ShowItem implements Examinable { * @since 4.17.0 * @sinceMinecraft 1.20.5 */ - public static @NotNull ShowItem showItem(final @NotNull Keyed item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @NotNull Map dataComponents) { + public static @NotNull ShowItem showItem(final @NotNull Keyed item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @NotNull Map dataComponents) { return new ShowItem(requireNonNull(item, "item").key(), count, null, dataComponents); } - private ShowItem(final @NotNull Key item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @Nullable BinaryTagHolder nbt, final @NotNull Map dataComponents) { + private ShowItem(final @NotNull Key item, final @Range(from = 0, to = Integer.MAX_VALUE) int count, final @Nullable BinaryTagHolder nbt, final @NotNull Map dataComponents) { this.item = item; this.count = count; this.nbt = nbt; @@ -592,7 +592,7 @@ private ShowItem(final @NotNull Key item, final @Range(from = 0, to = Integer.MA /** * Set the data components used on this item. * - *

This will clear any legacy nbt-format data on the item.

+ *

This will clear any legacy NBT-format data on the item.

* * @param holder the new data components to set * @return a show item data object that has the provided components From 27baf7edfe66bc7791b924482996e2f4156f9a2b Mon Sep 17 00:00:00 2001 From: zml Date: Tue, 23 Apr 2024 21:24:44 -0700 Subject: [PATCH 8/8] feat(api): Add ability to get id's of known data component value converter providers --- .../event/DataComponentValueConverterRegistry.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java b/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java index 0bfaf9cae..4b136c9c1 100644 --- a/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java +++ b/api/src/main/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistry.java @@ -33,6 +33,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.BiFunction; +import java.util.stream.Collectors; import net.kyori.adventure.key.Key; import net.kyori.adventure.util.Services; import net.kyori.examination.Examinable; @@ -56,6 +57,18 @@ public final class DataComponentValueConverterRegistry { private DataComponentValueConverterRegistry() { } + /** + * Get the id's of all registered conversion providers. + * + * @return an unmodifiable set of the known provider ids + * @since 4.1.7.0 + */ + public static Set knownProviders() { + return Collections.unmodifiableSet(PROVIDERS.stream() + .map(Provider::id) + .collect(Collectors.toSet())); + } + /** * Try to convert the data component value {@code in} to the provided output type. *